Secrets out of code
Keeping secrets out of code means passwords, API keys, tokens and private keys never appear in source, configuration files or build artifacts. The code holds only a reference, and the running workload gets the value at runtime from a secrets manager or a short-lived credential from its platform.
A committed secret is copied to every clone, fork and CI cache, and deleting the file leaves it in history. GitHub's own guidance is to revoke or rotate first, because a history rewrite cannot reach those copies. Blocking secrets at commit and push time stops most leaks before they spread, and loading them at runtime turns the fix for a leak into a rotation instead of a code change and redeploy.
- Who
- Every developer, with the platform team running the secrets manager and AppSec owning the scanners and leak response
- When
- Whenever code needs a credential, and at every commit, push and pull request
- How often
- Scanning on every commit and push; a full-history scan when you start, then regularly in CI
- You end up with
- Code that reads secrets by reference, scanners that block new secrets, and a rotated credential for every secret found in history
Do this
List the secrets the code uses
For each credential, record what it unlocks, who issued it, which workloads use it and who owns it. A database password, an API token, a signing key and an encryption key do different damage when stolen, so note the consequence before choosing where each lives. OWASP ASVS 5.0 V13.1.4 (level 3) asks for this list of critical secrets with a rotation schedule.
Read secrets at runtime, by reference
Replace each literal with a lookup: the code names the secret and gets the value from a secrets manager, or better, uses workload identity that issues short-lived credentials, such as OIDC federation from CI to the cloud instead of a stored cloud key. ASVS 5.0 V13.3.1 says secrets must not be in source code or build artifacts. Runtime secrets covers how the platform delivers them.
Keep local and example configuration clean
A common pattern is to commit a
.env.examplewith placeholder names and list.envand key files in.gitignore. Give developers their own scoped development credentials rather than copies of production ones. Keep real values out of test fixtures, Dockerfiles, CI logs and front-end or mobile bundles, which ship to every user.Block secrets before they leave the laptop
Run a secret scanner such as Gitleaks, TruffleHog or detect-secrets as a pre-commit hook, and turn on push protection at the host: GitHub push protection for the repository, which needs GitHub Secret Protection, or GitLab secret push protection on Ultimate. Restrict who may bypass a block, for example with GitHub's delegated bypass, and review each bypass.
Scan every pull request and the full history in CI
Hooks run on developer machines,
git commit --no-verifyskips them, and push protection checks only what is being pushed. Scan pull requests and the whole history in CI too, for examplegitleaks git, which exits with status 1 when it finds a leak. Record old findings you have already rotated in a baseline, so the job fails only on new ones.Treat every finding as a leak
A secret that reached a shared repository, log or ticket is compromised, even if it was deleted a minute later. Revoke or rotate it at the issuer first, check the issuer's logs from the time of exposure, then remove the copies. Rewriting history is cleanup, not the fix. Contain a leaked secret, then investigate and clean up has the runbook for each credential type.
Prove the controls work
Rotate one secret as a drill: issue the new value, move every consumer, revoke the old value and confirm the issuer rejects it. If the drill needs a commit or a rebuild, the secret is still in code. Then commit a dummy key on a throwaway branch and confirm that both the pre-commit hook and push protection block it.
Done when
No secret is in the repository or its history, and a leaked one can be rotated without changing code.
Mark it in the gap checkCommon mistakes
- Deleting the file or rewriting history instead of rotating. Clones, forks, cached views and pull requests that reference the commit keep the value, so the secret still works for anyone who copied it.
- Moving the secret into an environment variable baked into an image or a committed config file. OWASP notes that environment variables are generally readable by all processes and may end up in logs or dumps.
- Reading a clean scan as proof that no secret exists. Scanners match known formats and high-entropy strings, so a hand-made password or an unfamiliar token format can pass.
- Letting anyone with write access bypass push protection unreviewed. On GitHub any writer can bypass by giving a reason; without delegated bypass, nobody approves it before the secret lands.
- Sharing one secret across services and environments. OWASP warns this makes the source of a leak hard to identify, and rotating it breaks every consumer at once.
- Printing secrets in error messages or CI output. The value then sits in a log store that usually has far wider access than the secrets manager.
Where each kind of secret belongs
Prefer credentials the platform issues for minutes or hours, and keep the static ones that remain in a secrets manager. These are common placements, not the only ones.
| Secret | Where it belongs | How the code gets it |
|---|---|---|
| Cloud access for CI jobs | No stored key: OIDC federation from the CI system to the cloud provider | A short-lived token for each job, limited by repository, branch or environment |
| Cloud access for a workload | An attached role or managed identity | Credentials issued by the platform at runtime, with no key file |
| Database password | A secrets manager, or dynamic credentials with a lease | Fetched or mounted at start-up; rotated by the manager |
| Third-party API key | A secrets manager, with the narrowest scope the provider offers and a named owner | Fetched by name; rotated on a documented schedule and at once on exposure |
| Signing and encryption keys | A KMS or HSM; ASVS 5.0 V13.3.3 (level 3) asks for an isolated security module | The code asks the module to sign or decrypt, and the key never leaves it |
| Local development values | A .env file listed in .gitignore, or a personal entry in a development vault | Development credentials only, never production values |
Tools and guides
- Practice: Runtime secrets
- Practice: CI security checks
- Fix cards: tested fixes by stack
- Secrets management: storage, rotation and leaked-secret response
Follow one application credential from issuance through caching, rotation, revocation and restored backups, prove that its old value stops working, and respond correctly when a secret leaks.
- Contain a leaked secret, then investigate and clean up
Revoke an exposed API key, token or private key at its issuer before anything else, end the sessions it minted, check the provider's logs, then remove every copy and replace it with a credential that expires on its own.
- Workload identity federation: short-lived, scoped identities for services and CI
Trace how a running workload obtains a short-lived identity, write cloud trust policies that match immutable claims instead of names or wildcards, and keep the resulting credential limited by audience and authorization.
Standards
- NIST SSDF 1.1
- PW.5 Create Source Code by Adhering to Secure Coding Practices
- OWASP SAMM 2
- Secure Deployment
Sources
- OWASP Secrets Management Cheat Sheet
- OWASP ASVS 5.0.0: V13 Configuration (V13.1.4, V13.3.1, V13.3.3). Version 5.0.0.
- GitHub Docs: Push protection
- GitHub Docs: Secret scanning
- GitHub Docs: Removing sensitive data from a repository
- GitHub Docs: OpenID Connect in GitHub Actions
- GitLab Docs: Secret push protection
- Gitleaks: README (scan modes, pre-commit hook, exit codes, baseline)
- Git documentation: git commit (--no-verify)
- OWASP SAMM: Secure Deployment (Secret Management stream)
- NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1. February 2022.