Incydent z kontenerem: jak obrazy z publicznych registry przynoszą malware

0
89
3.2/5 - (4 votes)

Nawigacja:

Dlaczego obrazy z publicznych registry są dziś realnym wektorem ataku

Publiczne registry jako nowy „rynek zaufania”

Obrazy kontenerów z publicznych registry stały się tym, czym kiedyś były publiczne repozytoria pakietów systemowych: domyślnym źródłem komponentów. Docker Hub, GitHub Container Registry czy Quay to dla wielu zespołów naturalne miejsce, z którego pobiera się obrazy bez większej refleksji. Ten nawyk oszczędza czas, ale tworzy nowy, słabo kontrolowany „rynek zaufania”, który można łatwo nadużyć.

Każdy może opublikować obraz w publicznym registry. Część platform wprowadziła podstawową moderację i skanowanie, ale w praktyce ogromna liczba obrazów nigdy nie jest naprawdę zweryfikowana. Co gorsza, w środowiskach DevOps często liczy się to, by zadziałało szybko, a nie to, by było bezpieczne. Efekt: obrazy kontenerów stają się wrotami do ataku na całe środowisko – od stacji deweloperów, przez pipeline CI/CD, po klastry produkcyjne.

Zaufanie, jakie użytkownicy pokładają w nazwach typu node, nginx czy ubuntu, jest ogromne. Wystarczy jednak minimalna różnica w nazwie lub organizacji, by atakujący podszył się pod coś „znanego”, a użytkownik – często w pośpiechu – tego nie zauważył. Gdy taki obraz z publicznego registry trafi do Dockerfile lub pipeline, malware rozprzestrzenia się dalej automatycznie.

Presja na szybkość kontra bezpieczeństwo obrazów

Zespoły developerskie działają pod dużą presją czasu. Gdy brakuje biblioteki lub narzędzia w środowisku, naturalną reakcją jest: „znajdź gotowy obraz w registry, dorzuć do pipeline i jedziemy dalej”. Ten tryb pracy premiuje szybkość kosztem kontroli bezpieczeństwa. Nie chodzi o brak dobrej woli, tylko o realia: sprinty, terminy, produkcja „wczoraj”.

Wiele incydentów złośliwych obrazów z Docker Hub wynika właśnie z tego mechanizmu. Developer wybiera pierwszy wynik w wyszukiwarce w IDE, w dokumentacji blogowej albo w wynikach „docker search”. Rzadko sprawdza, kto jest autorem obrazu, z jakiego repozytorium kodu pochodzi, czy jest aktualizowany i w jaki sposób jest budowany. Malware w takim obrazie nie musi być wyszukane – wystarczy prosty skrypt exfiltrujący zmienne środowiskowe i pliki z sekretami, by wyrządzić poważną szkodę.

Presję na tempo dodatkowo wzmacnia kultura „sample code z internetu”. Wiele tutoriali Docker/Kubernetes używa przypadkowych obrazów z publicznych registry, często bez komentarza na temat bezpieczeństwa. Te przykłady są później kopiowane jeden do jednego do realnych systemów, nierzadko produkcyjnych. W ten sposób złośliwy lub niepewny obraz dostaje się do środowiska i żyje tam przez lata.

Zmiana perspektywy atakujących: od aplikacji do łańcucha dostaw

Atakujący przeszli długą drogę od prostych exploitów webowych do wyszukanych ataków na łańcuch dostaw oprogramowania. Z perspektywy przestępcy zainfekowanie pojedynczej aplikacji ma ograniczoną wartość. Znacznie atrakcyjniejszym celem jest punkt, przez który przechodzą dziesiątki czy setki aplikacji – pipeline CI/CD, rejestr obrazów, popularny base image.

Złośliwe obrazy kontenerów z publicznych registry spełniają ten warunek idealnie: jeden zainfekowany obraz może zostać „pullnięty” przez setki projektów, użyty jako baza do kolejnych obrazów i rozpropagowany dalej. Część organizacji w ogóle nie rejestruje, które zewnętrzne obrazy są używane, ani jak często są aktualizowane. To sytuacja, w której atakujący nie tylko łatwo się wstrzykuje, ale i długo pozostaje niezauważony.

Dodatkowym bodźcem jest popularyzacja infrastruktury jako kodu (IaC). Skoro cała architektura jest opisana w plikach YAML czy Terraform, wystarczy w jednym miejscu zmienić referencję obrazu bazowego na inny tag lub inne namespace. Zmiana przejdzie code review, bo „wygląda technicznie poprawnie”, a malware robi swoje w tle.

Krótkie przykłady głośnych incydentów

Na przestrzeni ostatnich lat pojawiło się kilka spektakularnych przypadków złośliwych obrazów kontenerów w publicznych registry. Do typowych scenariuszy należały:

  • obrazy w Docker Hub, które po uruchomieniu ściągały cryptominera i wykorzystywały zasoby chmury do kopania kryptowalut,
  • obrazy narzędziowe (np. pomocnicze do CI), które zawierały skrypty wysyłające zmienne środowiskowe (w tym klucze i tokeny) na zewnętrzny serwer,
  • obrazy podszywające się nazwą pod oficjalne, ale z dodatkową warstwą instalującą backdoora.

Większość z tych incydentów została wykryta dopiero wtedy, gdy ktoś zauważył nietypowe użycie zasobów (np. nagły wzrost zużycia CPU w klastrze) lub podejrzany ruch sieciowy do nieznanych adresów. Wspólnym mianownikiem była jedna rzecz: bezrefleksyjne zaufanie do obrazów z publicznych registry oraz brak systematycznego skanowania i monitoringu.

Jak działają obrazy kontenerów i registry – minimum techniczne potrzebne do zrozumienia ataku

Struktura obrazu kontenera: warstwy, manifest, metadane

