Atak na łańcuch dostaw w IT: ryzyko bibliotek, kontenerów i zależności w CI CD

0
23
Rate this post

Nawigacja:

Czym jest atak na łańcuch dostaw w IT – bez uproszczeń

Pojęcie łańcucha dostaw oprogramowania od kodu do produkcji

Łańcuch dostaw oprogramowania to cały zestaw kroków, narzędzi, ludzi i komponentów, które uczestniczą w powstaniu i wdrożeniu aplikacji: od pierwszego commita w repozytorium, przez system kontroli wersji, biblioteki open source, kompilatory, pipeline CI/CD, rejestry artefaktów i kontenerów, aż po serwery produkcyjne, na których kod faktycznie działa. Każdy z tych elementów może być punktem wejścia dla atakującego.

Kluczowe jest to, że atak na łańcuch dostaw oprogramowania zwykle nie polega na bezpośrednim ataku na ostateczny system ofiary (np. aplikację w produkcji), tylko na jednym z etapów pośrednich: złośliwej bibliotece, skompromitowanym obrazie kontenera, zainfekowanym narzędziu CI/CD czy kompromitacji repozytorium kodu. Z punktu widzenia bezpieczeństwa oznacza to, że nawet jeśli aplikacja na produkcji wydaje się poprawnie zabezpieczona, można ją „otruć” znacznie wcześniej, bez wywoływania alarmów w klasycznych systemach monitoringu.

Łańcuch dostaw obejmuje zwykle komponenty z wielu organizacji: wewnętrzne repozytoria kodu, zewnętrzne rejestry kontenerów, publiczne repozytoria pakietów (npm, PyPI, Maven Central, RubyGems), systemy CI/CD (Jenkins, GitHub Actions, GitLab CI, Azure DevOps, CircleCI i inne), a także narzędzia developerskie (IDE, pluginy, lokalne skrypty). Każdy z tych elementów ma własną powierzchnię ataku, inny model zaufania i inny poziom dojrzałości bezpieczeństwa.

Różnica między klasycznym włamaniem a kompromitacją komponentu pośredniego

Klasyczny scenariusz włamania to bezpośrednie uderzenie w cel: atak na publiczne API, exploity w aplikacji webowej, brute-force na loginy administratorów czy phishing na użytkowników końcowych. Atak na łańcuch dostaw oprogramowania jest bardziej pośredni: celem staje się komponent, który sam w sobie często nie jest „krytyczny” ani widoczny dla biznesu, ale ma wpływ na setki lub tysiące instalacji systemu docelowego.

Przykładowo, zamiast atakować bezpośrednio aplikację banku, napastnik próbuje skompromitować bibliotekę używaną w wielu projektach tego banku (np. paczkę npm lub bibliotekę Javy), albo infrastrukturę CI/CD, przez którą przechodzą wszystkie buildy i deploymenty. W efekcie złośliwy kod trafia do produktu w sposób „legalny” – jako część normalnego procesu dostarczania oprogramowania.

Różnica jest istotna również z punktu widzenia wykrycia: klasyczny atak generuje często nienaturalne wzorce ruchu (skanowania, próby logowania, nietypowe zapytania HTTP). Kompromitacja komponentu pośredniego zwykle wygląda jak „normalne” działanie: pobranie pakietu z rejestru, uruchomienie builda, wykonanie testów, deployment nowej wersji. Systemy bezpieczeństwa skoncentrowane na produkcji mogą takiego zdarzenia nawet nie zauważyć.

Głośne incydenty bez sensacji: SolarWinds, repozytoria pakietów

Przykład SolarWinds pokazał, że atak na łańcuch dostaw może trwać miesiącami, pozostać niewykryty i dotknąć setek organizacji. Atakujący uzyskali dostęp do infrastruktury buildowej, modyfikując proces kompilacji oprogramowania Orion. Zainfekowane binaria były następnie podpisywane zaufanym certyfikatem SolarWinds i dystrybuowane jako legalne aktualizacje. Dla klientów wszystko wyglądało absolutnie normalnie: nowa wersja, podpis cyfrowy, oficjalny kanał dystrybucji.

Repozytoria pakietów open source od lat są wykorzystywane jako wektor ataku. Ataki typu typosquatting (np. publikowanie paczki „expresss” zamiast „express” w npm) czy dependency confusion (udostępnienie w publicznym repozytorium paczki o tej samej nazwie, jak w prywatnym rejestrze firmy, ale z wyższym numerem wersji) prowadzą do sytuacji, w której narzędzia budujące automatycznie wybierają złośliwy pakiet. Bez żadnego „włamania” do firmy – całość dzieje się w ramach standardowego mechanizmu rozwiązywania zależności.

Warto zauważyć, że część głośnych incydentów nie wynikała z genialnych technik atakujących, ale z bardzo podstawowych błędów po stronie ofiar: brak kontroli, skąd pipeline pobiera paczki; działanie na domyślnych ustawieniach menedżerów pakietów; brak walidacji, że budowany artefakt faktycznie pochodzi ze „spodziewanego” miejsca.

Dlaczego ataki na łańcuch dostaw są trudne do wykrycia

Podstawowy problem polega na tym, że atak na łańcuch dostaw oprogramowania najczęściej „udaje normalność”. Build przeszedł? Testy przeszły? Deploy działa? Monitoring nie wykazuje oczywistych anomalii? Z perspektywy standardowych wskaźników wszystko wygląda poprawnie. Złośliwy komponent może być zaprojektowany tak, by uaktywnić się tylko w określonych warunkach (np. po stronie konkretnego klienta, w określonej konfiguracji, przy spełnieniu specyficznych zmiennych środowiskowych).

