Application Security Testing

Secrets Detection: Finding Leaked Credentials Before Attackers Do

Leaked API keys, tokens, and credentials in source code are one of the most preventable causes of production breaches. Here is how detection works, where it falls short, and how to build a program that catches leaks before they reach git history.

The fastest path from code repository to production breach often runs straight through an accidentally committed credential. An API key pushed alongside a feature branch, a database password left in a configuration file, a private key copy-pasted into a test script and forgotten — these are not exotic attack vectors. They are routine findings in almost every codebase that has grown beyond a handful of contributors. The good news is that secrets detection is one of the highest-return controls an engineering team can put in place. The challenge is doing it completely, because partial detection creates a false sense of coverage that can be more dangerous than no detection at all.

What Counts as a Secret (and What Scanners Often Miss)

A secret, in the context of application security, is any value that grants access to a system, service, or dataset when presented to an authentication mechanism. The obvious examples are API keys with recognizable prefixes, private RSA or EC keys in PEM format, and connection strings with embedded passwords. These are relatively easy to detect because they follow predictable structural patterns.

The harder cases are where most programs fall short. Short, high-entropy strings that lack a recognizable format — such as a randomly generated internal service token or a symmetric encryption key stored as a hex string — are technically detectable but produce enough false positives that teams disable the relevant rules over time. Secrets embedded in binary files, compiled assets, or serialized data structures are rarely covered. Credentials stored in non-obvious keys like authorization_value, access_code, or app_secret may not match regex patterns tuned for well-known vendors. And secrets that have been split across multiple lines, concatenated at runtime, or assembled from environment variable fragments are almost universally missed by pattern-matching scanners.

There is also a category of secrets that developers do not recognize as secrets: internal URLs with embedded authentication tokens in the query string, webhook signing keys that look like random noise, and TOTP seed values that appear to be base32-encoded data. A thorough secrets detection program needs rules covering not just format patterns but semantic context — what the surrounding variable names and code structure suggest about the purpose of a value.

For a broader view of how secrets fit into your overall application security testing program, it helps to understand them as one category within a larger set of security controls rather than an isolated tool problem.

How Secrets End Up in Code

Understanding the mechanics of how credentials reach version control is prerequisite to designing controls that actually prevent it. The failure modes are more varied than most teams expect.

Direct commits are the most discussed case: a developer hardcodes a credential during local development because it is faster than setting up a proper secrets management flow, intends to remove it before pushing, and forgets. This is common but not the dominant source of production leaks.

Configuration file drift is equally prevalent. Teams maintain .env, config.yaml, appsettings.json, or similar files locally with real credentials and rely on .gitignore to keep them out of version control. When a new developer joins, adds a file, and the .gitignore rule is slightly wrong — wrong path depth, incorrect glob pattern, or the file was already tracked before the ignore rule was added — the entire configuration file goes into the repository.

Test and seed data is another persistent source. Developers write tests against real services during development, commit the test files with embedded credentials, and the production keys live in test suites for months before anyone notices. Seed scripts, database fixtures, and example configuration files with real values instead of placeholder strings are variations of the same problem.

Merge artifacts are less obvious. During conflict resolution, the real credential from one branch gets selected over the placeholder from another. The merged result passes review because reviewers are focused on the conflict itself, not on whether the chosen value is a secret.

Third-party tooling and generated files can also introduce credentials. Build outputs, package lock files with registry authentication tokens, IDE project files, and certain framework scaffolding tools have all been documented sources of accidental credential exposure. These files are often excluded from human review because they are considered machine-generated and therefore safe.

How Secrets Detection Tools Work

Modern secrets detection operates on a combination of pattern matching, entropy analysis, and increasingly, semantic classification. Understanding the mechanics helps you evaluate which tools and configurations are appropriate for your environment.

Regex and pattern-based detection is the foundational layer. Tools maintain libraries of patterns for known credential formats: the specific prefix lengths used by major cloud providers, the character set and length constraints of common token formats, and the structural patterns of private key blocks. This approach has high precision for well-known formats but degrades quickly for internal or less common credential types.

Entropy analysis supplements pattern matching by flagging strings that are statistically unlikely to be human-readable text. High-entropy strings assigned to variable names with suspicious keywords (key, secret, token, password, credential) are flagged for review. Entropy analysis catches secrets that lack recognizable format patterns but generates meaningful false positive rates against things like UUIDs, hashes, and base64-encoded non-secret data.

Contextual and semantic analysis is where more sophisticated tools differentiate themselves. Rather than looking at a string in isolation, these approaches examine the surrounding code: the variable name, the assignment context, adjacent comments, and the file type. A high-entropy string assigned to DATABASE_PASSWORD in a .env file is treated differently from the same string appearing as a test assertion value.

