Przejdź do treści
CodeAuditAgent
Wszystkie artykuły

Sekrety w historii Gita: znajdź, zrotuj, zapobiegaj

Usunięcie zacommitowanego klucza API nie usuwa go z historii Gita. Jak znaleźć wyciekłe sekrety, zrotować je, przepisać historię i zapobiec kolejnym wyciekom.

· 7 min czytania · Lina Source LLC

Ktoś commituje plik .env albo wkleja działający klucz API do modułu konfiguracji, żeby coś szybko sprawdzić. Recenzent to zauważa, klucz zostaje usunięty w kolejnym commicie i wszyscy idą dalej. Klucz nadal tam jest. Git przechowuje każdą wersję każdego pliku, a każdy, kto może sklonować repozytorium, może odczytać commit, który go dodał.

Zakodowane na sztywno poświadczenia są śledzone jako CWE-798. Ten przewodnik opisuje, co robić, gdy takie poświadczenie już trafiło do historii: znaleźć wszystkie sekrety, zrotować je przed czymkolwiek innym, zdecydować, czy przepisanie historii jest tego warte, i ustawić zabezpieczenia, które zatrzymają następny wyciek.

Dlaczego usunięcie linii nie wystarczy

Commit usuwający sekret dodaje nową migawkę bez niego. Poprzednia migawka, z sekretem, wciąż jest wskazywana przez historię gałęzi. git log -p ją pokazuje, git checkout starego commita ją przywraca, a każdy istniejący klon i fork już ma kopię. Spłaszczenie gałęzi przed scaleniem też nie pomoże, jeśli pierwotne commity kiedykolwiek zostały wypchnięte: zdalne repozytorium może je nadal przechowywać, a każdy, kto je pobrał, ma je lokalnie.

W repozytorium publicznym zakładaj, że sekret został zobaczony. Automatyczne skrypty obserwują publiczne pushe pod kątem wzorców poświadczeń, a okno między pushem a pierwszym nadużyciem bywa bardzo krótkie. W repozytorium prywatnym ekspozycja jest mniejsza, ale nie zerowa: ma go każdy pracownik, kontraktor, system CI i integracja z dostępem do odczytu.

Krok 1: najpierw zrotuj

Rotacja to jedyny krok, który faktycznie usuwa ryzyko. Przepisanie historii ukrywa sekret przed przyszłymi klonami; nie robi nic z kopiami, które już istnieją. Dlatego unieważnij i wymień poświadczenie, zanim tkniesz repozytorium.

Zaplanuj rotację tak, żeby nie wywołała awarii. Wielu dostawców pozwala, by dwa klucze były ważne jednocześnie: utwórz nowy klucz, wdróż go wszędzie tam, gdzie używany był stary, potwierdź, że ruch się przeniósł, a potem unieważnij stary. Jeśli dostawca dopuszcza tylko jeden klucz, pogódź się z krótką przerwą; kilka minut niedostępności to lepszy wybór niż pozostawianie aktywnego poświadczenia, o którym wiadomo, że wyciekło, podczas koordynowania idealnego przełączenia.

  • Wygeneruj nowy klucz w konsoli dostawcy i wdróż go swoją zwykłą ścieżką konfiguracji.
  • Unieważnij stary klucz. Nie wystarczy przestać go używać; nieużywany ważny klucz nadal jest ważnym kluczem.
  • Sprawdź logi audytu dostawcy pod kątem aktywności starego klucza od daty commita: wywołania API, nowi użytkownicy, zmienione uprawnienia, nieoczekiwane rozliczenia.
  • Jeśli sekretem było hasło do bazy danych albo klucz podpisujący, zastanów się, co mógł odblokować. Wyciekły sekret podpisujący JWT oznacza, że każdy token mógł zostać sfałszowany, więc unieważnij istniejące sesje.
  • Zapisz, co zostało ujawnione, na jak długo i co zmieniłeś. Przyda się, gdy zapyta o to klient lub audytor.

Krok 2: znajdź wszystko, co wyciekło

