Hardcoded Secrets: Why They Still Reach Production and How Attackers Find Them
Published · Strata Security
On This Page
- What Counts as a Hardcoded Secret
- Why Developers Accidentally Commit Secrets
- Why Deleting a Commit Usually Isn't Enough
- How Attackers Discover Exposed Credentials
- Common Secrets Found in the Wild
- Real-World Attack Paths After Exposure
- Rotating and Revoking Credentials
- Secret Managers and Storage Practices
- Preventing Future Exposure
- Developer Secret Management Checklist
- Frequently Asked Questions
- Conclusion
1What Counts as a Hardcoded Secret
A hardcoded secret is any credential — an API key, password, access token, signing key, or database connection string — written directly into source code, a configuration file committed to version control, or compiled directly into an application binary, instead of supplied at runtime from an external, access-controlled source. The defining property isn't what the value looks like; it's that the secret's lifetime is tied to the code's lifetime rather than being independently issued, rotated, and revoked.
This matters because a hardcoded secret inherits every property of the file that contains it: it's copied every time the repository is cloned, preserved in every commit that ever existed, and — for a mobile app — shipped to every device that installs the compiled binary. A credential stored correctly in a secret manager can be rotated in one place. A hardcoded one has to be found and removed everywhere it was ever copied, which, as the next section covers, is a longer list than most developers expect.
2Why Developers Accidentally Commit Secrets
Almost no one commits a secret on purpose. It happens through a small number of repeatable patterns:
- A quick local test. A developer pastes a real API key directly into a script to confirm an integration works, intending to remove it before committing, and the commit happens before the cleanup does.
- A missing or incomplete
.gitignore. A.envfile is created for local development and the project's.gitignorewas never updated to exclude it — common in projects scaffolded quickly or by a tool that didn't generate one. - Copy-pasted example code. Documentation or a tutorial shows a working example with a placeholder key; a developer copies the pattern and substitutes a real value, then commits the file as-is.
- Infrastructure-as-code state files. Terraform's
.tfstatefile frequently contains live credentials and connection details in plaintext, and doesn't look like a "secrets file" the way a.envdoes — it's easy to commit without recognizing what's inside it. - A secret assumed to be "internal only." A developer reasons that a private repository is safe enough for a real credential, without accounting for every collaborator, CI integration, and fork that also has access to that same private repository.
3Why Deleting a Commit Usually Isn't Enough
Git is designed to preserve history, which is exactly what makes a deleted secret still recoverable. Removing a file — or a line — in a new commit doesn't erase the earlier commit where the secret was introduced; it only stops that secret from appearing in a fresh checkout of the current HEAD. Anyone with repository access can still find it.
# The secret was added in an earlier commit and removed in a later one.
# Neither git log nor a fresh clone hides it from someone who looks:
git log --all -p -- config.py | grep -i "api_key"
# Or, more directly, check out the exact commit before the deletion:
git checkout <commit-before-deletion> -- config.pyRemoving a secret from history for real requires rewriting it — withgit filter-repo or the BFG Repo-Cleaner — and force-pushing the rewritten history to every remote, which is itself disruptive to any collaborator who has the old history checked out locally. This is why rotating the credential at its source always comes first: history rewriting removes the secret from the repository, but only rotation stops the exposed value from still working if someone already copied it.
4How Attackers Discover Exposed Credentials
Finding exposed credentials at scale doesn't require targeting anyone specifically — several standing mechanisms surface them continuously:
- Automated scanning of public commits. GitHub's own secret scanning inspects public pushes in near real time and, for partnered providers, reports high-confidence matches directly to the credential issuer — but independent adversary-run scrapers watch the same public event stream and are not obligated to report anything.
- Code search and dorking. Simple, well-known search queries against GitHub's code search or general search engines for patterns like
"AWS_SECRET_ACCESS_KEY="or"BEGIN RSA PRIVATE KEY"surface exposed values across millions of public repositories with no custom tooling required. - Decompiled mobile binaries. A secret embedded in an Android or iOS app is retrievable directly from the compiled binary using freely available tools —
jadxfor Android DEX bytecode,class-dumporstringsfor iOS — no server access needed at all, since the app itself ships the secret to every installed device. - npm and PyPI package inspection. A secret accidentally bundled into a published package (rather than excluded via
.npmignoreor a build step) ships to everyone who installs that package, and package registries are trivially easy to search programmatically end to end. - Cached and archived copies. The Wayback Machine, search engine caches, and third-party repository mirrors can retain a public repository's content — or a JavaScript bundle's source map — even after the original is deleted or made private.
- Leaked internal tooling and gists. Public GitHub Gists, pasted terminal output shared in a public forum or support ticket, and CI build logs that echo environment variables are all lower-effort exposure paths than the repository itself.
5Common Secrets Found in the Wild
Some credential formats are recognizable enough that both defenders and attackers can pattern-match them directly:
| Secret Type | Recognizable Pattern | Risk If Exposed |
|---|---|---|
| AWS access key | AKIA[A-Z0-9]{16} | Full programmatic access to whatever the associated IAM policy allows — often broader than intended |
| Azure credentials | Client secret / connection string with AccountKey= | Access to Azure resources or storage scoped to the credential |
| Google Cloud credentials | Service account JSON key file | Full access to whatever roles the service account was granted — frequently over-scoped |
| Firebase | Server-side Admin SDK key (not the public web config) | Full read/write to the Firebase project, bypassing security rules entirely |
| Stripe secret key | sk_live_... | Ability to create charges, read customer payment data, and issue refunds |
| SendGrid / Twilio | SG.xxx / Account SID + Auth Token | Send email or SMS as the organization — commonly used for phishing and spam campaigns |
| GitHub token (PAT) | ghp_[A-Za-z0-9]{36} | Repository read/write access scoped to whatever the token was granted — often broad |
| SSH private key | -----BEGIN OPENSSH PRIVATE KEY----- | Direct server or repository access, no password required |
| Database password | Plaintext in a connection string | Full read/write access to the database, often including customer data |
| JWT signing secret | Any string used as an HMAC signing key | Ability to forge valid authentication tokens for any user, including admins |
Python — AWS Credentials
import boto3
s3 = boto3.client(
"s3",
aws_access_key_id="AKIAIOSFODNN7EXAMPLE",
aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
)import boto3
# boto3 resolves credentials from the environment, a shared config file,
# or an attached IAM role -- never a literal argument in source.
s3 = boto3.client("s3")JavaScript — SendGrid API Key
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey('SG.aBcD1234EXAMPLEKEYaBcD1234EXAMPLE')const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(process.env.SENDGRID_API_KEY)Kotlin — Android API Key
object ApiConfig {
const val API_KEY = "AIzaSyD-EXAMPLE_HARDCODED_KEY_1234567890"
}// build.gradle.kts injects this from local.properties (gitignored)
// or a CI secret -- it never exists as a string literal in version control.
object ApiConfig {
val API_KEY: String = BuildConfig.MAPS_API_KEY
}Even sourced this way, remember that anything reachable through BuildConfig still ends up compiled into the shipped APK as a string — appropriate for a restricted, client-scoped key, but not a substitute for keeping server-side secrets off the device entirely. See APK Static Analysis, Explained for how embedded strings are recovered from a compiled Android build.
Swift — iOS API Key
struct APIConfig {
static let apiKey = "sk_live_51H8xEXAMPLEHARDCODEDKEY"
}struct APIConfig {
// Provisioned into Keychain during setup or first launch --
// never a literal string in source.
static var apiKey: String {
KeychainService.shared.read(key: "api_key") ?? ""
}
}The same recovery techniques covered in IPA Security Testing, Explained apply here — a hardcoded Swift string literal is recoverable from the compiled binary with strings or class-dump, with no need to run the app at all.
6Real-World Attack Paths After Exposure
An exposed credential is rarely the end goal — it's a foothold. A few representative paths from discovery to actual damage:
- Cloud credential → cryptomining or data exfiltration. An exposed AWS or GCP key with broad IAM permissions is commonly used to spin up compute instances for cryptomining at the victim's expense, or to enumerate and exfiltrate data from accessible storage buckets and databases.
- Payment processor key → fraudulent charges and data access. A live Stripe secret key grants the ability to read customer payment metadata and create or refund charges — directly monetizable, and often not noticed until a chargeback or billing anomaly surfaces.
- Communications API key → phishing infrastructure. An exposed SendGrid or Twilio credential lets an attacker send email or SMS that appears to originate from a legitimate, trusted sender domain — valuable specifically because it inherits the victim organization's sender reputation.
- JWT signing secret → full authentication bypass. If an application's JWT signing secret is exposed, an attacker can forge a validly-signed token for any user ID, including an administrator account, without ever needing that user's actual password.
- GitHub token → repository and pipeline compromise. A personal access token with write access lets an attacker push malicious commits, modify CI/CD workflow files to exfiltrate other secrets, or add themselves as a collaborator — turning one leaked credential into a foothold across everything that repository's pipeline can reach.
7Rotating and Revoking Credentials
These are related but distinct responses, and the right one depends on whether there's evidence of active abuse:
| Action | What it does | When to use it |
|---|---|---|
| Rotate | Issue a new credential and update every consumer to use it, ideally before invalidating the old one | Exposure is suspected or historical, no evidence of active malicious use yet — avoids an unplanned outage |
| Revoke | Immediately invalidate the credential, accepting disruption to anything still using it | Direct evidence of active abuse (unfamiliar access-log activity, unexpected charges, unauthorized commits) |
The general response sequence once a hardcoded secret is confirmed:
- Rotate or revoke the credential at its source — the provider that issued it — before anything else.
- Check the provider's access/audit logs for the exposure window for any activity from unfamiliar IPs, regions, or usage patterns.
- Remove the secret from the current codebase and, separately, rewrite git history to remove it from every earlier commit.
- Update every service or environment that consumed the old credential with the new one before the old one is fully invalidated.
- Document the exposure and its root cause — the pattern that led to it is usually the thing worth fixing structurally, not just this one instance.
8Secret Managers and Storage Practices
Environment variables are a real improvement over a hardcoded literal, but they have limits worth knowing: a value in process.env is still plaintext in the running process and in most platforms' deployment configuration UI, still has to be set correctly and consistently across every environment, and still requires someone to manually update every consumer when it's rotated.
A dedicated secret manager — AWS Secrets Manager, HashiCorp Vault, Google Secret Manager, or a developer-focused option like Doppler — adds three things environment variables alone don't: centralized access control over exactly which service or person can read which secret, an audit trail of every access, and the ability to rotate a credential in one place and have every consumer pick up the new value automatically.
- A single-developer project or early-stage prototype: a
.envfile excluded from version control, plus your CI platform's built-in encrypted secrets storage, is a reasonable minimum. - Multiple services sharing the same credential: this is the point where a dedicated secret manager starts earning its added complexity — rotating in one place beats updating every consumer by hand.
- Regulated data or compliance obligations (payment data, health data): a secret manager with access logging is close to a baseline expectation, not an optional upgrade.
9Preventing Future Exposure
No single control catches everything reliably on its own — prevention works as layers:
- Pre-commit hooks. Tools like
gitleaksordetect-secretsscan a commit for known credential patterns before it's created, stopping the most common mistake at the earliest possible point. - CI-level scanning. The same scan should run again in the pipeline, since a pre-commit hook can be skipped, uninstalled, or simply not set up on every contributor's machine.
- A maintained
.gitignore..envfiles,.tfstatefiles, and any local credential file should be excluded by default in every project template, not added reactively after the first incident. - Code review culture. A reviewer who knows to look for a literal string that "looks like a key" catches what automated pattern matching sometimes misses — split values, encoded strings, or unfamiliar credential formats.
- Least-privilege credentials. Scoping every credential to only what it actually needs limits the blast radius of an exposure that does happen — an over-permissioned key turns a contained incident into an organization-wide one.
10Developer Secret Management Checklist
Before Writing Code
- A secret manager or environment-variable convention is decided before the first credential is needed, not improvised under deadline pressure.
.gitignorealready excludes.env,.tfstate, and any local credential files, from the project's first commit.
During Development
- No credential is ever pasted directly into source code, even "temporarily" for a local test.
- Pre-commit secret scanning is installed and active on every contributor's machine, not just recommended in a README.
In CI/CD
- Secrets are stored as encrypted pipeline variables, scoped to the specific job or environment that needs them.
- Secret and dependency scanning runs on every push, not on a manual or periodic schedule.
In Production
- Every credential is scoped to least privilege — the minimum access it actually needs, nothing broader.
- A rotation schedule exists for every long-lived credential, independent of whether an exposure has occurred.
After an Exposure
- The credential is rotated or revoked at its source immediately — before removing it from the codebase.
- Git history is rewritten to remove the secret, and every collaborator is notified to re-clone or re-fetch.
- Provider access logs are reviewed for the exposure window before considering the incident closed.
11Frequently Asked Questions
12Conclusion
Hardcoded secrets keep reaching production for the same handful of reasons: a quick local test that never got cleaned up, a missing .gitignore entry, a private repository assumed to be safe enough. None of these require a sophisticated attacker to exploit — they require an automated scanner, a public code search, or a decompiled binary, all of which are freely available today. Treating rotation, secret management, and pre-commit scanning as standard practice — not an incident response — is what keeps an accidental commit from becoming a real compromise.
Strata Security is one example of a platform that identifies exposed secrets automatically during repository and application security assessments, cross-referencing pattern and entropy-based detection across both current code and git history. See Repository Security Scanner for how that detection works, or Repository Security Assessment Checklist and CI/CD Security Controls Every Engineering Team Should Have for the broader repository and pipeline practices this fits into.
Apply This With the Decision Center
This article supports Repository Security Scanner — see the product page for how Strata applies these ideas directly.
Visit Repository Security Scanner →More From the Knowledge Center
- How Engineering Managers Quantify Application Security Risk
- CI/CD Security Controls Every Engineering Team Should Have
- Repository Security Assessment Checklist
- APK Static Analysis, Explained
- IPA Security Testing, Explained
- Vulnerability Prioritization Beyond CVSS
- How to Secure AI-Generated Code Before You Ship It