Complete Recovery Guide When Secrets Are Accidentally Pushed to GitHub¶
Public repositories are scanned by bots within minutes
When secrets are exposed on GitHub, immediate action is required.
Key Points¶
- Top priority: Revoke credentials Immediately invalidate leaked credentials to prevent further damage
- Complete removal from history Remove secrets from all commits using git filter-repo / BFG
- Prevention .gitignore, pre-commit hooks, and Secret Scanning to avoid recurrence
The Typical Accident¶
An config.json containing an OpenAI API key is accidentally pushed:
{
"openai_api_key": "sk-abcd1234...",
"database_url": "postgresql://user:password@localhost/db"
}
Credential revocation comes first. Invalidate leaked credentials in the service dashboard before cleaning Git history. The credentials can be exploited while you're still cleaning the repository. If you're in a hurry, jump to Phase 1.
Emergency Response¶
Phase 1: Revoke Credentials (0-5 minutes)¶
This is the most critical step. Execute before any Git operations.
| Leaked information | Action |
|---|---|
| API keys (AWS, OpenAI, Stripe, etc.) | Delete and reissue immediately in service dashboard |
| Database credentials | Change password immediately, suspend user permissions |
| SSH keys / certificates | Revoke and regenerate |
| GitHub Personal Access Token | Settings → Developer settings → Delete the token |
For public repositories, also change visibility to private (Settings → Change repository visibility).
Notify your team and security personnel promptly.
Phase 2: Complete Removal from Git History (5-30 minutes)¶
Step 1: Remove from current branch tracking¶
# Remove file from Git tracking (keep local file)
git rm --cached config.json
# Add to .gitignore
echo "config.json" >> .gitignore
# Commit and push
git add .gitignore
git commit -m "Remove config.json from tracking"
git push origin main
This alone is not enough
git rm --cached removes the file from the latest commit, but it remains in past commit history. Complete removal requires Step 2.
Step 2: Remove from entire history¶
Choose from three tools:
| Tool | Status | Characteristics |
|---|---|---|
| git filter-repo | Recommended | Python-based. Fast and safe. Recommended by official Git documentation |
| BFG Repo-Cleaner | Alternative | Java-based. Convenient for bulk password replacement |
| git filter-branch | Legacy | Shows deprecation warning since Git 2.36. Avoid for new use |
Method A: git filter-repo (Recommended)
# Install
pip install git-filter-repo
# Remove specific file from entire history
git filter-repo --invert-paths --path config.json
# Remove multiple files
git filter-repo --invert-paths \
--path config.json --path secrets.yml
git filter-repo notes
- Refuses to run outside a fresh clone (stops with a warning). Use
--forceto override - Removes remote configuration. Re-add with
git remote add origin <URL>after running
Method B: BFG Repo-Cleaner
# Download
# Check latest version at:
# https://rtyley.github.io/bfg-repo-cleaner/
java -jar bfg.jar --delete-files config.json .git
# Replace password/API key strings
java -jar bfg.jar --replace-text passwords.txt .git
BFG limitation
BFG is designed to protect files in the HEAD commit. Complete Step 1 (git rm --cached + commit) before running BFG.
Method C: git filter-branch (Legacy — not recommended)
Shows deprecation warning since Git 2.36. Kept here as reference for existing CI/CD scripts. Use git filter-repo for new work.
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch config.json' \
--prune-empty --tag-name-filter cat -- --all
Step 3: Cleanup and force push¶
If you used BFG or filter-branch, expire local references. git filter-repo handles this internally, so this step is not needed.
# Only needed for BFG / filter-branch
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push to remote (notify team first)
git push origin --force --all
git push origin --force --tags
Forks are not affected
Force pushes do not propagate to forks. If forks exist, contact fork owners directly or request removal through GitHub Support.
Step 4: Verify removal¶
# Check if file traces remain in history
git log --all --full-history -- config.json
# Search for specific strings in history
git log -S "sk-abcd1234" --all --oneline
Phase 3: Team Recovery and New Credentials¶
After force push, team members should run:
git stash # Save work in progress
git fetch origin
git reset --hard origin/main
git stash pop # Restore work if needed
Issue new credentials and include a template file in the repository:
cat > config.json.example << 'EOF'
{
"openai_api_key": "YOUR_API_KEY_HERE",
"database_url": "YOUR_DATABASE_URL_HERE"
}
EOF
git add config.json.example
git commit -m "Add config template"
git push origin main
Prevention¶
.gitignore Configuration¶
# Sensitive files
*.secret
*.key
*.pem
.env
.env.local
.env.production
config.json
secrets.yml
credentials.json
# Cloud credentials
.aws/
.gcp/
Pre-commit Hook¶
#!/bin/sh
# .git/hooks/pre-commit
# Run: chmod +x .git/hooks/pre-commit
SENSITIVE_PATTERNS=(
"sk-[a-zA-Z0-9]{20,}"
"AKIA[0-9A-Z]{16}"
"glpat-[a-zA-Z0-9]{20}"
"api_key.*=.*['\"][a-zA-Z0-9]{20,}['\"]"
)
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
if git diff --cached --diff-filter=ACMR --name-only -z \
| xargs -0 grep -l -E "$pattern" 2>/dev/null; then
echo "Potential secret detected!"
echo "Pattern: $pattern"
exit 1
fi
done
echo "Secret check passed"
git-secrets¶
# Install (macOS)
brew install git-secrets
# Enable in repository
git secrets --install
git secrets --register-aws
# Add custom patterns
git secrets --add 'sk-[a-zA-Z0-9]{20,}'
# Scan entire history
git secrets --scan
GitHub Secret Scanning¶
GitHub automatically detects patterns for major services (AWS, OpenAI, Stripe, etc.) and sends Secret scanning alerts.
Public vs Private repositories
- Public repositories: Free and automatically enabled. No configuration needed
- Private repositories: Requires GitHub Advanced Security (paid Enterprise feature). Not available on Individual or Team plans
Don't rely on Secret Scanning alone. Combine with pre-commit hooks and git-secrets for comprehensive protection.
Managing Secrets in GitHub Actions¶
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy with secrets
env:
API_KEY: ${{ secrets.API_KEY }}
run: |
echo "Deploying..."
Legal Considerations¶
GDPR 72-hour reporting obligation scope
The GDPR 72-hour reporting obligation to supervisory authorities applies when personal data of natural persons (names, email addresses, physical addresses, etc.) is breached and poses a risk to their rights and freedoms.
Reporting required: When DB dumps containing personal data, user lists, or email address lists are pushed. Report to your Data Protection Officer (DPO) immediately.
Usually not applicable: Leaks of API keys, passwords, or SSH keys alone. However, if these could have been used to access personal data, consider reporting.
FAQ¶
Q: Is git rm --cached enough?
No. git rm --cached removes the file from the latest commit, but it remains in past history. Use git filter-repo or BFG to remove from all history.
Q: Should I use git filter-repo or BFG?
git filter-repo is recommended. It's the tool recommended by official Git documentation as the replacement for git filter-branch. It's fast and safe. BFG is useful specifically for bulk text replacement (--replace-text). Avoid git filter-branch for new work.
Q: Do secrets remain in forks?
Yes. Force pushes don't propagate to forks. Contact fork owners directly, or request removal through GitHub Support.
Q: Is Secret Scanning enough protection?
Don't rely on it alone. Secret Scanning is free and automatic only for public repositories. Private repositories require paid GitHub Advanced Security. Custom or proprietary secrets may not be detected. Always combine with pre-commit hooks and git-secrets.
Related
Blob persistence in Git enables recovery of lost changes, but it also means data you want deleted lingers. For recovering staged changes lost by git checkout HEAD, see Recover Staged Changes Using git fsck.