Securing Leaked API Credentials

Beginner Security

An API key, database password, or service credential has been accidentally committed to a public Git repository, embedded in a publicly accessible file, or exposed in an application error response. The credential must be treated as fully compromised and rotated immediately, regardless of how briefly it was visible.

Symptoms

  • GitHub secret scanning alert or GitGuardian notification about a detected secret in a commit
  • Unexpected charges or quota exhaustion on a cloud/API service indicating unauthorised use
  • Unauthorised API calls appearing in service audit logs from unknown IP addresses
  • Error response body or stack trace visible in browser containing a database URL with credentials
  • `.env` file or `config.py` with credentials pushed to a public repository

Possible Root Causes

  • `.env` file accidentally staged and committed due to missing or incorrect `.gitignore` entry
  • Hardcoded credentials in source code pushed to a public or inadvertently public repository
  • Application error page (DEBUG=True in production) exposing database URL or secret key in traceback
  • Credentials embedded in a Docker image layer and pushed to Docker Hub
  • Secret written to application logs and log aggregator (Splunk, CloudWatch) with insufficient access control

Diagnosis Steps

1. Identify what was leaked and where

# Search git history for common secret patterns across all commits
git log --all --full-history -p -- '*.env' '*.py' '*.json' '*.yaml' '*.yml' \
  | grep -E "(api_key|secret|password|token|credential)" | head -40

# Use trufflehog to scan entire git history
trufflehog git file://. --json 2>/dev/null | python3 -m json.tool | head -100

# Or gitleaks (faster, no Python dependency)
gitleaks detect --source . --report-format json --report-path leaks.json
cat leaks.json | python3 -m json.tool

2. Confirm whether the secret is still live in the repo

# Check current HEAD for secrets
git show HEAD -- .env config.py settings.py | grep -E "(api_key|secret|password|DB_URL)"

# Check if the file is in .gitignore
cat .gitignore | grep -i "env\|secret\|credential"

3. Check for unauthorised usage

# For AWS keys: check CloudTrail for unexpected API calls
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=leaked-iam-user \
  --start-time $(date -d '24 hours ago' --iso-8601=seconds) \
  --output table

# For a web-exposed credential, check your access logs
grep "YOUR_EXPOSED_PATH" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn

4. Check if the credential is still valid

# Test AWS key validity
aws sts get-caller-identity --profile leaked_profile

# Test a database URL
psql "postgresql://user:password@host:5432/dbname" -c "SELECT 1;" 2>&1
# If it connects — credential still active, rotate immediately

Solution

Step 1: Rotate the credential IMMEDIATELY (do this first)

Do not wait to clean git history before rotating — time spent cleaning is time the credential is active.

# Example: rotate an AWS IAM access key
aws iam create-access-key --user-name service-account
# Note the new key — then delete the old one
aws iam delete-access-key --user-name service-account --access-key-id AKIA_OLD_KEY_ID

# Example: rotate a PostgreSQL password
psql -c "ALTER USER myapp WITH PASSWORD 'NEW_STRONG_PASSWORD';"

# Example: regenerate a GitHub personal access token
# Settings → Developer settings → Personal access tokens → Revoke → Generate new

Step 2: Update all systems using the old credential

# Update .env.prod on the server
ssh apps-us "cd /var/www/yourdomain.com && sed -i 's/OLD_KEY/NEW_KEY/' .env.prod && sudo systemctl restart gunicorn-app"

# Update the secret in 1Password (if using 1Password)
op item edit "project-myapp" --vault dev api_key="NEW_KEY"

Step 3: Remove the secret from git history

# Remove a specific file from ALL git history (nuclear option — rewrites history)
git filter-repo --path .env --invert-paths

# Or use BFG Repo Cleaner (faster for large repos)
java -jar bfg.jar --delete-files .env
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push --force

# IMPORTANT: All collaborators must re-clone — old clones retain the history

Step 4: Notify affected services and audit

  • Check service audit logs (AWS CloudTrail, GCP Audit Logs, Stripe Dashboard) for any actions taken with the leaked credential.
  • If a database credential leaked: audit all tables for unexpected inserts, deletes, or exports.
  • If a payment API key leaked: review recent transactions for fraudulent charges.

Step 5: Add protections to prevent re-occurrence

# Verify .gitignore covers secret files
echo '.env' >> .gitignore
echo '.env.*' >> .gitignore
echo '!.env.example' >> .gitignore
git add .gitignore && git commit -m "chore: ensure .env excluded from git"

Prevention

  • Add pre-commit secret scanning: Install pre-commit with the detect-secrets or gitleaks hook so secrets are caught before they ever reach git history.
  • Use a secrets manager: Store all credentials in 1Password, AWS Secrets Manager, or HashiCorp Vault — never hardcode or commit them to source control.
  • Never run DEBUG=True in production: Django and Flask debug pages print environment variables and stack traces containing database URLs. Set DEBUG=False and configure proper error pages.
  • Enable GitHub secret scanning: In repository Settings → Security → Secret scanning, enable both push protection (blocks the push) and alerts (detects existing secrets).
  • Principle of least privilege: Issue API keys with the minimum required permissions — a leaked read-only key causes far less damage than a leaked admin key.

Related Protocols

Related Terms

More in Security