Drugi aspekt to złożoność i głębokość zależności. Jeśli aplikacja korzysta z kilkuset paczek, a każda z nich z kolejnych kilkudziesięciu, śledzenie pełnej ścieżki odpowiedzialności jest w praktyce trudne. Informacja o tym, że złośliwy kod pochodzi z pakietu czwartego rzędu zależności, może pojawić się dopiero po dogłębnym audycie, często już po szkodzie.

Trzeci element to psychologia zaufania. Gdy zespół pracuje od lat z określonym zestawem narzędzi, rejestrów i bibliotek, powstaje silna iluzja bezpieczeństwa przez przyzwyczajenie. Jeżeli coś działało poprawnie przez długi czas, mało kto zakłada, że nagle mogło zostać złośliwie zmodyfikowane – szczególnie gdy zmiana przyszła w formie „niewinnej aktualizacji minorowej”.

Główne wektory ataku: biblioteki, kontenery, pipeline’y CI/CD

Biblioteki open source jako niezbędny, ale wrażliwy element

Zależności open source są dziś podstawą niemal każdego projektu. W typowej aplikacji biznesowej udział „własnego” kodu bywa mniejszy niż 20–30% – reszta to biblioteki, frameworki, sterowniki, pluginy. Im więcej gotowych komponentów, tym szybszy rozwój i niższy koszt, ale też większe pole ataku. Tę wymianę wygody na ryzyko widać bardzo wyraźnie przy analizie nowoczesnych stosów technologicznych.

Atakujący chętnie celują w biblioteki, bo raz wstrzyknięty złośliwy kod może trafić do wielu odbiorców: do każdej aplikacji, która tę bibliotekę pobierze i zbuduje. Jeżeli biblioteka ma wysoką popularność, a dodatkowo jest używana w newralgicznych sektorach (finanse, medycyna, administracja publiczna), skala potencjalnego zasięgu staje się atrakcyjna. Część ataków polega na przejęciu konta maintainerów lub repozytorium, a część na podszywaniu się pod istniejące pakiety.

Sama otwartość kodu nie rozwiązuje problemu. Fakt, że kod jest publicznie dostępny, nie oznacza automatycznie, że ktoś go regularnie i dokładnie przegląda. W wielu projektach liczba aktywnych maintainerów jest bardzo mała, a zaufanie społeczności opiera się bardziej na historii projektu i reputacji niż na ciągłej, formalnej weryfikacji bezpieczeństwa.

Obrazy kontenerów i rejestry jako niedoceniana powierzchnia ataku

Obrazy kontenerów stały się standardem dystrybucji oprogramowania. Niestety, wraz z popularyzacją Dockera i Kubernetes wiele zespołów zaczęło traktować obrazy jak „czarne skrzynki”. Pobierają obraz z Docker Huba, prywatnego rejestru lub jakiegoś tutoriala z internetu, sprawdzają, że aplikacja się uruchamia – i na tym kończy się weryfikacja. To prosty sposób, aby wprowadzić do środowiska produkcyjnego stare, podatne lub wręcz złośliwe komponenty.

Rejestry kontenerów (Docker Hub, GitHub Container Registry, GitLab Registry, ECR, GCR i inne) to odpowiednik repozytoriów pakietów. Jeżeli obraz nie jest zaufany, nie ma zweryfikowanego podpisu lub pochodzi z losowego konta, wgramy na serwer coś, co ma dostęp do sieci, do pamięci, do logów – i nie mamy pewności, co tam naprawdę działa. Zdarzały się przypadki obrazów zawierających ukryte koparki kryptowalut, backdoory SSH czy skrypty exfiltrujące dane.

W przeciwieństwie do paczek npm czy PyPI, obrazy kontenerów mogą zawierać cały system operacyjny z serwisami, demonami, narzędziami sieciowymi i debuggerami. Każdy taki komponent to potencjalna podatność. Obraz, który jest „wypchany” wszystkim możliwym oprogramowaniem, jest wygodny dla developera, ale stanowi idealny cel dla atakującego szukającego podatnego daemona lub starej biblioteki systemowej.

CI/CD, agenty i pluginy jako brama do wszystkiego

Systemy CI/CD (Jenkins, GitLab CI, GitHub Actions, Bamboo, TeamCity itd.) mają dostęp do tego, co najcenniejsze: kodu źródłowego, kluczy do repozytoriów, sekretów środowisk, kluczy do chmur, uprawnień do deploymentów. Kompromitacja serwera CI lub jego agenta zwykle otwiera drogę do pełnego przejęcia środowisk – od testowych po produkcyjne.

Do tego dochodzą pluginy i akcje CI/CD. Przykładowo, w GitHub Actions popularne są gotowe akcje tworzone przez społeczność. Wiele z nich jest świetnych, ale część jest utrzymywana okazjonalnie lub przez pojedyncze osoby. Jeżeli pipeline korzysta z zewnętrznej akcji z niezweryfikowanego źródła, wykonywany jest obcy kod z pełnymi uprawnieniami kontekstu CI. Taka akcja może zainstalować malware, wysłać sekrety do zewnętrznego serwera lub zmodyfikować artefakt przed publikacją.

