Instinktet er rigtigt, og løsningen er forkert. Du opdager en nøgle i koden, fjerner den, committer, og filen er ren. Hemmeligheden ligger stadig i din git-historik, præcis lige så læsbar som før — den er bare flyttet fra nutiden ind i fortiden.

Hvorfor en slettet hemmelighed bliver i git-historikken

Git gemmer ikke en række ændringer. Hver commit refererer til et fuldstændigt øjebliksbillede af projektet på det tidspunkt, og hvert eneste af de øjebliksbilleder bliver liggende i repositoryet. Når du sletter en linje og committer, har du tilføjet et nyt øjebliksbillede, hvor linjen mangler. Hvert tidligere øjebliksbillede er urørt.

git log -p, eller en diff af den commit, der fjernede den, viser nøglen i fuld længde. Det samme gør en klon lavet af hvem som helst på et hvilket som helst tidspunkt. Nøglen er ikke skjult og ikke udfaset — den er én kommando væk, permanent.

Det bliver værre i det almindelige tilfælde frem for det usædvanlige. Er repositoryet offentligt, blev det klonet og spejlet automatisk inden for minutter efter dit push, af tjenester, der findes for at indeksere offentlig kode. Nogle af dem drives af forskere; andre gør ikke. Er det privat, er eksponeringen mindre, men stadig reel: hver person og hvert automatiske system, der nogensinde har haft læseadgang, har en kopi i den klon, de lavede.

Den eneste løsning, der virker

Udskift nøglen. Tilbagekald den gamle hos udbyderen og udsted en ny. Det er løsningen. Alt andet er oprydning.

Det er værd at sige helt firkantet, for når fjernelsen bliver lavet af en AI-assistent, melder den meget ofte problemet som løst, så snart linjen er væk fra arbejdstræet — hvilket er sandt om filen og falsk om hemmeligheden. Indsætter du et fund i en assistent, er den instruktion, der skal med: at fjerne en nøgle fra koden er ikke det samme som at udskifte den, så sig, at den skal udskiftes hos udbyderen.

Udskiftning er også det eneste skridt, hvis virkning ikke afhænger af at vide, hvem der har set nøglen. Du kan ikke opremse klonerne. Du kan ugyldiggøre nøglen.

Hvad omskrivning af historikken opnår — og ikke opnår

Der findes værktøjer til at fjerne en hemmelighed fra hver eneste commit — git filter-repo er den nuværende anbefaling, og BFG Repo-Cleaner er den ældre, folk stadig griber efter — og efter en omskrivning vil en frisk klon ikke indeholde den. Det er reelt nyttigt til at mindske fremtidig eksponering, men forstå, hvad det ikke gør:

  • Det påvirker ikke eksisterende kloner. Alle, der allerede har hentet, har den oprindelige historik.
  • Det påvirker ikke forks, og på hostede platforme forbliver de gamle commits typisk tilgængelige via deres hash på ubestemt tid efter et force-push — på GitHub kræver det, at man kontakter support at få dem fjernet, og alt i et repositorys fork-netværk er fortsat tilgængeligt fra forkene.
  • Det omskriver hver commit-hash fra ændringen og frem, hvilket knækker hver branch, hver åben pull request og hver build-reference nedstrøms. På et delt repository er det en koordineret operation, ikke en hurtig løsning.

Gør det efter udskiftningen, hvis repositoryet fortsat skal bruges, og du vil have historikken ren. Aldrig i stedet for udskiftningen.

Hvorfor "vi slettede den" er sit eget detektionsproblem

Der er en praktisk konsekvens for værktøjer, som er nem at overse. En scanner, der kun læser arbejdstræets nuværende tilstand — filerne, som de er nu — kan slet ikke se noget af det her. Den melder repositoryet som rent, helt korrekt, og overser hele klassen.

At opdage det betyder at læse commit-diffs og ikke bare filer, og det brugbare design er at holde de to fund adskilt: meld en nøgle, der stadig findes i arbejdstræet, som én slags fund, og en nøgle, der kun ligger i historikken, som en anden. Den anden er præcis "vi slettede linjen og gik ud fra, at så var den ude af verden"-tilfældet, og den fortjener sit eget navn, fordi rådgivningen er forskellig — den første kræver en kodeændring og en udskiftning, den anden kræver kun udskiftningen.

