Secrets in the cloud — Secrets Manager, KMS, automatic rotation
Credentials in source code, unencrypted environment variables, or committed .env files are the starting point of a large share of cloud incidents. Managing secrets correctly requires dedicated tools, granular access control, and continuous rotation.
What not to do
# Bad: hardcoded password in code
DB_PASSWORD = "myPassword123!"
# Bad: .env with credentials committed to git
echo "AWS_SECRET_ACCESS_KEY=abc123" >> .env
git add .env && git commit -m "config"
# Bad: plain-text environment variable in ECS task definition
"environment": [{ "name": "DB_PASS", "value": "myPassword123!" }]
If credentials have already been committed, consider the repository compromised. Rotate immediately and use git filter-repo to scrub the history.
AWS Secrets Manager
Stores, rotates, and serves secrets in a controlled way. Access is audited via CloudTrail. Natively integrated with RDS for automatic rotation.
# Create a secret
aws secretsmanager create-secret \
--name prod/app/database \
--description "Production database credentials" \
--secret-string '{"username":"app_user","password":"S3cret!@#"}'
# Retrieve the secret (from the application)
aws secretsmanager get-secret-value \
--secret-id prod/app/database \
--query SecretString \
--output text
Consuming in code — never cache the value permanently:
import boto3, json
def get_db_credentials(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"])
creds = get_db_credentials("prod/app/database")
# creds["username"], creds["password"]
Automatic rotation
Secrets Manager can automatically rotate RDS passwords via Lambda:
# Enable rotation every 30 days (native RDS)
aws secretsmanager rotate-secret \
--secret-id prod/app/database \
--rotation-rules AutomaticallyAfterDays=30 \
--rotate-immediately
For custom secrets, implement a Lambda with the four rotation steps:
createSecret → setSecret → testSecret → finishSecret.
AWS KMS — key management
KMS manages cryptographic keys. Secrets Manager uses KMS under the hood. Use KMS directly when you need to encrypt your own data:
# Encrypt a value with KMS
aws kms encrypt \
--key-id alias/my-app-key \
--plaintext fileb://sensitive-data.txt \
--output text \
--query CiphertextBlob | base64 --decode > data.enc
# Decrypt
aws kms decrypt \
--ciphertext-blob fileb://data.enc \
--output text \
--query Plaintext | base64 --decode
KMS key policy — control who can use and who can administer:
{
"Statement": [
{
"Sid": "AdminKey",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789:role/kms-admin" },
"Action": ["kms:Create*", "kms:Delete*", "kms:Put*"],
"Resource": "*"
},
{
"Sid": "AppUsage",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789:role/app-role" },
"Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "*"
}
]
}
Reference secrets instead of copying them
Instead of fetching a secret and injecting it as an env var (which shows up in process logs), use a direct reference:
// ECS Task Definition — Secrets Manager reference
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/app/database:password::"
}
]
# Kubernetes — External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-secret
spec:
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: prod/app/database
property: password
Secrets checklist
- Never commit secrets to a repository (use pre-commit + gitleaks)
- Secrets Manager or Parameter Store (SecureString) for all secrets
- KMS with customer-managed key (CMK) for sensitive data
- Automatic rotation enabled (30-90 days)
- Access audited via CloudTrail
- No secret in plain-text environment variables
- IAM: only the application role can read the specific secret