Obraz kontenera nie jest jednym „wielkim plikiem”. To zestaw warstw (layers), opisany manifestem i uzupełniony metadanymi oraz tagami. Zrozumienie tej struktury ułatwia wykrywanie anomalii i identyfikowanie miejsca, w którym mogło dojść do wstrzyknięcia malware.

Każda warstwa odpowiada mniej więcej jednemu poleceniu w Dockerfile (RUN, COPY, ADD itp.). Warstwy są cache’owane i współdzielone między obrazami, co daje oszczędność miejsca. Z punktu widzenia atakującego to również okazja – wystarczy dodać jedną warstwę z podejrzanym skryptem, by całość obrazu zachowywała się złośliwie, choć pozostałe warstwy pochodzą z zaufanego źródła.

Manifest obrazu opisuje, które warstwy go tworzą, pod jaką architekturę jest zbudowany, jakie ma konfiguracje wejściowe (entrypoint, cmd, env). Metadane zawierają między innymi informacje o:

  • autorze obrazu,
  • dacie zbudowania,
  • base image,
  • etykietach (labels) – często z odwołaniem do repozytorium kodu.

Te pola pomagają w weryfikacji, czy obraz ma sensowną historię i czy jego pochodzenie jest w ogóle zrozumiałe. Brak lub chaotyczne metadane to często pierwsza czerwona flaga.

Typy registry: publiczne i prywatne w praktyce

Registry obrazów można z grubsza podzielić na publiczne i prywatne. Publiczne (Docker Hub, GitHub Container Registry, Quay.io, GitLab Registry w trybie publicznym) są dostępne dla każdego – zarówno do pobierania, jak i często do publikowania. Prywatne mogą być:

  • on-premises – własny registry uruchomione np. jako Harbor, Artifactory, Nexus,
  • w chmurze – prywatne przestrzenie w ECR (AWS), ACR (Azure), GCR/AR (Google Artifact Registry).

Publiczne registry służą głównie do dystrybucji szeroko używanych obrazów, open-source’owych projektów i oficjalnych komponentów vendorów. Prywatne registry mają inne zadanie: stać się kontrolowanym punktem, przez który przechodzą obrazy do użycia wewnątrz organizacji. Dobre praktyki zakładają, że do klastra produkcyjnego nie trafia nic bezpośrednio z publicznego registry.

W praktyce często wygląda to jednak inaczej: klastry mają otwarty dostęp do Docker Hub lub innych publicznych źródeł, bo „tak było szybciej postawić środowisko”. Z punktu widzenia bezpieczeństwa oznacza to, że każdy, kto dorwie się do konfiguracji deploymentu, może wskazać dowolny obraz z internetu – w tym złośliwy. Jeśli do tego dochodzą nadmierne uprawnienia kontenerów, skala ryzyka rośnie lawinowo.

Standardowy przepływ: developer → build → registry → deploy

Typowy łańcuch życia obrazu kontenera wygląda mniej więcej tak:

  1. Developer wybiera obraz bazowy (np. node:18-alpine) z publicznego registry.
  2. Tworzy Dockerfile i dodaje własny kod, zależności, konfigurację.
  3. Build pipeline buduje obraz i wysyła (push) go do registry – często również publicznego lub pół-publicznego.
  4. System orkiestracji (Kubernetes, ECS, Nomad) pobiera (pull) obraz z registry podczas deploymentu.

W tym przepływie pojawia się kilka miejsc, gdzie można wstrzyknąć malware:

  • w wyborze obrazu bazowego (zainfekowany base image z publicznego registry),
  • w samym Dockerfile (dodatkowe polecenia instalujące złośliwe komponenty),
  • w pipeline CI/CD (podmiana narzędzi budujących, runnerów, skryptów buildowych),
  • bezpośrednio w registry (podmiana taga obrazu lub całego obrazu, przejęcie konta maintenera).

Jeśli organizacja nie ma mechanizmów weryfikacji na żadnym z tych etapów, malware w obrazie może pozostać niewidoczne przez bardzo długi czas. Atakujący woli działać cicho – nie niszczy danych od razu, tylko buduje sobie trwałą obecność (persistence), zbiera informacje i czeka na dogodny moment.

Scenariusze ataków z użyciem złośliwych obrazów z publicznych registry

Podszywanie się pod popularne obrazy: typosquatting i mylne nazwy

Jedna z prostszych, ale wciąż skutecznych technik to tworzenie obrazów, które udają znane projekty. Zamiast oficjalnego library/nginx pojawia się obraz w namespace nginxcorp/nginx albo ngixn/nginx. Nazwa wygląda wiarygodnie, szczególnie w pośpiechu lub na małym ekranie.

Atakujący liczy na literówkę w Dockerfile albo na to, że developer wybierze obraz z listy w UI, nie sprawdzając profilu autora czy opisu. Wewnątrz obrazu może być standardowy Nginx, ale z dodatkową warstwą uruchamiającą skrypt pobierający i startujący malware:

  • cryptominer działający w tle,
  • reverse shell otwierający zdalny dostęp do kontenera,
  • narzędzie skanujące sieć wewnętrzną pod kątem innych usług.

Z zewnątrz kontener działa „normalnie” – serwuje HTTP, odpowiada na healthchecki. Administracja może przez długi czas nie zauważyć, że coś jest nie tak, poza nieco wyższym zużyciem zasobów czy sporadycznym ruchem do dziwnych adresów IP.

Przejęcie konta maintenera i podmiana obrazu

Kolejny scenariusz to kompromitacja konta osoby lub organizacji utrzymującej popularny obraz. Wystarczy wyciek hasła, tokenu dostępowego lub błąd w konfiguracji 2FA, aby atakujący mogli:

  • zbudować nową, zainfekowaną wersję obrazu,
  • oznaczyć ją jako najnowszą (latest) lub pod popularnym tagiem,
  • uspokoić użytkowników pozornie legalnym changelogiem.

