By admin , 29 April 2026

The cardinal rule

Anything committed to Git is permanent until you actively rewrite history. Anything pushed to a remote should be considered exposed - even if you delete it five seconds later, it may have been cloned, mirrored, indexed, or scraped. Treat every secret leak as a compromise.

Layer 1: never commit secrets

Use environment variables for runtime configuration. Use a secrets manager (Vault, AWS Secrets Manager, 1Password, Doppler) for storage. Commit only example files showing the structure:

# .env.example
DATABASE_URL=postgres://localhost/myapp
JWT_SECRET=replace-me

# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
secrets/

Layer 2: pre-commit detection

Run a secret scanner as a hook:

# Install gitleaks
brew install gitleaks
# As a pre-commit hook (with pre-commit framework)
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

gitleaks scans staged files for AWS keys, GitHub tokens, private key blocks, and dozens of other patterns. It also runs in CI:

gitleaks detect --redact --source=.

Layer 3: server-side rejection

A pre-receive hook on the server can reject pushes containing secrets. GitHub's secret scanning does this automatically for known vendors; self-hosted setups can use gitleaks in a server hook.

Layer 4: CI scanning

Run gitleaks or trufflehog on every PR. Even with hooks, secrets slip through; CI is the last fast-feedback gate.

- uses: gitleaks/gitleaks-action@v2
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Layer 5: encrypted secrets in Git

Sometimes you genuinely need to commit secrets - signing keys for CI, encrypted backups. Tools like git-crypt, SOPS, and sealed-secrets let you commit encrypted blobs that decrypt only with the right key:

# sops example
sops -e -i secrets.yaml      # encrypt in place
sops secrets.yaml            # edit transparently
git add secrets.yaml
git commit -m "Update encrypted secrets"

The cipher text is in Git; the key is not.

If a secret leaks

  1. Rotate immediately. The secret is compromised - assume it is in the wild.
  2. Audit usage logs. Look for unauthorised access during the leak window.
  3. Rewrite history. git filter-repo --replace-text patterns.txt redacts the secret from every commit.
  4. Force-push. Coordinate with the team; everyone reclones.
  5. Notify. If the secret was a customer credential, follow your incident response plan.
# patterns.txt
AKIAIOSFODNN7EXAMPLE==>REDACTED
sk_live_abc123==>REDACTED

git filter-repo rewrites every commit; SHAs change. Force-push, then have collaborators reclone.

Recovering on the host

Even after force-push, GitHub keeps "dangling" commits accessible by SHA for some time. Contact GitHub support to ensure full removal. Same for GitLab. Cached fork copies may still hold the secret.

The discipline

Defense in depth: hooks, scanners, server policy, encryption, education. The goal is making the easy thing (use environment variables) easier than the wrong thing (hardcode the API key). Once your team's reflex is "where is the secrets manager?", the leaks stop.