Search WebDev Opole

Offboarding po pięciu dniach: jak nie zostawić osieroconych kont i dostępów

Krótki staż nie zwalnia z pełnego cyklu odbierania dostępów — pokazujemy checklistę i automatyzację, która zamyka konta także po kilku dniach pracy.

Share

Zdjęcie ilustracyjne do artykułu (Security)
Credits to Martyn Seddon via Unsplash

Pięć dni w roli i odejście — taki nagłówek pojawił się w ostatnich dniach w serwisach informacyjnych (BBC). Komentarz polityczny do tej historii zostawiamy innym. Dla nas to przede wszystkim gotowy przypadek testowy: człowiek, który dostał dostępy do systemów na kilka dni, a nie na kilka lat.

Offboarding prawie nigdy nie zawodzi na etatowym pracowniku z ośmioletnim stażem — jego odejście jest zaplanowane, ktoś przygotowuje tickety, zespół wie, że trzeba odebrać laptopa. Zawodzi na stażyście, wykonawcy z trzymiesięczną umową, konsultancie na dwa tygodnie i na osobie, która w piątek zaczęła, a w środę już jej nie ma. Wszyscy oni przechodzą przez ten sam formularz provisioningowy, ale ich data końca bywa zapisana tylko w czyjejś głowie.

Ten tekst jest o tym, jak zrobić z offboardingu właściwość systemu, a nie zadanie do zapamiętania. O wykrywaniu kont, które zostały po ludziach, i o tym, jak nie zgubić tokena, którego nikt nigdy nie widział w żadnym tickecie.

„Przecież był tylko chwilę” — najgorsze zdanie w audycie

Argument brzmi rozsądnie, dopóki nie spojrzysz na listę dostępów. Konto w IdP zostało wyłączone, ale sesje i refresh tokeny żyją dalej. Token API ma datę wygaśnięcia za 90 dni, bo domyślnie nadawaliśmy takie. Klucz SSH wciąż leży w authorized_keys na dwóch hostach, bo nikt nie skojarzył go z człowiekiem. Do tego konto lokalne w panelu administracyjnym, deploy key w repozytorium i hasło do bazy wpisane do .env w projekcie, który „zaraz przejmiemy”.

Problem nie leży w złej woli zespołu. Leży w asymetrii kosztów: nadanie dostępu to jedno kliknięcie w self-service, odebranie to siedem decyzji rozrzuconych po różnych systemach. Kiedy brakuje właściciela procesu, wygrywa domyślne zachowanie — nic się nie dzieje. Konto zostaje, bo usunięcie wymaga odwagi, a dezaktywacja nie. Zostaje też dlatego, że nikt nie wie, kto miałby je usunąć.

W audycie „był tylko chwilę” nie jest okolicznością łagodzącą. Jest pytaniem: jak to możliwe, że osoba z pięciodniowym stażem zostawiła po sobie ślad, którego nie umiemy wskazać w raporcie?

Inwentaryzacja śladów: czego szukać po człowieku

Zanim cokolwiek zautomatyzujesz, musisz wiedzieć, co ma zostać odebrane. Praktyczna lista miejsc, w których zostają dostępy:

  • IdP i tożsamość — konto, aktywne sesje, refresh tokeny, enrolment MFA, członkostwa w grupach, zgody OAuth udzielone aplikacjom zewnętrznym.
  • Konta lokalne — bazy danych, panele administracyjne, legacy appki bez SSO, VPN z certyfikatem.
  • Konta techniczne — service accounty „przypisane do człowieka”, boty, konta deployowe, które przeżywają właściciela.
  • Sekrety — personal access tokeny, deploy keys, klucze SSH (GitHub, serwery, CI), certyfikaty, klucze dostępowe do chmury.
  • Chmura i IAM — użytkownicy IAM, założenia ról, service principals, zaufania federacyjne.
  • Repozytoria — collaboratorzy, członkostwa w zespołach, ownership pakietów, tokeny do rejestru obrazów.
  • SaaS — seaty, integracje, webhooki skonfigurowane na prywatne endpointy.
  • Infrastruktura — delegacje DNS, dostępy do panelu płatności, do rejestratora, do monitoringu.
  • Dane — skrzynka mailowa, aliasy, forwarding, kalendarz, dokumenty w chmurze. To najczęstsze źródło osieroconych zasobów, bo nikt nie myśli o tym jak o „dostępie”.