Z perspektywy użytkownika obraz pochodzi z tego samego repozytorium i zaufanego maintenera. Nowsza wersja często jest pobierana automatycznie (np. przy użyciu taga latest lub mechanizmów automatycznych aktualizacji). Rzadko kto analizuje zmiany w Dockerfile, jeśli update jest przedstawiony jako „minor fix” czy „security update”.

Właśnie dlatego wiele organizacji zaczyna wymagać dodatkowych sygnatur (podpisywanie obrazów), aby odróżnić autentyczne buildy od tych wgranych po przejęciu konta. Sam login i hasło do registry przestają być wystarczającą gwarancją pochodzenia obrazu.

Wstrzyknięcie malware do łańcucha CI/CD

Ataki na łańcuch CI/CD stają się coraz powszechniejsze. Zamiast próbować zainfekować każdy projekt osobno, przestępcy szukają sposobu, by dostać się do pipeline, który buduje setki obrazów. Jednym ze sposobów jest wykorzystanie zewnętrznych obrazów używanych jako:

  • build environment (np. maven:3-jdk, node:18),
  • narzędzia infrastrukturalne (np. terraform, ansible, awscli),
  • obrazy pomocnicze (lint, testy bezpieczeństwa, skanery).

Jeśli taki obraz jest zainfekowany, pipeline uruchamia go przy każdym buildzie, często z szerokim zestawem zmiennych środowiskowych, tajnych kluczy i dostępu do repozytoriów kodu. Skrypt w obrazie może:

  • wykraść tokeny dostępu do Git, chmury, registry,
  • wstrzykiwać backdoory bezpośrednio do budowanych obrazów,
  • eksfiltrować fragmenty kodu lub konfiguracji do zewnętrznego serwera.

Najbardziej podstępne są „ciche” złośliwe komponenty, które nie wpływają na wynik builda. Pipeline przechodzi na zielono, testy są OK, a jedynym śladem ataku są dodatkowe logi sieciowe, które łatwo zignorować w gąszczu standardowego hałasu.

„Ciche” malware w kontenerach: cryptominer, exfiltracja, reverse shell

Złośliwy obraz nie musi od razu niszczyć danych ani przejmować aplikacji. Bardziej opłacalne jest zachowanie dyskretne. Typowe przykłady:

Uprawnienia kontenera jako mnożnik szkód

Ten sam złośliwy obraz w dwóch różnych środowiskach może mieć zupełnie inne skutki. Kluczowe jest to, z jakimi uprawnieniami działa kontener. Jeśli kontener startuje z domyślnymi, mocno ograniczonymi capabilities i bez dostępu do hosta, malware ma znacznie mniej możliwości. Gdy jednak polityki są poluzowane, scenariusze stają się znacznie groźniejsze.

Najczęstsze „przyspieszacze” skutków ataku to:

  • uruchamianie kontenerów z --privileged lub szerokim zestawem capabilities,
  • montowanie gniazd i zasobów hosta (np. /var/run/docker.sock, /var/lib/kubelet),
  • dostęp do wrażliwych Secretów przez zmienne środowiskowe lub wolumeny,
  • brak ograniczeń sieciowych między przestrzeniami nazw (pods) i do internetu.

W takiej konfiguracji nawet prosty cryptominer może zostać rozbudowany w pełnoprawne narzędzie do breakoutu z kontenera. Jeśli kontener widzi gniazdo Dockera, złośliwe oprogramowanie może tworzyć nowe kontenery na hoście, montować dowolne wolumeny i w efekcie przejąć całą maszynę. To już nie jest „tylko” problem jednego microserwisu.

Łańcuchowe infekcje między kontenerami

Gdy złośliwy obraz trafi do środowiska, często nie zatrzymuje się na jednym kontenerze. W klasycznych sieciach płaskich kontener może:

  • skanować wewnętrzny zakres adresów IP klastra,
  • szukać usług z domyślnymi hasłami (Redis bez hasła, niezabezpieczone bazy, panele admina),
  • próbować wykraść tokeny serwisowe z innych usług poprzez błędną konfigurację sieciową lub API.

Jeśli konfiguracja sieciowa jest liberalna (np. brak network policies w Kubernetesie), zainfekowany kontener może zbudować sobie „mapę” środowiska. W kolejnych krokach malware próbuje:

  • podpiąć się pod ruch do kluczowych serwisów,
  • przejąć poświadczenia technicznych kont (service accounts),
  • rozszerzyć obecność na kolejne namespace’y i klastry.

Czasem wszystko zaczyna się od jednego, niepozornego narzędzia diagnostycznego uruchomionego „na szybko” z publicznego obrazu. Jeśli taki kontener ma dostęp do tej samej sieci co wrażliwe systemy, stanowi wygodny punkt wejścia do dalszej eksploracji.

Jak rozpoznać, że obraz może być złośliwy – sygnały ostrzegawcze

Metadane i profil autora, które nie trzymają się kupy

Pierwszym filtrem jest to, jak wygląda samo repozytorium obrazu w registry. Kilka rzeczy rzuca się w oczy już przy pobieżnym sprawdzeniu:

  • autor o losowej nazwie, bez historii, bez powiązań z projektem open source, który rzekomo reprezentuje,
  • brak opisu, dokumentacji, linku do repozytorium kodu lub strony projektu,
  • pojedynczy obraz o nazwie zbliżonej do znanego projektu, ale bez żadnej społeczności wokół,
  • dziwne lub sprzeczne etykiety (labels) w manifeście, np. inny autor w labelu niż w profilu.

Jeśli obraz ma naśladować coś oficjalnego (np. popularną bazę danych, narzędzie DevOps, framework), a jednocześnie nie ma jasnego powiązania ze źródłem projektu, to duży znak zapytania. Zwłaszcza gdy używany jest w krytycznych miejscach, takich jak pipeline czy produkcja.

