What counts
Database passwords, API keys, encryption keys, OAuth client secrets, SSH keys, JWT signing secrets. If an attacker could use it to access something - it's a secret.
Where secrets should never be
Source code: Committed to Git = compromised forever. History preserves everything.
Config files in version control: .env checked in. Private repo still gets cloned to laptops, CI runners.
Client-side code: Anything in JS bundle is public.
Log files: Connection strings in startup logs, tokens in request logs.
Error messages: Stack traces with secrets.
Where secrets should be
Environment variables: Simple approach. Limitations: no access control, auditing, rotation.
Secrets managers: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault.
What you get: access control, audit logs, automatic rotation, versioning, encryption.
# AWS Secrets Manager
client = boto3.client("secretsmanager")
response = client.get_secret_value(SecretId="myapp/database")
secret = json.loads(response["SecretString"])
Rotation
Rotate regularly. Rotate immediately after suspected compromise.
Design for rotation:
- Don't cache secrets forever. Re-read periodically.
- Support dual credentials during rotation.
- Test rotation in staging first.
Detection
Pre-commit hooks: gitleaks, detect-secrets, truffleHog.
gitleaks detect --source . --verbose
CI/CD scanning: Second line of defence.
GitHub/GitLab scanning: Built-in alerts for known secret patterns.
When a secret leaks
- Revoke immediately
- Rotate to new value
- Audit usage
- Fix root cause
The takeaway
Never put secrets in code, logs, error messages, or client-side bundles. Environment variables for simple. Secrets managers for production. Rotate regularly. Use pre-commit hooks.
When a secret leaks: revoke first, investigate second.
