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:
- Wywołujący ma prawo wywołać
StartSessionna tej maszynie. - 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ę:
- Obejmująca całą organizację service control policy już blokowała
ssm:StartSessionna każdej instancji otagowanej jako walidator. Trafiła tam kilka miesięcy wcześniej, przy pracach nad ochroną kluczy. - Nawet z tunelem jedyna maszyna, która mogła posłużyć za jump host, miała w firewallu egress tylko dla HTTPS i protokołu VPN. Żaden port walidatora nie był osiągalny.
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:
- poświadczenia roli IAM instancji;
- token tunelu, który pozwala uruchomić kopię firmowego tunelu z dowolnego miejsca;
- możliwość zatrzymania konektora i odcięcia VPN całemu zespołowi.
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:
ssm:SessionDocumentAccessCheck = truesprawia, że AWS sprawdza również, czy masz prawo do dokumentu, a nie tylko do instancji. Bez tego uprawnienie zawężone do instancji wciąż wydaje domyślny shell.- Dwa statementy, nie jeden. Warunki IAM dotyczą każdego zasobu w statemencie. Warunek na tagu instancji nigdy nie pasuje do ARN dokumentu, więc wrzucenie obu do jednego statementu po cichu blokuje dokument. Można stracić popołudnie na zastanawianiu się, czemu port-forwarding jest „zepsuty”.
${aws:userid}w Terraformie trzeba zapisać jako$${aws:userid}. Inaczej Terraform próbuje to zinterpolować jako własną zmienną.- Klucz warunku dla tagu to
ssm:resourceTag/…, a nie ogólnyaws:ResourceTag/…. Zespół miał tę lekcję już za sobą. Zły klucz warunku nie zgłasza błędu. Po prostu nigdy nie pasuje.
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
- Pierwszym miejscem, gdzie szukano blokady dla walidatorów, była polityka IAM w repo infrastruktury. Nie było jej tam: wycofano ją, kiedy zastąpiła ją SCP na poziomie organizacji. Dopiero stare dokumenty projektowe wskazały jej nowe miejsce.
terraform fmtstanowczo odmawia obsługi plików.hcl, a stara flaga Terragruntahclfmt --terragrunt-checkjuż nie istnieje. Obecna forma toterragrunt hcl fmt --file <path>. Gorzej, że zasady samego repo zabraniały w ogóle uruchamiania Terragrunta, a i tak go uruchomiono, z przyzwyczajenia. Niczego to nie zmieniło, ale zasada zapisana i ignorowana nie spełnia swojej roli.
Wskazówki dla czytelnika
- 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.
- 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.
ssm:StartSessionto shell, dopóki nie udowodnisz, że jest inaczej. Zawsze łącz go zssm:SessionDocumentAccessChecki jawnym dopuszczeniem dokumentu.- 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.
- Cicha infrastruktura to miękki cel. Konektory VPN, bastiony i runnery CI to „tylko hydraulika” aż do chwili, gdy ktoś dostanie na nich shell.