Kompromitacja agenta CI jest szczególnie groźna w środowiskach, gdzie agent ma dostęp do wielu projektów jednocześnie, działa z uprawnieniami roota, a izolacja między jobami jest symboliczna. W takiej sytuacji przejęcie jednego pipeline’u może prowadzić do lateral movement w całej infrastrukturze developerskiej, w tym do rejestrów kontenerów, serwerów testowych i deploymentów automatycznych.

Scenariusze łączone: zła biblioteka w obrazie, obraz w pipeline, pipeline na produkcji

Atakujący rzadko ogranicza się do jednego wektora. Bardziej typowy jest scenariusz, w którym kilka elementów łańcucha dostaw współdziała, aby zbudować trwały, trudny do usunięcia przyczółek.

Przykładowy scenariusz:

  • Złośliwy pakiet npm jest publikowany pod nazwą bardzo podobną do popularnej biblioteki (typosquatting).
  • Programista przez pomyłkę wpisuje nazwę z literówką w package.json. Build przechodzi, pipeline CI instaluje dependency bez ostrzeżeń.
  • Złośliwy pakiet dodaje do obrazu kontenera dodatkowy skrypt uruchamiany w czasie startu aplikacji, który komunikuje się z serwerem atakującego.
  • Obraz jest budowany automatycznie i pushowany do prywatnego rejestru kontenerów, a następnie automatycznie wdrażany na produkcję w Kubernetes.
  • Złośliwy kod eskaluje swoje uprawnienia w klastrze (np. poprzez źle ustawione Role/ClusterRole lub uprawnienia kontenera) i rozprzestrzenia się na inne serwisy.

Na każdym etapie wszystko wygląda „normalnie”: developer wykonał commit, CI wykonało build, obraz został zdeployowany przez ArgoCD lub inny operator GitOps. Żaden element sam w sobie nie musi wyglądać podejrzanie. Podejrzenia pojawiają się dopiero, gdy ktoś zauważy np. nietypowy ruch z kontenerów do zewnętrznego serwera – a wtedy łańcuch przyczyn może sięgać wielu tygodni wstecz.

Haker w kapturze pracuje przy laptopie w jasnym pomieszczeniu
Źródło: Pexels | Autor: Nikita Belokhonov

Zależności i biblioteki: jak rośnie ryzyko wraz z wygodą

Im szybciej instalujemy dependency, tym rzadziej je weryfikujemy

Kultura szybkiego prototypowania i gotowych snippetów z Stack Overflow sprzyja bezrefleksyjnemu instalowaniu paczek. Polecenia typu npm install cokolwiek, pip install coś czy go get github.com/... stały się odruchem. Na etapie „zróbmy, żeby działało” nikt zwykle nie zastanawia się, kto maintenuje daną paczkę, czy ma historię incydentów bezpieczeństwa, czy w ogóle jest potrzebna.

Ten wzorzec „najpierw instaluj, potem myśl” jest zrozumiały z perspektywy presji czasu. Problem pojawia się, gdy taki prototyp zaczyna żyć własnym życiem i staje się fundamentem kodu produkcyjnego. Paczki zainstalowane „na próbę” zostają w projekcie, lockfile utrwala konkretną wersję, a wraz z nią potencjalne podatności czy złośliwe funkcje.

W praktyce rzadko kto robi systematyczny przegląd zależności, które weszły na etapie eksperymentów. Z biegiem czasu dochodzi do klasycznej „erozji świadomości”: nikt nie pamięta, po co dana biblioteka została dodana, nikt nie jest w stanie szybko ocenić, czy można ją usunąć, więc zostaje. Im starszy projekt, tym trudniej przeprowadzić gruntowny refactoring zależności.

Ataki typosquatting, dependency confusion i złośliwe aktualizacje

Ataki na łańcuch dostaw bibliotek open source mają kilka powtarzalnych schematów:

  • Typosquatting – publikacja pakietu o nazwie bardzo podobnej do popularnej biblioteki (np. „react-domm”, „requests” zamiast „requests” dla Pythona, z innym znakiem w środku dla Maven). Różnica może być na tyle subtelna, że nie widać jej przy pobieżnym spojrzeniu. Menedżer pakietów instaluje to, co podano w pliku konfiguracyjnym, bez refleksji.
  • Łańcuch zależności transitivnych – problem, którego nikt świadomie nie wybiera

    Bezpośrednie zależności to ta część ryzyka, którą jeszcze da się mentalnie ogarnąć: ktoś dodał wpis do package.json, requirements.txt czy pom.xml, można go wskazać palcem w review. Prawdziwym problemem są zależności transitivne – wszystko to, co zostało wciągnięte do projektu „przy okazji”, bo jakaś biblioteka potrzebuje kolejnych bibliotek, a te następnych.

    W złożonych projektach drzewo zależności rośnie do setek, czasem tysięcy pakietów. Przy takiej skali nikt nie jest w stanie ręcznie ocenić ryzyka każdego komponentu. Menedżer pakietów rozwiązuje wersje wg własnych zasad (semver, lockfile, constraints), a rezultat jest akceptowany jako „dany”. To klasyczna iluzja kontroli: świadomie dodano 20 paczek, a realnie używa się 10 razy tyle.

    W praktyce główne problemy z transitivnymi zależnościami to:

  • Brak świadomości istnienia – wielu inżynierów nie potrafi odpowiedzieć, które biblioteki transitivne trafiają do artefaktu produkcyjnego ani jakie mają licencje.
  • Trudność w aktualizacji – zależności transitivne często są „przyspawane” przez constrainty w kilku miejscach jednocześnie; aktualizacja jednej paczki potrafi rozsadzić pół ekosystemu.
  • Efekt domina podatności – pojedyncza podatna biblioteka transitivna może trafić do kilkunastu usług w monorepo, a każde z tych miejsc wymaga osobnej akcji naprawczej.

