GitKeeping Secrets Out of Git

Managing Secrets in Git

Warning
Committing a secret (API key, password, database URL, private key) to a Git repository is one of the most common and costly security mistakes in software development. Thousands of valid AWS keys, Stripe API keys, and database passwords are discovered in public GitHub repositories every day by automated scanners. Once pushed to a public remote, assume the secret is compromised within minutes — not days. Take immediate action.
Why Deleting in the Next Commit Does NOT Help

A common misconception: "I'll just delete the secret in the next commit." This does not help at all. Git stores every version of every file in history. The secret is still accessible to anyone who clones the repository and checks out that earlier commit.

The secret is still in history even after 'deleting' it

Bash
# Commit 1: Added secret (bad)
git log --all --oneline
# a1b2c3d feat: add payment service  ← .env with STRIPE_KEY committed here
# d4e5f6g fix: remove .env file      ← deleted in this commit

# Anyone can still retrieve the secret
git show a1b2c3d:.env
# STRIPE_SECRET_KEY=sk_live_your_real_key_here
Prevention: The .env Pattern

The industry-standard approach is to commit an .env.example file with placeholder values and gitignore the actual .env file that contains real secrets.

.env.example (committed to repository)

Text
# Database
DATABASE_URL=postgres://user:password@host:5432/dbname

# Authentication
JWT_SECRET=your-jwt-secret-here
SESSION_SECRET=your-session-secret

# Third-party APIs
STRIPE_PUBLIC_KEY=pk_test_your_key_here
STRIPE_SECRET_KEY=sk_test_your_key_here
SENDGRID_API_KEY=SG.your_key_here

# AWS
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key

.gitignore entries for secrets

Text
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.production

# Credentials
*.pem
*.key
credentials.json
service-account.json
*.p12
Tip
Set up `.gitignore` before your very first commit. It is much easier to prevent a secret from entering history than to remove one after the fact. GitHub provides a comprehensive `.gitignore` template library at `github.com/github/gitignore`.
Environment Variables Over Config Files

The most secure approach is to avoid files entirely and pass secrets as environment variables directly. Most CI/CD platforms (GitHub Actions, GitLab CI, AWS CodeBuild) have built-in secret stores for this purpose.

Setting secrets in GitHub Actions

Bash
# In GitHub Actions workflow (secrets never appear in logs)
env:
  STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
Pre-Commit Detection Tools

Tool

Approach

Best For

git-secrets

Regex patterns via Git hooks (pre-commit)

AWS keys + custom patterns; simple setup

gitleaks

Config-driven, 100+ built-in detectors, SARIF output

CI/CD pipelines, comprehensive scanning

detect-secrets

Entropy-based + regex, generates a baseline file

Teams that need to audit and accept some false positives

truffleHog

Entropy + regex, scans entire git history

Historical audits, deep scans of existing repos

GitHub Secret Scanning

Automatic on push, 200+ provider patterns

GitHub-hosted repos; zero configuration needed

GitHub Secret Scanning
  • Public repositories: automatic secret scanning enabled for all public repos at no cost

  • Private repositories: requires GitHub Advanced Security (paid)

  • Scans for 200+ patterns from providers like AWS, Stripe, Google, Twilio, and more

  • When a secret is detected, GitHub notifies the repository owner and the service provider

  • Service providers (like Stripe and AWS) can automatically revoke detected tokens

  • Push protection: blocks a push containing a known secret pattern before it reaches the remote

What to Do If You Commit a Secret
Warning
The very first thing to do when you discover a committed secret is to **rotate it immediately** — regardless of whether the repository is public or private. Assume the secret is compromised the moment it was committed. Cleaning the history is a secondary concern; it does not undo any damage that may have already occurred. Revoke the old key/token/password first, then clean the history.
  • Step 1: ROTATE THE SECRET IMMEDIATELY — revoke the API key, rotate the password, invalidate the token

  • Step 2: Verify revocation — test that the old credentials no longer work

  • Step 3: Clean history — use git filter-repo to remove the secret from all commits

  • Step 4: Force push — push the rewritten history to all remotes

  • Step 5: Notify collaborators — everyone must delete their local clones and re-clone

  • Step 6: Update any forks — contact fork owners; GitHub does not auto-update forks

  • Step 7: Post-mortem — understand how it happened and add prevention measures

History Cleanup with git filter-repo

Remove a secret file from all history

Bash
# Install git filter-repo
pip install git-filter-repo

# IMPORTANT: clone a fresh copy first
git clone --mirror https://github.com/user/repo.git
cd repo.git

# Remove the file containing secrets from all history
git filter-repo --path .env --invert-paths
git filter-repo --path config/database.yml --invert-paths

# Force push to update the remote
git push --force --all
git push --force --tags

Replace a specific secret value in history (alternative)

Bash
# Replace the actual secret string with a placeholder in all commits
git filter-repo --replace-text <(echo "sk_live_actual_key==>REDACTED_KEY")
Warning
Even after cleaning history and force-pushing, remember that GitHub and other platforms cache content in their CDN. Forks retain the old history. Any developer who cloned before the cleanup still has the secret locally. Search engines may have indexed the content. The only safe assumption after a committed secret is that the credential is permanently compromised — rotation is non-negotiable.