Każdy użytkownik z uprawnieniami zapisu do repozytorium może odczytać wszystkie skonfigurowane w nim sekrety, dlatego poświadczenia używane przez Twoje workflow powinny mieć najmniejsze uprawnienia, na jakie pozwala dane zadanie. Dokumentacja GitHub o bezpiecznym użyciu mówi to samo o GITHUB_TOKEN: nadawaj mu minimalne wymagane uprawnienia. Pięć poniższych zadań zajmie po jednym dniu każde.
OWASP Top 10 CI/CD Security Risks wskazuje obszary problemów, których dotyka ten plan: niewłaściwe zarządzanie tożsamością i dostępem (CICD-SEC-2), nadużycia w łańcuchu zależności (CICD-SEC-3), poisoned pipeline execution (CICD-SEC-4), niedostateczna higiena poświadczeń (CICD-SEC-6) oraz niewłaściwa weryfikacja integralności artefaktów (CICD-SEC-9).
Poniedziałek: ogranicz to, co może zrobić token
Wyjdź z założenia, że każdy, kto może wypchnąć zmiany do repozytorium, może odczytać jego sekrety. Następnie zawęź to, do czego te poświadczenia mają dostęp.
GitHub zaleca, aby domyślne uprawnienie GITHUB_TOKEN do zawartości repozytorium ustawić na tylko do odczytu, a następnie podnosić je dla poszczególnych jobów. W pliku workflow wygląda to tak:
permissions:
contents: read
jobs:
release:
permissions:
contents: write
Przejdź przez każdy workflow i zadaj sobie pytanie, które joby naprawdę coś zapisują. Uprawnienia zapisu daj tylko im, a pozostałe zostaw na odczycie.
Wtorek: zrób audyt akcji, które uruchamiasz
Wypisz każdą akcję używaną w Twoich workflow, w tym te wywoływane z reusable workflows, którymi zarządzasz. Dla każdej z nich przeczytaj kod źródłowy akcji oraz repozytorium, które ją uruchamia.
Wytyczne GitHub dotyczące audytu sekretów jasno mówią, na co zwracać uwagę. Sprawdź, czy sekrety nie są wysyłane do niezamierzonych hostów i czy nie są jawnie wypisywane w logach.
Następnie zastosuj zasadę najmniejszych uprawnień do samych poświadczeń. Skoro dostęp zapisu do repozytorium oznacza dostęp do odczytu jego sekretów, każde poświadczenie używane w workflow powinno pozwalać wyłącznie na to, czego ten workflow potrzebuje. Token wdrożeniowy, który może administrować całym kontem w chmurze, nie przechodzi tego testu.
Środa: script injection
Niezaufane wartości z kontekstu, takie jak tytuł pull requesta, stają się problemem, gdy trafiają bezpośrednio do skryptu inline. Dokumentacja GitHub podaje dwa sposoby, jak tego uniknąć.
Zalecane podejście to akcja, która przyjmuje wartość z kontekstu jako argument, ponieważ wtedy wartość nigdy nie jest używana do wygenerowania skryptu powłoki. Jeśli musisz zostać przy skrypcie inline, przekaż wyrażenie przez pośrednią zmienną środowiskową:
- name: Check PR title
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ "$TITLE" =~ ^octocat ]]; then exit 0; else exit 1; fi
W przykładzie GitHub tytuł a"; ls $GITHUB_WORKSPACE" nie przechodzi sprawdzenia, a polecenie nie zostaje uruchomione. Wartość jest przechowywana w pamięci i używana jako zmienna, więc nie wpływa na generowanie skryptu. GitHub sugeruje też ujmowanie zmiennych powłoki w podwójne cudzysłowy, aby uniknąć word splittingu, co jest ogólną praktyką w pracy z powłoką.
Aby znaleźć kandydatów do poprawy, wyszukaj wyrażenia z kontekstu w swoich workflow i przeczytaj każde trafienie w bloku run::
grep -rn 'github.event' .github/workflows
Czwartek: sekrety i chronione środowiska
Umieść sekrety wdrożeniowe w środowiskach (environments) i wymagaj recenzentów. Job workflow nie ma dostępu do sekretów środowiska, dopóki recenzent go nie zatwierdzi.
GitHub jasno zaznacza, że automatyczne maskowanie nie jest gwarantowane, ponieważ wartość sekretu można przekształcić na wiele sposobów. Maskowanie opiera się w dużej mierze na znalezieniu dokładnego dopasowania i wykonuje je runner, więc sekret zostanie zamaskowany tylko wtedy, gdy job go użył i runner miał do niego dostęp. Potraktuj poniższą tabelę jako listę kontrolną przeglądu:
| Element przeglądu | Pytanie, na które trzeba odpowiedzieć |
|---|---|
| Zarejestrowane sekrety | Czy każdy z nich jest nadal potrzebny i kiedy był ostatnio rotowany? |
| Sekrety strukturalne | Czy któryś sekret jest blobem JSON, XML lub YAML? Podziel go na osobny sekret dla każdej wartości. |
| Wartości pochodne | Czy każda wartość zakodowana w Base64 lub URL-encoded albo wygenerowana (na przykład podpisany JWT) jest zarejestrowana jako sekret? |
| Inne dane wrażliwe | Czy dane, które nie są sekretem GitHub, są maskowane przez ::add-mask::VALUE? |
| Logi uruchomień | Czy po przetestowaniu poprawnych i niepoprawnych danych wejściowych sekrety pojawiają się w logach wyjścia lub błędów? |
| Środowiska | Czy sekrety produkcyjne są chronione wymaganymi recenzentami? |
Błędy mogą trafić do STDOUT i STDERR w sposób, którego nie planowałeś, więc po testach przeczytaj logi ręcznie. Jeśli niezamaskowany sekret trafi do logu uruchomienia, usuń log i zrotuj sekret. Usuń sekrety, które nie są już potrzebne, a pozostałe rotuj okresowo, co skraca czas, przez jaki skompromitowana wartość pozostaje ważna.
Piątek: odpowiedzialność za workflow i ich wyniki
Ta sekcja to nasza rada. Powyższe źródła jej nie zalecają i nie przypisujemy jej GitHub, OWASP ani NIST.
Ustal, kto może zmieniać workflow, które budują i wydają Twoje oprogramowanie, i zapisz to. Jeśli na swojej platformie używasz CODEOWNERS, rozsądną ścieżką do objęcia jest katalog .github/workflows. Następnie wskaż, który workflow tworzy artefakty wydania, bo to im będą ufać klienci lub systemy produkcyjne.
W kwestii artefaktów źródła idą tylko częściowo. OWASP wymienia niewłaściwą weryfikację integralności artefaktów jako CICD-SEC-9, a niedostateczne logowanie i widoczność jako CICD-SEC-10. Secure Software Development Framework (SP 800-218) od NIST dostarcza wspólnego słownika, którym nabywcy i dostawcy mogą posługiwać się w rozmowach o bezpiecznym wytwarzaniu oprogramowania. Żadne z nich nie podaje gotowej recepty na podpisywanie, więc ją pomijamy.
Na koniec ustal, kto zauważy zmiany w plikach workflow, sekretach lub zatwierdzeniach środowisk i w jaki sposób się o nich dowie.
Od czego zacząć
- Ustaw domyślne uprawnienie
GITHUB_TOKENdo zawartości repozytorium na tylko do odczytu i dodaj blokipermissionsdo jobów, które potrzebują więcej. - Wypisz wszystkie używane akcje, przeczytaj ich kod źródłowy i sprawdź, czy nie wysyłają sekretów do nieoczekiwanych hostów ani nie wypisują ich w logach.
- Wyszukaj wyrażenia z kontekstu w blokach
run:i przenieś je do pośredniej zmiennej środowiskowej lub do akcji. - Przenieś sekrety produkcyjne do środowiska z wymaganymi recenzentami, podziel sekrety strukturalne na pojedyncze wartości, zarejestruj wartości pochodne i zrotuj wszystko, co pojawiło się w logu.
- Zapisz, kto może zmieniać workflow budujące artefakty wydania i kto jest informowany o tych zmianach.
Jeśli chcesz uzyskać drugą opinię o swoich pipeline'ach, nasze prace nad bezpiecznym wytwarzaniem oprogramowania obejmują konfigurację CI/CD. Test penetracyjny może pokazać, czy pull request jest w stanie dotrzeć do Twoich sekretów.