Tools like Kira extend this further by scanning for secrets across codebases including in config files, environment files, and inline code — and flagging exposure risk based on the credential type and whether the credential appears to be active rather than a placeholder or example value. That distinction matters: a finding on an active production credential is categorically different from a finding on a value that reads your-api-key-here.

The Git History Problem

One of the most underappreciated aspects of secrets detection is that a credential removed from the current HEAD of a branch is not a credential that has been remediated. Git history is persistent and, in most repository configurations, accessible to everyone with clone access to the repository. A secret committed six months ago and deleted in the next commit has been present in every clone, every CI pipeline run, every automated backup, and every mirror of that repository since the moment it was introduced.

Critical assumption: Finding a secret in code is not enough. You must assume it has been compromised the moment it appears in any commit, not just the current HEAD. Credential rotation is mandatory, not optional, even if you remove the secret from history.

This has two practical consequences. First, repository scanning must cover the full commit history, not just the current working tree. Many teams configure their scanning tools to check only the latest state of files, which means credentials that were added and removed entirely within a historical commit range are never detected. Second, history rewriting is insufficient as a sole response to a leaked credential. Rewriting history with tools like git filter-repo removes the secret from the repository going forward, but it does not address the window of exposure that existed between the original commit and the remediation.

This is compounded in organizations that fork repositories, mirror them to multiple locations, or use CI systems that cache repository state. A complete response to a git history leak requires both history rewriting and credential rotation — and must account for every system that may have cloned or cached the repository during the exposure window.

For teams managing dependencies across repositories, the git history problem intersects directly with supply chain security concerns: a credential leaked in a transitive dependency's public history is as dangerous as one leaked in your own code.

Pre-commit Hooks vs. CI Scanning vs. Repository Scanning

Secrets detection can be applied at three distinct points in the development lifecycle, and each has a different risk profile, coverage scope, and maintenance burden. Most mature programs use all three in combination rather than relying on any single layer.

Pre-commit hooks run on the developer's local machine before a commit is recorded. They represent the earliest possible intervention point — catching a secret before it ever enters version control. The tradeoff is that pre-commit hooks are entirely voluntary: a developer can bypass them with git commit --no-verify, they require installation and maintenance on each developer machine, and they are not enforceable at the repository level without additional tooling. Pre-commit hooks are best understood as a developer experience improvement and a first line of defense, not a security control.

CI-based scanning runs during the continuous integration pipeline, typically triggered by pull requests or branch pushes. This layer is enforceable: you can fail a pipeline and block a merge when a secret is detected. CI scanning has access to the diff being introduced, which makes it fast and focused. The gap is that it typically scans only the changes in the current PR, not the full repository history, and it applies only to code going through the standard review workflow. Hotfixes pushed directly to protected branches, repositories with weak branch protection, and code merged before scanning policies were enforced all represent coverage gaps.

Repository scanning runs against the full repository — all files, all branches, and all commit history — on a periodic or continuous basis. This is the only layer that provides comprehensive coverage and can detect secrets that predated the adoption of other controls. Repository scanning is also the most resource-intensive and generates the most findings, which is why triage and prioritization workflows matter as much as the scanning itself.

The interaction between these layers with your broader vulnerability scanning program determines how quickly you can move from detection to remediation, and how completely you can account for your exposure surface. Teams that treat secrets scanning as a standalone tool rather than integrating it with their wider security workflow tend to accumulate findings without reducing actual risk.

Secrets Detection Coverage Matrix

Secret type Detection difficulty Common scanner gap Remediation
API keys Low — most have recognizable prefixes Keys split across variables or assembled at runtime Rotate immediately via provider dashboard; audit access logs
JWT secrets Medium — signing secrets look like generic strings Context-free entropy tools miss these without variable name analysis Rotate secret, invalidate all issued tokens, re-issue sessions
SSH private keys Low — PEM headers are highly distinctive Keys stored in binary or non-standard encoding Revoke key from all authorized_keys files; generate new keypair
Database connection strings Medium — format varies by driver and ORM Credentials embedded in DSN-style URIs rather than individual fields Rotate DB credentials; audit query logs for unauthorized access
OAuth tokens Medium — format is provider-specific Refresh tokens and long-lived tokens often missed Revoke token via OAuth provider; check token scope for blast radius
Cloud provider credentials Low to medium — access key IDs often have prefixes Session tokens and temporary credentials from STS sometimes missed Deactivate access key; review CloudTrail or equivalent for usage
Hardcoded passwords High — no structural pattern without context Password values without surrounding keyword context are rarely flagged Change password everywhere it is used; enforce secrets management going forward
Internal service tokens High — no external format standard Custom token formats not covered by default rule sets Rotate token; audit service-to-service call logs for abuse

Responding to a Confirmed Credential Leak

When a secret is confirmed as leaked — meaning it was present in a commit that was pushed to a remote repository — the response follows a specific sequence. The order matters because the window between discovery and remediation is itself an exposure window that needs to be minimized.

