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
Android 17 QPR2 Beta 7.1: The Performance Relief You've Been Waiting
Android 17 QPR2 Beta 7.1: Finally Fixing the QPR1 Performance Woes Verdict: Android 17 QPR2 Beta 7.1 is a crucial and highly anticipated software update for Google Pixel users, specifically targeting and resolving the
Ditch Merge & Center: The Better Way to Align Text in Excel
Learn to use Excel's "Center Across Selection" to align text across cells without merging, avoiding common data manipulation issues. This guide provides steps to utilize this newly accessible feature and add it to your Quick Access Toolbar.
AI's Confidence vs. Formal Methods' Rigor: Design Flaws & Verification
While AI accelerates code generation, formal methods remain critical for ensuring system correctness. Languages like TLA+ and tools like Quint enable developers to model and verify system designs before coding, catching subtle flaws. Quint makes formal methods more accessible with developer-friendly syntax and executability. This approach is vital for verifying AI-generated code through trace validation and model-based testing, ensuring reliability against real-world data.
Kojima Compares Divisive New Film 'Digger' to MGS2's Prescient Vision
Gaming icon Hideo Kojima has praised the divisive 2026 film *Digger*, drawing intriguing parallels to his own prophetic game, *Metal Gear Solid 2*. Both works, he explains, uniquely explore the dangers of misinformation and a fragmented digital reality, anxieties he shares with *Digger*'s director, Alejandro González Iñárritu.
Ephemeral PR Preview Environments on GKE: A Deep Dive
Every development team eventually grapples with the limitations of shared staging environments. The familiar scenario: multiple features deployed simultaneously, database migrations clashing, and the phrase "it works on
Galaxy Watch One UI 9 Beta: Promising Progress, But Patience Needed
Galaxy Watch 8, Watch Ultra, and Watch 7 are receiving a new One UI 9 Watch beta build with bug fixes and a security patch. Active development signals progress, but a stable release date remains elusive.