Tam, gdzie jest jeden zacommitowany sekret, często jest ich więcej. Przeszukaj całą historię, a nie tylko bieżące drzewo, i uwzględnij każdą gałąź oraz tag.

# Commity, które w całej historii dodały lub usunęły dany łańcuch
git log -p -S "sk_live_" --all

# Wyszukiwanie wyrażeniem regularnym w diffach wszystkich commitów
git log -p -G "AKIA[0-9A-Z]{16}" --all

# Pliki, które kiedykolwiek istniały pod podejrzaną ścieżką
git log --all --oneline -- .env config/production.json

# Dedykowane skanery sprawdzają setki znanych formatów poświadczeń
gitleaks git -v .
trufflehog git file://. --only-verified

gitleaks i trufflehog to skanery open source stworzone do tego zadania. Znają formaty kluczy popularnych dostawców, a trufflehog potrafi sprawdzić, czy znalezione poświadczenie jest wciąż aktywne. Starsze wydania gitleaks używają gitleaks detect --source . zamiast podkomendy git. Uruchom skaner raz na całej historii, a potem utrzymuj go na nowych commitach. Spodziewaj się fałszywych trafień, takich jak dane testowe czy przykładowe klucze w dokumentacji. Przejrzyj każde z nich, a potwierdzone fałszywe trafienia zapisz na liście wyjątków lub w pliku bazowym skanera, żeby kolejny przebieg pokazywał tylko nowe znaleziska.

Nie zapomnij o miejscach wokół repozytorium: logach CI, które wypisały zmienne środowiskowe, komentarzach w zgłoszeniach i pull requestach, stronach wiki, warstwach obrazów Dockera zbudowanych z repozytorium oraz gistach i wklejkach udostępnionych podczas debugowania.

Krok 3: zdecyduj, czy przepisywać historię

Po rotacji sekret jest bezużyteczny, więc przepisanie historii to raczej sprzątanie niż ograniczanie szkód. Wciąż warto to zrobić, gdy repozytorium jest publiczne, gdy sekret ujawnia coś poza sobą samym (wewnętrzną nazwę hosta, nazwę klienta) albo gdy wymagają tego przepisy. Ma to realne koszty: każdy współpracownik musi sklonować repozytorium od nowa, otwarte pull requesty zostają zaburzone, a skróty commitów się zmieniają. Wszystko, co odwołuje się do commita po skrócie, jak informacje o wydaniach, rejestry wdrożeń, linki w zgłoszeniach czy przypięte zależności w innych repozytoriach, będzie wskazywać commity, których na gałęzi już nie ma. Zapowiedz przepisanie z wyprzedzeniem, wybierz spokojny moment i najpierw scal lub zamknij otwarte pull requesty.

git filter-repo to narzędzie, które projekt Git zaleca do tego celu, w miejsce starszego git filter-branch. Działa na świeżym klonie i dla bezpieczeństwa usuwa zdalne repozytorium origin, więc przed wypchnięciem trzeba je dodać z powrotem.

# Zacznij od świeżego klonu
git clone [email protected]:acme/api.git api-clean
cd api-clean

# Wariant A: usuń plik z każdego commita
git filter-repo --invert-paths --path config/production.env

# Wariant B: zastąp łańcuchy sekretów wszędzie, gdzie występują
# replacements.txt zawiera jedną regułę w wierszu, na przykład:
#   PASTE_THE_OLD_KEY_HERE==>REDACTED
git filter-repo --replace-text ../replacements.txt

# filter-repo usuwa origin; dodaj go z powrotem i wypchnij na siłę
git remote add origin [email protected]:acme/api.git
git push --force --all
git push --force --tags

Czego przepisanie historii nie potrafi

  • Nie sięgnie do istniejących klonów, forków, pamięci podręcznych CI ani kopii zapasowych. Każdy, kto pobrał dane przed przepisaniem, zachowuje stare commity.
  • Na GitHubie referencje tworzone dla pull requestów są tylko do odczytu i mogą utrzymywać stare commity osiągalnymi. Usunięcie widoków z pamięci podręcznej i tych referencji wymaga kontaktu ze wsparciem GitHuba.
  • Współpracownicy, którzy pobiorą lub scalą dane ze starego klonu, mogą wypchnąć starą historię z powrotem. Poproś wszystkich o ponowne sklonowanie i na ten czas zabezpiecz gałąź.
  • Nie usuwa sekretu z żadnego innego miejsca, do którego został skopiowany: logów, zgłoszeń, wiadomości na czacie czy zbudowanych artefaktów.

