Fixing Leaked API Keys: A Developer's Guide to Git Security
Discovering an API key in Git history is a serious security incident requiring immediate action. This guide outlines a developer's step-by-step response, emphasizing the critical need to invalidate the compromised key first, then systematically remove it from code and Git history. It also covers best practices for prevention, including using environment variables or secret managers, implementing least privilege, and integrating automated secret scanning into your workflow.

Imagine the scenario: you've just pushed your latest code to GitHub, only to discover a sudden, unexplained surge in API usage or unexpected charges. You then find the culprit: an API key committed directly to your repository.
javascript const apiKey = "sk_live_123456789";
This is a stressful situation, but it's fixable. The most crucial rule to understand is this: if an API key has been committed to Git, assume it has been copied and compromised, even if you delete it immediately. Git's history preserves past file versions, making exposed credentials vulnerable to automated scanners. Simply deleting the key from the latest file doesn't secure the old key.
This guide will walk you through a comprehensive response, following a core workflow:
Invalidate → Investigate → Remove → Replace → Prevent
Let's start by clarifying what an API key is and then dive into the immediate emergency steps.
Understanding API Keys and Their Risks
An API key is a credential enabling an application to interact with a service. This could be anything from a payment processor or mapping service to a cloud platform or a custom company API. They are often referred to as secrets because their possession grants access to data, allows resource creation, modifies records, or generates charges on your account.
Not all API keys carry the same risk. Some browser-specific keys are designed to be publicly visible, though they should still have appropriate restrictions and quotas. However, a general rule applies:
If a credential can access private data, create resources, modify records, or generate charges, it should never be directly stored in your source code.
The Emergency Response: Immediate Actions
Upon discovering a leaked credential, the natural instinct might be to quickly delete the key and push another commit. Resist this urge. Your first and most critical priority is to render the leaked credential useless.
Consider a house key lost in a public place. Deleting a picture of the key is irrelevant if someone has already picked up the physical key. You must change the lock first.
Step 1: Revoke or Rotate the Leaked Key
Head to the dashboard of the service that issued the credential. Look for options like "Revoke," "Delete," "Disable," "Rotate," "Regenerate," or "Create new key." If key rotation is supported, it's often best to create a replacement key before disabling the old one to minimize application downtime during the update process. The key takeaway is that the original credential must no longer be usable. Do not reuse it, rename it, encode it, or move it; treat it as compromised.
Step 2: Investigate Suspicious Activity
After disabling the key, thoroughly check the provider's usage dashboard and logs. Look for:
- Sudden spikes in requests or requests from unfamiliar locations.
- Unexpected database queries, new cloud resources, or changes to permissions.
- Unusual payment activity or new deployments.
- Activity during times your application was typically inactive.
If the credential had broad permissions, assume that any resource within its scope may have been accessed or modified. Document your findings, including a timeline of events, which can be invaluable for internal reporting or communication with service providers.
Cleaning Up Your Codebase
Once the old key is disabled, you can safely remove it from your working files.
Step 3: Remove the Secret From Your Current Code
Instead of hardcoding the secret:
javascript const apiKey = "your-real-api-key";
Load the credential from an environment variable:
javascript const apiKey = process.env.API_KEY; if (!apiKey) { throw new Error("API_KEY is not configured"); }
In Python, this would look like:
python import os api_key = os.environ.get("API_KEY") if not api_key: raise RuntimeError("API_KEY is not configured")
The principle is straightforward: Source code → environment variable → secret value instead of Source code → hardcoded secret.
Step 4: Use a .env File for Local Development
For local development, environment variables can be stored in a .env file:
env API_KEY=your-local-development-key DATABASE_URL=your-local-database-url
Tools like dotenv (for Node.js) can load these values:
bash npm install dotenv
javascript import "dotenv/config"; const apiKey = process.env.API_KEY;
Critically, the .env file should not be committed to Git. Add it to your .gitignore:
gitignore
Environment files
.env .env.* !.env.example
Credential files
*.pem *.key credentials.json service-account.json
Local development files
.DS_Store
Remember, .gitignore only prevents future commits. If .env was already committed, you must untrack it while keeping the local file:
bash git rm --cached .env git add .gitignore git commit -m "Ignore local environment files"
This only removes it from future commits, not from Git history.
Step 5: Create a Safe .env.example
To inform other developers about required environment variables without exposing secrets, create a .env.example file. This file contains variable names, not real credentials, and can be committed:
env
Required API credential
API_KEY=
PostgreSQL connection string
DATABASE_URL=
Optional application port
PORT=3000
New developers can then copy this file and fill in their own values:
bash cp .env.example .env
Use clear, fake placeholders like API_KEY=replace-me-with-your-own-key.
Tackling Git History: The Hard Part
Step 6: Determine Whether the Secret Is Still in Git History
Git maintains a complete history. Even if your latest commit removes a key, an earlier commit might still contain it. For example:
Commit A: Add API key to config.js
Commit B: Update API integration
Commit C: Delete API key
Commit A still exposes the key. You can inspect a file's history with git log --all -- config.js or view an old version with git show COMMIT_ID:config.js.
To search for a known leaked value throughout history:
bash git log --all -S"your-leaked-key" --oneline
Assume the secret is in history until proven otherwise.
When Do You Need to Rewrite Git History?
- Never Committed: If the secret was only in your working directory, no history rewrite is needed. Remove it and
.gitignorethe file. - Committed Locally, Not Pushed: You can clean up local commits before pushing.
- Pushed to a Remote Repository (Public or Private): Treat the credential as compromised. Revoke it immediately. Then, decide if rewriting history is appropriate. Even private repositories are not entirely safe from credential leaks (e.g., through compromised accounts, CI logs, forks, or screenshots). The safest rule remains: never intentionally commit credentials to Git, even in a private repository.
Step 7: Remove the Secret From Git History
If the credential was committed, you'll likely need to rewrite history. Before doing so, create a backup:
bash git clone --mirror https://github.com/your-username/your-repository.git repository-backup.git
Option 1: Remove an Entire File
If the secret was in a dedicated file like .env, you can remove that file from the entire history using git filter-repo:
bash git filter-repo --path .env --invert-paths
Option 2: Replace a Secret Inside a File
If you need to keep the file but remove specific secrets from its history, create a temporary replacements.txt file (do not commit this file):
text your-leaked-key==>YOUR_API_KEY_HERE
Then run:
bash git filter-repo --replace-text replacements.txt
For multiple secrets:
text old-api-key==>REMOVED_API_KEY old-database-password==>REMOVED_DATABASE_PASSWORD old-token==>REMOVED_TOKEN
bash git filter-repo --replace-text replacements.txt
Always test this on your backup clone first. Delete replacements.txt immediately after use.
Step 8: Verify That the Secret Is Gone
Never assume the cleanup worked. Search for the leaked value again:
bash git log --all -S"your-leaked-key" --oneline
Inspect relevant files and commits. Also check other locations like pull requests, CI/CD logs, build artifacts, and documentation, as rewriting your repository doesn't erase copies that already exist elsewhere.
Step 9: Push the Cleaned History Carefully
Once verified, you may need to force-push the rewritten history:
bash git push --force --all origin git push --force --tags origin
Important Warning: Force-pushing rewritten history is highly disruptive. It changes commit hashes and impacts collaborators. Before proceeding on a shared project, communicate clearly with your team, coordinate the cleanup, and follow any organizational incident-response processes. Collaborators will typically need to re-clone the repository.
Long-Term Prevention and Best Practices
Step 10: Replace the Credential Everywhere
Create and use the replacement credential. Crucially, update every environment where your application runs. Don't forget non-production environments or CI/CD pipelines. A checklist helps:
- Local development
- Automated tests
- Staging and Production
- CI/CD variables
- Docker configuration
- Cloud deployment settings
- Scheduled scripts and serverless functions
After updating, test the application in each critical environment.
Step 11: Restrict the Replacement Key
The new credential should adhere to the principle of least privilege, having only the permissions it absolutely needs. Implement restrictions like:
- Read-only permissions or specific API scopes.
- Allowed IP addresses or domains.
- Environment-specific access (e.g.,
local-development-keyvs.production-key). - Request quotas, rate limits, or expiration dates.
What About Frontend Applications?
Frontend code runs on the user's device, making it inspectable. Browser API keys designed to be public should still be restricted. However, truly private credentials must never be placed in browser code. Instead, proxy requests through your own trusted backend:
javascript fetch("/api/data");
Your backend then securely communicates with the private service:
javascript
const response = await fetch(
"https://private-api.example.com/data",
{ headers: { Authorization: Bearer ${process.env.PRIVATE_API_TOKEN} } }
);
Environment Variables vs. Secret Managers
While environment variables are practical for local development and smaller applications, larger production systems benefit from dedicated secret managers. These systems offer centralized storage, access controls, auditing, automated rotation, versioning, and better integration with deployment systems, ensuring your source code doesn't store production secrets.
Add Secret Scanning to Your Workflow
Humans make mistakes. Automated secret scanning tools like Gitleaks, TruffleHog, or detect-secrets can catch credentials before they enter a repository. Integrate these into your CI/CD pipeline. For example, a basic GitHub Actions workflow might use Gitleaks:
yaml name: Secret Scan on: push: pull_request: jobs: scan: runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Scan for secrets uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Additionally, consider using Git hooks (e.g., pre-commit hooks) as an extra local safety net, and always review your staged diff carefully before committing.
FAQ
Q: Why can't I just delete the commit with the leaked key?
A: Deleting a commit from your local branch doesn't remove it from Git's underlying object database. If that commit was ever pushed to a remote repository, or if other developers cloned your repository before the deletion, the data is likely still accessible in their local histories or on the remote server. Git history rewriting tools like git filter-repo are designed to permanently remove objects across all branches and tags.
Q: What's the difference between rotating and revoking an API key?
A: Revoking an API key immediately invalidates it, making it unusable. Rotating an API key typically involves generating a new key and then invalidating the old one, often with a brief overlap or grace period to allow applications to transition to the new key without downtime. Rotation is generally preferred if supported, as it can be less disruptive during the credential update process.
Q: Is storing API keys in environment variables truly secure for production?
A: For many scenarios, environment variables offer a reasonable level of security, especially when managed by secure deployment platforms that inject them at runtime. However, for highly sensitive production environments or complex distributed systems, a dedicated secret manager (like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault) provides superior features such as centralized control, fine-grained access policies, auditing, automated rotation, and encryption at rest and in transit, which environment variables alone do not natively offer.
Related articles
Professor Murder Rides the Subway is a forgotten slice of dance punk
In a recent digital archaeology expedition, Terrence O'Brien, Weekend Editor at The Verge, unearthed and lauded Professor Murder's 2006 EP, "Professor Murder Rides the Subway," as a quintessential, yet largely
Persona 6 Release Window Teased, Physical Edition Shakes Things Up
Persona 6's release window is now estimated between March 2027 and January 2028, based on Persona 4 Revival's launch and Sony's disc production end. Interestingly, physical copies are confirmed as PS5-exclusive in Japan, sparking debate amid the industry's digital shift.
Meta's Data Center Robots: A Glimpse into the Future of Work
Verdict: A Transformative, Yet Troubling, Push Meta's ambitious move to integrate robots into its data centers marks a significant step towards automating the backbone of the digital world. While promising efficiencies,
Pixel 11: A Security Step Back for Privacy Advocates
Pixel 11: The Verdict on Security and Custom ROMs Quick Verdict: The Google Pixel 11 series marks a significant regression for privacy and security enthusiasts. The confirmed omission of Arm's Memory Tagging Extensions
Reimagining Classic IM: Exploring Open OSCAR Server in Go
Open OSCAR Server is an open-source, Go-based instant messaging server compatible with classic AIM and ICQ clients. It enables developers and enthusiasts to self-host a private IM server, reviving the functionality of these legacy platforms. The project boasts broad client compatibility, detailed protocol implementations, and a management API for administration.
Android Auto Troubleshooting: Your Go-To Fix Guide
Quick Verdict: Your Essential Guide to a Smooth Ride Android Auto, when it works, seamlessly integrates your smartphone into your car's infotainment system, putting navigation, messages, and media right at your




