Dlaczego bezpieczeństwo w CI/CD to nie „dodatek po godzinach”
Cel jest prosty: automatyzacja ma przyspieszać wdrożenia, a nie otwierać boczne drzwi do infrastruktury. Pipeline CI/CD, który kiedyś był tylko „skryptem do deploya”, dzisiaj staje się centralnym mózgiem całego procesu wytwarzania oprogramowania. To tam lądują tokeny, klucze, hasła do rejestrów, uprawnienia do chmury i dostęp do produkcji.
W praktyce oznacza to, że pipeline CI/CD jest nową „koroną królestwa”. Przez niego przechodzą:
- dostępy do repozytoriów kodu i artefaktów,
- uprawnienia do tworzenia i usuwania zasobów w chmurze,
- sekrety aplikacyjne wykorzystywane w runtime,
- obrazy Docker z wbudowanymi zależnościami systemowymi i bibliotecznymi.
Jeżeli ktoś przejmie kontrolę nad Twoim pipeline, nie musi włamywać się osobno do każdego serwera. Wystarczy, że zmieni obraz Docker, doda kroki w jobach lub wykorzysta istniejące tokeny, aby wstrzyknąć złośliwy kod prosto na produkcję.
Konsekwencje zaniedbań w bezpieczeństwie CI/CD
Brak podstawowych zabezpieczeń w CI/CD bardzo szybko przekłada się na konkretne incydenty. Najczęstsze skutki to:
- wyciek sekretów – hasła do baz danych, klucze API do zewnętrznych usług, tokeny do chmury wylatują do logów, artefaktów lub publicznych repozytoriów,
- przejęcie kont serwisowych – napastnik kradnie token bota CI i wykorzystuje go do pushowania kodu, tagów lub obrazów,
- deployment z malware – zainfekowany obraz Docker trafia do rejestru, a potem jest wdrażany jak gdyby nigdy nic w środowiskach,
- eskalacja uprawnień – zbyt szerokie role w chmurze pozwalają z poziomu pipeline na pełne zarządzanie infrastrukturą.
W wielu zespołach nadal pokutuje podejście „najpierw dowieźć funkcjonalność, bezpieczeństwo dorobimy później”. Problem w tym, że w CI/CD „później” często oznacza „nigdy” – bo pipeline rośnie, komplikuje się, a każdy dodatkowy check jest traktowany jak hamulec.
Od „deploy szybciej” do „deploy szybko i bezpiecznie”
Transformacja myślenia polega na tym, aby nie traktować bezpieczeństwa jako osobnego toru. Kontrole bezpieczeństwa powinny być częścią standardowego przepływu, a nie dodatkowymi zadaniami „jak starczy czasu”. Da się to zrobić tak, aby nie zabić prędkości delivery.
Przykładowo:
- skanowanie obrazów Docker może być wykonywane równolegle z testami jednostkowymi,
- secret scanning może działać systemowo w repozytorium, a nie jako ręczne zadanie przed releasem,
- reguły „fail build” dla podatności mogą być stopniowo zaostrzane w miarę poprawy jakości kodu.
Ambicją nie jest pipeline, który nie przepuści niczego poniżej „idealnego stanu bezpieczeństwa” – bo taki pipeline szybko wyląduje w koszu. Celem jest sensowny kompromis: minimalizowanie ryzyka przy akceptowalnym koszcie i opóźnieniu.
Jak znaleźć balans między szybkością a kontrolą
Dobrze zaprojektowany, bezpieczny pipeline CI/CD nie blokuje developmentu bez powodu. Zamiast „wszystko albo nic” przydają się różne poziomy reakcji w zależności od wagi problemu:
- krytyczne podatności w obrazie Docker – blokada builda lub deployu,
- średnie zagrożenia – ostrzeżenie w pipeline, ticket do backlogu security,
- niskie zagrożenia – raport okresowy do przeglądu technicznego długu.
Podobnie z sekretami: ujawniony AWS Access Key w repo powinien natychmiast wywalić pipeline i trafić do automatycznej procedury rotacji. Ale pojedynczy parametr konfiguracyjny z testowego środowiska to raczej temat do porządnego sprzątania niż awaryjnej narady całej firmy.
„Niewinny” plik .env i duży problem na produkcji
Scenariusz z życia: zespół wrzuca do repozytorium przykładowy plik .env. Początkowo bez sekretów, tylko placeholdery. Z czasem ktoś „na chwilę” dopisuje prawdziwe hasła do bazy testowej, potem do stagingu. Plik trafia do publicznego forka, a skaner Internetu znajduje go po kilku godzinach.
Pipeline CI/CD buduje obrazy Docker, pakując wszystko z katalogu aplikacji. Plik .env ląduje w obrazie, a następnie jest deployowany w kilku środowiskach. Po kilku dniach incydent: dziwny ruch na bazie, nieautoryzowane zapytania. Okazuje się, że ktoś wykorzystał wyciek z publicznego forka, przeniknął na testy, a stamtąd krok po kroku przeanalizował infrastrukturę.
Wystarczyłby prosty secret scanning w repozytorium i reguła „nigdy nie kopiuj plików .env do obrazu Docker”, aby ten łańcuch zdarzeń przerwać na pierwszym etapie.
Fundamenty: model zagrożeń wokół CI/CD i obrazów Docker
Zanim pojawią się narzędzia, przydaje się trzeźwy model zagrożeń. Bez niego łatwo przepłacać za skanery, a jednocześnie zostawiać otwarte „okna w piwnicy”.
Główne wektory ataku na pipeline i infrastrukturę
Środowisko CI/CD to nie tylko sam plik konfiguracyjny pipeline. W praktyce na stole leży cały łańcuch komponentów:
- repozytoria kodu – GitHub, GitLab, Bitbucket; często zintegrowane z pipeline i botami,
- same narzędzia CI – GitHub Actions, GitLab CI, Jenkins, Azure DevOps, CircleCI itp.,
- workery / runners – maszyny, na których wykonywane są joby, z dostępem do kodu i sekretów,
- rejestry obrazów – Docker Hub, Harbor, ECR, GCR, GitLab Container Registry,
- systemy artefaktów – Maven/NuGet registries, artefactories, S3-like magazyny.
Każdy z tych elementów może być wejściem dla napastnika. Przykłady wektorów ataku:
- zgadnięcie/wyłudzenie hasła do konta developera, który ma prawo modyfikować pipeline,
- wstrzyknięcie złośliwego obrazu bazowego z publicznego rejestru,
- kompromitacja self-hosted runnera i podmiana binariów używanych w buildzie,
- wyciek tokenu CI z logów i użycie go do pobrania lub podmiany artefaktów.
Pipeline CI/CD jest często łącznikiem między strefą „świata zewnętrznego” (publiczne dependencje, publiczne obrazy, kod z forka) a strefą „wewnętrzną” (prywatne rejestry, bazy, VPC). Z punktu widzenia napastnika to wymarzone miejsce do przeskoku między domenami.
Typy atakujących i scenariusze zagrożeń
Nie każdy atakujący celuje od razu w Twoją produkcję. Często celem jest „po prostu” wykorzystanie zasobów:
- „script kiddie” kopiący krypto – próbuje odpalić koparkę kryptowalut na runnerach CI albo w kontenerach,
- atakujący interesujący się łańcuchem dostaw – próbuje wstrzyknąć złośliwy kod do obrazu, który dalej trafi do klientów,
- dostawca (świadomie lub nie) z zainfekowanym obrazem bazowym – Ty po prostu FROM someimage:latest i po zawodach,
- insider – ktoś z organizacji, kto ma uprawnienia do pipeline i używa ich niezgodnie z polityką.
Do tego dochodzą bardziej wysublimowane scenariusze, jak np. atak na supply chain przez przejęcie biblioteki open source, która jest instalowana w pipeline. Ciężko się przed tym bronić w stu procentach, ale da się ograniczyć szkody i wykrywać anomalie.
Obszary ryzyka w bezpieczeństwie CI/CD
Rozsądny model zagrożeń porządkuje ryzyka w kilku głównych kategoriach:
- kod źródłowy – złośliwe zmiany, tylnie furtki, brak code review,
- zależności aplikacji (SCA) – biblioteki z podatnościami, typosquatting w repozytoriach,
- obrazy Docker i system bazowy – niezałatane pakiety OS, przestarzałe obrazy, root w kontenerach,
- sekrety – ujawnione w repozytorium, logach, artefaktach, w samych obrazach,
- infrastruktura pipeline – uprawnienia runnerów, dostęp do chmury, konfiguracja sieci,
- artefakty i rejestry – integrytet, podpisy, uprawnienia do push/pull.
Nie ma sensu maksymalnie usztywniać tylko jednego elementu, ignorując resztę. Np. super-bezpieczne obrazy Docker niewiele dadzą, jeśli tokeny do chmury są ogólnodostępne w logach pipeline.
Rola kont serwisowych i tokenów w CI/CD
Większość akcji w CI/CD odbywa się z użyciem kont serwisowych i różnego rodzaju tokenów:
- tokeny bota CI do odczytu/zapisu w repozytorium,
- klucze do rejestrów obrazów (username/password lub token, w chmurach najczęściej tymczasowe),
- role i klucze do chmury (AWS IAM, GCP Service Accounts, Azure Service Principals),
- API keys do narzędzi zewnętrznych: skanery, monitoring, ticketing.
Jeśli token ma zbyt szerokie uprawnienia, pipeline staje się złotą przepustką: wystarczy przejąć ten token, aby wykonywać te same operacje, co cały system CI. Dlatego kluczowa jest zasada least privilege – każde konto serwisowe ma dokładnie taki zakres uprawnień, jaki jest niezbędny do konkretnego joba.
Dlaczego Docker jest tak atrakcyjnym celem
Kontenery rozwiązały masę problemów operacyjnych, ale dorzuciły nową warstwę ryzyka. Obrazy Docker są wręcz idealnym celem dla ataków typu supply chain:
- są cache’owane warstwami; złośliwy fragment w jednej warstwie może być użyty wielokrotnie,
- są masowo pobierane z publicznych rejestrów, gdzie nie każdy obraz jest weryfikowany lub podpisany,
- często używają tagu latest, który może zmienić się w dowolnym momencie bez Twojej wiedzy,
- bywają buildowane z nieprzemyślanymi Dockerfile, które kopiują pliki z sekretami lub zostawiają narzędzia buildowe.
Dlatego w bezpiecznym CI/CD skanowanie obrazów Docker i polityka korzystania z rejestrów nie jest „nice to have”, tylko standardowa część procesu, tak jak testy jednostkowe.

