Secrets management — never in code, vaults (Vault, Secrets Manager)
Secrets are any data that grants access to a resource: passwords, API keys, tokens, certificates, private keys. When a secret leaks, the attacker has direct access to the protected resource — database, payment service, cloud account. The most common leak is still the most avoidable: hardcoded values committed to a repository.
Why Never Hardcode Secrets
# Bad — hardcoded secret
DATABASE_URL = "postgresql://admin:S3cr3t@db.example.com/prod"
API_KEY = "sk-live-abc123xyz"
# Commit + push = secret exposed forever in git history
Git stores the full history. Even if you delete the secret in a later commit, it remains accessible via git log or GitHub history. Tools like truffleHog and gitleaks constantly scan public and private repositories looking for exactly this.
Correct Alternatives
1. Environment Variables
The simplest solution for local development and small projects:
# .env (never commit — add to .gitignore)
DATABASE_URL=postgresql://admin:S3cr3t@db.example.com/prod
API_KEY=sk-live-abc123xyz
import os
db_url = os.environ["DATABASE_URL"] # raises explicit error if absent
api_key = os.getenv("API_KEY", "") # empty default — use with care
Limitation: local .env does not scale to multiple services or secret rotation.
2. HashiCorp Vault
Vault is an open-source secret vault with granular access control, audit logging, and automatic rotation.
# Write a secret to Vault
vault kv put secret/myapp/db \
url="postgresql://admin:S3cr3t@db.example.com/prod" \
password="S3cr3t"
# Read at deploy / runtime
vault kv get -field=url secret/myapp/db
import hvac
client = hvac.Client(url="https://vault.example.com", token=os.environ["VAULT_TOKEN"])
secret = client.secrets.kv.read_secret_version(path="myapp/db")
db_url = secret["data"]["data"]["url"]
The application never loads the secret from a file — it fetches from Vault at runtime using a short-lived token.
3. AWS Secrets Manager
For AWS workloads, Secrets Manager integrates with IAM for access control and supports automatic rotation:
import boto3, json
def get_secret(secret_name: str) -> dict:
client = boto3.client("secretsmanager", region_name="us-east-1")
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
db_creds = get_secret("prod/myapp/db")
conn = psycopg2.connect(
host=db_creds["host"],
user=db_creds["username"],
password=db_creds["password"],
)
The EC2/ECS instance uses an IAM role with minimal permissions — no credentials in the code or container.
Other Vaults
| Platform | Service |
|---|---|
| GCP | Secret Manager |
| Azure | Key Vault |
| GitHub | Actions Secrets |
| Kubernetes | Secrets (+ external-secrets operator) |
Leak Detection
Enable in your repository:
# gitleaks — scans git history
gitleaks detect --source . --verbose
# truffleHog — detects high entropy strings (possible keys)
trufflehog git file://. --only-verified
On GitHub, enable Secret Scanning in repository settings — GitHub blocks pushes matching known API key patterns.
Secret Rotation
A secret that never changes is a permanent risk. Define:
- Periodic rotation (e.g., 90 days for API keys, 30 for database passwords).
- Immediate rotation when compromise is suspected.
- Zero-downtime rotation: write new secret, update app, revoke old one.
Checklist
-
.envand config files with secrets in.gitignore -
git logaudited — no secrets in history - Secrets accessed via environment variables or a vault
- Rotation configured and tested
- Secret scanning enabled in the repository
- No secrets in application logs