Bez automatycznego generowania SBOM (Software Bill of Materials) i systematycznego skanowania SCA (Software Composition Analysis) trudno nawet odpowiedzieć na podstawowe pytanie: czy ta konkretna podatność dotyczy naszego kodu? Reakcją bywa albo paraliż („nie wiemy, więc nic nie robimy”), albo nerwowe podnoszenie wszystkiego „na ślepo”, co generuje kolejny dług techniczny.

Refaktoryzacja zależności: kiedy mniej naprawdę znaczy więcej

Sam fakt, że biblioteka jest popularna, nie jest argumentem, by ją utrzymywać w projekcie bez końca. W dojrzałych zespołach pojawia się okresowa praktyka „odchudzania dependency tree” – przeglądania, które paczki faktycznie są używane, a które stanowią relikt dawnych eksperymentów.

Taki przegląd zwykle obejmuje kilka kroków:

  • Analizę użycia importów w kodzie (statycznie lub dynamicznie w czasie testów).
  • Identyfikację bibliotek, które można zastąpić wbudowanymi funkcjami języka lub prostym własnym kodem.
  • Weryfikację, czy te same funkcje nie są dostarczane przez kilka różnych paczek (np. kilka różnych wrapperów HTTP).

Refaktoryzacja zależności jest niewdzięczna, bo rzadko przynosi widoczne nowe funkcje. Z punktu widzenia ryzyka jest jednak jednym z najbardziej opłacalnych działań: każde usunięte dependency to:

  • mniej potencjalnych podatności,
  • mniej nieznanego kodu wykonywanego w runtime,
  • mniej pracy przy przyszłych aktualizacjach.

Nie chodzi o ideologiczną krucjatę przeciw bibliotekom, ale o świadomy balans. Jeżeli prosta funkcja formatująca datę wymaga wciągnięcia rozbudowanej paczki z własnym systemem pluginów, to ciężar ryzyka jest nieproporcjonalny do korzyści.

Kontenery i obrazy bazowe: z czego naprawdę składa się Twój runtime

„Oficjalny obraz” nie znaczy „bezpieczny i świeży”

Określenia w rodzaju „official image” czy „verified publisher” bywają odbierane jako nieformalny certyfikat bezpieczeństwa. Tymczasem w praktyce oznaczają raczej, że ktoś ma kontrolę nad repozytorium i spełnia minimalne kryteria platformy. Aktualność łatek bezpieczeństwa, model wydawniczy i proces review to już osobna historia.

Typowy błąd to przyjęcie założenia, że skoro obraz ma dobrą nazwę (np. node:latest, python:3.10), to można go bezrefleksyjnie używać. Problemów jest kilka:

  • latest jako ruletka – tag latest nie oznacza „najbezpieczniejszy”, tylko „domyślny”. Może się zmienić w dowolnym momencie, zrywając deterministyczność buildów.
  • Różny poziom dbałości między maintainerami – oficjalny obraz popularnego języka może być utrzymywany wzorowo, ale już obraz narzędzia niszowego – sporadycznie.
  • Dziedziczenie podatności – nawet solidnie utrzymany obraz bazowy może czasowo zawierać podatności w warstwie systemowej (glibc, openssl itd.), które trzeba śledzić i aktualizować.

Bez dedykowanego procesu przeglądu i skanowania obrazów „oficjalność” i „popularność” stają się jedynie zastępczymi wskaźnikami zaufania. Działają dopóki nic się nie stanie, a gdy dojdzie do incydentu, trudno wytłumaczyć, na czym konkretnie to zaufanie było oparte.

Warstwy obrazu i „ukryty bagaż” runtime’u

Każda warstwa w obrazie kontenera to snapshot systemu plików. Przy częstym korzystaniu z gotowych Dockerfile’i wciąga się do obrazu znacznie więcej narzędzi, niż jest faktycznie potrzebne do uruchomienia aplikacji. Kompilatory, debugery, klienty baz danych, narzędzia sieciowe – wszystko to zostaje w runtime, jeśli nie zastosuje się rozdzielenia faz build/run.

Z perspektywy atakującego to wygodne środowisko pracy. Jeżeli wewnątrz kontenera dostępne jest curl, wget, pełny bash, a do tego klienty do głównych usług w chmurze, wykonanie kolejnych kroków ataku jest prostsze. Każdy dodatkowy binarny komponent to:

  • kolejna potencjalna podatność,
  • kolejny wektor eskalacji (np. przez złośliwe pluginy do narzędzi),
  • kolejne narzędzie ułatwiające poruszanie się po środowisku.

Modelem, do którego sporo organizacji dąży, jest rozdzielenie:

  • obrazu buildowego – ciężkiego, z pełnym toolchainem, używanego wyłącznie w CI,
  • obrazu runtime – możliwie najlżejszego, zawierającego tylko to, co potrzebne do uruchomienia binarki.

Takie podejście nie usuwa ryzyka całkowicie, ale znacząco je ogranicza. Co istotne, upraszcza też analizę bezpieczeństwa: im mniejszy obraz, tym łatwiej go przeskanować, zrozumieć i utrzymać.

Obrazy „scratch”, distroless i minimalne dystrybucje – zalety i pułapki

