Secrets und große Dateien sauber aus der Historie entfernen
Ein versehentlich committetes Datenbank-Passwort oder eine mehrere Gigabyte große node_modules-Historie lässt sich nicht mit einem neuen Commit reparieren, denn alte Versionen bleiben für immer im Repository erhalten. Dieser Artikel zeigt, wann eine History-Bereinigung wirklich notwendig ist, wie der BFG Repo-Cleaner Secrets und große Dateien schnell entfernt, und welche Koordination ein Force-Push im Team erfordert.
Inhaltsverzeichnis
- 1. Wann Git-History-Bereinigung wirklich notwendig ist
- 2. Wann ein neuer Commit oder .gitignore reicht
- 3. Was ist der BFG Repo-Cleaner
- 4. Installation des BFG Repo-Cleaner
- 5. Große Dateien entfernen mit bfg --delete-files
- 6. Secrets tilgen mit bfg --replace-text
- 7. Vergleich zu git filter-branch
- 8. Vergleich zu git filter-repo
- 9. Force-Push, Team-Koordination und Post-Cleanup-Schritte
- 10. Zusammenfassung
- 11. FAQ
1. Wann Git-History-Bereinigung wirklich notwendig ist
Ein Grund für eine echte History-Bereinigung liegt vor, wenn zwei Kategorien von Daten den Weg in die Commit-Historie gefunden haben: geleakte Zugangsdaten wie API-Keys, Datenbank-Passwörter oder private SSH-Keys, und versehentlich committete Binärdateien wie ein Datenbank-Dump, ein komplettes node_modules-Verzeichnis oder ein kompiliertes Build-Artefakt. Beide Fälle haben eine gemeinsame Eigenschaft: Ein einfacher git rm gefolgt von einem neuen Commit entfernt die Datei zwar aus dem aktuellen Arbeitsstand, der alte Blob bleibt aber unwiderruflich im Objektspeicher erhalten und ist über git log -p, git show <hash>:pfad oder jeden vorhandenen Klon weiterhin abrufbar.
Bei einem geleakten Secret bedeutet das ein bleibendes Sicherheitsrisiko, unabhängig davon, wie lange der Commit schon zurückliegt. Bei einer großen Binärdatei bedeutet es ein Repository, das dauerhaft aufgebläht bleibt, selbst wenn die Datei im aktuellen HEAD längst wieder verschwunden ist, denn git clone lädt weiterhin die komplette Historie inklusive aller alten Blobs herunter. Genau für diese beiden Szenarien, Secret-Entfernung und Größenreduktion, wurde der BFG Repo-Cleaner gebaut.
2. Wann ein neuer Commit oder .gitignore reicht
Nicht jede unerwünschte Datei in der Historie rechtfertigt eine History-Bereinigung. Wurde beispielsweise eine .env.example-Datei ohne echte Werte committet, ein Debug-Log ohne sensible Inhalte, oder eine mittlerweile harmlose Konfigurationsdatei, reicht ein regulärer Commit mit git rm --cached datei und ein ergänzter Eintrag in .gitignore vollkommen aus. Die Datei verschwindet aus zukünftigen Commits, und die Historie bleibt unangetastet, mit allen referenzierten Commit-Hashes, Tags und Pull-Request-Verweisen intakt.
Der entscheidende Abwägungspunkt ist Risiko gegen Nutzen: Ein History-Rewrite ändert jeden nachfolgenden Commit-Hash, erzwingt einen koordinierten Force-Push und kann bei einem unvorsichtigen Team mehr Chaos anrichten als das ursprüngliche Problem selbst. Bei einem kleinen, privaten Repository mit theoretischem statt echtem Risiko, etwa einem internen Tool ohne Außenzugriff, überwiegt oft der Aufwand den tatsächlichen Sicherheitsgewinn. Die Faustregel: Ist die Datei aktuell noch relevant für Angreifer oder Speicherplatz, rechtfertigt das den Rewrite. Ist sie es nicht mehr, reicht Prävention.
3. Was ist der BFG Repo-Cleaner
Der BFG Repo-Cleaner ist ein Java-basiertes Kommandozeilentool von Roberto Tyley, das gezielt für genau zwei Aufgaben entwickelt wurde: das Entfernen großer oder namentlich bestimmter Dateien aus der gesamten Git-Historie, und das Ersetzen von Textmustern wie Passwörtern oder Tokens in allen historischen Commits. Anders als das eingebaute git filter-branch ist BFG kein generisches Werkzeug für beliebige History-Transformationen, sondern bewusst auf diese beiden Use Cases zugeschnitten, was sich direkt in einer deutlich einfacheren Bedienung niederschlägt.
Technisch arbeitet BFG nicht wie filter-branch mit einem Checkout jedes einzelnen Commits, sondern liest die Objektdatenbank direkt und schreibt nur die betroffenen Blobs und Commit-Objekte neu, während unveränderte Bäume unangetastet bleiben. Das Ergebnis ist ein Durchlauf, der für ein mittelgroßes Repository typischerweise Sekunden bis wenige Minuten dauert, während dieselbe Operation mit filter-branch je nach Historienlänge Stunden in Anspruch nehmen kann. BFG liegt als einzelne .jar-Datei vor und benötigt lediglich eine installierte Java Runtime.
# Install BFG Repo-Cleaner (macOS via Homebrew)
$ brew install bfg
$ java -version
openjdk version "17.0.9" 2023-10-17
# Alternative: download the jar directly (Linux/Windows/macOS)
$ curl -LO https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar
$ java -jar bfg-1.14.0.jar --version
BFG 1.14.0
# Optional: shell alias for daily use
$ echo 'alias bfg="java -jar ~/bin/bfg-1.14.0.jar"' >> ~/.bashrc
4. Installation des BFG Repo-Cleaner
Die Installation des BFG Repo-Cleaner ist auf allen gängigen Plattformen unkompliziert. Auf macOS installiert brew install bfg das Tool inklusive Java-Abhängigkeiten über Homebrew. Unter Linux und Windows lädt man die aktuelle .jar-Datei direkt von der offiziellen Projektseite herunter und ruft sie über java -jar bfg.jar auf. Voraussetzung ist in beiden Fällen eine installierte Java Runtime Environment ab Version 8, die sich mit java -version prüfen lässt.
Für den täglichen Gebrauch empfiehlt sich ein Shell-Alias, der den vollen java -jar-Aufruf abkürzt und BFG wie einen nativen Befehl nutzbar macht. Wichtig vor dem ersten produktiven Einsatz: BFG arbeitet niemals auf einem regulären Arbeitsklon, sondern verlangt einen frischen, gespiegelten Klon des Repositorys über git clone --mirror. Dieser Mirror-Klon enthält alle Branches, Tags und Refs in ihrer internen Git-Repräsentation und ist die einzige sichere Arbeitsgrundlage für eine History-Bereinigung.
# BFG never works on a regular working clone: create a fresh mirror clone first
$ git clone --mirror git@github.com:mironsoft/example-repo.git
$ cd example-repo.git
# Remove a specific file by name from the entire history
$ bfg --delete-files dump.sql
...
Deleted files
-------------
dump.sql (12 commits)
# Remove all blobs larger than 10 MB, regardless of file name
$ bfg --strip-blobs-bigger-than 10M
# Remove a whole directory, e.g. an accidentally committed node_modules
$ bfg --delete-folders node_modules --no-blob-protection
5. Große Dateien entfernen mit bfg --delete-files
Der Befehl bfg --delete-files entfernt Dateien anhand ihres Namens oder eines Glob-Patterns aus jedem Commit der Historie, unabhängig davon, wann und wo sie erstellt wurden. Ein typischer Aufruf bfg --delete-files dump.sql durchsucht die gesamte Historie nach Dateien mit exakt diesem Namen und entfernt jedes Vorkommen, ohne dass Pfad oder Commit-Reihenfolge manuell angegeben werden müssen. Für Muster wie *.zip oder node_modules funktioniert dieselbe Syntax mit Wildcards oder Verzeichnisnamen.
Eine ergänzende Option ist --strip-blobs-bigger-than 10M, die unabhängig vom Dateinamen jeden Blob entfernt, der eine festgelegte Größe überschreitet, praktisch für Repositories, in denen große Dateien unter wechselnden Namen wiederholt committet wurden. BFG protokolliert nach jedem Lauf detailliert, welche Commits verändert wurden, und schreibt eine Zuordnungstabelle alter zu neuer Commit-Hashes nach .git/bfg-report. Diese Reports sind die Grundlage, um vor dem finalen Push zu verifizieren, dass tatsächlich nur die beabsichtigten Objekte entfernt wurden.
6. Secrets tilgen mit bfg --replace-text
Für das Entfernen von Secrets nutzt man bfg --replace-text patterns.txt, wobei die Datei patterns.txt zeilenweise Textmuster oder reguläre Ausdrücke enthält, die BFG in jeder betroffenen Datei jedes historischen Commits durch einen Platzhalter wie ***REMOVED*** ersetzt. Anders als beim Löschen ganzer Dateien bleibt die Datei selbst und ihre Struktur erhalten, nur der sensible Textinhalt wird unkenntlich gemacht, was sich besonders für Konfigurationsdateien eignet, die neben dem Secret auch legitime, weiterhin benötigte Einträge enthalten.
Die Patterns-Datei unterstützt sowohl feste Strings als auch reguläre Ausdrücke mit dem Suffix ==regex, was etwa das Ersetzen aller Werte eines bestimmten Umgebungsvariablen-Musters in einem einzigen Durchlauf erlaubt. Wichtig: BFG ersetzt Text nur in Dateien, die nicht als Binärdatei erkannt werden, und arbeitet zeilenbasiert, weshalb mehrzeilige Secrets in einem einzelnen Muster nicht zuverlässig erfasst werden. Für solche Sonderfälle ist eine zusätzliche manuelle Prüfung des generierten Reports vor dem Push unverzichtbar.
# replace-patterns.txt: one pattern per line for bfg --replace-text
# Plain strings are matched literally
sk_live_51H8x9k2mN3pQ7rS
# Suffix ==regex enables full regular expression matching
DB_PASSWORD=.*==>DB_PASSWORD=***REMOVED***==regex
AWS_SECRET_ACCESS_KEY=[A-Za-z0-9/+=]{40}==regex
# A trailing ==> value overrides the default ***REMOVED*** placeholder
ghp_[a-zA-Z0-9]{36}==>***TOKEN_REMOVED***==regex
7. Vergleich zu git filter-branch
git filter-branch war jahrelang das einzige in Git eingebaute Werkzeug für History-Rewrites und ist entsprechend generisch: Es checkt jeden Commit einzeln aus, führt einen beliebigen Filter-Befehl aus, und committet das Ergebnis neu, was für jede erdenkliche Transformation funktioniert, aber genau deshalb auch außergewöhnlich langsam ist. Bei einem Repository mit mehreren tausend Commits kann ein einzelner filter-branch-Lauf mit einem --tree-filter mehrere Stunden dauern, weil für jeden Commit ein vollständiger Checkout in ein temporäres Verzeichnis erfolgt.
Die Git-Dokumentation selbst warnt seit mehreren Jahren explizit vor filter-branch und bezeichnet es in der Man-Page als "deprecated", mit dem klaren Hinweis, stattdessen git filter-repo zu verwenden. Neben der Geschwindigkeit ist auch die Fehleranfälligkeit ein Problem: Die Kombination aus Shell-Filtern, impliziten Umgebungsvariablen und den zahlreichen Edge Cases bei Merges und leeren Commits führt in der Praxis regelmäßig zu stillen, schwer zu entdeckenden Fehlern in der neu geschriebenen Historie. BFG und filter-repo umgehen dieses Problem durch native, direkte Manipulation der Objektdatenbank statt wiederholter Checkouts.
8. Vergleich zu git filter-repo
git filter-repo ist der von den Git-Maintainern selbst entwickelte und offiziell empfohlene Nachfolger von filter-branch. Als Python-Tool arbeitet es, ähnlich wie BFG, direkt auf der Objektdatenbank und erreicht damit vergleichbare Geschwindigkeiten, bringt aber ein deutlich größeres Funktionsspektrum mit: Pfade umbenennen, Commit-Messages per Callback transformieren, Autoren global ersetzen, oder komplexe bedingte Filterregeln definieren, die weit über das Löschen von Dateien und Textersetzung hinausgehen, die BFG bewusst als einzige zwei Use Cases abdeckt.
Dieser Funktionsumfang hat seinen Preis: Die Kommandozeilensyntax von filter-repo ist deutlich komplexer als die zwei BFG-Flags, die Dokumentation setzt Python-Kenntnisse für fortgeschrittene Callbacks voraus, und auch filter-repo verlangt zwingend einen frischen Klon statt eines bestehenden Arbeitsverzeichnisses. Für die beiden häufigsten Fälle, Secret-Entfernung und Größenreduktion, bleibt BFG wegen seiner minimalen Lernkurve meist die pragmatischere Wahl. Sobald aber komplexere Umstrukturierungen nötig sind, etwa das Aufteilen eines Monorepos oder das Umschreiben von Autorendaten nach einer Firmenfusion, ist filter-repo das mächtigere und offiziell unterstützte Werkzeug.
# git filter-repo: official replacement for filter-branch (pip install)
$ pip install git-filter-repo
# filter-repo also requires a fresh clone, never an existing working copy
$ git clone git@github.com:mironsoft/example-repo.git fresh-clone
$ cd fresh-clone
# Equivalent of bfg --delete-files, with glob support
$ git filter-repo --path dump.sql --invert-paths
# Equivalent of bfg --strip-blobs-bigger-than
$ git filter-repo --strip-blobs-bigger-than 10M
# filter-repo can do things BFG cannot: rewrite author identity globally
$ git filter-repo --mailmap .mailmap
9. Force-Push, Team-Koordination und Post-Cleanup-Schritte
Der mit Abstand kritischste Schritt jeder History-Bereinigung liegt nicht im Tool, sondern in der Koordination: Ein Rewrite ändert den SHA-Hash jedes betroffenen Commits und aller nachfolgenden Commits, wodurch die neue Historie mit jedem existierenden Klon inkompatibel wird. Ein Force-Push ist danach zwingend erforderlich, typischerweise mit git push --force origin refs/heads/* refs/tags/* vom bereinigten Mirror-Klon aus, und muss mit dem gesamten Team, laufenden CI-Pipelines und offenen Pull Requests abgestimmt werden, idealerweise in einem angekündigten Wartungsfenster.
Nach dem Force-Push darf kein Teammitglied seinen alten Klon per git pull oder git rebase aktualisieren, denn beide Befehle können die eigentlich entfernten Objekte über lokale Referenzen und Reflogs unbemerkt wieder zurück auf den Server pushen und die Bereinigung damit faktisch rückgängig machen. Der einzig sichere Weg ist, den alten Klon vollständig zu löschen und das Repository neu zu klonen. Auf dem Server selbst müssen zusätzlich git reflog expire --expire=now --all und git gc --prune=now --aggressive laufen, um die alten Objekte tatsächlich aus den Packfiles zu entfernen und Speicherplatz freizugeben.
Selbst nach einer technisch erfolgreichen Bereinigung gilt eine unverrückbare Regel: Jedes geleakte Credential muss trotzdem rotiert werden. Alte Forks, lokale Klone anderer Entwickler, CI-Artefakt-Caches und von GitHub selbst zwischengespeicherte Commit-Ansichten können das Secret noch enthalten, unabhängig davon, wie gründlich die eigene Historie bereinigt wurde. Die folgende Tabelle vergleicht die drei Werkzeuge im Überblick.
# Run BFG against the mirror clone using the patterns file
$ bfg --replace-text replace-patterns.txt
# Inspect the report before touching the remote
$ cat .git/bfg-report/*/cache-stats.txt
$ git log --all --oneline | head -5 # verify commit hashes changed
# Clean up dangling refs BFG leaves behind, then push everything
$ cd example-repo.git
$ git reflog expire --expire=now --all
$ git gc --prune=now --aggressive
# Coordinated force-push: rewrite every branch and tag on the remote
$ git push --force origin refs/heads/* refs/tags/*
| Kriterium | git filter-branch | BFG Repo-Cleaner | git filter-repo |
|---|---|---|---|
| Geschwindigkeit | Extrem langsam, checkt jeden Commit einzeln aus | Sehr schnell, direktes Objektdatenbank-Streaming | Ähnlich schnell wie BFG, optimierte Implementierung |
| Bedienung | Komplexe, fehleranfällige Shell-Filter-Syntax | Zwei einfache Flags: --delete-files, --replace-text | Mächtig, aber deutlich steilere Lernkurve |
| Offizieller Support | Deprecated, von Git selbst nicht mehr empfohlen | Community-Tool, weit verbreitet und stabil | Offiziell vom Git-Projekt empfohlen |
| Flexibilität | Beliebig flexibel, aber gefährlich generisch | Bewusst eingeschränkt auf zwei Use Cases | Volle Flexibilität für beliebige Rewrites |
| Voraussetzung | In Git enthalten, aber Bash-Kenntnisse nötig | Java Runtime, sonst keine Abhängigkeiten | Python 3 und git, frischer Klon zwingend |
Für die überwiegende Mehrheit der Fälle, Secret-Entfernung und Größenreduktion, ist BFG die schnellste und unkomplizierteste Wahl. Nur bei deutlich komplexeren Restrukturierungen lohnt sich der Umstieg auf filter-repo, und von filter-branch raten selbst die Git-Maintainer inzwischen aktiv ab.
Mironsoft
Git-Sicherheit, Repository-Wartung und koordinierte History-Rewrites für PHP- und Magento-Teams
Sichere History-Bereinigung ohne Chaos im Team?
Wir planen und begleiten Git-History-Rewrites für Teams, von der Auswahl des richtigen Tools über die Koordination des Force-Push-Fensters bis zur Rotation aller betroffenen Zugangsdaten.
History-Audit
Analyse der Repository-Historie auf Secrets, große Binärdateien und Kandidaten für ein Rewrite
Koordinierter Rewrite
Geplanter BFG- oder filter-repo-Lauf inklusive Wartungsfenster und Team-Kommunikation
Absicherung danach
Branch-Protection-Regeln, Secret-Scanning und Rotation aller betroffenen Zugangsdaten
10. Zusammenfassung
Eine Git-History-Bereinigung ist nur dann gerechtfertigt, wenn tatsächlich geleakte Zugangsdaten oder dauerhaft aufblähende Binärdateien in der Historie stecken, nicht bei jeder unerwünschten, aber harmlosen Datei. Der BFG Repo-Cleaner löst genau diese beiden Fälle mit den Befehlen --delete-files und --replace-text deutlich schneller und einfacher als das veraltete git filter-branch, ohne dabei die Flexibilität des offiziell empfohlenen git filter-repo zu benötigen.
Der technische Rewrite ist dabei nur die halbe Arbeit. Da sich jeder Commit-Hash nach dem Rewrite ändert, braucht es einen koordinierten Force-Push, ein klar kommuniziertes Wartungsfenster, einen kompletten Neuklon bei jedem Teammitglied statt eines Pulls, sowie reflog expire und git gc --prune=now --aggressive auf dem Server, um Speicherplatz wirklich freizugeben. Und unabhängig vom Erfolg der Bereinigung gilt: Geleakte Credentials gehören immer rotiert.
Git-History bereinigen mit BFG Repo-Cleaner, das Wichtigste auf einen Blick
Wann ein Rewrite nötig ist
Nur bei geleakten Secrets oder dauerhaft aufblähenden Binärdateien. Ein neuer Commit plus .gitignore reicht sonst aus.
BFG Repo-Cleaner
Java-Tool mit zwei Befehlen: --delete-files für große Dateien, --replace-text für Secrets. Verlangt einen Mirror-Klon.
filter-branch vs. filter-repo
filter-branch ist deprecated und langsam. filter-repo ist der offiziell empfohlene, mächtigere, aber komplexere Nachfolger.
Force-Push & Koordination
Neuklon statt Pull für alle, git gc --prune=now --aggressive auf dem Server, und immer betroffene Credentials rotieren.