Secrets Management
Status: Complete
Category: Security
Default enforcement: Hard
Author: PushBackLog team
Tags
- Topic: security, quality
- Skillset: backend, devops
- Technology: generic
- Stage: execution, deployment
Summary
Secrets (API keys, tokens, passwords, certificates) must never appear in source code, logs, or version control. They must be stored in a dedicated secrets management system and accessed at runtime via environment variables or a secrets API, with rotation, auditing, and least-privilege access enforced.
Rationale
Git is forever
Once a secret is committed to a git repository — even briefly, even on a branch, even if the commit is removed from history — it must be considered compromised. Attackers actively scan GitHub, GitLab, and public container registries for credential patterns. Tools like truffleHog and GitGuardian exist specifically to scan commit history for leaked secrets. A secret in a .env file committed “just for testing” and then deleted can be found in the reflog for years.
Secrets in code cannot be rotated on a per-consumer basis
When a secret is embedded in application code, every instance of the application shares it and cannot be individually audited or revoked. A secrets management system enables per-service access, per-environment scoping, audit trails, and seamless rotation — none of which are possible when secrets live in source files.
OWASP A04 and A02
Hardcoded credentials directly satisfy OWASP A04 (Cryptographic Failures) criteria and contribute to A02 (Security Misconfiguration). They are also among the most commonly exploited vectors in software supply chain attacks — a compromised package that reads process.env or equivalent will exfiltrate embedded secrets.
Guidance
Secret storage options
| Tool | Type | Best for |
|---|---|---|
| HashiCorp Vault | Self-hosted or cloud secret store | Complex secret routing, dynamic secrets, large teams |
| AWS Secrets Manager | Cloud-native | AWS workloads; native IAM-controlled access |
| AWS Parameter Store (SSM) | Cloud-native | Simple key-value secrets on AWS, cost-effective |
| GCP Secret Manager | Cloud-native | GCP workloads |
| Azure Key Vault | Cloud-native | Azure workloads |
| Doppler / Infisical | SaaS secret manager | Cross-environment, developer-friendly |
| 1Password Secrets Automation | SaaS | Teams already on 1Password |
The runtime injection pattern
Secrets should never be baked into build artefacts. The 12-Factor pattern requires that secrets are injected at runtime via environment variables or fetched from a secrets API at startup:
# Bad: in source code
const STRIPE_KEY = 'sk_live_abc123...';
# Bad: committed .env file
STRIPE_KEY=sk_live_abc123...
# Good: read from environment at runtime (value injected by platform)
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY;
# The platform (Kubernetes secret, ECS task definition, Doppler sync) sets the value.
Pre-commit scanning
Install a pre-commit hook that scans for secret patterns before code is committed. This is the cheapest layer of defence:
# Using detect-secrets
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Add to .pre-commit-config.yaml
Other tools: gitleaks, trufflehog, git-secrets.
Rotation policy
| Secret type | Rotation frequency |
|---|---|
| CI/CD deploy tokens | On every team member departure; quarterly minimum |
| API keys to external services | Quarterly; immediately on any suspected exposure |
| Database credentials | On every incident; semi-annually otherwise |
| JWT signing keys | Annually minimum; more often if short-lived tokens are used |
| TLS certificates | Before expiry (automate with Let’s Encrypt / ACM) |
Examples
Pre-commit hook with gitleaks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
pre-commit install
# Now gitleaks runs on every `git commit`
Kubernetes secret injection
# Kubernetes pod spec
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
The secret value is stored in Kubernetes Secrets (ideally backed by Vault or AWS Secrets Manager via the secrets store CSI driver) and injected at pod startup — never in the Dockerfile or application code.
Anti-patterns
1. .env files committed to git
The most common vector. .env must be in .gitignore and should be .env.example in source control (showing required keys without values).
2. Secrets hardcoded as string literals in source
const client = new StripeClient('sk_live_abc123...'); // Never do this
3. Sharing secrets via Slack, email, or shared documents
Uncontrolled channels leave secrets with no audit trail, no revocation mechanism, and permanent unencrypted copies in message history.
4. Same secret across environments
A development secret exposure should not compromise production. Every environment (dev, staging, prod) must have its own set of secrets, minimally scoped.
5. Logging request/response bodies that may contain secrets
Authorisation headers, request bodies including passwords or tokens, and API responses may contain sensitive values. Log sanitisation must strip known sensitive field names before writing to any log store.
Related practices
Part of the PushBackLog Best Practices Library. Suggest improvements →