Ta lista nie musi być od razu kompletna. Ważne, żeby była wersjonowana w repozytorium razem z kodem, który ją egzekwuje — inaczej rozjedzie się z rzeczywistością w ciągu kwartału.

Maszyna stanów: dostęp z datą ważności od pierwszego dnia

Kluczowa zmiana to przeniesienie momentu decyzji. Zamiast pytać „kiedy odejdzie i kto wtedy wyłączy konto”, pytamy przy nadawaniu: do kiedy ten dostęp ma żyć. Data końca umowy jest znana w dniu jej podpisania — także dla pięciodniowych epizodów.

Minimalny model danych wygląda tak:

CREATE TABLE access_grants (
  id          bigserial PRIMARY KEY,
  subject_id  text        NOT NULL,   -- identyfikator osoby z HR
  system      text        NOT NULL,   -- 'github', 'aws', 'vpn'
  principal   text        NOT NULL,   -- login, ARN, fingerprint klucza
  owner       text        NOT NULL,   -- kto odpowiada za dostęp
  granted_at  timestamptz NOT NULL DEFAULT now(),
  expires_at  timestamptz NOT NULL,
  state       text        NOT NULL DEFAULT 'active'
              CHECK (state IN ('active', 'revoking', 'revoked')),
  revoked_at  timestamptz
);

CREATE UNIQUE INDEX access_grants_active
  ON access_grants (system, principal)
  WHERE state = 'active';

expires_at nie jest wpisywane ręcznie — jest wyliczane z kontraktu, z twardym limitem, żeby nikt nie dostał dostępu „na zawsze” przez literówkę. Dopiero potem uruchamiasz nocny reconciler, który domyka wygasłe granty:

def revoke(conn, grant_id):
    with conn.transaction():
        row = conn.execute("""
            UPDATE access_grants
               SET state = 'revoking'
             WHERE id = %s AND state = 'active'
         RETURNING system, principal
        """, (grant_id,)).fetchone()
        if row is None:
            return  # ktoś już to robi — operacja jest idempotentna
        driver_for(row["system"]).revoke(row["principal"])
        conn.execute("""
            UPDATE access_grants
               SET state = 'revoked', revoked_at = now()
             WHERE id = %s
        """, (grant_id,))

Trzy detale, które decydują o tym, czy to działa w praktyce. Po pierwsze, revoke() w każdym driverze musi być idempotentne — wywołane dwa razy nie może się wysypać. Po drugie, błąd wywołania nie może zniknąć: trafia do kolejki retry z alertem, bo cichy wyjątek w nocy to dokładnie ten scenariusz, którego się boisz. Po trzecie, konta w IdP nie usuwamy w dniu odejścia — przechodzą w stan disabled z unieważnieniem sesji. Twarde usunięcie po okresie retencji chroni ślad audytowy i pozwala wrócić kontraktorowi bez kolizji loginów.

Checklista: co wyłączyć od razu, a co wygasić po karencji

  • 0–1 h: unieważnienie sesji i refresh tokenów, disabled w IdP, unieważnienie PAT-ów i kluczy SSH, odebranie tokenów MFA i sprzętu, rotacja haseł współdzielonych, odebranie dostępu do VPN.
  • do 24 h: dostępy w repozytoriach, klucze dostępowe w chmurze i zaufania ról, seaty w SaaS, listy mailingowe, aliasy, delegacje skrzynki, przekazanie on-call, dostępy do DNS i paneli.
  • karencja 30 dni: skrzynka z auto-odpowiedzią i delegacją do przełożonego (z wyłączonym forwardingiem na zewnątrz), katalog domowy, dane w bucketach, przekazanie własności dokumentów.
  • purge po retencji: twarde usunięcie konta i wpis w logu audytowym.

Przekazanie własności to element offboardingu, nie zadanie „do sprawdzenia później”. Repozytorium bez ownera, domena bez opiekuna i skrzynka bez delegata to ten sam problem co aktywne konto.

Raport krzyżowy: jak znaleźć osierocone konta