Hasła w rodzaju „distroless” czy „scratch” są czasem traktowane jako magiczne rozwiązanie problemów bezpieczeństwa kontenerów. Rzeczywistość jest bardziej złożona. Minimalne obrazy faktycznie redukują powierzchnię ataku, ale:

  • utrudniają debugowanie – brak powłoki, brak standardowych narzędzi, wymagają innych nawyków operacyjnych,
  • przenoszą część ryzyka w inne miejsce – np. do procesu budowania statycznych binarek, linkowania, weryfikacji zależności,
  • wymagają lepszej obserwowalności – logowania, metryk, tracerów, bo „zajrzenie” do kontenera staje się trudniejsze.

Przejście na distroless ma sens, gdy zespół ma już:

  • dobrze poukładane logowanie i metryki,
  • proces budowania binarek możliwy do odtworzenia (reproducible builds, pinned toolchain),
  • praktyki debugowania oparte bardziej na telemetry niż na „ssh do kontenera i zobacz”.

W przeciwnym razie można osiągnąć efekt odwrotny do zamierzonego: środowisko będzie teoretycznie „bezpieczniejsze”, ale każda awaria skończy się improwizowanymi obejściami, np. przebudową obrazów na szybko z dodanym bash. Taki chaos zwykle prowadzi do osłabienia kontroli nad tym, co faktycznie trafia na produkcję.

Specjaliści cyberbezpieczeństwa przy komputerach w ciemnym pomieszczeniu
Źródło: Pexels | Autor: Tima Miroshnichenko

Infrastruktura CI/CD jako cel – od konfiguracji po pluginy

Konfiguracja pipeline’ów jako kod zaufany bardziej niż powinna

Pliki YAML czy definicje pipeline’ów w repozytorium są często traktowane jako „nudna infrastruktura”, której nikt nie recenzuje tak skrupulatnie jak kodu aplikacyjnego. To poważny błąd: pipeline decyduje, jaki kod jest wykonywany z jakimi uprawnieniami, gdzie trafiają artefakty, jakimi kluczami są podpisywane.

Typowe problemy w konfiguracjach CI/CD obejmują:

  • nadmierne uprawnienia jobów (pełne role w chmurze, dostęp do wszystkich sekretów),
  • brak separacji kontekstów dla gałęzi (PR z forka ma ten sam poziom dostępu co main),
  • sztywno wpisane klucze i hasła w konfiguracjach zamiast użycia dedykowanych mechanizmów secret managementu,
  • brak walidacji zmian w pipeline’ach – merge bez review lub review „na wiarę”.

Attack surface rośnie wraz z każdą nową funkcją dodawaną „na szybko”: dodatkowym krokiem deployu, nowym jobem do zarządzania infrastrukturą, kolejnym hookiem testowym. Gdy definicje pipeline’ów nie podlegają tym samym standardom przeglądu co kod, stają się cichym wektorem eskalacji.

Pluginy, akcje i rozszerzenia – kod z zewnątrz w najczulszym miejscu

Ekosystem pluginów i akcji CI/CD rozwiązuje realny problem – nikt nie ma czasu pisać od zera integracji do każdego narzędzia. Problem zaczyna się tam, gdzie do pipeline’ów trafiają komponenty, których nikt w organizacji nie przeanalizował choćby pobieżnie.

Kilka charakterystycznych pułapek:

  • użycie akcji z nieokreśloną wersją (np. @master, @main), co pozwala na „cichy” update bez kontroli,
  • pluginy utrzymywane przez pojedynczą osobę, bez transparentnego procesu release’ów,
  • brak lockfile/registry mirror dla rozszerzeń – pipeline za każdym razem pobiera to, co akurat jest w publicznym repozytorium.

Bez jasnej polityki – jakie pluginy są dozwolone, jak są weryfikowane, jak często audytowane – infrastruktura CI/CD staje się miejscem, gdzie „bierzemy co działa”. Na krótką metę przyspiesza to pracę. Na dłuższą tworzy środowisko, w którym atak typu „przejęcie maintainerów popularnej akcji” ma natychmiastowy wpływ na wiele pipeline’ów naraz.

Agenci CI jako newralgiczny element zaufania

To na agentach CI wykonywany jest kod, który:

  • buduje artefakty,
  • ma dostęp do sekretów,
  • często łączy się z infrastrukturą produkcyjną (deployment, migracje).

Jeżeli agent działa w trybie „pet servera” (jeden, długo żyjący host, ręcznie konfigurowany, z wieloma narzędziami), staje się single point of failure. Kompromitacja takiego hosta:

  • daje dostęp do cache’ów buildowych i artefaktów przed podpisaniem,
  • pozwala na modyfikację pipeline’ów i skryptów lokalnych,
  • umożliwia zbieranie sekretów w trakcie jobów (np. z env, zmiennych, plików konfiguracyjnych).

Bezpieczniejszy model opiera się na:

  • ephemeralnych agentach (krótkotrwałe instancje dla pojedynczych jobów lub niewielkich batchy),
  • ograniczeniu dostępu sieciowego (agenci „ciągną” artefakty i konfigurację z centralnego, ściśle kontrolowanego miejsca zamiast mieć otwarty dostęp „wszędzie”),
  • braku ręcznej konfiguracji – wszystko, co potrzebne, jest opisywane jako kod i rekonstruowane automatycznie.

To nie usuwa ryzyka całkowicie, ale znacznie utrudnia zbudowanie trwałego przyczółka przez atakującego. Każdy agent żyje krótko, ma wąsko zdefiniowaną rolę i zostawia ślad w logach infrastruktury.

