Database passwords, API keys, encryption keys must be stored securely — not in code, not in .env files checked into git, but in encrypted secret stores that only the right compute platform can access. The gotcha: accidentally committing a secret means rotating the value, auditing logs, and hoping no one got there first.
5 min read
Database password in code:
WRONG ← anyone who reads the repo can access production
Database password in .env (checked into git):
STILL WRONG ← git history preserves it forever, even after "deletion"
Database password in local .env (gitignored):
BETTER, but...
→ multiple developers have copies
→ laptop theft = database access
→ no rotation/access audit trail
Database password in AWS Secrets Manager:
RIGHT ← encrypted, rotated on schedule, audited, only accessible from
specific resources (Lambda, ECS tasks) with the right IAM role
You have: a database password, a third-party API key, an encryption key, a signing key, a webhook HMAC secret. Each needs:
A single .env file in someone's laptop meets zero of these requirements.
AWS Secrets Manager is a managed service for storing and rotating secrets. Internally, it encrypts everything with KMS (Key Management Service), logs all access, and automates rotation.
Store a secret:
aws secretsmanager create-secret \
--name prod/database/password \
--secret-string "mySecurePassword123"Retrieve it from a Lambda or ECS task (only the IAM role grants permission):
import boto3
secrets = boto3.client('secretsmanager')
response = secrets.get_secret_value(SecretId='prod/database/password')
password = response['SecretString']Lambda and ECS tasks have an IAM role attached that grants secretsmanager:GetSecretValue on specific secret ARNs — no hardcoded credentials needed. The infrastructure itself proves identity.
Terraform needs secrets to configure resources (database root password, API keys for providers). Never put actual secrets in .tf files — use terraform.tfvars (which is gitignored) or input variables:
variable "database_password" {
type = string
sensitive = true # don't print this in terraform output
description = "Root password for the database"
}
resource "aws_db_instance" "main" {
password = var.database_password
# ...
}Pass the value via environment variable or command-line:
export TF_VAR_database_password="mySecurePassword123"
terraform applyOr from a file:
terraform apply -var-file=prod-secrets.tfvarsprod-secrets.tfvars is gitignored, stored on the machine running Terraform (or passed in CI/CD via GitHub Secrets / encrypted CI variables).
If a secret is ever committed to git (even briefly, even in history):
Better yet: prevent this via tooling:
# .pre-commit-config.yaml (or similar)
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.0.0
hooks:
- id: trufflehog
name: TruffleHog (detect secrets)
args: ['filesystem', '--json']
stages: [commit]This runs before each commit and fails if secrets (API keys, passwords, private keys) are detected.
Secrets differ across environments. Use namespaced secret names:
dev/database/password — for development deploymentsstaging/database/password — for stagingprod/database/password — for productionYour code reads an environment variable to determine which secret to fetch:
env = os.getenv('ENVIRONMENT', 'dev')
secret_name = f'{env}/database/password'
password = secrets_client.get_secret_value(SecretId=secret_name)This keeps the code identical across environments; the ENVIRONMENT variable (which is safe and harmless to log) determines which secrets are used.
When a secret expires, databases usually need a grace period where both old and new passwords work. AWS Secrets Manager supports this:
# Configure automatic rotation
secrets.rotate_secret(
SecretId='prod/database/password',
RotationLambdaARN='arn:aws:lambda:...',
RotationRules={
'AutomaticallyAfterDays': 30
}
)AWS calls a Lambda on schedule, which updates the database with a new password, stores the new password in Secrets Manager, and tells the database to retire the old one. Applications reading the secret always get the latest value.
Services calling other services (Lambda calling an internal API, a cron job calling a billing endpoint) need to authenticate. Options:
Bad: embed a static API key in code. Better: store the API key in Secrets Manager, retrieve it like any other secret. Best: use short-lived tokens issued by the system itself.
For internal APIs, use your existing JWT infrastructure (same private key you use for user auth) but with a synthetic "service account" subject:
token = jwt.encode(
{'sub': 'service:billing-worker', 'exp': datetime.utcnow() + timedelta(hours=1)},
secret_key,
algorithm='HS256'
)
# Calling service includes the token
response = requests.post(
'https://internal.example.com/charge',
headers={'Authorization': f'Bearer {token}'}
)The receiving service validates the JWT (same way it validates user JWTs) and checks that the sub claim is a known service account. No separate API key to manage; the token expires automatically; audit logs show the exact service that called.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is it dangerous to commit .env files with secrets to git, even if later deleted?
2. What does AWS Secrets Manager provide that a local .env file does not?
3. How should Terraform access database passwords without checking them into git?
4. What is a service account, and why is it better than a static API key for machine-to-machine authentication?
Cloud Computing & Infrastructure