Token, który umarł, gdy nikt nie wdrażał
Krótka historia detektywistyczna ze świata CI/CD.
Objaw
Pull request zostaje scalony i rusza potok wdrożeniowy. Wszystko zielone: siedem zbudowanych obrazów Dockera, testy jednostkowe, testy integracyjne, pełny zestaw testów end-to-end w przeglądarce. A potem ostatni job, ten, który przekazuje wydanie do repozytorium GitOps, robi się czerwony po niecałej sekundzie:
RequestError [HttpError]: Bad credentials status: '401' url: 'https://api.github.com/repos/<org>/<gitops-repo>/dispatches'
Kod był w porządku. I tak nigdy nie miał trafić do środowiska dev.
Kształt potoku
Projekt dzieli „budowanie” i „wdrażanie” na dwa repozytoria, co jest typowym układem GitOps:
repo aplikacji repo GitOps ┌──────────────────────────┐ ┌──────────────────────────┐ │ budowa obrazów │ │ podbicie wersji obrazu │ │ uruchomienie testów │ │ dla env=dev|stg|prod │ │ trigger_the_deployment ──┼── POST ───►│ ArgoCD synchronizuje │ │ repository_dispatch │ (token) │ klaster │ └──────────────────────────┘ └──────────────────────────┘
Ostatni krok wysyła zdarzenie repository_dispatch do repo GitOps z małym ładunkiem JSON: które środowisko, która usługa, która wersja. Wywołanie API innego repozytorium wymaga poświadczenia sięgającego poza repo, w którym działa workflow. Wbudowany GITHUB_TOKEN tego nie potrafi, więc lata temu ktoś utworzył personal access token i zapisał go jako sekret.
Zły zakręt: uruchomienie, którego nie było
Pierwsza próba przeczytania logów nieudanego joba z terminala zwróciła to:
failed to get run: HTTP 404: Not Found
Uruchomienie było wyraźnie widoczne w przeglądarce. Repozytorium jednak nie zniknęło. CLI gh miało zalogowane trzy konta GitHuba, a aktywne należało do innej organizacji. GitHub dla prywatnych repo, których nie widzisz, odpowiada 404 zamiast 403, więc „brak dostępu” wygląda dokładnie jak „nie ma czegoś takiego”. Uruchomienie tego jednego polecenia z tokenem innego konta, bez globalnego przełączania czegokolwiek, rozwiązało sprawę:
GH_TOKEN=$(gh auth token -u <right-account>) gh run view --job <id> --log-failed
Drobiazg, ale to już drugi raz, kiedy dokładnie to przebranie kosztowało czas na tej maszynie.
Właściwe czytanie 401
Kod statusu mówi więcej niż komunikat. GitHub zwraca:
- 401 Bad credentials, kiedy odrzucony zostaje sam token: wygasły, unieważniony, źle sformatowany albo jego właściciel zniknął;
- 403 albo 404, kiedy token jest ważny, ale nie wolno mu zrobić tego temu repozytorium.
To więc nie była łamigłówka z uprawnieniami. Nikt nie musiał porównywać zakresów. Token był po prostu martwy.
Zwrot akcji: oś czasu
Historia uruchomień wyjaśniła, jak umarł niezauważony:
ostatnie udane wdrożenie ──────────── ~11 miesięcy, zero uruchomień ───────────► pierwsza porażka
● ✗ ✗ ✗
Projekt był cicho przez prawie rok. Token przestał działać gdzieś w tej przerwie i nic nie próbowało go użyć aż do pierwszego scalenia po przerwie. Wszystkie testy przeszły, a padł sam ostatni krok.
Drugim zwrotem było szybkie przeszukanie plików workflow. Ten sam sekret zasilał workflow stagingu (dwa razy, ścieżka standardowa i hotfix) oraz workflow produkcji, które uruchamiają się tylko przy tagu i wydaniu. Od przerwy nikt ich też nie uruchomił. Głośna porażka dev to była ta szczęśliwa wersja: następne wydanie produkcyjne zbudowałoby wszystko, a potem niczego nie wdrożyło.
Poprawka: przestać w ogóle posiadać token
Oczywistą poprawką jest nowy personal access token. Właściciel obciążenia tego nie chciał i słusznie. PAT zawsze uwierzytelnia się jako jeden człowiek, a długowieczny PAT to dokładnie to, jak do tego w ogóle doszło.
Zamiennikiem jest GitHub App, najbliższy odpowiednik konta serwisowego, jaki ma GitHub:
- Utwórz ją na poziomie organizacji, nie w repozytorium. Webhook wyłączony, bez OAuth i dokładnie jedno uprawnienie: Contents: Read and write, którego wymaga
repository_dispatch. - Zainstaluj ją tylko w repozytorium docelowym (GitOps).
- Zapisz jej ID i klucz prywatny jako sekrety w repozytorium źródłowym (aplikacji), tym, które uruchamia workflow.
- Przed każdym dispatchem wybij krótkotrwały token, ograniczony do tego jednego docelowego repo:
- name: Generate the GitOps token
id: gitops_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: <org>
repositories: <gitops-repo>
- name: Emit repository_dispatch
uses: <dispatch-action>
with:
token: ${{ steps.gitops_token.outputs.token }}
Token żyje godzinę i jest tworzony od nowa przy każdym uruchomieniu. Nie zostaje nic długowiecznego, co mogłoby po cichu wygasnąć w trakcie następnego cichego roku. Wszystkie cztery kroki dispatch dostały zmianę w jednym pull requeście.
Ostatnia niespodzianka: scalenie poprawki niczego nie wdrożyło
PR został scalony. Żaden potok nie ruszył.
Workflow dev miał paths-ignore: [".github/**", ...], co jest rozsądne, bo edycja konfiguracji CI nie powinna ponownie wdrażać aplikacji. Ale ten PR dotykał tylko .github/. Poprawka workflow wdrożeniowego była dokładnie tym rodzajem zmiany, który workflow wdrożeniowy ignoruje. Przetestowanie jej wymaga ręcznego uruchomienia workflow_dispatch albo następnej prawdziwej zmiany w kodzie. W chwili pisania to uruchomienie wciąż czeka, więc historia uczciwie kończy się na „powinno działać”, a nie „działa”.
Wskazówki dla czytelnika
- Czytaj kod statusu przed komunikatem. 401 znaczy „wymień poświadczenie”. 403/404 znaczy „napraw dostęp”. Wymagają różnych poprawek.
- Długowieczne tokeny umierają w cichych okresach. Jeśli krok potoku od takiego zależy, dowie się o tym pierwsze uruchomienie po przerwie. Wybieraj poświadczenia wybijane na każde uruchomienie (tokeny GitHub App, OIDC) zamiast przechowywanych.
- Kiedy psuje się poświadczenie jednego środowiska, wyszukaj każde inne miejsce, gdzie jest używane. Środowiska, które wdrażasz najrzadziej, odkryjesz na końcu, zwykle w dniu wydania.
GITHUB_TOKENzatrzymuje się na granicy repo. Przekazania między repozytoriami potrzebują prawdziwej tożsamości. Niech to będzie App zainstalowana tylko w repo docelowym, z jedynym uprawnieniem, jakiego potrzebuje wywołanie.- Filtry ścieżek w CI dotyczą też poprawek CI. Jeśli workflowy wdrożeniowe ignorują
.github/**, poprawka jednego z nich nie uruchomi się sama. Zachowaj wyzwalaczworkflow_dispatchi zaplanuj ręczne uruchomienie. - 404 z CLI na czymś, co widzisz w przeglądarce, to zwykle złe konto, a nie brakujący zasób.