Modele zaufania w łańcuchu dostaw – gdzie naprawdę „ufasz”

Zaufanie domyślne vs. zaufanie wypracowane

Większość zespołów funkcjonuje na nieformalnym modelu „zaufania domyślnego”: jeżeli coś jest w oficjalnym rejestrze, ma dużo gwiazdek na GitHubie i „wszyscy tego używają”, to jest traktowane jako bezpieczne. Ten model działa dopóki nie pojawi się incydent, a potem okazuje się, że nikt nie potrafi wskazać, kto właściwie co zatwierdził.

Przeciwnym biegunem jest zaufanie wypracowane: komponent zostaje włączony po przejściu określonego minimum weryfikacji – choćby prostego:

  • sprawdzenie historii projektu i częstotliwości releasów,
  • przegląd zgłoszeń bezpieczeństwa i reakcji maintainerów,
  • ocena poziomu aktywności community i liczby osób spoza jednej firmy utrzymującej projekt.

Ten drugi model bywa uznawany za „zbyt ciężki” dla małych zespołów, ale w praktyce da się go skalować. Niekoniecznie każdy komponent wymaga pełnego audytu; często wystarcza prosty podział na klasy ryzyka:

  • komponenty krytyczne – mające dostęp do sekretów, wykonujące kod, integrujące się z infrastrukturą (wymagają szczegółowego przeglądu),
  • Klasy ryzyka i poziomy kontroli

    Podział komponentów na klasy ryzyka jest bardziej użyteczny niż binarne „dopuszczony / zakazany”. Pozwala z góry ustalić, ile wysiłku inwestuje się w weryfikację. Praktyczny podział bywa trójstopniowy:

  • komponenty krytyczne – wykonywane w uprzywilejowanym kontekście, mają dostęp do sekretów lub infrastruktury (agent CI, plugin do deploymentu, operator Kubernetesa),
  • komponenty istotne – używane w ścieżkach produkcyjnych, ale bez bezpośredniego dostępu do sekretów czy zarządzania infrastrukturą (frameworki, biblioteki sieciowe, ORM-y),
  • komponenty pomocnicze – narzędzia developerskie, biblioteki do testów, formatery, lintery.

Dla każdej klasy można zdefiniować minimalny poziom kontroli. Przykładowo:

  • dla komponentów pomocniczych – wystarczy automatyczne skanowanie SBOM i alerty przy znanych podatnościach,
  • dla komponentów istotnych – dodatkowy przegląd przy dodaniu nowej zależności lub major upgradem,
  • dla komponentów krytycznych – formalne zatwierdzenie przez zespół bezpieczeństwa, pinned wersje, własne mirrory i okresowe audyty konfiguracji.

Regułą jest, że z czasem komponenty „awansują” w tej klasyfikacji. Biblioteka, która zaczyna jako narzędzie pomocnicze, z czasem ląduje w ścieżce produkcyjnej i staje się istotna. Bez przeglądu tego katalogu co pewien okres model zaufania rozjeżdża się z rzeczywistością.

Łańcuch sygnatur i pochodzenie artefaktów

Zaufanie do artefaktów rzadko da się oprzeć wyłącznie na recenzji kodu. W praktyce potrzebny jest łańcuch sygnatur: kto co zbudował, gdzie, kiedy, z jakich źródeł. Bez tego każde „to na pewno build z naszego CI” jest tylko domniemaniem.

Narzędzia w rodzaju podpisywanych artefaktów, attestations czy mechanizmów typu Sigstore próbują ten problem ograniczyć. Same z siebie nie rozwiązują jednak kluczowej kwestii: jak bardzo ufasz miejscu, w którym podpis powstał. Jeżeli:

  • klucze do podpisywania są przechowywane w tym samym środowisku co agenci CI,
  • pipeline ma możliwość wypchnięcia własnych binarek i ich natychmiastowego podpisania,
  • brakuje rozdzielenia ról między „budującym” a „podpisującym”,

to złośliwy build może zostać sygnowany tak samo „wiarygodnie” jak każdy inny. Sygnatura wtedy potwierdza jedynie, że coś przeszło przez ten sam zaufany, ale potencjalnie skompromitowany proces.

Stabilniejszy model opiera się na kilku zasadach:

  • klucz do podpisywania jest zarządzany poza CI (HSM, KMS),
  • podpis jest nadawany dopiero po przejściu określonych kontroli (testy, skany, weryfikacja SBOM),
  • informacja o środowisku buildowym (wersje narzędzi, konfiguracja) jest częścią metadanych podpisu.

Trzeba też zaakceptować, że łańcuch zaufania nie jest absolutny: ma sens tylko do poziomu, na którym jesteśmy w stanie weryfikować i monitorować infrastrukturę. Reszta to świadome ryzyko, a nie „magia podpisów”.

Interfejs komputera z danymi cyberbezpieczeństwa podczas analizy ryzyka
Źródło: Pexels | Autor: Tima Miroshnichenko

Minimalne standardy bezpieczeństwa dla bibliotek i zależności

Co w praktyce oznacza „minimalny standard”

