Tunel, który tak naprawdę był shellem

Krótka historia detektywistyczna o AWS Systems Manager, jednym niewinnie wyglądającym uprawnieniu i pytaniu o niewłaściwe serwery.

Prośba

Dzień wcześniej osoba z zespołu backendu w Firmie (regulowanej platformie fintech) dostała wąską rolę, nadaną imiennie. Pozwalała uruchomić shell Django w produkcyjnym klastrze Kubernetesa. Droga do niego prowadziła przez klienta VPN typu zero-trust, bo endpoint API klastra jest prywatny.

Klient VPN źle działał na jej laptopie. Napisała więc skrypt, który go omija, i poprosiła o trzy kolejne uprawnienia AWS:

ssm:StartSession
ssm:TerminateSession
ssm:ResumeSession

Osoba prowadząca platformę, która miała to zatwierdzić, martwiła się o jedną konkretną rzecz: „Upewnij się, że to nie wejdzie na maszyny EC2, na których działają nasze walidatory blockchaina. Konsola i tak pokazuje, że SSM jest na nich niedostępny… ale kto wie.”

Uczciwe pytanie. Okazało się, że nie to, które należało zadać.

Trop pierwszy: walidatory naprawdę są zamknięte

Zacznijmy tam, gdzie leży obawa. Żeby sesja AWS Systems Manager dotarła do maszyny, muszą być spełnione dwa warunki:

  1. Wywołujący ma prawo wywołać StartSession na tej maszynie.
  2. Agent SSM na maszynie zarejestrował się w Systems Manager. Do tego potrzebne jest uprawnienie w własnej roli IAM maszyny, zwykle AmazonSSMManagedInstanceCore.

Walidatory działają jako nody EKS na Bottlerocket. Bottlerocket ma wbudowanego agenta SSM, więc wygląda na to, że drzwi mogą być otwarte. Ale rola node group miała tylko domyślne polityki workerów EKS. Agent startuje, nie może się zarejestrować i node nigdy nie pojawia się w Fleet Manager. Dokładnie to pokazywała konsola: „SSM niedostępny”.

Żadne uprawnienie nadane inżynierowi tego nie zmieni. Zamek jest po drugiej stronie drzwi.

Dwie kolejne warstwy domykały sprawę:

Werdykt w sprawie pytania, tak jak je zadano: bezpiecznie. Potrójnie.

Zwrot akcji: to które maszyny są zarejestrowane?

Skoro do walidatorów nie da się dotrzeć, to z czym właściwie łączy się skrypt? StartSession musi mieć jakiś cel.

Przeszukanie kodu infrastruktury pod kątem AmazonSSMManagedInstanceCore dało dokładnie jedno trafienie: rolę małej pary instancji EC2, na których działa konektor tunelu zero-trust. To ten sam komponent, który stoi za klientem VPN sprawiającym inżynierowi kłopoty. Te instancje były zarządzane przez SSM celowo, żeby zespół platformy mógł je administrować bez SSH. Ich security group była też jedyną, która miała dostęp do prywatnego API Kubernetesa na porcie 443.

Skrypt niemal na pewno robił więc to:

 laptop ──SSM port-forward──▶ EC2 z konektorem tunelu ──443──▶ prywatne API Kubernetesa

Rozsądne obejście. Ale spójrzmy, co samo żądane uprawnienie daje.

Pułapka w ssm:StartSession

StartSession na * nie znaczy „może przekierować port”. Znaczy „może otworzyć sesję”. Jeśli nie wskażesz dokumentu sesji, Session Manager użyje domyślnego, a domyślny to interaktywny shell. Shell działa jako ssm-user, który ma sudo bez hasła.

Na konektorze tunelu shell roota daje:

Walidatorom nic nie groziło. Zagrożona była maszyna, którą wszyscy traktowali jak hydraulikę.

Poprawka: daj tunel, nie shell

Uprawnienie zostało, ale zawężone w trzech wymiarach.

┌─ StartSession ───────────────────────────────────────────────────────┐
│ A:   instance/*  WHERE ssm:resourceTag/Name = tunnel-connector-*     │
│ B:   document/AWS-StartPortForwardingSessionToRemoteHost             │
│ A+B: ssm:SessionDocumentAccessCheck = true                           │
└──────────────────────────────────────────────────────────────────────┘
┌─ TerminateSession / ResumeSession ───────────────────────────────────┐
│ session/*  WHERE ssm:resourceTag/aws:ssmmessages:session-id          │
│                  = ${aws:userid}                (tylko własne sesje) │
└──────────────────────────────────────────────────────────────────────┘

Szczegóły, które mają znaczenie:

Czego polityka zrobić nie może, to ograniczyć, dokąd prowadzi przekierowanie. Dokument port-forward przyjmuje host i portNumber jako dowolne parametry, a IAM nie potrafi ich zawęzić. Prawdziwą granicą jest egress security group jump hosta, który tu obejmował tylko HTTPS. To mniej więcej tyle, ile inżynier i tak miał przez VPN, więc ryzyko zaakceptowano. Następny krok, gdyby był kiedyś potrzebny, to własny dokument sesji z na sztywno ustawionym hostem i portem.

Drobne pomyłki po drodze

Wskazówki dla czytelnika

  1. Gdy ktoś pyta „czy to dosięgnie X?”, zapytaj też „co to może dosięgnąć?”. Spisz, które maszyny faktycznie są zarządzane przez SSM. To one są ekspozycją, a nie maszyny, o które się martwisz.
  2. O osiągalności przez SSM decyduje strona docelowa. Brak zarejestrowanego agenta oznacza brak sesji, bez względu na uprawnienia wywołującego. Sprawdź rolę instancji, zanim zaczniesz się spierać o politykę użytkownika.
  3. ssm:StartSession to shell, dopóki nie udowodnisz, że jest inaczej. Zawsze łącz go z ssm:SessionDocumentAccessCheck i jawnym dopuszczeniem dokumentu.
  4. Uprawnienia do port-forward są tylko tak wąskie jak sieć jump hosta. IAM nie przypnie celu. Security groups albo własny dokument już tak.
  5. Cicha infrastruktura to miękki cel. Konektory VPN, bastiony i runnery CI to „tylko hydraulika” aż do chwili, gdy ktoś dostanie na nich shell.