Dlatego kolejność ma znaczenie. Najpierw rotacja, potem sprzątanie. Przepisana historia z działającym kluczem to złudne poczucie bezpieczeństwa. Po wypchnięciu uruchom skaner na świeżym klonie, żeby potwierdzić, że sekret naprawdę zniknął ze wszystkich gałęzi i tagów.

Krok 4: zapobiegnij następnemu

Celem jest utrudnić zacommitowanie sekretu i przyspieszyć zauważenie takiego zdarzenia. Żaden pojedynczy mechanizm nie robi obu rzeczy naraz, więc warstwuj je.

Trzymaj sekrety poza ścieżką kodu

Wczytuj poświadczenia ze zmiennych środowiskowych lub z menedżera sekretów w czasie działania i sprawdzaj przy starcie, czy te, których potrzebujesz, są obecne. Commituj plik .env.example z wartościami zastępczymi, a .env umieść w .gitignore od pierwszego commita. Dla produkcji menedżer sekretów, taki jak AWS Secrets Manager, Google Secret Manager, HashiCorp Vault albo szyfrowane ustawienia środowiskowe Twojej platformy, daje kontrolę dostępu, logi audytu i łatwiejszą rotację.

Blokuj sekrety, zanim zostaną zacommitowane

Hook pre-commit wychwytuje pomyłkę na maszynie programisty, zanim w ogóle dotrze ona do zdalnego repozytorium. Framework pre-commit sprowadza to do kilku linii konfiguracji, którą może zainstalować każdy współpracownik.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: vX.Y.Z  # przypnij do najnowszego taga wydania
    hooks:
      - id: gitleaks

# Instalacja raz na klon
#   pip install pre-commit
#   pre-commit install

Hooki są lokalne i można je pominąć, więc jako zabezpieczenie uruchamiaj ten sam skaner w CI przy każdym pushu. Na GitHubie włącz skanowanie sekretów i ochronę pushy tam, gdzie Twój plan to obsługuje; ochrona pushy odrzuca po stronie serwera pushe zawierające rozpoznane tokeny dostawców.

Zmniejsz szkody z wycieków, których nie zauważysz

  • Ogranicz każdy klucz do minimalnych uprawnień, a tam, gdzie dostawca na to pozwala, do konkretnych adresów IP lub referrerów.
  • Wybieraj poświadczenia krótkożyciowe, takie jak federacja OIDC z CI do dostawcy chmury, zamiast długowiecznych kluczy statycznych.
  • Używaj osobnych kluczy dla każdego środowiska, żeby wyciekły klucz testowy nie mógł dotknąć produkcji.
  • Prowadź instrukcję rotacji dla każdego krytycznego sekretu, żeby rotacja pod presją była rutyną, a nie improwizacją.

Gdzie w tym wszystkim jest przegląd kodu

Skanery dobrze dopasowują znane formaty; słabiej radzą sobie z kontekstem, takim jak hasło składane z dwóch stałych czy domyślne poświadczenie w awaryjnej gałęzi konfiguracji. Przejrzenie kodu wychwytuje takie przypadki. CodeAuditAgent oznacza zakodowane na sztywno poświadczenia jako CWE-798 z zacytowanym dowodem, gdy audytuje domyślną gałąź publicznego repozytorium albo wklejony fragment kodu. Czyta bieżący kod, a nie historię commitów, więc do przeszłych commitów zostaw na miejscu skaner historii.

Krótko: zacommitowany sekret to sekret wyciekły. Zrotuj go dzisiaj, posprzątaj historię, jeśli warto ponieść koszty, i spraw, by następny wyciek zatrzymał się na hooku pre-commit, a nie na publicznej stronie.