Określenie „minimalne standardy bezpieczeństwa” bywa nadużywane. W kontekście bibliotek chodzi zwykle o zestaw wymagań, które da się utrzymać operacyjnie, zamiast idealnej listy życzeń. Typowy szkielet takiego standardu obejmuje:

  • kontrolę pochodzenia – skąd pobierana jest biblioteka, czy używany jest oficjalny rejestr, mirror, prywatne proxy,
  • pining wersji – brak zależności z „luźnymi” zakresami typu ^ czy ~ w krytycznych komponentach,
  • monitoring podatności – zautomatyzowane skanowanie lockfile / SBOM,
  • proces aktualizacji – jasne zasady, kiedy i jak bumpowane są wersje (w tym security patch releases),
  • rejestr wyjątków – lista bibliotek zaakceptowanych mimo znanych ryzyk, z uzasadnieniem i datą przeglądu.

Taki zestaw nie gwarantuje odporności na każdy atak, ale usuwa podstawowe „łatwe” ścieżki: przypadkowe wciągnięcie złośliwego pakietu, niekontrolowane aktualizacje z dependency hell, niezałatane CVE leżące miesiącami w produkcji.

Pochodzenie pakietów: oficjalny rejestr to nie wszystko

Przyjęło się, że skoro biblioteka jest w „oficjalnym” rejestrze (npm, PyPI, Maven Central, crates.io), to można ją traktować jako względnie bezpieczną. Z perspektywy łańcucha dostaw to mit. Publiczny rejestr jest jedynie punktem dystrybucji; kontrola tego, co do niego trafia, jest ograniczona.

Praktyczną obroną jest warstwa pośrednia:

  • wewnętrzny mirror lub proxy rejestru,
  • white-lista dopuszczonych nazw lub vendorowanie krytycznych zależności,
  • zakaz bezpośrednich połączeń z publicznymi rejestrami z produkcyjnych pipeline’ów.

Nawet prosty cache artefaktów w prywatnym rejestrze daje zysk: w razie kompromitacji publicznego ekosystemu zespół ma czas na reakcję, bo buildy nie pobierają „z automatu” najnowszych wersji z internetu.

Pinning wersji i kontrolowane aktualizacje

Luźne zakresy wersji są wygodne na etapie prototypu. Na produkcji pozwalają na ciche wprowadzenie złośliwej lub wadliwej wersji zależności bez żadnej zmiany w kodzie. Atak typu dependency confusion czy przejęcie maintainerów wtedy od razu przekłada się na działający system.

Praktyczniejszy modeli aktualizacji opiera się na:

  • pełnym pinningu w lockfile (dokładne wersje, bez zakresów),
  • dedykowanym procesie bumpowania wersji (np. okresowe PR-y generowane przez bota),
  • oddzielnym traktowaniu poprawek bezpieczeństwa – te powinny mieć krótszą ścieżkę wdrożenia.

W mniejszych zespołach naturalną barierą staje się „brak czasu na aktualizacje”. Bez automatyzacji kończy się to latami technicznego długu. Wtedy każde wymuszone przez bezpieczeństwo podbicie wersji oznacza jednocześnie ryzykowny skok o kilka majorów do przodu. Z punktu widzenia łańcucha dostaw jest to sytuacja znacznie gorsza niż regularne, małe zmiany.

Skanowanie zależności: co jest realnie osiągalne

Narzędzia SCA (Software Composition Analysis) generują imponujące raporty, ale same raporty nie zamykają ryzyka. Typowy problem to:

  • nadmiar zgłoszeń o niskim znaczeniu,
  • brak powiązania z realnych wykorzystaniem funkcji podatnej,
  • brak rozróżnienia między build-time a runtime (biblioteki używane tylko w testach lub narzędziach).

Z perspektywy praktycznej ważniejsze od 100% „czystości” raportu jest:

  • wypracowanie progu akceptacji (które kombinacje „severity + exploitability” blokują release),
  • powiązanie alertów z kontekstem – czy podatna funkcjonalność jest w ogóle wywoływana,
  • kategoryzacja alertów według krytyczności komponentu (wspomniane klasy ryzyka).

Standardem powinno być także wersjonowanie samych raportów i SBOM-ów. Bez tego trudno odpowiedzieć na pytanie, czy dana podatność była obecna w konkretnym release i czy została później usunięta.

Biblioteki porzucone i „martwe” projekty

Znacząca część ekosystemu open source to projekty w praktyce porzucone: brak releasów, brak reakcji na zgłoszenia, znikoma aktywność maintainerów. Tego typu zależność w krytycznym fragmencie systemu jest tykającym zegarem.

Zwykle można:

  • poszukać utrzymywanego forka (ale wtedy pojawia się pytanie o nowe zaufanie),
  • zastąpić bibliotekę inną, nawet kosztem chwilowego spadku wygody,
  • przejąć utrzymanie we własnym zakresie (vendorowanie kodu, lokalny fork z własnym procesem review).

Wyjątkiem są sytuacje, gdy biblioteka jest głęboko wbudowana w framework albo ekosystem. Wtedy realistyczne może być tylko ograniczanie ekspozycji (sandboxing, izolacja komponentu) i ścisłe monitorowanie, czy nie pojawił się jednak utrzymywany zamiennik.

Jak twardo podejść do kontenerów: budowanie, skanowanie, utrzymanie

Budowanie obrazów: od „Dockerfile kopia z bloga” do kontrolowanego procesu

Większość problemów bezpieczeństwa kontenerów zaczyna się w Dockerfile, który ktoś kiedyś skopiował z pierwszego wyniku wyszukiwarki. Ten plik żyje później latami, doklejane są do niego kolejne polecenia, a nikt nie wraca do założeń początkowych.

