Managing Secrets in Git
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
# 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)
# 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
# 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
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
# 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
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-repoto remove the secret from all commitsStep 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
# 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)
# Replace the actual secret string with a placeholder in all commits git filter-repo --replace-text <(echo "sk_live_actual_key==>REDACTED_KEY")