Skip to content

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 informationAction
API keys (AWS, OpenAI, Stripe, etc.)Delete and reissue immediately in service dashboard
Database credentialsChange password immediately, suspend user permissions
SSH keys / certificatesRevoke and regenerate
GitHub Personal Access TokenSettings → 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:

ToolStatusCharacteristics
git filter-repoRecommendedPython-based. Fast and safe. Recommended by official Git documentation
BFG Repo-CleanerAlternativeJava-based. Convenient for bulk password replacement
git filter-branchLegacyShows 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 --force to 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..."

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.