One exposed API key can cost thousands in cloud bills. One leaked database password can lead to a data breach. Yet secrets accidentally committed to Git repositories remain one of the most common security vulnerabilities in software development. This guide covers prevention, detection, and remediation strategies to keep your credentials safe.
The Secret Exposure Problem
┌─────────────────────────────────────────────────────────────┐
│ SECRET EXPOSURE TIMELINE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Developer commits secret │
│ │ │
│ ▼ │
│ Push to remote ──────► Secret in Git history FOREVER │
│ │ │
│ ▼ │
│ Bot scanners find it (minutes to hours) │
│ │ │
│ ▼ │
│ Attackers exploit credential │
│ │ │
│ ▼ │
│ Breach / Financial damage / Data loss │
│ │
└─────────────────────────────────────────────────────────────┘
Studies show that thousands of new secrets are exposed on GitHub daily. Automated bots continuously scan public repositories, often exploiting exposed credentials within minutes of their commit.
Why Deleting Isn't Enough
# This does NOT remove the secret from history
git rm config/secrets.yml
git commit -m "Remove secrets"
git push
# Anyone can still see it
git log --all --full-history -- config/secrets.yml
git show <commit-hash>:config/secrets.yml
Git is designed to never lose data. Every commit is permanent in history unless explicitly rewritten.
Prevention: Stop Secrets Before They're Committed
Tool Comparison
| Tool | Type | Best For | Patterns | CI/CD |
|---|---|---|---|---|
| git-secrets | Pre-commit hook | Prevention | Custom + AWS | Limited |
| gitleaks | Scanner | Detection + CI | 150+ built-in | Excellent |
| truffleHog | Scanner | Deep scanning | Entropy + regex | Good |
| detect-secrets | Baseline + hook | Enterprise | Extensible | Good |
| GitHub Secret Scanning | Platform | GitHub repos | 100+ providers | Native |
Setting Up git-secrets
git-secrets by AWS Labs prevents commits containing secrets:
# Install
brew install git-secrets # macOS
# or
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets && make install
# Initialize in repository
cd your-repo
git secrets --install
# Add AWS patterns (detects access keys, secret keys)
git secrets --register-aws
# Add custom patterns
git secrets --add 'private_key'
git secrets --add 'PRIVATE KEY'
git secrets --add --allowed 'AKIAEXAMPLE' # Allow specific false positive
How it works:
┌──────────────────────────────────────────────────────┐
│ git commit │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ pre-commit │ │
│ │ hook runs │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ git-secrets │ │
│ │ scans staged │ │
│ │ files │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ No secrets │ │ Secret found │ │
│ │ Commit OK │ │ BLOCKED │ │
│ └─────────────┘ └─────────────┘ │
└──────────────────────────────────────────────────────┘
Setting Up gitleaks
gitleaks provides comprehensive secret detection with 150+ built-in patterns:
# Install
brew install gitleaks # macOS
# or download from https://github.com/gitleaks/gitleaks/releases
# Scan current state
gitleaks detect --source . --verbose
# Scan entire history
gitleaks detect --source . --verbose --log-opts="--all"
# Generate report
gitleaks detect --source . --report-format json --report-path report.json
Custom configuration (.gitleaks.toml):
[extend]
useDefault = true
[[rules]]
id = "custom-api-key"
description = "Custom API Key"
regex = '''(?i)mycompany[_-]?api[_-]?key\s*[=:]\s*['"]?([a-zA-Z0-9]{32,})['"]?'''
secretGroup = 1
[[rules]]
id = "internal-token"
description = "Internal Service Token"
regex = '''INT_TOKEN_[A-Z0-9]{16,}'''
[allowlist]
paths = [
'''\.gitleaks\.toml$''',
'''.*test.*''',
'''.*_test\.go$''',
]
Pre-commit Framework Integration
Use the pre-commit framework for multiple hooks:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.1
hooks:
- id: gitleaks
- repo: https://github.com/awslabs/git-secrets
rev: master
hooks:
- id: git-secrets
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
Install and run:
pip install pre-commit
pre-commit install
pre-commit run --all-files # Test on existing files
Detection: Finding Secrets in Existing Repositories
Full History Scan
# gitleaks - scan all branches and history
gitleaks detect \
--source . \
--log-opts="--all --full-history" \
--report-format sarif \
--report-path gitleaks-report.sarif
# truffleHog - entropy and pattern detection
pip install truffleHog
trufflehog git file://. --only-verified --json > trufflehog-report.json
# truffleHog for remote repos
trufflehog git https://github.com/org/repo --only-verified
CI/CD Integration
GitHub Actions:
name: Secret Scanning
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for scanning
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # Optional for extra features
GitLab CI:
secret_detection:
stage: test
image: zricethezav/gitleaks:latest
script:
- gitleaks detect --source . --verbose --exit-code 1
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
GitHub Secret Scanning
Enable native GitHub scanning:
- Go to Settings > Code security and analysis
- Enable Secret scanning
- For private repos, requires GitHub Advanced Security
GitHub automatically:
- Scans pushes for known secret patterns
- Alerts repository admins
- Optionally notifies the service provider (who may revoke the secret)
Remediation: When Secrets Are Exposed
Immediate Response Checklist
□ 1. ROTATE THE SECRET IMMEDIATELY
- Don't wait for history cleanup
- Assume the secret is compromised
□ 2. AUDIT ACCESS LOGS
- Check if the secret was used maliciously
- Review cloud provider activity logs
□ 3. REMOVE FROM HISTORY
- Use BFG or git filter-branch
- Force push to all branches
□ 4. NOTIFY COLLABORATORS
- They must re-clone or reset
- Old clones still have the secret
□ 5. ENABLE PREVENTIVE MEASURES
- Install pre-commit hooks
- Enable GitHub secret scanning
Removing Secrets from History
Option 1: BFG Repo-Cleaner (Recommended - Faster)
# Download BFG
wget https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar
# Create file with secrets to remove
echo "AKIAIOSFODNN7EXAMPLE" > secrets-to-remove.txt
echo "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" >> secrets-to-remove.txt
# Clone a fresh mirror
git clone --mirror [email protected]:org/repo.git
# Run BFG
java -jar bfg-1.14.0.jar --replace-text secrets-to-remove.txt repo.git
# Clean up
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push
git push --force
Option 2: git filter-repo (Modern Replacement for filter-branch)
pip install git-filter-repo
# Replace secret with ***REMOVED***
git filter-repo --replace-text <(echo 'AKIAIOSFODNN7EXAMPLE==>***REMOVED***')
# Or use expressions file
cat > expressions.txt << EOF
AKIAIOSFODNN7EXAMPLE==>***REMOVED***
regex:password\s*=\s*['"][^'"]+['"]==>password='***REMOVED***'
EOF
git filter-repo --replace-text expressions.txt
Option 3: git filter-branch (Legacy - Slower)
# Remove specific file from all history
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch path/to/secret/file' \
--prune-empty --tag-name-filter cat -- --all
# Clean up
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push origin --force --all
git push origin --force --tags
Post-Cleanup Requirements
# All team members must either:
# Option 1: Fresh clone (safest)
rm -rf repo
git clone [email protected]:org/repo.git
# Option 2: Reset existing clone
git fetch origin
git reset --hard origin/main
git reflog expire --expire=now --all
git gc --prune=now --aggressive
Best Practices for Secret Management
Environment-Based Configuration
┌─────────────────────────────────────────────────────────────┐
│ SECRET HIERARCHY │
├─────────────────────────────────────────────────────────────┤
│ │
│ PRODUCTION │
│ ├── AWS Secrets Manager / HashiCorp Vault │
│ ├── Injected at runtime via IAM roles │
│ └── Never in code or config files │
│ │
│ CI/CD │
│ ├── GitHub Actions Secrets / GitLab CI Variables │
│ ├── OIDC for cloud authentication (no static keys) │
│ └── Scoped to environments (staging, production) │
│ │
│ LOCAL DEVELOPMENT │
│ ├── .env files (in .gitignore) │
│ ├── .env.example with placeholders (committed) │
│ └── Secret manager CLI tools (1Password, Vault) │
│ │
└─────────────────────────────────────────────────────────────┘
.gitignore Patterns for Secrets
# Environment files
.env
.env.local
.env.*.local
.env.production
.env.development
# Credential files
credentials.json
service-account.json
*-credentials.json
*.pem
*.key
id_rsa
id_ed25519
# Cloud provider configs
.aws/credentials
.azure/credentials
gcloud-service-key.json
# IDE and tool configs that might contain secrets
.idea/**/dataSources/
.vscode/settings.json
# Terraform state (contains secrets)
*.tfstate
*.tfstate.*
.terraform/
# Kubernetes secrets
*-secret.yaml
*-secrets.yaml
Template Pattern
Commit example files, ignore real ones:
# .env.example (committed)
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
API_KEY=your-api-key-here
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# .env (in .gitignore - never committed)
DATABASE_URL=postgresql://admin:[email protected]:5432/myapp
API_KEY=sk_live_abc123...
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=wJalrXUtn...
Secret Rotation Strategy
# Example rotation schedule
secrets:
api_keys:
rotation: 90 days
notification: 14 days before expiry
database_passwords:
rotation: 30 days
automated: true # Use AWS Secrets Manager rotation
jwt_signing_keys:
rotation: 180 days
overlap_period: 24 hours # Support old and new keys
oauth_client_secrets:
rotation: 365 days
requires: Manual application update
Organization-Wide Setup
Global Git Hooks
Configure git-secrets for all new repositories:
# Set up template directory
mkdir -p ~/.git-templates/hooks
git secrets --install ~/.git-templates/hooks
# Configure git to use templates
git config --global init.templateDir ~/.git-templates
# Add patterns globally
git secrets --add --global 'PRIVATE KEY'
git secrets --add --global --allowed 'AKIAEXAMPLE'
git secrets --register-aws --global
# New repos automatically get hooks
git init new-project # Has git-secrets hooks
Baseline for Existing Secrets
For repositories with existing (known, accepted) secrets:
# Create baseline with detect-secrets
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Audit the baseline
detect-secrets audit .secrets.baseline
# Future scans compare against baseline
detect-secrets scan --baseline .secrets.baseline
Monitoring and Alerting
# GitHub Actions: Weekly full scan with notifications
name: Weekly Secret Audit
on:
schedule:
- cron: '0 9 * * 1' # Monday 9 AM
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run gitleaks
id: gitleaks
uses: gitleaks/gitleaks-action@v2
continue-on-error: true
- name: Notify on findings
if: steps.gitleaks.outcome == 'failure'
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "⚠️ Secrets detected in ${{ github.repository }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Quick Reference
Common Secret Patterns
| Secret Type | Example Pattern | Detection |
|---|---|---|
| AWS Access Key | AKIA[0-9A-Z]{16} | Built-in |
| AWS Secret Key | 40-char base64 | Built-in |
| GitHub Token | ghp_[a-zA-Z0-9]{36} | Built-in |
| Slack Token | xox[baprs]-[a-zA-Z0-9-]+ | Built-in |
| Stripe Key | sk_live_[a-zA-Z0-9]{24,} | Built-in |
| Private Key | -----BEGIN.*PRIVATE KEY----- | Built-in |
| Generic API Key | api[_-]?key.*['\"][a-zA-Z0-9]{16,}['\"] | Custom |
Emergency Commands
# Immediately scan for secrets
gitleaks detect --source . --verbose
# Check if specific string is in history
git log -S "AKIAIOSFODNN7EXAMPLE" --all
# Find commits that touched a file
git log --all --full-history -- path/to/secrets.env
# Quick removal (creates new history)
git filter-repo --path path/to/secrets.env --invert-paths
Related Resources
- Git & GitHub Complete Guide - Hub for all Git guides
- GitHub Actions Security - Secure CI/CD workflows
- GitHub Repository Security - Branch protection and access control
- Git Hooks Automation - Pre-commit and automation