Exposed Credentials in Git: 90-Minute Incident Response Playbook
You just got a Slack message: someone found an AWS access key in your public GitHub repo. Or Slack's security team emailed you. Or your secret scanning alert fired. It does not matter how you found out -- what matters is what you do in the next 90 minutes.
The single biggest mistake teams make is spending the first 30 minutes trying to scrub the git history. That is backwards. An exposed credential is a live key that an attacker may already be using. Revoking it is the only action that eliminates the risk. History scrubbing is a compliance cleanup that can happen afterward.
This playbook gives you the exact sequence of actions for the first 90 minutes, the blast radius assessment checklist, communication templates, and the steps to prevent recurrence.
Minutes 0 to 15: Revoke First, Investigate Second
Your single priority: kill the credential.
Do not stop to investigate whether the key was actually used. Do not wait for management approval. Do not scrub history first. Revoke immediately.
AWS IAM keys
- Go to IAM > Users > [user] > Security credentials
- Find the exposed access key and click Deactivate (not Delete yet -- deactivate first to confirm nothing breaks)
- Wait 5 minutes; if no production alerts fire, click Delete
- Create a new key pair for the legitimate service that was using it
Or via CLI if you have credentials available:
aws iam update-access-key \
--access-key-id AKIA... \
--status Inactive \
--user-name [username]
GitHub personal access tokens / GitHub Actions secrets
- Settings > Developer settings > Personal access tokens > Revoke
- For organization tokens: Settings > Developer settings > GitHub Apps / OAuth Apps
- For Actions secrets: The secret itself is not the token -- find the underlying service credential and revoke that
Generic API keys (Stripe, Twilio, SendGrid, etc.)
Every major SaaS API has a key revocation path in their dashboard. Go directly to the provider's dashboard, not through your own UI. Most providers have a "Roll API key" option that revokes and reissues in one step.
Database passwords
-- PostgreSQL
ALTER USER app_user WITH PASSWORD 'new_strong_password';
-- MySQL
ALTER USER 'app_user'@'%' IDENTIFIED BY 'new_strong_password';
-- SQL Server
ALTER LOGIN app_user WITH PASSWORD = 'new_strong_password';
Update the secret in your secrets manager (Vault, AWS Secrets Manager, Azure Key Vault) and trigger a deployment to pick up the new credential. Do not manually update application config files.
Identify credential type
AWS key, GitHub token, API key, database password, certificate private key, OAuth client secret
Revoke at the source
Go to the issuing provider's dashboard and revoke/deactivate immediately -- before anything else
Issue a replacement
Create a new credential immediately so the legitimate service can be updated without downtime
Update secrets manager
Rotate the credential in Vault / AWS Secrets Manager / Azure Key Vault so apps pick up the new value on next deployment or secret refresh
Confirm nothing broke
Check monitoring dashboards and error rates for 5 minutes to confirm the revocation did not break dependent services
Minutes 15 to 45: Blast Radius Assessment
With the credential revoked, you now have time to understand what happened and whether the attacker did anything with it.
Check cloud provider access logs immediately
AWS CloudTrail -- query for the exposed access key ID:
# Find all API calls made by the exposed key in the last 30 days
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA... \
--start-time $(date -v-30d +%Y-%m-%dT%H:%M:%SZ) \
--query 'Events[*].{Time:EventTime,Event:EventName,Region:CloudTrailEvent}'
Key things to look for in CloudTrail:
CreateUser,CreateRole,AttachUserPolicy-- attacker creating persistenceGetSecretValue-- accessing other secretsRunInstances,CreateKeyPair-- spinning up infrastructure for cryptomining or C2S3:GetObjecton buckets you did not know the key had access to- API calls from unusual geographic regions or IP addresses
GCP Cloud Audit Logs
gcloud logging read \
'protoPayload.authenticationInfo.principalEmail="[service-account]"' \
--freshness=30d \
--format=json | jq '.[] | {time: .timestamp, method: .protoPayload.methodName, ip: .protoPayload.requestMetadata.callerIp}'
Azure Monitor / Entra ID Sign-in Logs
Search the sign-in logs for the service principal or application that owned the exposed credential. Look for sign-ins from unfamiliar IPs or countries.
GitHub audit log (for GitHub tokens):
# If you have GitHub Enterprise or GitHub.com org audit log access
gh api /orgs/{org}/audit-log \
--jq '.[] | select(.actor == "[username]") | {action: .action, created_at: .created_at, ip: ."@ip"}'
What to document during assessment
Answer these questions before your incident report:
- When was the commit made that introduced the credential?
- When was the repository made public (if it was previously private)?
- How long was the credential exposed and accessible?
- Was the credential actually used by an attacker? (Check access logs for unexpected IPs, times, or API calls)
- What resources did the credential have access to? (IAM policy, OAuth scopes, database permissions)
- Is there any evidence of data exfiltration? (S3 GetObject on sensitive buckets, large data transfers)
- Who else may have seen the credential? (Check the repo star/fork history; if it was forked, assume it was harvested)
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Minutes 45 to 75: Clean the Repository
Once the credential is revoked and the blast radius is understood, clean the repository. Note: this step is for compliance and to prevent the credential from being used by someone who cached the repo history -- it does NOT eliminate risk for any attacker who already cloned the repo.
Step 1: Delete the file or commit (simple case)
If the secret was added in the most recent commit:
# Remove the secret from the latest commit
git rm --cached [file-with-secret]
echo "[file-with-secret]" >> .gitignore
git add .gitignore
git commit --amend --no-edit
git push --force-with-lease origin [branch]
Step 2: Remove from full git history (BFG Repo Cleaner -- recommended)
BFG is faster and simpler than git filter-branch:
# Install BFG
brew install bfg
# Create a text file with the secret value (one per line)
echo 'AKIA...' > secrets.txt
echo 'actual-secret-value-here' >> secrets.txt
# Run BFG on a mirror clone of the repo
git clone --mirror https://github.com/org/repo.git
bfg --replace-text secrets.txt repo.git
# Clean and push
cd repo.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push --force
Step 3: Invalidate GitHub's cache
Even after force-pushing, GitHub caches commit history. Contact GitHub support to flush the cache for the specific commits, or make the repository private temporarily to limit exposure while the cache expires.
Step 4: Check for forks
Anyone who forked the repository before you cleaned it has the full history. Use the GitHub API to list all forks:
gh api /repos/{owner}/{repo}/forks --jq '.[].full_name'
If the repo has forks, assume the secret is permanently harvested. The cleanup is still worth doing to prevent casual discovery, but treat it as a compromised credential regardless of the history scrub.
Minutes 75 to 90: Notify and Document
Internal notification template
Send to your security team, engineering leadership, and legal/compliance within 90 minutes of detection:
Subject: [SECURITY INCIDENT] Exposed Credential - [Service/Key Type] - RESOLVED
Summary:
A [AWS IAM key / API token / database credential] was found in a public
GitHub repository. The credential has been revoked as of [TIME UTC].
Timeline:
- [DATE TIME]: Credential committed to repository [URL] by [user/commit]
- [DATE TIME]: Repository made public (if applicable)
- [DATE TIME]: Exposure detected via [method]
- [DATE TIME]: Credential revoked
- [DATE TIME]: New credential issued and deployed
Blast Radius:
- Credential type: [type]
- Resources in scope: [IAM policy / API scopes / database access]
- Evidence of attacker use: [Yes/No -- detail if yes]
- Data potentially exposed: [description or None identified]
Immediate Actions Taken:
1. Credential revoked at [TIME]
2. New credential issued and deployed at [TIME]
3. Git history cleaned (in progress / complete)
4. Access logs reviewed -- [findings]
Next Steps:
- [Pre-commit hook / secret scanning enforcement deployment by DATE]
- [Incident review meeting: DATE/TIME]
- [Customer/regulator notification if required: DATE]
Severity: [Low/Medium/High based on blast radius assessment]
Incident Commander: [Name]
When to notify customers or regulators
- If CloudTrail/audit logs show the credential was used to access customer data: notify affected customers and assess regulatory notification requirements
- GDPR: 72-hour clock to supervisory authority if personal data was accessed
- HIPAA: 60-day notification requirement if PHI was accessed
- If no evidence of attacker use and the blast radius is limited to internal infrastructure: internal notification only is typically sufficient
- Always loop in legal before external notifications
Prevention: Stop This From Happening Again
Pre-commit secret scanning (prevent at the source)
git-secrets (AWS) or detect-secrets (Yelp) run at commit time:
# Install detect-secrets
pip install detect-secrets
# Initialize baseline (scan existing codebase)
detect-secrets scan > .secrets.baseline
# Add pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
detect-secrets-hook --baseline .secrets.baseline
EOF
chmod +x .git/hooks/pre-commit
For team-wide enforcement, use pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
GitHub push protection (prevent before the push reaches GitHub)
Enable in GitHub Advanced Security settings. Blocks pushes containing known secret patterns before they reach the repository. Free for public repositories, requires GitHub Advanced Security for private repos.
Repository-level secret scanning
- GitHub secret scanning: Settings > Security > Secret scanning (free for public repos, included in GHAS for private)
- GitGuardian: monitors all commits across your organization in real time, sends alerts within seconds
- Gitleaks: open-source option that runs in CI/CD
# GitHub Actions: run Gitleaks on every PR
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Move secrets to environment variables and a secrets manager
No secret should ever live in source code. The architecture:
- Secrets stored in AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault
- Applications fetch secrets at runtime via the SDK, not from environment config files
- CI/CD secrets live in the pipeline's secret store (GitHub Actions Secrets, GitLab CI Variables), not in
.envfiles committed to the repo - Local development uses
.env.localfiles that are in.gitignoreand never committed
The bottom line
Exposed credentials are a sprint, not a marathon. The first 15 minutes are all that matter for risk reduction: revoke the credential, issue a replacement. Everything after that is forensics, cleanup, and gap closure. Ninety percent of teams that get breached from credential exposure had the credential exposed for days before it was revoked -- because they focused on the history scrub instead of the kill switch. Revoke first. Always.
Frequently asked questions
Should I delete or deactivate the exposed AWS key first?
Deactivate first, then delete after you confirm nothing broke. Deactivation immediately makes the key unusable for attackers but lets you reactivate if it turns out a critical service was using it and you have not yet deployed the replacement. Wait 5-10 minutes after deactivation, check your monitoring and error rates, then delete once confirmed. Deleting immediately risks a gap if you have not yet updated all services using the old key.
Does removing the commit from git history protect me?
Not if anyone has already cloned or forked the repo. Git history scrubbing prevents casual discovery by future visitors, but it cannot recall data that was already cloned. Any service that monitors GitHub for secrets (including attacker tooling) may have already harvested the credential. The only thing that eliminates risk is credential revocation. Treat history scrubbing as a compliance cleanup, not a security control.
How do I know if an attacker already used my exposed key?
Check your cloud provider's audit logs immediately. For AWS, use CloudTrail and search by the exposed access key ID. Look for: API calls from unexpected IP addresses or geographic regions, unusual API calls like CreateUser or RunInstances, access to S3 buckets containing sensitive data, or any calls that fall outside normal application behavior. If you see unexpected activity, escalate to a full incident response -- the blast radius extends beyond the credential itself.
What if the exposed credential has no CloudTrail or audit log coverage?
For credentials without centralized audit log coverage (some legacy systems, third-party API keys), you have to work backward: contact the API provider and ask if they have access logs for the key, check for unexpected charges or usage in the provider's dashboard, review any application-level logs that recorded API responses, and assume the credential was harvested if it was exposed in a public repo for more than a few minutes. The absence of audit logs is itself a gap to remediate.
When do I need to notify customers about an exposed credential?
Notify customers if the exposed credential provided access to customer data and there is any evidence it was used by an unauthorized party. If CloudTrail shows no unexpected access and the credential only had access to internal infrastructure, customer notification is typically not required. Always consult legal before notifying customers or regulators. GDPR requires notification to the supervisory authority within 72 hours of confirming a personal data breach; HIPAA requires notification within 60 days of discovering a PHI breach.
What is the fastest way to prevent secrets from being committed in the future?
Three layered controls: (1) Pre-commit hooks using detect-secrets or git-secrets -- runs before the commit is created on the developer's machine; (2) GitHub push protection -- blocks pushes containing known secret patterns before they reach the remote repository; (3) Repository-level secret scanning with GitGuardian or GitHub Advanced Security -- continuously monitors all new commits organization-wide and alerts within seconds of detection. Layer all three: pre-commit catches it first, push protection is the backstop, repository scanning catches anything that slips through.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, weekly, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals weekly.
The Mythos Brief is free.
AI that finds 27-year-old zero-days. What it means for your security program.
