Secrets
Leaked an API Key on GitHub? What to Do in the First Hour
By the BugSafer Security Team · · 6 min read
If you leaked an API key on GitHub, the first thing to do is revoke it at the provider, then rotate it. Deleting the commit or making the repository private does not undo the exposure, because the key may already have been copied, indexed, or scraped. Treat the key as compromised the moment it was pushed to a public repository.
The good news is that most leaks are recoverable if you act quickly and in the right order. The bad news is that the instinct to "clean up the repo first" wastes the minutes that matter most. This article walks through a calm, ordered incident checklist for the first hour: revoke and rotate, check provider logs, purge the secret from history, and put guardrails in place so it does not happen again.
Step 1: Revoke first, investigate second
Your first action is to invalidate the credential. Not rotate, not delete the repo, not edit the commit — revoke. Revocation is the only step that actually stops an attacker from using the key, and it takes seconds at most providers.
The exact mechanism depends on the provider:
- Cloud providers (AWS, Google Cloud, Azure): deactivate or delete the access key, service account key, or client secret in the console or CLI.
- SaaS APIs (payment processors, email providers, CI systems): find the API key or token in the dashboard and revoke or regenerate it.
- Personal access tokens (GitHub, GitLab): delete the token from your account settings.
- Database credentials: change the password and, if possible, terminate existing sessions.
For AWS access keys, for example, you can deactivate a key immediately without deleting it, which preserves it for later audit:
# Deactivate the leaked key right away (stops use, keeps it for audit)
aws iam update-access-key \
--access-key-id AKIAEXAMPLE123456 \
--status Inactive \
--user-name deploy-bot
# Then create a replacement and update your systems
aws iam create-access-key --user-name deploy-bot
Rotate means issuing a new credential and updating every system that used the old one — CI pipelines, environment variables, secret managers, local .env files. Do not skip this. A revoked key that is still referenced in your deployment config will cause an outage, and a "rotated" key that is still valid somewhere is not rotated at all.
One nuance: if the key is a short-lived token (an OAuth token with a one-hour expiry, for instance), revocation may be less urgent, but you should still revoke it rather than wait for expiry. You do not know who has it.
Step 2: Check provider logs for misuse
Once the key is dead, find out whether anyone used it. Most providers keep audit or access logs that show when a credential was used, from where, and for what. This is where you determine whether you have a quiet leak or an active incident.
Look for:
- Requests from unfamiliar IP addresses or regions. Compare against your normal deployment infrastructure.
- Unusual API calls. Data exfiltration, resource creation (crypto mining is a common motive), permission changes, or billing spikes.
- Timing. Was the key used in the window between the push and the revocation? Public repositories are scraped by automated bots, often within minutes.
# Example: pull recent CloudTrail events for a specific access key
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLE123456 \
--start-time 2024-01-01T00:00:00Z \
--max-results 50
If you find evidence of misuse, escalate: rotate any related credentials, review what the key had permission to access, check for persistence (new users, roles, or keys created by the attacker), and follow your organization's incident response process. If the key had broad permissions, assume the blast radius is broad too.
If the logs are clean, that is reassuring but not proof of safety — logging may be incomplete, and the key may have been copied for later use. Proceed with the rest of the checklist regardless.
Step 3: Purge the secret from git history
Now you can clean up. Understand what you are doing: removing a secret from the current version of a file does nothing for the copy that lives in every past commit. Git stores history, and anyone who cloned or forked the repo has that history. Purging is about reducing future exposure, not undoing the past — which is why steps 1 and 2 come first.
The vulnerable pattern looks like this:
# config.py — committed to the repo
API_KEY = "sk_live_51H8xExampleKeyValue1234567890"
Removing the line in a new commit leaves the key in history:
# config.py — fixed going forward, but the old commit still contains the key
import os
API_KEY = os.environ["API_KEY"]
To rewrite history, use a tool designed for it. git filter-repo is the current recommended option (it replaces the older git filter-branch):
# Install git-filter-repo, then rewrite all commits that touched the file
git filter-repo --path config.py --invert-paths
# Or replace the literal string across all history
git filter-repo --replace-text replacements.txt
# replacements.txt contains: sk_live_51H8xExampleKeyValue1234567890==>REDACTED
After rewriting, you must force-push and coordinate with everyone who has a clone:
git push --force --all
git push --force --tags
Important caveats:
- Force-pushing rewrites shared history. Teammates must re-clone or reset hard, or they will reintroduce the old commits.
- Forks and clones persist. You cannot purge copies you do not control. This is another reason revocation is mandatory.
- Caches and mirrors may retain the old data. GitHub, for example, may keep unreachable objects accessible for a period after a force-push. If the repository is public, consider whether the secret's exposure window already made it public knowledge.
- If the repo is public and the key was live, assume it was scraped. Automated tools scan public pushes continuously.
If rewriting history is too disruptive, an alternative is to treat the repository as compromised, rotate everything it touched, and start fresh — but for most teams, git filter-repo plus a coordinated force-push is the right move.
Step 4: Prevent recurrence
The final step is making sure this does not happen again. A leak is a process failure, not just a mistake, and the fix belongs in tooling and workflow rather than in a reminder to "be careful."
Keep secrets out of the repo by design. Use environment variables or a secret manager, and commit a .env.example with placeholder values instead of a real .env:
# .gitignore
.env
.env.*
!.env.example
*.pem
# .env.example — safe to commit
API_KEY=your-key-here
DATABASE_URL=postgres://user:password@localhost:5432/db
Add pre-commit scanning. Tools like gitleaks or detect-secrets catch secrets before they are committed:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Scan in CI. Run the same scanner on every push and pull request so a bypassed local hook still gets caught. Fail the build on findings.
Enable push protection. GitHub's secret scanning push protection blocks commits containing recognized secret patterns before they land. Turn it on for your organization.
Protect your default branch. Require pull requests and reviews via branch protection so a secret cannot be pushed directly to main without a second pair of eyes.
Scope credentials tightly. Give each key the minimum permissions it needs and a short lifetime where the provider supports it. A leaked read-only key with a one-hour expiry is a much smaller problem than a leaked admin key with no expiry.
Checklist
- Revoke the leaked credential at the provider immediately.
- Issue a replacement and update every system that used the old one.
- Check provider audit logs for unfamiliar IPs, regions, or API calls.
- Escalate if you find misuse; check for persistence and related credentials.
- Remove the secret from the current code and replace it with an environment variable or secret manager reference.
- Purge the secret from git history with
git filter-repo, then force-push. - Tell your team to re-clone or reset so they do not reintroduce the old commits.
- Add
.envand key files to.gitignore; commit.env.exampleinstead. - Add pre-commit and CI secret scanning; fail builds on findings.
- Enable push protection and branch protection on your repositories.
- Review credential scopes and lifetimes across the project.
FAQ
Does deleting the commit remove the leaked key from GitHub?
No. Deleting a commit or amending it leaves the old object reachable in the repository's history and in any clones or forks. You need to rewrite history with a tool like git filter-repo and force-push, and even then, copies you do not control may persist. This is why you revoke the key first — cleanup is secondary to making the credential useless.
How quickly can a leaked key be abused after it is pushed?
Public repositories are scanned by automated bots, and abuse can happen within minutes of a push. There is no safe grace period, which is why revocation is the first action rather than an afterthought. Treat any key pushed to a public repo as compromised from the moment it was pushed.
What if the key had limited permissions or was already expired?
Revoke it anyway. Limited permissions reduce the blast radius but do not eliminate it, and an expired key may still reveal information about your systems or be a stepping stone. Check the logs to confirm nothing happened, then rotate and clean up as usual. Consistency here prevents the "it was probably fine" habit that leads to real incidents.
Check your repositories for leaked secrets
BugSafer scans your GitHub repositories for committed API keys, tokens and private keys, and reports each one with masked evidence so you can rotate it.