Nielogiczne Dockerfile i podejrzane polecenia RUN

Gdy jest dostęp do Dockerfile lub historii buildów, kolejnym krokiem jest analiza tego, co faktycznie dzieje się w warstwach. Kilka symptomów, które powinny zapalić lampkę kontrolną:

  • polecenia curl/wget ściągające binarki lub skrypty z mało znanych domen (szczególnie z IP zamiast nazw domenowych),
  • instalacja narzędzi z pominięciem pakietów dystrybucji (np. bash <(curl ...) zamiast paczek z repozytorium),
  • rozpakowywanie plików do nietypowych lokalizacji (/tmp/.x, /var/tmp/.hidden, katalogi ukryte),
  • uruchamianie procesów w tle (&) bez jasnego uzasadnienia w kontekście aplikacji.

Zdarza się też, że złośliwe komponenty są „sklejone” w jednym długim poleceniu RUN z mnóstwem operatorów && i ||, żeby utrudnić czytanie. Jeśli przy normalnej aplikacji webowej w Dockerfile nagle pojawia się instalacja narzędzi do skanowania sieci czy klienta TOR, jest to co najmniej dziwne.

Nietypowe entrypointy i procesy „towarzyszące”

Kolejnym aspektem, który często zdradza malware, jest sposób uruchamiania kontenera. Z pozoru entrypoint wskazuje na aplikację (np. serwer HTTP), ale w tle startują dodatkowe procesy:

  • obok głównego procesu pojawia się binarka o losowej nazwie (./a.out, kworker w nietypowej ścieżce),
  • entrypoint to skrypt shellowy, który najpierw uruchamia kilka poleceń sieciowych, a dopiero potem aplikację,
  • procesy pozostają w tle po zakończeniu głównej aplikacji, utrzymując kontener przy życiu.

W praktyce widać to dobrze w poleceniu ps uruchomionym wewnątrz kontenera lub w metrykach monitoringu (np. agent widzi kilka procesów, choć spodziewana była jedna aplikacja). Jeśli kontener o prostym zadaniu ma bogate „życie wewnętrzne”, warto się temu przyjrzeć.

Ruch sieciowy i DNS, który nie pasuje do roli aplikacji

Nie trzeba od razu mieć zaawansowanego XDR, żeby zauważyć pierwsze anomalie. Nawet podstawowy monitoring potrafi pokazać:

  • częste połączenia wychodzące do zagranicznych adresów IP, niezwiązanych z dostawcami usług aplikacji,
  • zapytania DNS do domen, które nic nie mówią zespołowi (lub świeżo zarejestrowanych),
  • stały, równomierny ruch sieciowy niezależny od obciążenia aplikacji (typowe dla cryptominerów lub beaconów C2).

W aplikacji biznesowej raczej nie jest potrzebne utrzymywanie stałych połączeń z adresem IP, o którym nikt w zespole nie słyszał. Jeśli taki wzorzec powtarza się w kilku replikach tego samego obrazu, to mocny sygnał, że warto go odizolować i przeanalizować.

Pracownik portowy zabezpiecza kontenery cargo w deszczu
Źródło: Pexels | Autor: Simon R. Minshall

Typowe wektory malware w obrazach kontenerów – co atakujący tam „wsadzają”

Cryptominery: prosty zarobek kosztem Twojej infrastruktury

Najbardziej rozpowszechnionym typem złośliwego oprogramowania w kontenerach są cryptominery. Kuszą atakujących z kilku powodów:

  • łatwo je wbudować w obraz jako małą, statyczną binarkę,
  • są stosunkowo proste do ukrycia (działają jako proces o neutralnej nazwie, np. kdevtmpfsi),
  • infrastruktura kontenerowa często oferuje duże moce obliczeniowe, czasem z GPU.

W obrazie cryptominer zwykle pojawia się jako dodatkowy plik uruchamiany z entrypointu lub CMD w tle. W konfiguracji zaszyte są adresy pooli miningowych i porty komunikacyjne. Zespół operacyjny widzi tylko zwiększone zużycie CPU/GPU i rachunki, które ciężko wytłumaczyć normalnym ruchem.

Implanty do eksfiltracji danych i poświadczeń

Druga kategoria to narzędzia nastawione na zbieranie danych. Nie chodzi tylko o bazy klientów – celem są również poświadczenia, konfiguracje, klucze API. Taki implant może:

  • cyklicznie przeszukiwać system plików w poszukiwaniu plików .env, kluczy SSH, plików konfiguracyjnych,
  • odczytywać zmienne środowiskowe i Secret-y montowane jako wolumeny,
  • łapać konfiguracje narzędzi chmurowych (~/.aws/credentials, gcloud, az).

Zebrane dane są następnie wysyłane partiami na serwer C2 lub przechowywane lokalnie do czasu kolejnego połączenia. Atakujący może spokojnie analizować zebrane poświadczenia i planować dalszą eskalację uprawnień – już poza samym klastrem.

Backdoory i reverse shells: trwały dostęp do środowiska

Kolejnym elementem, który często trafia do złośliwych obrazów, są backdoory. Technicznie najprostszy wariant to reverse shell:

  • po starcie kontener nawiązuje połączenie TCP/UDP do konkretnego hosta i portu,
  • na drugim końcu czeka listener atakującego,
  • po połączeniu atakujący ma interaktywną powłokę w kontenerze.

Bardziej zaawansowane warianty używają szyfrowania, losowych interwałów połączeń, a nawet tunelowania ruchu przez legalne usługi (np. serwisy chmurowe), żeby trudniej je było wykryć. Niekiedy backdoor jest aktywowany tylko w określonych warunkach, np. przy ustawionej konkretnej zmiennej środowiskowej albo przy wykryciu określonego hostname’u.

Narzędzia do lateral movement i eskalacji uprawnień