Sådan forhindrer du den næste

Hold nøgler helt ude af repositoryet. Miljøvariabler, en secret manager eller en .env-fil, der ligger i .gitignore fra den allerførste commit — ikke tilføjet senere, for det er sådan, filen bliver committet én gang, før nogen opdager det.

Tjek pull requests, ikke kun main. Det er det mest værdifulde tidspunkt. En hemmelighed fanget i en pull request bliver rettet ved at omskrive en branch, ingen andre har hentet; den samme hemmelighed fanget efter merge ligger i historikken for altid og kræver en udskiftning. Forskellen på de to omkostninger er enorm — og tjekket er det samme tjek.

Tjek pull requests med et workflow, du har hærdet. Selve tjekket kører i CI, så det er værd at læse hvad den pipeline kan nå, før man stoler på resultatet.

Commit en eksempelfil i stedet. En .env.example med variabelnavnene og åbenlyst falske værdier giver nye udviklere det, de har brug for, uden at en rigtig nøgle nogensinde er i nærheden af repositoryet.

Gå ud fra, at detektion er ufuldkommen, og foretræk kortlivede nøgler. Nøgler, der udløber af sig selv, mindsker værdien af hver eneste læk — også dem, ingen finder.

Jeg har slettet en committet API-nøgle — er den sikker nu?

Nej. Git gemmer et øjebliksbillede for hver commit, så at slette linjen tilføjer et nyt øjebliksbillede uden den, mens hvert tidligere øjebliksbillede beholder nøglen i fuld længde. Hvem som helst kan læse den med en enkelt kommando, og alle, der allerede har klonet repositoryet, har den. Den eneste løsning, der virker, er at udskifte nøglen: tilbagekald den gamle hos udbyderen og udsted en ny. At fjerne den fra koden er oprydning, ikke udbedring.

Fjerner omskrivning af git-historikken en lækket hemmelighed?

Kun delvist. Omskrivningen fjerner hemmeligheden fra repositoryet, så en frisk klon ikke indeholder den, men den påvirker ikke kloner, der allerede findes, den når ikke forks, og på hostede platforme forbliver de gamle commits typisk tilgængelige via hash på ubestemt tid efter et force-push, medmindre man beder platformen fjerne dem. Den omskriver også hver commit-hash fra det punkt og frem og knækker åbne branches og pull requests. Gør det efter udskiftningen, hvis du vil have historikken ren — aldrig i stedet for.

Hvordan undgår jeg, at hemmeligheder bliver committet?

Hold nøgler helt ude af repositoryet: brug miljøvariabler eller en secret manager, og læg .env-filen i .gitignore i den allerførste commit og ikke senere, eftersom det at tilføje den senere er præcis dét, der gør, at filen bliver committet én gang, før nogen opdager det. Commit en .env.example med variabelnavne og åbenlyst falske værdier, så nye udviklere har det, de skal bruge. Scan derefter pull requests, for en hemmelighed fanget før merge koster en branch-omskrivning i stedet for en udskiftning.

Hvorfor overser scannere hemmeligheder, der er slettet?

Fordi de fleste scannere læser arbejdstræets nuværende tilstand — filerne, som de er nu — og en slettet nøgle står ikke i nogen nuværende fil. Den ligger i commit-historikken, hvilket kræver, at man læser diffs og ikke bare filer. En scanner, der kun kigger på arbejdstræet, vil helt korrekt melde repositoryet rent, mens nøglen fortsat er én kommando væk for enhver med en klon.

Hvor CodeControl passer ind

CodeControl kører to gennemgange over hver sin flade: arbejdstræet og den nyere commit-historik. De holdes adskilt — historik-fundet melder kun nøgler, arbejdstræet ikke længere indeholder, hvilket er præcis "vi slettede linjen"-tilfældet. Det tjekker også hver pull request mod de linjer, den tilføjer, og sender resultatet tilbage som et check, så hemmeligheden bliver fanget, mens rettelsen stadig er en branch-omskrivning. Selve værdien bliver aldrig gemt: et fund bærer filen, linjen, detektoren og et maskeret uddrag.