Kilka praktyk, które zmieniają sytuację z „chaos” na „kontrolowany kompromis”:

  • centralny katalog bazowych Dockerfile / szablonów, recenzowany tak jak kod,
  • zakaz budowania z latest i nieokreślonych tagów obrazów bazowych,
  • multi-stage buildy jako standard, a nie „fajny dodatek”,
  • minimalizacja warstw, które mają dostęp do sekretów (np. klucze do dependency managerów w osobnym etapie, usuwanym z finalnego obrazu).

Dobrą praktyką jest też unifikacja narzędzi: ten sam sposób instalacji zależności w CI, lokalnie i w obrazie (o ile to możliwe). Każda „specjalna” ścieżka zwiększa szansę, że jedna z nich wymknie się spod kontroli.

Skanowanie obrazów: nie tylko CVE, ale i konfiguracja

Skanery obrazów kontenerowych często kojarzone są głównie z wyszukiwaniem znanych podatności w pakietach systemowych. To ważna część obrazu, ale nie jedyna. Obraz może być formalnie „czysty” pod względem CVE, a jednocześnie:

  • uruchamiać procesy jako root bez realnej potrzeby,
  • zawierać w środku klucze lub tokeny,
  • mieć otwarte porty i usługi, które nie są używane.

Pełniejsze podejście łączy:

  • skanowanie CVE w pakietach (system + userland),
  • analizę konfiguracji (USER, CAPABILITIES, otwarte porty, entrypoint),
  • weryfikację SBOM obrazu (co dokładnie się w nim znajduje, z jakich źródeł).

Kolejnym krokiem jest wprowadzenie polityk w rejestrze obrazów: blokowanie pushów, które nie mają aktualnego raportu skanowania, albo które przekraczają określony próg ryzyka. To nie jest rozwiązanie doskonałe – bywa uciążliwe przy fałszywych alarmach – ale mocno utrudnia przypadkowe wdrożenie obrazu z rażącymi problemami.

Ciągłe utrzymanie: obrazy starzeją się szybciej niż kod

Aplikacja może się nie zmieniać tygodniami, ale obraz kontenera oparty na dystrybucji Linuxa starzeje się codziennie. Gromadzą się poprawki bezpieczeństwa, aktualizacje pakietów, nowe wersje narzędzi. Brak procesu odświeżania powoduje, że nawet „stabilna” usługa zaczyna być obciążeniem w łańcuchu dostaw.

Kilka praktyk, które pomagają:

  • regularny rebuild obrazów z tym samym kodem, ale aktualną bazą (np. raz na tydzień / dwa),
  • monitoring CVE per obraz, a nie tylko per pakiet – zespół ma widoczność, które usługi wymagają przebudowy,
  • oznaczanie obrazów datą budowy i wersją bazy (np. w labelach), co pozwala łatwo odróżnić „świeże” od „starych”.

W praktyce często wychodzi na jaw, że najbardziej przestarzałe obrazy należą do mało spektakularnych usług: helperów, migratorów, narzędzi administracyjnych. Z punktu widzenia atakującego to często lepszy punkt wejścia niż główny frontend, który jest pod baczną obserwacją.

Ograniczanie uprawnień i capabilities w runtime

Podejście „uruchamiamy wszystko jako root, bo tak prościej” było do pewnego momentu powszechne. Teraz jest to jeden z pierwszych sygnałów, że organizacja ma luźne podejście do bezpieczeństwa kontenerów. Mimo to przejście na ograniczone uprawnienia bywa trudniejsze, niż się początkowo wydaje.

Poza oczywistym USER nonroot trzeba zwrócić uwagę na:

  • domyślne capabilities – wiele aplikacji nie potrzebuje większości z nich,
  • możliwość zapisu do systemu plików – realnie potrzebne są zwykle wybrane katalogi,
  • interakcję z hostem – montowane wolumeny, dostęp do socketów (np. Docker socket, kubelet).

Co warto zapamiętać

  • Łańcuch dostaw oprogramowania obejmuje cały proces od pierwszego commita po serwery produkcyjne, a każdy pośredni element (repozytorium, biblioteka, CI/CD, rejestr kontenerów, IDE) jest potencjalnym punktem wejścia dla atakującego.
  • Ataki na łańcuch dostaw omijają bezpośredni atak na system produkcyjny i zamiast tego celują w komponenty „zaplecza”, dzięki czemu złośliwy kod trafia do produktu w pełni legalnym kanałem – jako zwykła aktualizacja czy zależność.
  • Tego typu incydenty są trudniejsze do wykrycia niż klasyczne włamania, bo zachowanie systemu wygląda jak standardowy proces build–test–deploy, bez typowych sygnałów jak skanowanie portów czy nietypowe żądania HTTP.
  • Repozytoria pakietów open source (npm, PyPI, Maven Central itd.) stanowią krytyczny wektor ataku; techniki takie jak typosquatting czy dependency confusion wykorzystują domyślne ustawienia menedżerów pakietów i brak kontroli źródeł zależności.
  • Głośne incydenty, jak SolarWinds, pokazują, że kompromitacja infrastruktury buildowej pozwala podpisywać złośliwe binaria zaufanym certyfikatem i dystrybuować je jak zwykłe aktualizacje, co wzmacnia fałszywe poczucie bezpieczeństwa po stronie odbiorców.
  • Złożoność zależności (wielopoziomowe łańcuchy paczek) i przyzwyczajenie do „zaufanych” narzędzi sprzyjają przeoczeniu problemu; źródło złośliwego kodu bywa ukryte w odległej, pośredniej bibliotece, do której nikt świadomie nie sięgał.