W wielu złośliwych obrazach oprócz samego payloadu znajdują się narzędzia wspomagające dalszy ruch po środowisku. Mogą to być:

  • skanery portów (np. zintegrowany nmap, własne lekkie skanery TCP),
  • eksploity na znane luki w jądrze lub runtime, próby breakoutu z kontenera,
  • narzędzia do enumeracji zasobów chmurowych na podstawie dostępnych credów (np. skrypty wykorzystujące AWS/GCP/Azure SDK).

Te komponenty na pierwszy rzut oka mogą wyglądać jak zwykłe „toolboxy DevOps-owe” – zestaw narzędzi administracyjnych. Różnica polega na tym, że są uruchamiane automatycznie i działają w trybie agresywnego skanowania, a nie manualnego debugowania.

Hooki w narzędziach developerskich i buildowych

Zdarza się, że złośliwe obrazy nie atakują bezpośrednio produkcji, lecz infrastrukturę buildową i stanowiska developerów. Przykładowo:

  • obraz IDE w przeglądarce zawiera rozszerzenie przechwytujące fragmenty kodu i konfiguracji,
  • obraz kompilatora ma zmodyfikowane skrypty post-build, które dorzucają dodatkowe klasy/biblioteki do artefaktów,
  • obrazy narzędzi testowych zbierają dane o strukturze mikroserwisów, endpointach API i wynikach security testów.

Atakujący dzięki temu uczy się środowiska i może wstrzyknąć malware w miejsca, które mają największą szansę pozostać niezauważone – np. w biblioteki współdzielone przez kilkadziesiąt usług.

Studium przypadku 1: zainfekowany obraz „pomocniczy” w pipeline CI/CD

Tło: niewinne narzędzie w YAML-u pipeline’u

W jednej z firm średniej wielkości zespół DevOps postanowił uporządkować pipeline’y CI/CD. Zamiast instalować narzędzia na runnerach, zaczęto używać gotowych obrazów z publicznego registry: osobno do lintowania, testów jednostkowych, budowania artefaktów i skanowania bezpieczeństwa. Konfiguracja w YAML-u stała się czytelna, a utrzymanie prostsze.

W którymś momencie ktoś dodał nowy krok: „deploy-helper”, używający obrazu z Docker Huba o nazwie zbliżonej do znanego narzędzia do wdrożeń. Obraz działał – deploymenty przechodziły, logi wyglądały sensownie. Nikt nie miał powodu, żeby go kwestionować, szczególnie że pomógł skrócić skrypty wdrożeniowe.

Objawy: drobne anomalie w ruchu i niepokojące alerty

Po kilku tygodniach zespół bezpieczeństwa zauważył nietypowy ruch wychodzący z sieci runnerów CI/CD do pewnego dostawcy VPS-ów. Połączenia pojawiały się głównie w godzinach buildów i deploymentów, ale nie było jasne, z których kontenerów dokładnie pochodziły.

Równolegle jeden z cloud engineerów otrzymał alert o nietypowym użyciu tokenu z uprawnieniami do zarządzania ECR/ACR. Token był używany do listowania i pobierania obrazów z rejestru w godzinach nocnych, mimo że żaden zaplanowany pipeline nie powinien wtedy działać. Na początku uznano to za błąd w konfiguracji harmonogramu, dopiero powtarzalność zdarzeń wzbudziła większe zainteresowanie.

Analiza: co naprawdę robił obraz „deploy-helper”

Po odizolowaniu środowiska CI/CD i uruchomieniu podejrzanego obrazu w kontrolowanych warunkach okazało się, że entrypoint składa się z dwóch części. Pierwsza, widoczna w dokumentacji, rzeczywiście wykonywała operacje związane z deploymentem. Druga była ukryta w jednym z plików shellowych dołączonych do obrazu.

Ukryty fragment skryptu:

  • odczytywał zmienne środowiskowe z tokenami dostępu do registry i chmury,
  • Jak atakujący wykorzystał dostęp do środowiska buildowego

  • przeszukiwał /proc/self/environ i pliki konfiguracyjne runnera w poszukiwaniu zmiennych z tokenami (CI_JOB_TOKEN, klucze do registry, credy do chmury),
  • serializował je do prostego formatu tekstowego, szyfrował lekką biblioteką z prekompilowanej binarki,
  • wysyłał paczki danych metodą HTTP POST na niepozorną domenę podszywającą się pod usługę telemetryczną.

Dodatkowo obraz zawierał mały binarny klient CLI do wybranego providera chmurowego. W pipeline nie był wywoływany wprost – uruchamiał się tylko wtedy, gdy wykrył obecność specyficznych zmiennych środowiskowych (np. prefiksu AWS_ lub AZURE_). Na tej podstawie enumerował:

  • listę registry kontenerowych,
  • repozytoria kodu skojarzone z kontem technicznym,
  • nazwy klastrów Kubernetes i powiązane role IAM.

Cała ta aktywność odbywała się w tle, po zakończonym kroku deploymentu. Z punktu widzenia logów pipeline’u wszystko kończyło się statusem „success”. Jedynym śladem były kilkusekundowe opóźnienia i zwiększone użycie sieci na runnerze.

Konsekwencje: cicha eskalacja poza CI/CD

Zebrane poświadczenia umożliwiły atakującemu wykonanie kilku kroków poza samym środowiskiem CI/CD. Najpierw zdalnie logował się do registry kontenerowego, wykorzystując wykradzione tokeny:

  • pobierał obrazy należące do krytycznych usług,
  • analizował ich zawartość pod kątem dodatkowych credów (np. wbudowane pliki konfiguracyjne z testów integracyjnych),
  • sprawdzał, które obrazy są używane szeroko – w wielu serwisach i środowiskach.

