DevOps Tutorial · Containers and Kubernetes · Lesson 21 of 25
Configuration and Secrets
Keep configuration out of images and secrets out of Git: environment variables, ConfigMaps, secret managers and rotation.
Config belongs to the environment
The same image runs in dev, staging and production, so anything that differs between them is configuration supplied at deploy time (the Twelve-Factor App principle): URLs, feature flags, log levels, and secrets.
cat > app.sh <<'SCRIPT'
set -euo pipefail
: "${DATABASE_URL:?DATABASE_URL must be set}" # fail fast if required config is missing
echo "log level: ${LOG_LEVEL:-info}" # default when unset
echo "connecting to ${DATABASE_URL%%@*}@***" # never print the full secret
SCRIPT
LOG_LEVEL=debug DATABASE_URL="postgres://app:pw@db:5432/app" bash app.sh
bash app.sh 2>&1 | sed 's/^app.sh: line [0-9]*: //' || true
Output
log level: debug connecting to postgres://app:pw@*** DATABASE_URL: DATABASE_URL must be set
Run this on your computer — see “Get Started” for setup.
Where configuration lives
| Kind | Store | Examples |
|---|---|---|
| Non-secret config | Environment variables, Kubernetes ConfigMaps, parameter stores | LOG_LEVEL, API_BASE_URL |
| Secrets | A secret manager | AWS Secrets Manager or SSM Parameter Store, Azure Key Vault, Google Secret Manager, HashiCorp Vault |
| Secrets in Kubernetes | Secrets synced from the manager (External Secrets Operator, CSI driver) | DB passwords, API keys |
Rules for secrets
- Never commit them to Git (scan with gitleaks or GitHub secret scanning; a leaked key must be rotated, since deleting the commit isn't enough).
- Never bake them into images or print them in CI logs.
- Prefer no secret at all: workload identity (IAM roles for service accounts, Azure managed identities, GCP Workload Identity) instead of access keys.
- Rotate automatically and give each service its own credentials.
import re
PATTERNS = {"AWS access key": r"AKIA[0-9A-Z]{16}", "private key": r"-----BEGIN [A-Z ]*PRIVATE KEY-----",
"password in URL": r"://[^:/\s]+:[^@\s]+@"}
diff = [
"+LOG_LEVEL=info",
"+AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE",
"+DATABASE_URL=postgres://app:[email protected]:5432/app",
]
for line in diff:
for name, pattern in PATTERNS.items():
if re.search(pattern, line):
print(f"BLOCKED ({name}): {line[:30]}...")
Output
BLOCKED (AWS access key): +AWS_ACCESS_KEY_ID=AKIAIOSFODN... BLOCKED (password in URL): +DATABASE_URL=postgres://app:h...
Test yourself
A database password was committed and pushed, then removed in the next commit. What must you do?
Nothing, it was removedRotate the passwordRename the repositoryMake the repository private
Show answer
Answer: Rotate the password
The password is still in Git history and anyone who cloned it has it. Treat it as compromised and rotate.