Zadanie GitHub Actions to dobre miejsce do kradzieży sekretów. Przechowuje klucze wdrożeniowe, tokeny rejestrów i poświadczenia chmurowe, a do tego uruchamia kod, który ktoś inny napisał przed chwilą: akcje zewnętrzne, pakiety i ich skrypty instalacyjne. Obsługa sekretów w GitHub chroni przed pomyłkami. Nie zaprojektowano jej, by zatrzymywała kod działający już wewnątrz zadania.
W tym wpisie omawiamy, jak działają sekrety w GitHub Actions, pięć sposobów, w jakie wyciekają w praktyce, oraz mechanizmy, które zamykają każdy z nich.
Jak działają sekrety GitHub Actions
Sekrety istnieją na trzech poziomach: organizacji, repozytorium i środowiska. Gdy ta sama nazwa występuje na więcej niż jednym poziomie, wygrywa najbardziej szczegółowy: środowisko przed repozytorium, repozytorium przed organizacją.
Kilka limitów i zasad, które warto znać:
- Rozmiar i liczba. Do 1000 sekretów organizacji, 100 sekretów repozytorium i 100 sekretów środowiska, każdy o rozmiarze do 48 KB. Workflow widzi każdy sekret repozytorium i środowiska, ale tylko pierwszych 100 sekretów organizacji w kolejności alfabetycznej.
- Forki nie dostają nic. Poza
GITHUB_TOKENsekrety nie są przekazywane do workflowów uruchamianych przez pull request z forka. - Dependabot jest traktowany jak fork. Workflowy uruchamiane przez Dependabota dostają token tylko do odczytu i wyłącznie sekrety Dependabota, a nie sekrety Actions.
- Reusable workflows wymagają przekazania sekretów.
secrets: inheritprzekazuje je wszystkie niejawnie, w obrębie tej samej organizacji lub przedsiębiorstwa. Sekretów środowiska nie można przekazywać przezworkflow_call. - Środowiska ograniczają dostęp. Przy wymaganych recenzentach zadanie nie może odczytać sekretów środowiska, dopóki ktoś go nie zatwierdzi. Reguły gałęzi wdrożeniowych określają, które gałęzie w ogóle mogą korzystać ze środowiska.
Co robi maskowanie, a czego nie
GitHub usuwa wartości sekretów z logów, ale polega przy tym na dokładnym dopasowaniu. Własna dokumentacja GitHub wymienia, kiedy to zawodzi:
- sekret wewnątrz danych strukturalnych, takich jak JSON lub YAML
- wartość pochodna sekretu, na przykład token wymieniony na inny token, chyba że zarejestrują ją Państwo przez
::add-mask:: - sekret, który został przekształcony, na przykład zakodowany w base64
Maskowanie chroni przed przypadkowym wypisaniem sekretu. Nie robi nic przeciwko kodowi, który najpierw ten sekret zakoduje.
Jak sekrety wyciekają z workflowów
1. Przejęta akcja je wypisuje
W marcu 2025 roku atakujący przejął tj-actions/changed-files (CVE-2025-30066), akcję używaną przez ponad 23 000 repozytoriów. Przeniósł istniejące tagi wersji akcji na złośliwy commit, więc każdy workflow odwołujący się do tagu takiego jak @v45 uruchamiał jego kod. Bezpieczne były tylko workflowy przypięte do pełnego SHA commita.
Ładunek odczytywał sekrety z pamięci runnera i wypisywał je do logu zadania, zakodowane dwukrotnie w base64, aby maskowanie ich nie wychwyciło. W repozytoriach publicznych te logi może przeczytać każdy. Atak zaczął się od innej akcji, reviewdog/action-setup (CVE-2025-30154), a wcześniej od osobistego tokenu dostępu skradzionego przez workflow pull_request_target.
2. Workflow pull_request_target uruchamia niezaufany kod
pull_request_target działa w kontekście repozytorium bazowego, z jego sekretami i tokenem z prawem zapisu, nawet gdy pull request pochodzi z forka. Jest to bezpieczne tylko dopóty, dopóki workflow nigdy nie uruchamia kodu z pull requesta. Gdy workflow pobiera head pull requesta i go buduje, każdy, kto może otworzyć pull request, może odczytać wszystkie sekrety workflow. GitHub Security Lab nazywa je „pwn requests”.
GitHub stopniowo to zawęża. Od grudnia 2025 roku pull_request_target zawsze używa pliku workflow z gałęzi domyślnej. Od lipca 2026 roku actions/checkout odmawia pobrania kodu z forka w workflowach pull_request_target używających znanych niebezpiecznych wzorców, a mechanizmy ochrony wykonywania workflowów, ogólnie dostępne od września 2026 roku, od 2 listopada 2026 roku będą domyślnie wyłączać pull_request_target w repozytoriach publicznych bez polityki zdarzeń.
3. Niezaufane dane wejściowe trafiają do skryptu
Wyrażenia workflow są rozwijane przed uruchomieniem powłoki. Krok taki jak ten:
- run: echo "Title: ${{ github.event.issue.title }}"
wykonuje wszystko, co zawiera tytuł zgłoszenia. Atakujący otwiera zgłoszenie z poleceniem powłoki w tytule, a zadanie je uruchamia, mając sekrety zadania w zasięgu. Niezaufane wartości należy przekazywać przez zmienną środowiskową, aby powłoka traktowała je jako dane:
- env:
TITLE: ${{ github.event.issue.title }}
run: echo "Title: $TITLE"
4. Cache i artefakty wynoszą je na zewnątrz
- Artefakty. W 2024 roku badanie ArtiPACKED zespołu Unit 42 pokazało, że
actions/checkoutdomyślnie zapisujeGITHUB_TOKENw.git/config, więc wysłanie katalogu roboczego jako artefaktu ujawnia token każdemu, kto może go pobrać.upload-artifactod września 2024 roku domyślnie wyklucza pliki ukryte, aactions/checkoutv6 przechowuje poświadczenie poza katalogiem checkoutu, ale starsze przypięte wersje i niestandardowe kroki wysyłania nadal powodują wycieki. - Cache. Atakujący, który może uruchomić kod w dowolnym workflow repozytorium, może zapełnić jego cache, wypchnąć prawdziwe wpisy i podłożyć zatruty wpis pod tym samym kluczem. Późniejszy uprzywilejowany workflow go przywraca. W maju 2026 roku ten łańcuch, zaczynający się od
pull_request_targeti kończący tokenem OIDC odczytanym z pamięci runnera, pozwolił atakującemu opublikować 84 złośliwe wersje 42 pakietów TanStack.
5. Zależność wysyła je na serwer
Skrypty instalacyjne działają z takim samym dostępem jak reszta zadania. Robak Shai-Hulud, po raz pierwszy zaobserwowany w npm we wrześniu 2025 roku, działał podczas npm install, zbierał tokeny npm, tokeny GitHub i klucze chmurowe ze środowiska i publikował je w publicznych repozytoriach GitHub. Jego fala z listopada 2025 roku działała na etapie preinstall i rejestrowała zainfekowaną maszynę jako runner self-hosted, aby atakujący mógł wrócić później.
Ta droga nie wymaga żadnego błędu w Państwa workflow. Wystarczy jedna przejęta wersja gdziekolwiek w drzewie zależności, a sekrety wychodzą przez sieć.
Jak je zabezpieczyć
Zastąpić przechowywane klucze chmurowe przez OIDC. Zamiast trzymać klucz AWS, Azure lub Google Cloud jako sekret, zadanie może poprosić wydawcę OIDC w GitHub o krótkotrwały token i wymienić go u dostawcy chmury. Polityka zaufania decyduje, które repozytorium, gałąź lub środowisko może przyjąć rolę, a poświadczenie wygasa wraz z zadaniem. Zadanie potrzebuje permissions: id-token: write. Tokeny OIDC nadal można ukraść z działającego zadania, jak pokazał przypadek TanStack, ale skradziony token jest wart znacznie mniej niż klucz, który nigdy nie wygasa.
Przypinać akcje do pełnego SHA commita. Dokumentacja GitHub nazywa to jedynym sposobem używania akcji jako niezmiennego wydania. Tagi można przenieść, a SHA nie. Od sierpnia 2025 roku polityka dozwolonych akcji może wymuszać przypinanie do SHA i blokować konkretne akcje w całej organizacji. Dependabot i Renovate mogą na bieżąco aktualizować przypięte SHA.
Dawać GITHUB_TOKEN minimum potrzebnych uprawnień. Ustawić domyślnie tylko odczyt i przyznawać zakresy zapisu dla poszczególnych zadań. Nowe organizacje i repozytoria mają domyślnie tylko odczyt od lutego 2023 roku, ale starsze zachowały liberalne ustawienie domyślne, o ile nikt go nie zmienił. Więcej w artykule Uprawnienia GITHUB_TOKEN: najmniejsze uprawnienia dla każdego workflow.
Zawężać sekrety do kroku, który ich potrzebuje. Sekret w env: kroku znajduje się tylko w środowisku tego kroku. Sekret ustawiony na poziomie zadania jest widoczny dla każdego kroku, łącznie z każdą akcją zewnętrzną.
Umieszczać sekrety produkcyjne za środowiskami. Wymagani recenzenci i reguły gałęzi wdrożeniowych sprawiają, że workflow na gałęzi funkcjonalnej lub uruchomiony przez pull request nie może ich odczytać.
Traktować pull_request_target i niezaufane dane jak wykonanie kodu. Unikać pull_request_target, o ile nie jest potrzebny, nigdy nie budować w nim kodu z pull requesta i przekazywać dane zdarzeń przez zmienne środowiskowe. CodeQL potrafi sprawdzać workflowy pod kątem wstrzyknięć i wzorców pwn request; dla GitHub Actions jest ogólnie dostępny od kwietnia 2025 roku.
Włączyć secret scanning i push protection. Wychwytują sekrety zatwierdzone w repozytorium, a to inny rodzaj wycieku niż powyższe, ale równie powszechny.
Ograniczyć, dokąd zadanie może się łączyć. Większość zadań potrzebuje krótkiej listy celów: rejestrów pakietów, rejestrów kontenerów i kilku API. Jeśli zadanie może sięgać tylko tam, sekret odczytany przez złośliwy kod nie ma dokąd trafić. Więcej w artykule jak polityka egress zatrzymuje eksfiltrację z GitHub Actions.
Rotować po każdej ekspozycji. Jeśli workflow uruchomił przejętą akcję lub pakiet, należy założyć, że skradziono każdy sekret, który mógł odczytać. Trzeba unieważnić i wydać na nowo wszystkie, a nie tylko te, które prawdopodobnie zostały użyte.
Gdzie pasuje CRACI
CRACI uruchamia Państwa zadania GitHub Actions na własnych runnerach: wystarczy zmienić runs-on na craci, a workflowy, sekrety i historia uruchomień pozostają w GitHub. Dwie funkcje są wymierzone w opisane wyżej wycieki.
- Polityka egress, która w razie awarii blokuje ruch (fail closed). Określają Państwo, z którymi źródłami pakietów, rejestrami i hostami zadanie może się łączyć. Polityka jest walidowana przed startem zadania i egzekwowana na runnerze, więc połączenie z czymkolwiek innym jest blokowane, a przy naruszeniu polityki otrzymują Państwo alert.
- Zapis tego, co zrobiło zadanie. CRACI rejestruje każdy pakiet, który każde zadanie faktycznie pobrało, łącznie z pakietami przywróconymi z cache, oraz ślad połączeń sieciowych zadania. Gdy zostanie ogłoszony kolejny przejęty pakiet, mogą Państwo zobaczyć, które buildy go pobrały i z czym się łączyły, zamiast zgadywać na podstawie lockfile.
Każde zadanie działa we własnej maszynie wirtualnej, więc nic, co pozostawi jedno zadanie, nie czeka na następne. CRACI nie skanuje Państwa kodu pod kątem sekretów; do tego należy zachować secret scanning w GitHub.
Prosimy umówić demo, aby zobaczyć zapis jednego z Państwa buildów.