W następnym kroku spróbował wstrzyknąć złośliwe warstwy do jednego z często wykorzystywanych obrazów bazowych aplikacji. W tym wypadku ochroniła firmę polityka immutability obrazów (brak możliwości nadpisania taga pushowanego z CI) oraz podpisywanie artefaktów – zmodyfikowany obraz nie przechodził walidacji w czasie deploymentu.

Udało mu się natomiast zreplikować niektóre obrazy do własnej infrastruktury, co w praktyce oznaczało wyciek części kodu i konfiguracji. Dla wielu organizacji to bardziej realny problem niż sama dostępność – w obrazach często znajdują się konfiguracje dla partnerów, nazwy wewnętrznych usług, a czasem nawet fragmenty danych testowych.

Dlaczego klasyczne skanery obrazów nie wystarczyły

Po pierwszej fali podejrzeń devsecops sięgnął po standardowe skanery obrazów pod kątem CVE i malware. Wynik: kilka średniej wagi luk w bibliotekach systemowych, ale żadnych jednoznacznych sygnałów o backdoorze. Powody były dość typowe:

  • złośliwy kod był schowany w zwykłych skryptach bash, a nie w popularnych rodzinach malware,
  • binarka odpowiedzialna za szyfrowanie i wysyłkę danych miała neutralną nazwę i została skompilowana statycznie,
  • ruch wychodzący szedł na domenę, która naprawdę hostowała prostą usługę telemetryczną – atakujący wynajął jej subdomenę.

Skanery signature-based z natury mają ograniczony wgląd w logikę skryptów. Dodatkowo analiza dynamiczna obrazów na etapie builda bywa pomijana – pipeline musi być szybki, a narzędzia, które „odpalają” kontener w sandboxie, często uznaje się za zbyt ciężkie. Tę lukę wykorzystał atakujący.

Jak można było zatrzymać atak wcześniej

Zespół przeanalizował łańcuch zdarzeń „wstecz” i wyłapał kilka miejsc, w których incydent dałoby się przerwać. Nie wszystkie wymagają drogich narzędzi – część to czysta higiena procesowa.

  • Weryfikacja pochodzenia obrazów pomocniczych. „Deploy-helper” pochodził z konta autora z jednym obrazem i kilkoma pobraniami, bez linków do kodu źródłowego. Prosty proces akceptacji nowych obrazów do pipeline’u (np. krótki peer review i checklista) prawdopodobnie zatrzymałby go na wejściu.
  • Ograniczenie zakresu tokenów CI/CD. Runner używał stałego tokenu z szerokimi uprawnieniami do listowania i pobierania obrazów z wszystkich repozytoriów. Zastosowanie osobnych, scoped tokenów per projekt ograniczyłoby zasięg eksfiltracji.
  • Segmentacja sieci dla runnerów. Kontenery z pipeline’u miały pełny dostęp do internetu bez wyraźnych reguł. Blokada ruchu do nieznanych domen i dopuszczanie tylko listy „znanych dobrych” (registry, Git, chmura) znacznie utrudniłaby komunikację z serwerem C2.
  • Telemetria z poziomu kontenera. Nawet prosty agent zbierający listę procesów i destynacji sieciowych dla kontenerów CI/CD pokazałby, że w kroku deploymentu pojawiają się dodatkowe procesy i połączenia niepasujące do zadania.

Po wdrożeniu tych zmian kolejne obrazy pomocnicze są dodawane dużo ostrożniej. DevOpsi mieli obawy, że tempo pracy dramatycznie spadnie, ale w praktyce większość „trusted images” to kilka zweryfikowanych baz, z których buduje się swoje warianty, zamiast ściągania przypadkowych gotowców.

Studium przypadku 2: oficjalnie wyglądający obraz bazy danych z niespodzianką

Pozornie bezpieczny wybór z publicznego registry

Inny incydent dotyczył zespołu, który migrował kilka wewnętrznych usług na kontenery. Do środowiska testowego potrzebowali relacyjnej bazy danych. Zamiast korzystać z popularnego, oficjalnego obrazu, wybrali „lżejszy” wariant utrzymywany przez społeczność – mniejszy rozmiar, ciekawy opis, deklarowana optymalizacja pod CI.

Obraz był dobrze wypozycjonowany w wynikach wyszukiwania, miał sporo pobrań i kilka gwiazdek. Nazwa przestrzeni w registry różniła się od oficjalnej o jeden znak. Mało kto to zauważa, zwłaszcza pod presją czasu, gdy celem jest „żeby wreszcie wstał testowy stack”.

Ukryty mechanizm replikacji i wycieku danych

Na pierwszy rzut oka baza działała poprawnie. Testy przechodziły, migracje schematów również. Dopiero po pewnym czasie monitoring zwrócił uwagę na dość regularny, niewielki ruch TCP z portów innych niż standardowy port bazy. Analiza obrazu ujawniła parę ciekawych elementów:

  • dodatkowy serwis uruchamiany w supervisord obok właściwej bazy,
  • skrypt init.d, który po utworzeniu pierwszej tabeli w bazie włączał mechanizm „replikacji”,
  • część konfiguracji „replikacyjnej” wstrzykniętą przez zmienne środowiskowe, ale z domyślną, zaszytą w obrazie destynacją.

Ten „mechanizm replikacji” w praktyce:

  • podsłuchiwał zapytania SQL oraz odpowiedzi bazy za pomocą rozszerzenia na poziomie serwera,
  • anonimizował wybrane kolumny (np. maskował część adresu e-mail) – co z perspektywy testowego środowiska wyglądało „rozsądnie”,
  • od czasu do czasu wysyłał zrzuty wybranych tabel na zewnętrzny host, przez zaszyfrowany tunel.

Ruch był dość sprytnie ukryty: tunel korzystał z portów i protokołów, które w organizacji były otwarte dla narzędzi monitorujących. Dodatkowo paczki miały rozmiary przypominające typową telemetrię – brak wyraźnych „pików” utrudniał wyłapanie anomalii.

