Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Hardcoded secrets — API keys, passwords, and tokens embedded directly in source code — are one of the most common causes of cloud infrastructure breaches. This guide explains the risks, shows you how to find them using automated tools, and walks through the correct remediation process including git history cleanup.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
Hardcoded Secrets: What They Are, Why They''re Dangerous, and How to Find Them
A hardcoded secret is any sensitive credential — an API key, database password, private key, access token, or encryption key — that is embedded directly in source code rather than being loaded from environment variables or a secrets manager at runtime.
This is one of the most pervasive security problems in software development, and it is responsible for a disproportionate share of cloud infrastructure breaches. The pattern is almost always the same: a developer commits an API key to test something quickly, forgets about it, and it gets pushed to a remote repository. Automated scanners find it within minutes.
This guide covers what counts as a hardcoded secret, why they are dangerous, how to detect them, and the correct remediation process.
What Counts as a Hardcoded Secret?
Any of the following in your source code qualifies as a hardcoded secret:
// Database password
const DB_PASSWORD = "Sup3rS3cr3t!";
// AWS credentials
const AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE";
const AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
// API keys
const STRIPE_SECRET_KEY = "sk_live_51Abc...";
const OPENAI_API_KEY = "sk-proj-...";
// JWT secrets
const JWT_SECRET = "my-very-secret-jwt-signing-key";
// Private keys (even partial)
const PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\\nMIIEow...";
// Webhook secrets
const SLACK_SIGNING_SECRET = "8f742231b10e8888abcd99baac5a9e7f";It also includes secrets in:
- Configuration files committed to git (config.json, settings.py, .env files)
- SQL migration scripts with hardcoded passwords
- Dockerfile ENV instructions with actual secret values
- Test files that use real production credentials
- Comments that document a secret value
Why Hardcoded Secrets Are Dangerous
Public Repositories Are Scraped Continuously
GitHub, GitLab, and Bitbucket public repositories are scanned by automated tools operated by both security researchers and malicious actors. Within minutes of any push containing a secret, it can be found and extracted. GitGuardian, for example, scanned over 6 million commits on GitHub in 2023 and found over 1 million secrets exposed — an increase of 67% over the prior year.
Git History Persists After Deletion
This is the most misunderstood aspect of hardcoded secrets. Deleting a file or replacing a secret in a commit does not remove it from the repository. The secret continues to exist in every previous commit that contained it. Anyone who has cloned the repo — including CI/CD systems, other developers, and public forks — may have the secret in their local git history.
A commit deleting a secret is not remediation. It is a signal to anyone watching the repository that a secret just existed at that path.
Private Repositories Are Not Necessarily Safe
Secrets in private repositories can still be exposed through: - An employee leaving and taking a local clone - A compromised developer account with repository access - A third-party integration with read access to the repo (CI/CD, code review tools) - The repository accidentally being made public
Treat any committed secret as compromised, regardless of repository visibility.
Real-World Breach Examples
Toyota (2023): A subcontractor committed API keys for Toyota''s Connected Service cloud infrastructure to a public GitHub repository. The keys were exposed for nearly 5 years before discovery, exposing location data for approximately 2.15 million customers.
Uber (2016): Developers committed AWS credentials to a private GitHub repository. Attackers gained access to the repo (through a separate breach), used the credentials to access AWS, and exfiltrated data on 57 million users.
Twitch (2021): In the massive Twitch data leak, internal tools with hardcoded credentials were among the exposed assets, enabling further lateral movement through infrastructure.
The pattern is consistent: a committed secret, a pathway to that secret, and significant damage.
How to Detect Hardcoded Secrets
ZeriFlow Advanced Scan
ZeriFlow''s Advanced Scan accepts a GitHub repository URL (public or private with a token) or a ZIP file upload and scans the codebase for hardcoded secrets using pattern matching and entropy analysis. It covers the full commit history, not just the current HEAD.
This is the fastest way to get a complete picture: you get a report listing the file, line number, commit hash, and type of secret found.
truffleHog (CLI)
truffleHog is an open-source secret scanner that checks both regex patterns and high-entropy strings:
# Install
pip install trufflehog
# Scan a local git repository (including history)
trufflehog git file://. --only-verified
# Scan a remote GitHub repo
trufflehog github --repo https://github.com/yourorg/yourrepo
# Scan a specific branch
trufflehog git https://github.com/yourorg/yourrepo --branch mainThe --only-verified flag attempts to verify that discovered credentials are valid (e.g. by making an API call), reducing false positives.
git-secrets (AWS)
AWS''s git-secrets tool prevents committing secrets that match defined patterns:
# Install
brew install git-secrets # macOS
# Set up for your repo
cd your-repo
git secrets --install
git secrets --register-aws # Register AWS credential patterns
# Scan existing history
git secrets --scan-historyGitleaks
Another popular option, available as a binary and Docker image:
# Install
brew install gitleaks
# Scan current directory
gitleaks detect --source . -v
# Scan git history
gitleaks detect --source . --log-opts="HEAD~100..HEAD"Pre-commit Hook
The best defence is preventing secrets from being committed at all:
# .git/hooks/pre-commit
#!/bin/bash
# Run gitleaks before every commit
gitleaks protect --staged -v
if [ $? -ne 0 ]; then
echo "Secret detected in staged changes. Commit blocked."
exit 1
fiOr use the pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaksInstall and activate:
pip install pre-commit
pre-commit installEnvironment Variables: The Right Alternative
The standard replacement for hardcoded secrets is environment variables — values injected into the process at runtime from the host environment rather than the codebase.
Local Development
Use a .env file (and add it to .gitignore immediately):
# .env (never commit this)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
STRIPE_SECRET_KEY=sk_live_51...
OPENAI_API_KEY=sk-proj-...
JWT_SECRET=a-long-random-string-generated-with-openssl-rand-hex-32// Load in your application (Node.js)
import ''dotenv/config'';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const db = createPool(process.env.DATABASE_URL);Always add .env to .gitignore:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.production" >> .gitignoreCommit a .env.example file with placeholder values so developers know which variables are needed:
# .env.example (safe to commit)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
STRIPE_SECRET_KEY=sk_live_YOUR_KEY_HERE
OPENAI_API_KEY=sk-proj-YOUR_KEY_HERE
JWT_SECRET=generate-with-openssl-rand-hex-32Production: Secrets Managers
For production environments, environment variables injected via your deployment platform are the minimum. For anything sensitive, a dedicated secrets manager is better:
AWS Secrets Manager:
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: "eu-west-1" });
async function getSecret(secretName) {
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString);
}
// Usage
const { STRIPE_SECRET_KEY } = await getSecret(''myapp/production/stripe'');HashiCorp Vault:
# Read a secret
vault kv get secret/myapp/stripe
# In CI/CD
export STRIPE_SECRET_KEY=$(vault kv get -field=secret_key secret/myapp/stripe)Platform-managed secrets: Vercel, Railway, Render, Fly.io, and similar platforms all have built-in secret management that injects values as environment variables at runtime without exposing them in code or logs.
Removing Secrets from Git History
If you have found a secret in your git history, there are two required steps:
Step 1: Rotate the Secret Immediately
Before touching the git history, rotate the compromised credential. Revoke the existing API key, generate a new one, and update all places where it is used. Assume the old secret is compromised and has been used maliciously — check your audit logs for the affected service.
Step 2: Remove from Git History
Use git filter-repo (the recommended tool; git filter-branch is deprecated):
# Install
pip install git-filter-repo
# Replace the secret value with a placeholder in all commits
git filter-repo --replace-text <(echo "sk_live_abc123==>REMOVED_SECRET")
# Or remove an entire file from history
git filter-repo --path config/secrets.json --invert-pathsAfter running git filter-repo, the local history is rewritten. You then need to force-push:
git push origin --force --all
git push origin --force --tagsWarn all contributors: Anyone who has cloned the repository needs to re-clone or perform a hard reset. Old clones still contain the secret in history.
Step 3: Check for Forks and Archives
If the repository was ever public, forks and cached copies may exist on GitHub''s servers or external archive services. GitHub has a --delete-files option in its secret scanning alerts that can help, but the only safe assumption is that the secret was exposed and must be rotated.
CI/CD Integration
Add secret scanning to your CI/CD pipeline to catch secrets before they reach the remote repository:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for complete scanning
- name: Scan for secrets with gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}This runs on every push and pull request, failing the build if any secrets are detected.
Summary
Hardcoded secrets are one of the most easily preventable causes of serious security incidents:
- Never commit credentials, API keys, or secrets to version control
- Use environment variables locally and a secrets manager in production
- Add
.envto.gitignorebefore writing any secrets to it - Install a pre-commit hook to block secret commits before they happen
- If a secret has been committed: rotate first, then purge history with
git filter-repo - Assume any committed secret is compromised regardless of repository visibility
To scan your codebase for existing hardcoded secrets alongside a full security audit, use ZeriFlow''s Advanced Scan — it analyses GitHub repositories or uploaded codebases for secrets, CVEs, and insecure patterns in a single pass.