Warum gelöschte Secrets in Git nie wirklich weg sind
Wer Datenbankpasswörter oder API-Schlüssel direkt im Code speichert, schafft ein Risiko, das auch nach dem Löschen bestehen bleibt, weil Git jede Version dauerhaft aufbewahrt. Dieser Artikel zeigt, wie Magento-Teams Zugangsdaten sauber aus dem Code fernhalten, mit env.php Mustern, Umgebungsvariablen, dedizierten Secrets Managern wie Vault, konsequenter Rotation und automatisiertem Leak Scanning.
Inhaltsverzeichnis
- 1. Warum hartcodierte Secrets ein andauerndes Risiko sind
- 2. Git-Historie: Warum gelöschte Secrets nicht wirklich weg sind
- 3. Magento env.php und Umgebungsvariablen-Pattern
- 4. .gitignore-Disziplin für lokale Konfigurationsdateien
- 5. Dedizierte Secrets-Manager: Vault, AWS Secrets Manager & Co.
- 6. Secret-Rotation: Turnus, Automatisierung, Verantwortlichkeiten
- 7. Leaks erkennen: Scanning-Tools und Pre-Commit-Hooks
- 8. Incident Response: Was tun nach einem geleakten Secret
- 9. Speicheransätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum hartcodierte Secrets ein andauerndes Risiko sind
Ein Secret ist jeder Wert, der Zugriff auf ein System, eine Datenbank oder einen Drittanbieter-Dienst gewährt: Datenbankpasswort, API-Token, JWT-Signaturschlüssel, der Crypt-Key aus app/etc/env.php, ein OAuth-Client-Secret oder die Zugangsdaten in der auth.json für repo.magento.com. Sobald ein solcher Wert direkt im Quellcode steht, egal ob als Konstante, Default-Parameter oder Testdatensatz, wird er Teil der Versionshistorie und damit für jeden mit Repository-Zugriff sichtbar: aktuelle Teammitglieder, ehemalige Mitarbeiter mit lokalem Klon, externe Dienstleister, CI-Systeme und in vielen Fällen auch automatisierte Backups des Repositories.
Das Risiko wächst mit der Reichweite des Repositories. Ein privates Repo mit drei Entwicklern hat eine überschaubare Angriffsfläche, ein Repository mit CI/CD-Anbindung, mehreren Forks und externen Contractor-Zugängen dagegen nicht. Für Magento-Shops kommt die Compliance-Dimension hinzu: Wer personenbezogene Daten oder Zahlungsinformationen verarbeitet, unterliegt DSGVO- und bei Kartendaten PCI-DSS-Anforderungen, die den Umgang mit Zugangsdaten explizit regeln. Ein geleaktes Datenbankpasswort ist dabei besonders kritisch, weil es direkten, ungefilterten Zugriff auf die komplette Datenbank gewährt, ganz ohne Umweg über eine Anwendungsschicht mit eigener Autorisierungslogik.
2. Git-Historie: Warum gelöschte Secrets nicht wirklich weg sind
Ein häufiges Missverständnis: Ein Secret aus einer Datei zu entfernen und den Commit zu pushen, entfernt es aus dem Repository. Tatsächlich bleibt der alte Blob-Inhalt in .git/objects vollständig erhalten, solange irgendein Commit, Branch, Tag oder Reflog-Eintrag ihn referenziert. Jeder, der das Repository klont oder bereits einen älteren Stand ausgecheckt hat, kann mit git log -p oder git show auf den alten Commit zugreifen und den Klartext-Wert lesen, unabhängig davon, wie viele neue Commits seitdem entstanden sind.
Ein Secret wirklich aus der Historie zu entfernen erfordert eine History-Rewrite, meist mit dem BFG Repo-Cleaner oder git filter-repo. Beide Tools durchsuchen alle Commits, ersetzen den betroffenen Inhalt und schreiben neue Commit-Hashes. Das bricht jeden bestehenden Klon: Ein einfacher git pull funktioniert danach nicht mehr, alle Beteiligten müssen neu klonen oder ihre lokalen Branches manuell rebasen. Wichtig: Die History-Rewrite ist ein nachgelagerter Aufräumschritt. Der erste und wichtigste Schritt nach einem Leak ist immer die Rotation des betroffenen Secrets, weil die Rewrite alte Kopien in Forks, CI-Caches und lokalen Klonen ohnehin nicht erreicht.
#!/usr/bin/env bash
# Purge a leaked secret from the entire Git history with git-filter-repo
# WARNING: this rewrites every commit hash, coordinate with the whole team first
# Install: pip install git-filter-repo (or brew install git-filter-repo)
# 1. Rotate the leaked secret FIRST, before touching the history
# (a history rewrite does not reach forks, CI caches or old local clones)
# 2. Remove a specific file from every commit
git filter-repo --path app/etc/env.php --invert-paths
# 3. Or replace a specific string pattern across all blobs
echo 'DB_PASSWORD_OLD_VALUE==>REDACTED' > replacements.txt
git filter-repo --replace-text replacements.txt
# 4. Force-push the rewritten history (coordinate downtime with the team)
git push origin --force --all
git push origin --force --tags
# 5. Every team member must re-clone or hard-reset their local copy,
# a normal "git pull" will not work after a history rewrite
3. Magento env.php und Umgebungsvariablen-Pattern
Magento speichert Datenbankzugang, Crypt-Key, Cache-Backend-Konfiguration und Session-Handler-Einstellungen standardmäßig in app/etc/env.php als PHP-Array im Klartext. Die Datei wird von bin/magento setup:install generiert und ist im Standard-.gitignore von Magento bereits ausgeschlossen, wird aber in der Praxis trotzdem häufig versehentlich committet, etwa wenn Entwickler .gitignore lokal überschreiben oder die Datei mit git add -f erzwingen, um eine funktionierende lokale Konfiguration im Team zu teilen.
Das robustere Pattern ersetzt hartcodierte Werte in env.php durch Aufrufe von getenv(), sodass die eigentlichen Zugangsdaten ausschließlich als Umgebungsvariablen im Deployment-Ziel existieren, gesetzt über Docker-Compose, Kubernetes-Secrets oder die CI/CD-Pipeline, niemals im Repository. Eine env.php.dist mit Platzhaltern dient als Vorlage und wird versioniert; die echte env.php wird lokal oder beim Deployment aus Umgebungsvariablen generiert. Dieses Muster funktioniert auch für Drittanbieter-Module, die eigene Konfigurationswerte in env.php ablegen, etwa Payment-Gateway-Keys oder Suchindex-Zugangsdaten.
<?php
declare(strict_types=1);
/**
* app/etc/env.php generated from environment variables instead of hardcoded values.
* The real file stays out of version control (see .gitignore); only
* env.php.dist with placeholders is committed as a template.
*/
return [
'db' => [
'connection' => [
'default' => [
'host' => getenv('MAGE_DB_HOST') ?: 'db',
'dbname' => getenv('MAGE_DB_NAME') ?: 'magento',
'username' => getenv('MAGE_DB_USER') ?: '',
'password' => getenv('MAGE_DB_PASSWORD') ?: '',
],
],
],
'crypt' => [
// Never hardcode the crypt key, it decrypts every encrypted value in the DB
'key' => getenv('MAGE_CRYPT_KEY') ?: '',
],
'cache' => [
'frontend' => [
'default' => [
'backend' => 'Cm_Cache_Backend_Redis',
'backend_options' => [
'server' => getenv('MAGE_REDIS_HOST') ?: '127.0.0.1',
'password' => getenv('MAGE_REDIS_PASSWORD') ?: '',
],
],
],
],
];
4. .gitignore-Disziplin für lokale Konfigurationsdateien
Magentos Standard-.gitignore schließt bereits die wichtigsten sensiblen Pfade aus: app/etc/env.php, var/, generated/, pub/media/ und pub/static/. Häufig übersehen wird composer's auth.json im Projekt-Root oder in ~/.composer/, die die Zugangsdaten für repo.magento.com sowie für private Composer-Repositories im Klartext enthält. Diese Datei gehört niemals ins Repository, auch nicht in abgewandelter oder teilweise redigierter Form, weil Composer-Tokens vollen Lesezugriff auf kommerzielle Magento-Pakete gewähren.
Wichtig ist die Reihenfolge: .gitignore muss vor dem ersten Commit einer sensiblen Datei existieren, denn ein nachträglicher Eintrag verhindert nur zukünftige Commits, nicht die bereits vorhandene Historie. Ein zusätzlicher Schutzwall ist ein Pre-Commit-Hook, der Dateien wie env.php oder auth.json aktiv blockiert, statt sich allein auf die Disziplin einzelner Entwickler zu verlassen. Für Teams mit mehreren Umgebungen empfiehlt sich zusätzlich ein zentrales, strikt zugriffsbeschränktes Repository nur für Deployment-Konfiguration, getrennt vom Anwendungscode.
# .gitignore additions for a Magento 2 project
# Never commit generated configuration that contains live credentials
/app/etc/env.php
/app/etc/config.php.bak
/auth.json
/.env
/.env.*
!/.env.example
# Generated and cached content also has no place in version control
/var/
/generated/
/pub/static/
/pub/media/*
!/pub/media/.htaccess
# Editor and local override files that sometimes carry test credentials
/.idea/
/*.local.php
5. Dedizierte Secrets-Manager: Vault, AWS Secrets Manager & Co.
Für Setups mit mehreren Umgebungen, mehreren Teams oder regulatorischen Anforderungen reicht .gitignore-Disziplin allein nicht aus. Dedizierte Secrets-Manager wie HashiCorp Vault, AWS Secrets Manager, Azure Key Vault oder Google Secret Manager zentralisieren Zugangsdaten mit feingranularer Zugriffskontrolle, vollständigem Audit-Log jedes Lesezugriffs und, im Fall von Vault, dynamisch generierten, zeitlich begrenzten Datenbankzugangsdaten statt statischer Passwörter.
Das Integrationsmuster für Magento: Beim Container-Start oder Deployment ruft ein Entrypoint-Skript die benötigten Secrets aus dem Vault ab und setzt sie als Umgebungsvariablen oder schreibt sie in eine env.php, die niemals im Image gebacken oder im Repository gespeichert wird. Vaults dynamische Secrets-Engine für MySQL kann sogar bei jedem Deployment ein frisches, kurzlebiges Datenbankpasswort ausstellen, das nach Ablauf der Lease automatisch invalidiert wird, ohne dass ein Mensch manuell rotieren muss. Der Mehraufwand beim initialen Setup lohnt sich vor allem für Teams mit Compliance-Pflichten oder mehr als einer Handvoll Umgebungen.
# .github/workflows/deploy.yml (excerpt)
# Secrets are fetched at deploy time, never stored in the repository or the image
name: deploy-production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fetch secrets from HashiCorp Vault
uses: hashicorp/vault-action@v3
with:
url: https://vault.mironsoft.internal:8200
method: jwt
role: magento-production
secrets: |
secret/data/magento/production db_password | MAGE_DB_PASSWORD ;
secret/data/magento/production crypt_key | MAGE_CRYPT_KEY
- name: Deploy with env vars injected at runtime
run: |
ssh deploy@prod "MAGE_DB_PASSWORD='${MAGE_DB_PASSWORD}' \
MAGE_CRYPT_KEY='${MAGE_CRYPT_KEY}' \
bash /srv/magento/deploy.sh"
6. Secret-Rotation: Turnus, Automatisierung, Verantwortlichkeiten
Rotation begrenzt das Zeitfenster, in dem ein kompromittiertes Secret nutzbar ist. Ein statisches Datenbankpasswort, das seit drei Jahren unverändert ist, hat ein deutlich größeres Schadenspotenzial als eines, das automatisiert alle 24 Stunden wechselt, selbst wenn beide irgendwann geleakt werden. In der Praxis bewährt sich eine Kombination: kritische Secrets wie DB-Zugang und Crypt-Key über einen Secrets-Manager mit kurzer TTL automatisch rotieren, weniger kritische Werte wie interne API-Keys mindestens quartalsweise sowie sofort bei jedem Personalwechsel mit privilegiertem Zugriff.
Zero-Downtime-Rotation erfordert ein zweistufiges Muster: Zunächst wird das neue Secret zusätzlich zum alten aktiviert, sodass beide parallel funktionieren, danach werden alle Verbraucher auf das neue Secret umgestellt, und erst danach wird das alte deaktiviert. Für Magento-Zahlungsanbieter-Integrationen bedeutet das etwa, den API-Key im Zahlungsgateway-Dashboard vorab als zweiten gültigen Key anzulegen, die Payment-Modul-Konfiguration umzustellen und erst nach erfolgreicher Verifikation den alten Key zu widerrufen. Ohne dieses Muster drohen Downtime-Fenster bei jeder Rotation.
7. Leaks erkennen: Scanning-Tools und Pre-Commit-Hooks
Automatisiertes Secret-Scanning ist die zweite Verteidigungslinie neben .gitignore. Tools wie gitleaks, trufflehog und git-secrets durchsuchen Diffs, komplette Repository-Historien oder CI-Pipelines nach Mustern, die typischen Secrets ähneln: AWS-Access-Keys, private RSA-Schlüssel, JWT-Token-Strukturen, generische hochentropische Zeichenketten neben Variablennamen wie password oder secret. Als Pre-Commit-Hook eingebunden, blockiert gitleaks protect --staged einen Commit bereits lokal, bevor das Secret überhaupt gepusht wird, was den günstigsten Zeitpunkt zum Verhindern eines Leaks darstellt.
Pre-Commit-Hooks allein reichen nicht, weil sie umgangen werden können, etwa mit git commit --no-verify. Eine zweite Prüfung in der CI-Pipeline, die jeden Push und regelmäßig die komplette Historie scannt, fängt Leaks ab, die den lokalen Hook umgangen haben. GitHub und GitLab bieten zusätzlich native Secret-Scanning-Funktionen, die bekannte Provider-Token-Formate wie AWS- oder Stripe-Keys automatisch erkennen und den jeweiligen Anbieter proaktiv benachrichtigen können, teilweise sogar bevor das Team selbst reagiert.
#!/usr/bin/env bash
# .git/hooks/pre-commit: block commits that contain secrets
# Install gitleaks first: brew install gitleaks (or download from GitHub releases)
set -euo pipefail
if ! command -v gitleaks &> /dev/null; then
echo "[WARN] gitleaks not installed, skipping secret scan" >&2
exit 0
fi
# Scan only the staged diff, fails the commit if a likely secret is found
if ! gitleaks protect --staged --verbose; then
echo "[BLOCKED] Potential secret detected in staged changes." >&2
echo "Remove the secret, rotate it if it was ever committed before, and retry." >&2
exit 1
fi
8. Incident Response: Was tun nach einem geleakten Secret
Die Reihenfolge nach einem entdeckten Leak entscheidet über den tatsächlichen Schaden. Erster und dringendster Schritt: Das betroffene Secret sofort rotieren beziehungsweise widerrufen, nicht erst die Git-Historie bereinigen. Eine History-Rewrite kann Tage dauern, bis alle Klone aktualisiert sind, während ein rotiertes Passwort das Secret ab dem Moment der Rotation wertlos macht, unabhängig davon, wie viele Kopien bereits existieren.
Zweiter Schritt: Blast-Radius einschätzen, etwa über Zugriffslogs der Datenbank oder des betroffenen Dienstes, um ungewöhnliche Zugriffe im Zeitraum zwischen Leak und Rotation zu identifizieren. Dritter Schritt: Bei Zugriff auf personenbezogene Daten prüfen, ob eine DSGVO-Meldepflicht innerhalb von 72 Stunden an die Aufsichtsbehörde greift. Vierter Schritt: History-Rewrite als Aufräumarbeit und ein Post-Mortem, das die Ursache behebt, meist durch Einführung von Pre-Commit-Scanning oder eines Secrets-Managers, damit derselbe Fehler nicht erneut passiert.
9. Speicheransätze im Vergleich
Jeder Ansatz zum Umgang mit Zugangsdaten hat eigene Konsequenzen für Rotationsaufwand, Nachvollziehbarkeit und den Schaden im Ernstfall. Die folgende Übersicht stellt den unsicheren dem empfohlenen Ansatz je Dimension gegenüber.
| Dimension | Unsicher: Secret im Code | Empfohlen: Secrets-Manager | Vorteil |
|---|---|---|---|
| Rotation | Erfordert Code-Änderung und Deploy | Automatische Rotation per TTL | Kein Deploy für Rotation nötig |
| Audit-Trail | Kein Log, wer wann gelesen hat | Vault/AWS loggen jeden Zugriff | Nachvollziehbarkeit bei Vorfällen |
| Blast Radius bei Leak | Ewig in der Git-Historie exponiert | Kurzlebige, dynamische Zugangsdaten | Zeitfenster für Missbrauch minimal |
| Zugriffskontrolle | Jeder mit Repo-Zugriff sieht alles | Feingranulare Policies pro Rolle | Least Privilege durchsetzbar |
| CI/CD-Sichtbarkeit | Landet in Build-Logs oder Image-Layern | Injektion zur Laufzeit, nie gebacken | Kein Secret im Image oder Log |
Mironsoft
Secrets-Management, env.php-Hardening und Vault-Integration für Magento-Shops
Zugangsdaten endlich sauber aus dem Code fernhalten?
Wir durchsuchen eure Git-Historie auf geleakte Secrets, bereinigen sie mit git filter-repo, führen env.php-Pattern mit Umgebungsvariablen ein und binden Vault oder AWS Secrets Manager in eure Deployment-Pipeline ein.
Git-Historie-Audit
Vollständiges Scanning der Repository-Historie mit gitleaks und trufflehog
Vault-Integration
HashiCorp Vault oder AWS Secrets Manager in CI/CD und Deployment einbinden
Rotationsprozesse
Zero-Downtime-Rotation für Datenbank, Crypt-Key und Payment-API-Keys
10. Zusammenfassung
Das Grundproblem beim Secrets-Management ist selten technischer Natur, sondern eine Frage der Disziplin und der richtigen Reihenfolge. Ein Secret aus dem Code zu löschen genügt nicht, solange die Git-Historie es weiterhin enthält; nur eine History-Rewrite mit BFG oder git filter-repo entfernt es wirklich, und selbst dann erst nach vorheriger Rotation. Magentos env.php sollte niemals Klartext-Zugangsdaten enthalten, sondern Werte über getenv() aus der Umgebung beziehen, mit einer versionierten env.php.dist als Vorlage. .gitignore muss von Anfang an existieren, nicht nachträglich, und wird durch Pre-Commit-Hooks mit gitleaks zusätzlich abgesichert.
Für Teams mit mehreren Umgebungen oder Compliance-Pflichten lohnt sich der Umstieg auf einen dedizierten Secrets-Manager wie Vault oder AWS Secrets Manager, der Rotation automatisiert, jeden Zugriff protokolliert und dynamische, kurzlebige Zugangsdaten ausstellen kann. Im Ernstfall eines Leaks gilt immer dieselbe Reihenfolge: zuerst rotieren, dann den Blast-Radius über Zugriffslogs prüfen, danach die DSGVO-Meldepflicht bewerten und erst zuletzt die Historie bereinigen.
Secrets-Management: Zugangsdaten nicht im Code, das Wichtigste auf einen Blick
Git-Historie ist dauerhaft
Löschen reicht nicht. Nur BFG Repo-Cleaner oder git filter-repo entfernen ein Secret wirklich, danach ist Force-Push für alle Klone nötig.
env.php Pattern
getenv() statt Klartext, env.php.dist als versionierte Vorlage, echte Werte nur zur Laufzeit gesetzt.
Secrets-Manager
Vault oder AWS Secrets Manager für Rotation, Audit-Trail und dynamische, kurzlebige Zugangsdaten bei größeren Setups.
Incident Response
Erst rotieren, dann Blast-Radius prüfen, DSGVO-Meldepflicht bewerten, zuletzt die Historie bereinigen.