Ryzyko przeniesienia złośliwego obrazu z testów do produkcji

W tym konkretnym przypadku zrzuty danych obejmowały głównie środowisko testowe. W wielu firmach to i tak bolesny scenariusz, bo w testach bywają anonimizowane, ale prawdziwe dane. Prawdziwy problem polega jednak na tym, że raz zaakceptowany obraz testowy ma tendencję do „awansu”:

  • ktoś proponuje jego użycie w środowisku UAT,
  • po kilku sprintach trafia jako „tymczasowy” również do produkcji,
  • „tymczasowe” rozwiązania żyją później latami.

Gdyby ten dokładny obraz doczekał się takiej ścieżki, exfiltracja objęłaby w końcu realne dane klientów. Co gorsza, osadzony w obrazie kod nie wymagał żadnych zmian – aktywował się automatycznie, w zależności od wykrytych nazw tabel czy schematów (np. users, orders, payments).

Jak odróżnić „prawie oficjalny” obraz od faktycznie zaufanego

Po incydencie zespół stworzył prostą listę kryteriów dla obrazów, które mogą pełnić rolę krytycznych komponentów (baza danych, broker wiadomości, cache). Obraz nie przechodzi do katalogu „zaufanych”, jeśli nie spełnia większości z nich:

  • pochodzi z organizacji, która ma czytelny profil – stronę www, repozytoria kodu, historię aktywności,
  • ma wyraźnie wskazany kod źródłowy, najlepiej z procesem builda opisanym w Dockerfile’u,
  • warstwy obrazu nie zawierają „niespodzianek” – dodatkowych serwisów, supervise-rów, agentów,
  • nazwa przestrzeni (namespace) nie podszywa się pod oficjalnego dostawcę, a tagi są sensownie wersjonowane,
  • obraz przechodzi manualny przegląd przynajmniej raz, zanim trafi do katalogu.

Dla krytycznych komponentów dołożono też proste reguły runtime’owe. Baza danych z założenia nie powinna inicjować aktywnych połączeń wychodzących poza konkretnymi wyjątkami (np. monitoring, backup). Każde inne połączenie jest traktowane jak potencjalny sygnał alarmowy.

Praktyczne zasady budowania zaufanego łańcucha obrazów

Ograniczenie powierzchni: mniej obrazów, za to kontrolowanych

Najczęściej spotykany wzorzec w organizacjach po pierwszym incydencie to „odchudzanie” katalogu używanych obrazów. Zamiast:

  • setek różnych obrazów z Docker Huba, GitHuba i prywatnych kont,
  • różnych wariantów tego samego narzędzia (np. kilka wersji node, python, „light”, „slim”, „alpine”),
  • ad-hoc ściąganych obrazów przez developerów podczas debugowania,

zespół przechodzi na model:

  • kilkunastu–kilkudziesięciu bazowych obrazów w prywatnym registry,
  • własnych „wrapperów” na popularne narzędzia (lint, test, build),
  • jasnej procedury dodania nowego obrazu do katalogu „approved”.

Taki katalog nie musi być od razu formalnym „golden image programem”. Często wystarczy jedna grupa odpowiedzialna (np. platform team) i prosty proces: zgłoszenie potrzeby, wstępny threat modeling, szybka analiza Dockerfile’a, statyczne skanowanie i minimalny test runtime’owy.

Budowanie własnych obrazów z oficjalnych baz

Dobrym kompromisem między wygodą a bezpieczeństwem jest traktowanie oficjalnych obrazów jako „surowca”, a nie gotowego produktu. Zamiast:

image: randomuser/some-linter:latest

lepiej utrzymywać własny Dockerfile:

FROM golang:1.22-alpine AS builder
RUN go install github.com/org/some-linter@vX.Y.Z

FROM alpine:3.19
COPY --from=builder /go/bin/some-linter /usr/local/bin/some-linter
ENTRYPOINT ["some-linter"]

Taki model daje kilka korzyści:

  • pełną widoczność, co ląduje w obrazie,
  • łatwą kontrolę wersji – zmiana tagu golang czy alpine jest świadomą decyzją,
  • możliwość włączenia własnych skanerów i testów na etapie builda,
  • brak zależności od prywatnych kont użytkowników w publicznym registry.

Dla niektórych narzędzi oznacza to odrobinę więcej pracy na początku, ale w dłuższej perspektywie upraszcza utrzymanie. Zamiast szukać „magicznych” gotowców, zespół wie, że wszystko pochodzi z jednego, powtarzalnego procesu builda.

Wymuszanie polityk obrazu na poziomie klastra

Nawet najlepiej zorganizowane registry nie pomoże, jeśli dowolny developer może w Deployment wpisać obraz z publicznego źródła. W kontenerowych środowiskach produkcyjnych coraz częściej wykorzystuje się mechanizmy:

  • Admission control (np. Gatekeeper/OPA, Kyverno) – polityki blokujące deploy obrazów spoza dozwolonych registry lub bez odpowiednich adnotacji,
  • wymóg podpisu obrazu (cosign, Notation) – klaster przyjmuje tylko obrazy podpisane przez określony klucz,
  • podział namespace’ów – „piaskownice” do eksperymentów z luźniejszymi zasadami oraz namespace’y produkcyjne z twardymi ograniczeniami.

Najczęściej zadawane pytania (FAQ)

Czy obrazy z publicznych registry Docker Hub/GitHub są bezpieczne?

Obrazy z publicznych registry nie są z definicji ani bezpieczne, ani niebezpieczne – problem w tym, że bardzo często nikt ich realnie nie weryfikuje. Każdy może opublikować obraz, a podstawowe skanowanie platformy wychwytuje głównie znane podatności, niekoniecznie złośliwy kod czy backdoory.

