Geheimen in Git voorkomen is één van die dingen waar iedereen het over eens is en waar bijna niemand een systeem voor heeft. Eén gecommite API-key of .env-bestand is genoeg om een sleutel te rouleren, een leverancier te bellen en jezelf een half weekend te veroorzaken. Het probleem: zodra je pusht, is de history publiek toegankelijk voor iedereen met leesrechten. Force-push helpt maar deels — mirrors, forks, CI-artifacts en cached blobs bij je Git-host houden een kopie in leven. Dit artikel behandelt een drie-lagen-aanpak (.gitignore, pre-commit hook, CI-scan), plus wat te doen als het tóch gebeurt.
Stack-agnostisch: dit werkt voor WordPress-thema-repo’s, Laravel-apps, Statamic-projecten, Node-services en alles daartussen.

Waarom één laag nooit genoeg is
Geen enkele van de drie lagen hieronder is op zichzelf voldoende:
.gitignorevergeet je onvermijdelijk een keer bij te werken (nieuwe secret-file, nieuwe extensie).- Pre-commit hooks kunnen worden overgeslagen met
git commit --no-verify— bewust of per ongeluk. - CI-scans vangen alles, maar pas nádat de commit al in de branch staat en je hem moet gaan wegwerken.
Alleen de drie lagen samen leveren een stack die in de praktijk niet-triviaal te omzeilen is. Dat is defense-in-depth voor secrets management. Voor context: goede lokale .env-hygiëne op de server hoort bij .env-configuratie op de server, maar bij Git-repo’s ligt het probleem eerder in de commit-flow dan op de server.
Geheimen in Git voorkomen: pre-commit hooks met gitleaks

Laag 1 — .gitignore correct opzetten
De eerste vangrail is triviaal maar wordt structureel onderschat. Een basale .gitignore voor een gemengde stack:
# Env en secrets
.env
.env.*
!.env.example
# Sleutels en certs
*.pem
*.key
*.crt
id_rsa
id_rsa.pub
# Credential-bestanden
credentials.json
service-account*.json
aws.credentials
# WordPress
wp-config.php
# IDE-configuraties met tokens
.vscode/settings.json
.idea/workspace.xmlVerifieer altijd met git check-ignore -v .env voordat je commit. Als het commando niets teruggeeft, wordt het bestand niet genegeerd — dat is de dagelijkse valkuil bij nieuwe project-templates.
Belangrijk: .env in .gitignore zetten helpt níet als het bestand al eerder is gecommit. git rm --cached .env haalt het uit de index, maar de history bevat het nog steeds. Zie laag 3-reactieve stappen verderop.
Laag 2 — pre-commit hook lokaal met gitleaks
De volgende laag: automatisch scannen bij elke commit. Het pre-commit framework (oorsprong bij Yelp, community-maintained) is een dunne wrapper die hooks configureert per repo, met een gedeelde config. Installatie eenmalig systeembreed (pip install pre-commit of brew install pre-commit) plus per repo een .pre-commit-config.yaml:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaksActiveren met pre-commit install in de repo (eenmalig). Vanaf dat moment scant gitleaks op GitHub elke commit op ruim 160 built-in secret-patronen (AWS-keys, Stripe-keys, private RSA keys, generic high-entropy strings, etc.).
Trufflehog is een goed alternatief, met name als je last hebt van false positives — de --only-verified optie doet een live-check tegen de bijhorende API’s en drukt false positives in de praktijk sterk. Nadeel: die verificatie kost tijd en werkt niet in air-gapped omgevingen.
Laag 3 — CI/CD-scan in pipeline
Pre-commit is best-effort — iemand kan git commit --no-verify gebruiken, of vergeten pre-commit install uit te voeren op een nieuwe checkout. De CI-scan is de vangrail die niet omzeilbaar is (mits je merge protection goed staat).
Voor GitHub Actions:
name: Gitleaks
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2Zet daarna in de branch protection rules: “Require status checks to pass before merging” met de gitleaks-check als vereist. Vanaf dat moment kan geen enkele PR met een detected secret worden gemerged, ongeacht wat de developer lokaal doet.
Voor GitLab: gitleaks --source . --report-format sarif --report-path gitleaks.sarif in je .gitlab-ci.yml. Voor Bitbucket: vergelijkbaar patroon.
.gitleaksignore en false positives
Elke gitleaks-hit heeft een Fingerprint (commit-SHA plus regel plus rule-id). Bewuste test-data of documented example-tokens verplaats je naar een .gitleaksignore-bestand:
# Test fixture, geen echte key
abc123def456...:tests/fixtures/stripe-response.json:stripe-access-token:12Inline uitzonderen kan ook: voeg # gitleaks:allow toe aan de regel met de “hit”. Gebruik dit spaarzaam — de allow-annotaties zijn een audit-risico bij een security-review.
Voor eigen patronen (interne API-keys, custom-token-formaten) maak je een gitleaks.toml in de repo:
[extend]
useDefault = true
[[rules]]
id = "lypii-internal-token"
description = "Interne project-token"
regex = '''lypii_[a-z0-9]{32}'''
tags = ["key", "internal"]useDefault = true is cruciaal: zonder deze regel vervangt jouw config de 160+ built-in rules in plaats van ze aan te vullen.
Wat als een secret al gecommit is