Step one is immediate rotation, not removal. The instinct to delete the file or remove the credential from the codebase first is understandable but backwards from a security perspective. The credential should be invalidated at the source — revoked in the provider's dashboard, rotated in the secrets manager, or deactivated in the relevant system — before any other action. Once the credential is invalidated, it no longer matters whether the old value is accessible; it cannot be used to authenticate.

Step two is blast radius assessment. What access did the exposed credential grant? To what systems, with what permissions, and over what time period? This requires checking access logs, audit trails, and activity records for the exposed credential from the time of the first commit in which it appeared. Many teams skip this step because it is uncomfortable, but it is the only way to determine whether the leak resulted in unauthorized access.

Step three is history remediation. Once the credential is invalidated and the blast radius is understood, the repository history can be cleaned. This involves rewriting history using a tool like git filter-repo to remove the secret from all commits, force-pushing the rewritten history to all remotes, and coordinating with all contributors to re-clone or reset their local copies. This step is disruptive and should be scoped carefully — rewriting history on a large shared repository has operational costs that need to be planned for.

Step four is process improvement. Each confirmed credential leak is a signal that a prevention control either does not exist or failed. The appropriate follow-up is identifying which layer should have caught the leak and why it did not — whether that is a missing pre-commit hook, a CI rule that was not enforced, or a gap in repository scanning coverage. Treat it as a near-miss analysis rather than a blame exercise, and update controls accordingly.

Embedding secrets detection into your secure coding practices from the start of a project — rather than retrofitting it later — significantly reduces both the frequency of leaks and the cost of response when they occur.

FAQ

What is the difference between secrets detection and secrets management?

Secrets detection is the practice of finding credentials that have been placed in locations where they should not be — source code, commit history, configuration files checked into version control, or container images. It is a reactive and surveillance-oriented discipline: you are looking for things that have gone wrong. Secrets management, by contrast, is the infrastructure and workflow practice of storing, distributing, rotating, and auditing credentials in a controlled way — using systems like HashiCorp Vault, AWS Secrets Manager, or equivalent platforms. The two are complementary rather than competing. Secrets management reduces the likelihood that a credential ends up in the wrong place; secrets detection catches cases where it does anyway, either because a developer bypassed the secrets management workflow, because a legacy system was not migrated, or because a credential entered the codebase through a non-standard path such as a dependency, a generated file, or a merge artifact. Teams that implement secrets management without secrets detection are relying entirely on the assumption that their management workflow is being followed consistently, which is an assumption that production incidents repeatedly contradict. Both controls are necessary.

How do I remove a leaked secret from git history without breaking the repository?

The standard approach is to use git filter-repo, which is the tool currently recommended by the Git project for history rewriting (the older BFG Repo Cleaner and git filter-branch are still in use but have been superseded). The process involves identifying all commits and branches where the secret appears, running git filter-repo with appropriate arguments to replace or remove the value, and then force-pushing the rewritten history to all remotes. Before starting, coordinate with all contributors: once history is rewritten, anyone with a local clone will have a diverged history that needs to be resolved, typically by re-cloning rather than rebasing. You also need to invalidate any GitHub, GitLab, or Bitbucket caches of the original history — most platforms require a support request to purge cached content after a history rewrite. If the repository is public, assume the secret has been indexed by third-party scanning services and treat rotation as mandatory regardless of how quickly you rewrite history. Rewriting history is a recovery step, not a remediation step. Credential rotation must happen first and independently.

Can secrets detection tools find secrets that are obfuscated or base64-encoded?

Detection of obfuscated secrets depends heavily on the obfuscation method and the sophistication of the scanning tool. Simple base64 encoding is handled by better tools: the scanner decodes base64 blobs and runs pattern matching against the decoded content, which catches credentials that have been naively encoded without any expectation of evading detection. The harder cases involve multi-step encoding, string splitting, or runtime assembly from fragments. A secret that is split into three string literals concatenated in a constructor, or assembled from individually innocuous-looking environment variable lookups, is effectively invisible to pattern-matching approaches because no single scannable location contains the complete credential. Some tools address this by tracking data flow through the code rather than just scanning static text, which improves coverage but also significantly increases analysis complexity and false positive potential. Kira takes a codebase-aware approach to secrets scanning, looking at how values are used in context rather than treating each file in isolation, which improves recall for non-obvious credential patterns. For high-assurance environments, the most reliable approach is to treat any high-entropy string in a security-sensitive location as potentially a secret regardless of whether it matches a known pattern, and to require code review specifically focused on credential handling for changes touching configuration, authentication, and service integration code.

Related resources

See what Kira finds in your stack.

Kira runs autonomously on your codebase and delivers verified, exploitable findings with proof. Not alerts. Not maybes.

Get started free