Projekt architektury bezpiecznego pipeline: od commit do produkcji
Bezpieczny pipeline CI/CD to nie pojedynczy skrypt, ale cała architektura. Już na etapie projektowania warto ustalić, gdzie i jak będą wykonywane kontrole bezpieczeństwa, oraz kto ma prawo je zmieniać.
Podział pipeline na etapy i punkty kontrolne
Przemyślany pipeline da się podzielić na względnie stałe etapy, gdzie każdemu etapowi przypisane są określone kontrole:
- build – kompilacja, budowa obrazów Docker, generowanie artefaktów,
- test – testy jednostkowe, integracyjne, E2E,
- security checks – SAST, SCA, skanowanie obrazów Docker, secret scanning,
- deploy – rollout do środowisk (dev, test, staging, prod).
Punkty, w których najczęściej „wstrzykuje się” kontrole bezpieczeństwa:
- po commit/push – lekkie skany (linting, SAST, podstawowy secret scanning),
- po buildzie obrazu – skanowanie obrazów Docker pod kątem podatności,
- przed deployem na wyższe środowisko – bardziej rygorystyczne progi dla podatności,
- cyklicznie – skanowanie istniejących obrazów i zależności pod kątem nowych podatności.
Część kontroli może działać „w tle” (raporty, ostrzeżenia), a część pełni rolę security gate, który fizycznie blokuje przejście dalej przy poważnych problemach.
Oddzielenie ról i zarządzanie zmianami w pipeline
Jednym z częstszych błędów jest traktowanie konfiguracji pipeline jak zwykłego kodu aplikacyjnego, do którego każdy developer może wprowadzać dowolne modyfikacje. Pipeline CI/CD musi mieć bardziej restrykcyjne zasady zmian.
Przykładowy podział ról:
- developerzy – mogą dodawać/zmieniać joby funkcjonalne (build, testy) w swoim zakresie,
- DevOps/SRE – definiują infrastrukturę pipeline (runnery, integracje z chmurą, secrets store),
- zespół bezpieczeństwa – zatwierdza i utrzymuje reguły security (skanery, progi fail, polityki).
Segmentacja środowisk i kontrola przepływu artefaktów
Bezpieczny pipeline to również jasne granice: które artefakty skąd pochodzą i dokąd mogą trafić. „Jeden rejestr do wszystkiego” brzmi wygodnie, ale z perspektywy bezpieczeństwa przypomina przechowywanie benzyny, zapałek i serwera produkcyjnego w jednym pokoju.
Praktyczny model segmentacji:
- rejestr buildowy – tymczasowe obrazy z etapu build/test, niewystawione na świat, z krótkim TTL,
- rejestr test/staging – obrazy, które przeszły minimalne kontrole jakości i bezpieczeństwa,
- rejestr produkcyjny – tylko artefakty podpisane, oznaczone jako release, z silnymi politykami dostępu.
Między tymi strefami powinny istnieć „śluzy” kontrolne. Promocja obrazu z rejestru buildowego do testowego odbywa się wyłącznie przez pipeline, po spełnieniu warunków: testy zaliczone, skany wykonane, progi bezpieczeństwa niespełnione tylko w akceptowalnym zakresie (np. wyłącznie znane i udokumentowane wyjątki).
Bezpośredni docker push developera do rejestru produkcyjnego to czerwone światło. Jeśli da się to w ogóle technicznie wykonać, ktoś wcześniej zaspał przy projektowaniu uprawnień.
Bezpieczne zarządzanie runnerami i infrastrukturą CI
Runnerzy (workery) to miejsce, gdzie „dzieje się magia”, ale też gdzie może się wydarzyć najwięcej złego. Mają dostęp do kodu, tokenów, często do sieci wewnętrznej.
Podstawowe zasady twardnienia runnerów:
- oddzielaj self-hosted od managed – jeśli narzędzie CI oferuje własne, ephemeryczne workery, używaj ich tam, gdzie możesz; self-hosted tylko gdy naprawdę trzeba (specyficzny hardware, prywatna sieć),
- efemeryczność – runner puchnie od tymczasowych plików, cache’y, narzędzi; im szybciej jest niszczony po jobie, tym mniejsza powierzchnia ataku,
- brak stałych sekretów „na maszynie” – wszystko przez mechanizmy tymczasowych tokenów, role z przydziałem czasu, secret store; zero
.envwiecznie leżących na dysku, - izolacja sieciowa – runner od buildów frontendu nie musi mieć dostępu do bazy produkcyjnej, nawet pośrednio,
- minimalny system bazowy – obrazy systemowe runnerów bez zbędnych pakietów; mniej narzędzi → mniej podatności.
Dobrym nawykiem jest traktowanie runnera jak krótkotrwałego kontenera: startuje, wykonuje jedną serię jobów, po czym jest niszczony. W chmurach można to zrealizować np. przez autoscaling grup instancji, które po bezczynności są terminowane.
Bezpieczny Docker od podstaw: budowanie obrazów, które da się bronić
Nawet najlepszy skaner niewiele zrobi, jeśli obraz ma fatalny „genotyp”: ogromną powierzchnię ataku, brak rozdziału środowisk build/run i przy okazji pół dysku sekretów w środku. Podstawą jest taki Dockerfile, który nie robi rzeczy wstydliwych.
Dobór bazowych obrazów i zarządzanie tagami
Bazowy obraz to odpowiednik „systemu operacyjnego” aplikacji. Dwie decyzje, które najczęściej robią różnicę:
- minimalne dystrybucje – obrazy typu
alpine,distroless, oficjalneslimod vendorów; mniej pakietów = mniej CVE, - rezygnacja z
latest– zawsze używaj konkretnych tagów (np.node:20.11-alpine) i procesów ich aktualizacji.
Obraz bazowy powinien pochodzić z zaufanego rejestru: oficjalny wydawca, Twój wewnętrzny rejestr „zahartowanych” baz, albo przynajmniej obraz podpisany cyfrowo. Pull z przypadkowego namespace’u w publicznym rejestrze to proszenie się o supply-chain drama.
Wzorzec multi-stage build i rozdzielenie build/run
Najczęściej pomijany element bezpieczeństwa Docker: zupełne rozdzielenie warstwy build i runtime. Służy do tego multi-stage build.
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o app ./cmd/app
FROM gcr.io/distroless/base-debian12
COPY --from=build /src/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
W efekcie:
- w finalnym obrazie nie ma kompilatora,
git, narzędzi debugowych, - wielkość obrazu spada często kilkukrotnie,
- skaner ma mniej do analizy, a Ty mniej potencjalnych podatności.
W aplikacjach node/python multi-stage pozwala utrzymać w finalnym obrazie wyłącznie runtime dependencies, bez całego ekosystemu devtooli, testów i node_modules z build stage.
Uruchamianie procesów bez uprawnień roota
Proces działający jako root w kontenerze to klasyka. Niby „tylko w kontenerze”, ale:
- ułatwia eskalację uprawnień w razie wyjścia poza kontener,
- łagodzi skutki błędnej konfiguracji hosta (np. brak
rootlessw runtime), - łamię zasadę najmniejszych uprawnień praktycznie w całej aplikacji.
Prosty zabieg robi dużą różnicę:
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser:appgroup
Niektóre base imagy (np. distroless czy oficjalne obrazy językowe) oferują już gotowego użytkownika nie-root. Wtedy wystarczy przełączyć się na niego lub sprawdzić domyślną konfigurację.
Ograniczanie powierzchni ataku w Dockerfile
Prosty przegląd typowych grzechów w Dockerfile:
- nadmiarowe pakiety – instalowanie całego
build-essential, gdy wystarczy pojedynczy kompilator w pierwszym etapie, - nieczyszczone cache – brak
rm -rf /var/lib/apt/lists/*czy mechanizmów skracających życie cache’a managera pakietów, - kopiowanie całego repo –
COPY . .w kontekście, gdzie leżą lokalne pliki z sekretami, - otwarte porty „na zapas” –
EXPOSE 80 443 8080 9090„bo kiedyś się przyda”.
Minimalny, „obronny” Dockerfile:
- kopiuje tylko to, co potrzebne do builda i runtime,
- czyści po sobie menedżera pakietów,
- definiuje
USERi precyzyjneEXPOSE, - oddziela etap build (z narzędziami) od etapu runtime (lekki obraz).
Unikanie sekretów w obrazach i warstwach
Umieszczenie sekretu w obrazie to nie tylko ENV AWS_SECRET_KEY=.... Wystarczy, że plik konfiguracyjny z kluczem OAuth trafi do COPY . ., a potem zostanie usunięty – w warstwach obrazu i tak pozostanie.
Bezpieczne wzorce:
- sekrety dostarczane poza obrazem – przez
docker run -e, ConfigMap/Secret w Kubernetesa, param store’y, - do builda używaj build secrets (np.
--secretwdocker buildxlubbuildkit), które nie trafiają do warstw, - walidacja obrazów pod kątem sekretów – dedykowany secret scanning w pipeline.
Jeśli w Dockerfile pojawia się jakikolwiek klucz, hasło czy token – to prawie na pewno jest błąd projektowy, nie wyjątek.

Skanowanie obrazów Docker: narzędzia, integracje i sensowne polityki
Skanowanie obrazów jest jak przegląd techniczny auta. Bez niego długo się pojedzie, dopóki coś nie odpadnie przy 140 km/h. Klucz tkwi w tym, żeby skan był sensowny i wykonalny w realnym pipeline, a nie trwał godzinami i produkował setki fałszywie „krytycznych” alertów.
Rodzaje skanerów i co tak naprawdę sprawdzają
Pod hasłem „skaner obrazów” kryje się kilka mechanizmów:
- skan pakietów systemowych (OS-level) – porównanie z bazami CVE dla dystrybucji (apk, apt, yum),
- skan zależności aplikacyjnych – analiza
package-lock.json,requirements.txt,pom.xmlitd. w samym obrazie, - analiza konfiguracji – wykrywanie niebezpiecznych ustawień (np. sshd, suid, world writable),
- secret scanning – wykrywanie wzorców kluczy API, haseł, certyfikatów w warstwach.
Nie każde narzędzie robi wszystko. Popularne skanery (Trivy, Grype, Anchore, Clair, narzędzia komercyjne) różnią się zakresem i dokładnością. Przy wdrażaniu w pipeline dobrze jest jasno zdefiniować: co ma być skanowane i jaki poziom szczegółowości jest akceptowalny przy danym etapie.
Integracja skanowania z pipeline: gdzie i kiedy skanować
Skan można odpalić „ręcznie z boku”, ale dopiero automatyzacja w pipeline daje realną wartość. Sprawdzone punkty włączenia:
- po zbudowaniu obrazu – na gałęzi feature lub PR; szybszy, bardziej liberalny skan, który nie musi blokować merge, ale generuje raport,
- przed wdrożeniem na środowisko wyższe (test/staging/prod) – pełny skan z rygorystycznymi progami, mogący zablokować deploy,
- cykliczne skany istniejących obrazów w rejestrze – wykrywanie nowych podatności w już wdrożonych wersjach.
Scenariusz z praktyki: raz dziennie pipeline uruchamia skaner na wszystkich obrazach z rejestru produkcyjnego. Jeśli pojawia się nowa krytyczna podatność, generowany jest ticket + automatyczne MR z podbiciem base image i aktualizacją zależności. Ludzie podejmują decyzję, kiedy zdeployować poprawkę, ale nie szukają jej w panice po Slacku.
Pragmatyczne polityki bezpieczeństwa: progi i wyjątki
Największy błąd przy wprowadzaniu skanowania: „blokujemy wszystko powyżej Medium”. Efekt: miesiąc później wszyscy mają wyciszone powiadomienia, a w systemie wisi tysiąc niezamkniętych alertów.
Lepsze podejście:
- różne progi dla różnych środowisk – na dev dopuszczalne są znane CVE z niskim/średnim poziomem, na prod latch krytyczne i część wysokich,
- whitelisting / risk acceptance – możliwość udokumentowanego „zaakceptowania” konkretnej podatności (np. brak exploitu, niewykorzystywana funkcja pakietu),
- limity ilościowe – np. „deploy blokowany, jeśli są niezaakceptowane CVE Critical lub więcej niż X niezaakceptowanych High”,
- czasowe wyjątki – akceptacja CVE na określony czas (np. 30 dni); po tym okresie blokada wraca, jeśli nic z tym nie zrobiono.
Takie podejście utrzymuje równowagę: pipeline chroni przed oczywistym ryzykiem, ale nie paraliżuje zespołu z dnia na dzień.
Przykładowa integracja skanera w CI (Trivy + GitLab CI)
Prosta integracja dla obrazu budowanego w pipeline może wyglądać następująco:
image: alpine:3.19
stages:
- build
- scan
build-image:
stage: build
script:
- docker build -t registry.example.com/app:${CI_COMMIT_SHA} .
- docker push registry.example.com/app:${CI_COMMIT_SHA}
services:
- docker:dind
scan-image:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity CRITICAL,HIGH
registry.example.com/app:${CI_COMMIT_SHA}
allow_failure: false
needs: ["build-image"]
W tym wariancie job scan-image zatrzyma pipeline, jeśli w obrazie wykryte zostaną niezaakceptowane podatności o poziomie HIGH lub CRITICAL. W realnym projekcie da się to rozszerzyć o raporty w formacie SARIF, integrację z issue trackerem czy różne progi w zależności od gałęzi.
Skanowanie rejestru i zarządzanie „starymi” obrazami
Wielu zespołów skupia się na skanowaniu obrazów w momencie builda, ignorując to, co już leży od dawna w rejestrze. A to właśnie te stare, rzadko aktualizowane usługi często działają w najważniejszych miejscach.
Dobry proces obejmuje:
- periodyczne skanowanie wszystkich tagów w rejestrze produkcyjnym,
- tagowanie wersji (np. semver + commit SHA), tak aby dało się jednoznacznie wskazać podatne obrazy,
- politykę retencji – usuwanie starych, nieużywanych tagów po określonym czasie,
- powiązanie z monitoringiem – np. metryka „liczba usług na prod z niezaakceptowanymi krytycznymi CVE”.
Łączenie skanowania z kontrolą dostępu: kto może przepchnąć podatny obraz
Nawet najlepszy skaner niewiele daje, jeśli wystarczy jeden „admin wszystkiego”, żeby ręcznie przepchnąć obraz z CRITICAL-ami na produkcję, bo „termin goni”. Dlatego technika musi się zgrać z uprawnieniami i procesem.
Przy projektowaniu kontroli dostępu do rejestru i pipeline dobrze zadziała kilka prostych zasad:
- rozróżnienie ról – kto może zbudować obraz, kto go zatwierdzić, a kto zdeployować; te trzy funkcje nie powinny zlewać się w jedną osobę na stałe,
- blokady na poziomie rejestru – możliwość oznaczenia tagu jako „approved for prod”, który może nadawać tylko określona rola (np. SecOps / Tech Lead),
- branch protection + code owners – merge do gałęzi produkcyjnych wymagający review osób, które rozumieją konsekwencje zaakceptowania ryzyka,
- audytowalne override – jeśli trzeba obejść politykę skanera, to tylko jawnie: z komentarzem, powodem i czasowym zakresem.
Praktyczna konfiguracja:
- w GitLab/GitHub tylko runner z określoną tożsamością może pushować do przestrzeni typu
registry.example.com/prod/*, - tagi
:latestsą zakazane w deployu na prod – używane są wyłącznie konkretne wersje, które przeszły skan i proces akceptacji, - do nadania etykiety „approved-for-prod” na MR wymagany jest review osoby z roli „Security Champion” w danym zespole.
Od strony kulturowej ważna rzecz: decyzja „deployujemy z tą podatnością czy nie” powinna być świadomie podjęta, udokumentowana i możliwa do odtworzenia, a nie „ktoś kliknął, bo mu się spieszyło na lunch”.
Skanowanie w pipeline a wydajność: jak nie utopić CI w minutach
Częsty argument przeciwko rozbudowanemu skanowaniu brzmi: „przecież pipeline będzie trwał wieczność”. Tego da się uniknąć, jeśli korzysta się z cache i sensownie rozbija zadania.
Kilka trików, które zwykle działają:
- cache baz CVE – większość skanerów (Trivy, Grype) korzysta z lokalnej bazy podatności; jej pobranie można keszować między jobami (cache w GitLab CI, actions cache w GitHubie),
- równoległe skanowanie – rozdzielenie skanu OS-level i skanu zależności aplikacyjnych na osobne joby w tej samej fazie,
- warunkowe uruchamianie – pełny skan tylko na merge request i gałęzie releasowe, a na lokalnych feature branchach – szybszy, uproszczony wariant,
- pre-skan base image – osobny pipeline dla wspólnego base image; jeśli jest czysty, nie ma sensu za każdym razem weryfikować całego systemu od zera.
Przykładowy trik z praktyki: dla usług Node.js pipeline uruchamia szybki skan package-lock.json „na sucho” (bez budowy obrazu). Jeśli tam są krytyczne CVE, nie ma sensu w ogóle odpalać pełnego builda i skanowania kontenerowego – oszczędza to minuty na każdej zmianie.
Bezpieczne zarządzanie sekretami w pipeline: od „.env w repo” do KMS
Skanowanie obrazów rozwiązuje tylko część problemu. Kolejna duża dziura to sposób przekazywania sekretów w samym pipeline: dostępy do chmury, tokeny do rejestru, hasła do baz.
Typowe antywzorce w zarządzaniu sekretami CI/CD
Przed uporządkowaniem tematu dobrze nazwać to, co zwykle boli najbardziej. Kilka klasyków:
- sekrety w repozytorium – pliki
.envwrzucone do gita, zaszyfrowane „autorskim” algorytmem lub po prostu w plaintext (często w historii od lat), - sekrety w zmiennych środowiskowych bez ograniczeń – globalne zmienne CI widoczne dla wszystkich projektów, wszystkich branchy i wszystkich jobów,
- sekrety współdzielone między środowiskami – ten sam klucz do bazy używany na dev, staging i prod, „bo inaczej się pogubimy”,
- przeklejanie sekretów w logach – echo tokenów w logach jobów CI („dla debugowania”), brak maskowania w konfiguracji.
Każdy z tych punktów kończy się tak samo: wyciek sekretu to kwestia czasu, a potem długa zabawa w rotację kluczy i „kto widział ten log sprzed 8 miesięcy”.
Źródło prawdy dla sekretów: dedykowane sejfy, nie YAML-e
Najzdrowszy model to jeden, centralny magazyn sekretów, do którego pipeline ma ściśle ograniczony dostęp. Opcje zależą od stacku:
- chmury: AWS Secrets Manager / SSM Parameter Store, Azure Key Vault, GCP Secret Manager,
- on-prem / hybrid: HashiCorp Vault, 1Password Secrets Automation, wbudowane sejfy w GitLab/GitHub jako warstwa „pośrednia”,
- systemy orkiestracji: Kubernetes Secrets (ale lepiej traktować je jako warstwę dystrybucji, nie root of trust).
Mechanizm jest z grubsza ten sam: pipeline uwierzytelnia się do sejfu (najlepiej bez stałych kluczy, o czym za chwilę), pobiera minimalny zestaw tajemnic potrzebnych do danego joba, używa ich w pamięci i nie zapisuje na dysk, jeśli nie ma absolutnej konieczności.
Rozdzielenie sekretów per środowisko i per usługa
Spory zysk bezpieczeństwa (i porządku) daje proste zasadnicze rozdzielenie sekretów:
- osobne zestawy dla
dev,test,staging,prod, - osobne przestrzenie/sejfy per aplikacja lub domena biznesowa,
- brak „master-sekretu”, który otwiera wszystkie drzwi.
Przykładowa konwencja w Secrets Managerze:
/app/orders/dev/db/password/app/orders/prod/db/password/app/payments/prod/gateway/api-key
Rola używana przez pipeline usługi orders na środowisku staging po prostu nie ma prawa odczytać nic z gałęzi /app/payments/* ani z /prod. To mocno ogranicza skutki kompromitacji pojedynczego joba CI.
Bezpieczne użycie sekretów w GitLab CI / GitHub Actions
GitLab i GitHub oferują natywne mechanizmy sekretów (Protected Variables / Encrypted Secrets). One same w sobie nie rozwiązują całego problemu, ale są sensowną warstwą pośrednią między sejfem a jobem.
Przykładowe dobre praktyki dla GitLab CI:
- używanie
protected: truedla zmiennych używanych namain/ prod, - oznaczenie zmiennych jako
masked, aby nie pojawiały się w logach, - scope zmiennych per environment (np.
CI_ENVIRONMENT_NAME == "prod"), a nie globalnie w całej grupie, - przekazywanie z GitLaba tylko krótkotrwałych tokenów dostępowych, wygenerowanych z sejfu/KMS, a nie „twardych” haseł.
Dla GitHub Actions analogicznie:
- sekrety na poziomie repozytorium / environment, nie całej organizacji,
- używanie Environments z wymaganym review dla deployów na prod,
- integracja z OIDC do chmury zamiast przechowywania stałych kluczy.
Przykład: pobieranie sekretów z Vault w pipeline
Fragment konfiguracji GitLab CI, który pokazuje integrację z HashiCorp Vault bez wstrzykiwania stałych tokenów:
stages:
- build
- deploy
variables:
VAULT_ADDR: "https://vault.example.com"
before_script:
- apk add --no-cache curl jq
deploy-staging:
stage: deploy
image: alpine:3.19
script:
# Uwierzytelnienie za pomocą JWT runnera (Vault JWT auth)
- |
VAULT_TOKEN=$(curl -s --request POST "$VAULT_ADDR/v1/auth/jwt/login"
--data "{"role": "gitlab-staging", "jwt": "${CI_JOB_JWT}"}" | jq -r .auth.client_token)
# Pobranie sekretnych danych dla usługi
- |
DB_PASSWORD=$(curl -s --header "X-Vault-Token: $VAULT_TOKEN"
"$VAULT_ADDR/v1/kv/data/app/orders/staging/db" | jq -r .data.data.password)
- export DB_PASSWORD
- ./deploy.sh # używa $DB_PASSWORD
environment:
name: staging
W tym scenariuszu job używa tokenu JWT dostarczonego przez GitLaba (CI_JOB_JWT) do uwierzytelnienia się w Vault. Nie ma w YAML-u żadnego stałego hasła do sejfu; nawet jeśli ktoś wyciągnie konfigurację pipeline, bez ważnego JWT i uprawnień runnera niewiele zdziała.
Ochrona kluczy dostępowych: od stałych kluczy do tożsamości OIDC
Najsłabszym punktem wielu pipeline’ów jest sposób, w jaki CI łączy się z chmurą czy innymi systemami. Długotrwałe klucze w zmiennych środowiskowych to proszenie się o kłopoty.
Dlaczego stałe klucze w CI to zły pomysł
Statyczne klucze dostępowe (AWS Access Key / Secret, service account key w GCP, Azure Service Principal z niekończącym się hasłem) mają kilka nieprzyjemnych cech:
- są kopiowalne – raz wyciekają i można nimi handlować jak kuponem na pizzę,
- są trudne w rotacji – odkręcenie szkód po wycieku oznacza szukanie, gdzie jeszcze są wpięte,
- są często nadmiarowe – „bo jak damy mniejsze uprawnienia, to coś przestanie działać”,
- działają poza kontekstem – nie wiadomo, czy klucz jest używany przez pipeline, lokalne skrypty czy czyjąś maszynę.
Dlatego tam, gdzie się da, lepiej iść w stronę krótkotrwałych poświadczeń opartych o tożsamość joba CI, a nie statycznego sekretu.
Tożsamość OIDC dla CI: autentykacja do chmury bez kluczy
Nowoczesne platformy (GitHub Actions, GitLab, CircleCI) oferują integrację z chmurami na bazie OpenID Connect (OIDC). Idea jest prosta: pipeline wystawia podpisany token z informacją „ten job pochodzi z repo X, z gałęzi Y”, a chmura na tej podstawie wydaje krótkotrwałe uprawnienia.
Przykład: GitHub Actions + AWS.
- W AWS IAM definiujesz rolę z zaufaniem do providera OIDC GitHuba i warunkami typu:
repo: org/repo,branch: main. - W workflow Actions używasz
aws-actions/configure-aws-credentialszrole-to-assume. - Job otrzymuje tymczasowe poświadczenia AWS (STS) ważne np. kilkanaście minut, tylko w ramach danego joba.
Fragment workflow:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy-role
aws-region: eu-central-1
- name: Deploy
run: |
aws ecs update-service ... # używa tymczasowych poświadczeń
Nie ma tu żadnego AWS_SECRET_ACCESS_KEY w panelu GitHuba. Nawet jeśli ktoś wyniesie YAML z workflow, nic z tego nie ma – prawdziwym „kluczem” jest tożsamość joba w momencie uruchomienia.
Różnicowanie ról i uprawnień per gałąź i środowisko
OIDC i role chmurowe dobrze się składają z zasadą „inna moc na innym środowisku”. Zamiast jednej roli ci-deploy-everywhere lepiej zdefiniować kilka węższych:
ci-deploy-dev– pełne uprawnienia do namespace’u dev, brak dostępu do prod,ci-deploy-staging– ograniczony zestaw: tworzenie releasów, ale np. bez prawa modyfikacji KMS,ci-deploy-prod– minimalny zestaw akcji potrzebnych do wywołania konkretnego mechanizmu deployu (np.ecs:UpdateServicei nic więcej).
Warunki przypisania roli definiuje się po obu stronach:
- w providrze OIDC (GitHub/GitLab) – które joby mają prawo prosić o token,
- w chmurze – jakie atrybuty tokenu (repo, branch, job name) pozwalają na przyjęcie danej roli.
Praktyczny przykład: tylko job deploy_prod z gałęzi main w repozytorium org/core-api może przyjąć rolę ci-deploy-prod. PR-y z forka, feature brancha czy inne repozytoria nawet nie ruszą tej roli.
Rotacja i audyt kluczy, które muszą zostać
Najczęściej zadawane pytania (FAQ)
Dlaczego bezpieczeństwo w CI/CD jest aż tak ważne?
Pipeline CI/CD stał się centrum nerwowym procesu wytwarzania oprogramowania. To przez niego przechodzą klucze do chmury, tokeny dostępu, hasła do baz danych i obrazy Docker, które później lądują na produkcji. Jeżeli ktoś przejmie ten punkt, nie musi atakować każdego serwera osobno – wystarczy, że zmodyfikuje pipeline lub obraz, a złośliwy kod wdroży się „sam”.
Brak zabezpieczeń kończy się bardzo konkretnymi incydentami: wyciekiem sekretów do logów lub repozytoriów, przejęciem kont serwisowych CI, wdrożeniem zainfekowanych obrazów czy eskalacją uprawnień w chmurze. Mówiąc prościej: dziura w CI/CD często oznacza otwarte drzwi do całej infrastruktury.
Jak bezpiecznie skanować obrazy Docker w pipeline CI/CD?
Skanowanie obrazów Docker najlepiej wpiąć jako stały krok pipeline, a nie jednorazowe „security sprinty”. Typowe podejście to uruchamianie skanera (np. Trivy, Grype, Snyk) zaraz po zbudowaniu obrazu i przed wrzuceniem go do rejestru. Dla krytycznych podatności ustaw regułę blokującą build lub deploy, a dla średnich i niskich – generuj raporty i tickety.
Dobrą praktyką jest też ograniczenie liczby warstw i bazowanie na minimalnych obrazach (alpine, distroless). Im mniej pakietów w obrazie, tym mniejsza powierzchnia ataku i krótsza lista CVE, które trzeba później gasić.
Jak przechowywać sekrety w CI/CD, żeby nie wyciekały?
Sekretów nie trzyma się w repozytorium, plikach .env ani w Dockerfile. Zamiast tego użyj wbudowanych mechanizmów systemu CI (GitHub Actions Secrets, GitLab CI Variables, Azure DevOps Library) lub dedykowanego sejfu, np. HashiCorp Vault, AWS Secrets Manager, SSM Parameter Store.
W praktyce oznacza to: w pipeline odwołujesz się do zmiennych środowiskowych udostępnianych runnerowi, nie logujesz ich wartości (maskowanie w logach) i rotujesz klucze automatycznie w razie podejrzenia wycieku. Dobrym uzupełnieniem jest secret scanning w repozytorium, który wyłapuje „zbłąkane” hasła wrzucone do kodu.
Jak uniknąć wycieku sekretów przez pliki .env i obrazy Docker?
Podstawowa zasada: pliki .env z prawdziwymi danymi nigdy nie powinny trafiać do repozytorium ani do obrazu Docker. Do repo można wrzucić co najwyżej szablon .env.example bez realnych wartości. W Dockerfile dodaj reguły .dockerignore, które wycinają .env, .git i inne wrażliwe pliki z kontekstu builda.
Dodatkowo skaner sekretów (np. Gitleaks, TruffleHog) uruchamiany w CI potrafi wychwycić, że ktoś jednak „na chwilę” dopisał hasło do pliku. To ta chwila, która zwykle zostaje na lata – jeśli nie masz automatycznej blokady i alertu.
Jak znaleźć balans między szybkością wdrożeń a bezpieczeństwem CI/CD?
Zamiast stawiać mur nie do przejścia, lepiej wprowadzić poziomowanie reakcji. Krytyczne podatności w obrazie, wyciek klucza do chmury czy złośliwa zmiana w pipeline powinny natychmiast blokować build lub deploy. Średnie problemy mogą kończyć się ostrzeżeniem i automatycznym ticketem, a drobne – trafiać do okresowego przeglądu długu technicznego.
Część kontroli da się uruchomić równolegle z istniejącymi krokami, np. skan obrazów obok testów jednostkowych, secret scanning na hookach pre-commit i w CI. Dzięki temu pipeline nie zamienia się w godzinny maraton, a poziom bezpieczeństwa rośnie stopniowo, a nie „od jutra wszystko na czerwono”.
Jakie są najczęstsze wektory ataku na pipeline CI/CD?
Najczęstsze scenariusze to przejęcie konta developera z prawem edycji pipeline, wyciek tokenu CI z logów, wstrzyknięcie złośliwego obrazu bazowego z publicznego rejestru albo kompromitacja runnera (self‑hosted), który ma dostęp do kodu i sekretów. Pipeline bywa też „mostem” między światem zewnętrznym (publiczne biblioteki, forki) a siecią wewnętrzną, więc atakujący chętnie wykorzystują go jako punkt przeskoku.
Żeby to utrudnić, stosuje się m.in. silne uwierzytelnianie do repozytoriów i narzędzi CI, ograniczenie uprawnień runnerów, korzystanie z zaufanych obrazów bazowych, podpisywanie artefaktów oraz reguły, które uniemożliwiają modyfikację pipeline bez review.
Jakie dobre praktyki zabezpieczają obrazy Docker używane w CI/CD?
Bezpieczny obraz zaczyna się od dobrego fundamentu: oficjalne lub zaufane obrazy bazowe, minimalne dystrybucje, regularne aktualizacje. Do tego dochodzi uruchamianie aplikacji bez uprawnień roota, ograniczenie liczby otwartych portów, usuwanie narzędzi niepotrzebnych w runtime (kompilatory, debugery) i jasne tagowanie wersji zamiast magicznego :latest.
Kolejny krok to włączenie automatycznego skanowania obrazów w rejestrze (ECR, GCR, Harbor, GitLab Registry) i w pipeline. Wiele zespołów dorzuca jeszcze podpisywanie obrazów (Cosign, Notary) i polityki w klastrze, które wymuszają używanie tylko podpisanych, zweryfikowanych obrazów – dzięki temu trudniej „po cichu” podmienić kontener na zainfekowany.
Źródła
- NIST Special Publication 800-204C: Implementation of DevSecOps for a Microservices-based Application with Service Mesh. National Institute of Standards and Technology (2022) – Ramy DevSecOps, bezpieczeństwo pipeline i automatyzacji
- NIST Special Publication 800-204A: Building Secure Microservices-based Applications Using Service-Mesh Architecture. National Institute of Standards and Technology (2019) – Model zagrożeń i kontrola komunikacji usług, istotne dla CI/CD
- OWASP CI/CD Security Cheat Sheet. OWASP Foundation – Dobre praktyki zabezpieczania pipeline CI/CD i runnerów
- Kubernetes Security and Observability: A Holistic Approach to Securing Containers and Cloud Native Applications. O’Reilly Media (2022) – Rozdziały o skanowaniu obrazów, rejestrach i supply chain
- Secure Your Software Supply Chain: A Practical Guide. Google Cloud – Praktyki SLSA, skanowanie artefaktów i ochrona pipeline