Ontdek je een leak in de history, dan is de volgorde van stappen belangrijker dan de snelheid van reageren. In mijn ervaring is de meest gemaakte fout dat men eerst de history probeert te herschrijven en pas daarna de sleutel roteert — daarmee heeft de aanvaller een venster van uren tot dagen om de nog-geldige sleutel te gebruiken.
Stap 1: roteer de sleutel of het wachtwoord in het externe systeem. Dit is de belangrijkste stap. AWS-key: nieuwe key aanmaken, oude direct deactiveren. Stripe-key: rollen via dashboard. Wachtwoord: wijzigen in de bron. Zolang de gecompromitteerde credential geldig is, blijft de leak actief — óók als je hem uit de Git-history rewrit. Zie key-rotatie na een leak voor de bredere incident-response-context.
Stap 2: verwijder de commit uit history. Met git filter-repo (moderne aanpak) of BFG Repo-Cleaner (oudere maar simpelere aanpak):
git filter-repo --path .env --invert-pathsOf voor een specifieke string in blobs:
git filter-repo --replace-text <(echo 'oude_secret==>REDACTED')Stap 3: force-push naar remote. git push --force-with-lease origin main — plus alle andere branches waar de secret in stond.
Stap 4: controleer mirrors, forks en CI-artifacts. Fork-eigenaren zien de rewrite niet automatisch. Open forks moeten óók de rewrite doen, of anders geïnformeerd worden. CI-artifacts (log-bestanden, build-outputs) op GitHub/GitLab bevatten mogelijk de secret in leesbare vorm; die opschonen via de repo-instellingen. Pull requests met de secret in de diff sluiten en cache-bare blobs melden bij support van je Git-host voor een volledige purge.
Belangrijk: zelfs met alle vier stappen is er geen garantie dat de secret niet gecopied is door een aanvaller die de repo tussentijds monitorde. Roteren blijft de enige echte remediation; de history-rewrite is opruimen achteraf.
Multi-stack noot
De aanpak werkt identiek op elke stack. Verschillen zitten alleen in wélke files je in .gitignore zet:
- WordPress-thema of -plugin:
wp-config.php,.env(bij Bedrock/Sage),debug.log. - Laravel:
.env,storage/,bootstrap/cache/,oauth-*.key. - Statamic:
.env,storage/,bootstrap/cache/. - Node/Next.js:
.env.local,.env.production,node_modules/. - Custom PHP:
config/local.php,secrets/,*.credentials.
De pre-commit hook en CI-scan zijn overal hetzelfde. Alleen de eerste laag (.gitignore) is stack-specifiek.
Wanneer schakel je Lypii in
Setup van pre-commit hook plus CI-scan per repo kost ongeveer 1 tot 2 uur, afhankelijk van hoe veel bestaande false positives moeten worden weggewerkt.
- Fix €70 per repo — pre-commit installatie, gitleaks-config, CI-scan aanzetten,
.gitleaksignoreopzetten voor bestaande false positives. - Support-abonnement €95 per maand — inclusief periodieke history-scan (retro-actief zoeken naar leaks in oude commits), rule-updates en trufflehog-verificatie waar zinvol.
- Uur-tarief €105 — voor incident-response bij een actieve leak: rotatie-coördinatie, filter-repo runs, mirror-opschoning.
Verbeteren boven herbouwen: dit is toevoegen aan je bestaande repo-workflow, geen migratie.
Multi-stack opmerking: de defense-in-depth-aanpak (.gitignore, pre-commit, CI) werkt hetzelfde op WordPress-, Laravel-, Statamic-, Node- en custom-repos. Alleen laag 1 (.gitignore) verschilt per stack.