Bezpieczne stają się dopiero te obrazy, które samodzielnie sprawdzisz, zreprodukujesz proces ich budowania i wprowadzisz do własnego, kontrolowanego registry. Ślepe zaufanie do pierwszego wyniku w wyszukiwarce Docker Hub to najszybsza droga do wciągnięcia malware do CI/CD lub klastra produkcyjnego.

Jak rozpoznać, że obraz kontenera może zawierać malware?

Pierwsze sygnały ostrzegawcze to: anonimowy lub mało znany autor, brak sensownych metadanych (labels, odwołania do repozytorium kodu), bardzo rzadkie aktualizacje albo przeciwnie – dziwnie częste zmiany bez changeloga, oraz nazwa łudząco podobna do oficjalnego obrazu (np. „nginx-official”, inna organizacja o bardzo podobnej nazwie).

Kolejny krok to techniczna weryfikacja: przejrzenie Dockerfile (jeśli jest dostępny), analiza warstw obrazu, skan podatności i malware, testowe uruchomienie w izolowanym środowisku z monitoringiem ruchu sieciowego i zużycia zasobów. Jeśli po starcie kontenera pojawia się nieuzasadniony outbound do obcych adresów lub nagłe zużycie CPU, to silny sygnał, że w środku dzieje się coś podejrzanego.

Jak bezpiecznie korzystać z publicznych obrazów w Docker Hub i innych registry?

Najbezpieczniejszy model to traktowanie publicznego registry tylko jako źródła „surowca”, który najpierw przechodzi przez Twoje procesy: skanowanie, budowanie, testy i dopiero potem trafia do prywatnego registry. Klaster produkcyjny powinien pobierać obrazy wyłącznie z tego zaufanego, wewnętrznego źródła.

W praktyce pomaga kilka prostych zasad:

  • korzystaj z oficjalnych obrazów i zweryfikowanych organizacji (np. „library/nginx”, a nie przypadkowy użytkownik),
  • zamrażaj konkretne tagi (np. node:18-alpine), a nie latest,
  • włącz automatyczne skanowanie obrazów w pipeline CI/CD,
  • twórz własne „golden images” jako bazę dla wszystkich projektów.

Jakie są najczęstsze typy ataków z użyciem złośliwych obrazów kontenerów?

Najczęstsze scenariusze to:

  • cryptomining – obraz po uruchomieniu dociąga i uruchamia koparkę kryptowalut, zjadając CPU i pamięć w klastrze lub chmurze,
  • kradzież sekretów – skrypty w kontenerze zbierają zmienne środowiskowe, pliki z kluczami i tokenami, a następnie wysyłają je na zewnętrzny serwer,
  • backdoor – dodatkowa warstwa w obrazie otwiera ukryte porty, dodaje konta albo instaluje narzędzia pozwalające na późniejszy zdalny dostęp.

Często takie ataki pozostają długo niewidoczne, bo kontener nominalnie „robi to, co trzeba” (np. buduje projekt w CI), a szkodliwe funkcje są sprytnie ukryte w tle i ujawniają się dopiero przy analizie logów lub nienaturalnym zużyciu zasobów.

Jak minimalna zmiana nazwy obrazu może prowadzić do ataku?

W praktyce wystarczy literówka lub inny namespace, by podszyć się pod znany projekt. Zamiast oficjalnego node ktoś uruchamia nodes albo obraz z organizacji o nazwie różniącej się jedną literą. W pośpiechu – szczególnie kopiując przykład z bloga czy Stack Overflow – bardzo łatwo tego nie zauważyć.

Gdy taki obraz trafi do Dockerfile, plików Helm, manifestów Kubernetes czy Terraform, jest dalej powielany przez kolejne serwisy i środowiska. Atakujący inwestuje w jeden „dobrze wyglądający” złośliwy obraz, a potem korzysta z tego, że kod infrastruktury jest kopiowany i używany latami bez przeglądu pod kątem bezpieczeństwa.

Co mogą zrobić zespoły DevOps/Dev, gdy są pod presją czasu, a chcą uniknąć złośliwych obrazów?

Kluczem jest wprowadzenie kilku lekkich nawyków i automatyzacji, które nie zabijają tempa pracy. Zamiast za każdym razem „brać pierwszy lepszy obraz z internetu”, opłaca się utrzymywać wewnętrzny katalog rekomendowanych base images, zatwierdzonych przez bezpieczeństwo czy platform team.

Dobrze działa też:

  • szablon Dockerfile/Helm z już ustawionymi zaufanymi obrazami,
  • automatyczny gate w CI/CD: build przechodzi dalej tylko, jeśli obraz pochodzi z określonej listy registry/namespace,
  • proste checklisty do code review: „czy obraz jest oficjalny?”, „czy znamy źródło repozytorium?”, „czy jest skan podatności?”.

Dzięki temu deweloper nie musi być ekspertem od bezpieczeństwa – wystarczy, że trzyma się kilku jasnych reguł, a resztę przejmuje pipeline i narzędzia.

Czy prywatne registry (ECR, ACR, Harbor) całkowicie rozwiązują problem malware w obrazach?

Prywatne registry znacząco zmniejsza ryzyko, ale go nie eliminuje. Jeśli do środka bez żadnej kontroli wciągniesz obraz z publicznego źródła, to złośliwa warstwa po prostu zmieni adres przechowywania – wciąż będzie tak samo groźna.

Prywatne registry staje się realnie „bezpieczną bramą” dopiero wtedy, gdy:

  • masz polityki, które blokują push niezeskanowanych obrazów,
  • ograniczasz, kto może publikować obrazy i z jakich pipeline’ów,
  • regularnie skanujesz istniejące obrazy i usuwasz przestarzałe lub podejrzane.

To dobre narzędzie, ale wymaga świadomej konfiguracji i powiązania z procesami DevSecOps – samo w sobie nie zatrzyma złośliwego obrazu, jeśli ktoś wcześniej bezrefleksyjnie go zaakceptuje.