Najprostsze narzędzie, jakie znam, to różnica dwóch zbiorów. Ludzie aktywni w HR kontra principaly aktywne w systemie. Różnica w obie strony jest informacją.

# aktywne osoby wg HR
curl -s "$HR_API/employees?status=active" | jq -r '.[].login' | sort > /tmp/humans.txt

# aktywne konta w IdP
curl -s "$IDP_API/users?status=active" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.users[].userName' | sort > /tmp/idp.txt

# ma konto, nie ma umowy
comm -13 /tmp/humans.txt /tmp/idp.txt

# ma umowę, nie ma konta (blokada onboardingu)
comm -23 /tmp/humans.txt /tmp/idp.txt

comm wymaga posortowanych plików, dlatego sort nie jest tu ozdobnikiem. Drugi wynik też ma wartość — pokazuje, że ktoś pracuje bez konta, czyli że provisioning poszedł bokiem.

Do tego dwie kolumny, bez których raport jest bezużyteczny: owner i last_activity. Principal bez właściciela to kandydat na sierotę niezależnie od tego, czy wygląda jak człowiek. Dlatego konta techniczne powinny mieć deklarowanego ownera w pliku w repozytorium, a CI powinien failować, gdy pojawia się nowe konto bez wpisu. Wtedy lista własności nie starzeje się dyskretnie.

Warto mierzyć trzy liczby i pokazywać je co miesiąc: odsetek aktywnych grantów z ustawionym expires_at, liczbę kandydatów na sieroty oraz czas od daty końca umowy do stanu revoked (p50 i p95). Pierwsza liczba mówi o projekcie systemu, trzecia o tym, czy komukolwiek zależy.

Test na sucho, zanim zabraknie czasu

Procedury nie sprawdza się w dniu odejścia — sprawdza się ją wtedy, gdy nikt się nie spieszy. Ćwiczenie jest tanie: wybierz ochotnika, załóż mu konto „fake leaver” w środowisku staging (albo w osobnym tenancie), nadaj kilka dostępów i odpal proces z datą końca ustawioną na wczoraj. Zmierz, ile czasu zajęło dojście do revoked i co wymagało ręcznej decyzji. Każda taka decyzja to miejsce, które w piątek o 22:00 zamieni się w telefon do jednej osoby.

Osobno przetestuj awarie. Co się dzieje, gdy HR API nie odpowiada? Sensowna odpowiedź to fail-closed dla nowych nadań i kolejka retry z alertem dla odbiorów — nigdy „przepuść, sprawdzimy później”. Sprawdź też, czy raport krzyżowy faktycznie widzi konto bez ownera, i czy potrafisz wskazać, kto ma dostęp do produkcyjnej bazy sprzed dwóch lat.

Runbook trzymaj w repozytorium obok kodu, który go wykonuje. Dokument w narzędziu bez wersjonowania rozjedzie się z rzeczywistością po pierwszej zmianie w pipeline’ie, a w krytycznym momencie nikt nie będzie miał czasu go szukać. Powtarzaj próbę co kwartał — nie po to, żeby mieć certyfikat, ale dlatego, że ludzie w zespole się zmieniają, a wraz z nimi wiedza o tym, gdzie co leży.

Wnioski

Pięciodniowy epizod nie jest przypadkiem brzegowym. Krótkie staże, kontrakty, praktykanci i zewnętrzni wykonawcy to stała część rzeczywistości, a nie wyjątek od reguły. System, który zakłada, że wszyscy zostają na lata, będzie działał tylko dla tych, którzy zostają na lata.

Reguła projektowa jest prosta: każdy dostęp ma ownera i datę ważności, a jeśli ich nie ma, nie powinien istnieć. Data ważności wyliczana z umowy, nie wpisywana ręcznie. Odbiór wykonywany przez maszynę, nie przez pamięć zespołu. Raport krzyżowy uruchamiany cyklicznie, bo rzeczywistość i tak będzie się rozjeżdżać — chodzi tylko o to, żeby rozjazd został wykryty w tygodniach, a nie w trakcie audytu.

Jeżeli z całej tej historii da się wyciągnąć jedną techniczną lekcję, to tę: pytanie „kto jeszcze ma dostępy po tym człowieku” powinno mieć odpowiedź w SQL-u, a nie w czyjejś głowie.

Keep reading