Secrets Management: Keeping Credentials Out of Your Code

Hardcoded passwords are a security vulnerability and a compliance failure. AWS Secrets Manager, environment variables, and secret rotation done right. Skill 15 of 20.

business skills
security
secrets management
AWS
DevOps
Author

Jong-Hoon Kim

Published

April 24, 2026

1 Why secrets management is non-negotiable

A developer pushes a database connection string to a public GitHub repository by accident. Within 15 minutes, a bot discovers it and begins exfiltrating patient data. This is not a hypothetical — it happens dozens of times per day across GitHub (1).

Any health department client will conduct at minimum a basic security review before signing a contract. “Where are your database passwords stored?” is question one. “In the code” or “in a .env file in the repository” ends the conversation.

Proper secrets management is both a security practice and a commercial requirement. It is also straightforward to implement.

2 The hierarchy of bad to good

❌ Hardcoded in source code:   password = "secret123"
❌ In .env committed to git:   DATABASE_URL=postgresql://...
⚠  In .env not committed:     ok for local dev only
✓  Environment variable at runtime: -e DB_PASSWORD=...
✓  AWS Secrets Manager:        retrieved at startup, rotated automatically
✓  HashiCorp Vault:            enterprise-grade, audited, dynamic secrets

3 Environment variables: the baseline

The minimum safe approach: never put secrets in source code. Pass them as environment variables at runtime.

Docker run:

docker run -p 8000:8000 \
  -e DATABASE_URL="postgresql://user:pass@host:5432/db" \
  -e API_SECRET_KEY="$(openssl rand -hex 32)" \
  dt-api:latest

R code that reads them:

db_url  <- Sys.getenv("DATABASE_URL",   unset = stop("DATABASE_URL not set"))
api_key <- Sys.getenv("API_SECRET_KEY", unset = stop("API_SECRET_KEY not set"))
# Demonstrate safe secret reading pattern in R
safe_get_env <- function(key, required = TRUE) {
  val <- Sys.getenv(key, unset = NA_character_)
  if (is.na(val) || nchar(val) == 0) {
    if (required) stop(sprintf("Required environment variable '%s' not set.", key))
    return(NULL)
  }
  val
}

# Show what the pattern produces
tryCatch(
  safe_get_env("DATABASE_URL"),   # this env var is not set
  error = function(e) cat("Error (expected):", conditionMessage(e), "\n")
)
Error (expected): Required environment variable 'DATABASE_URL' not set. 
# Simulate a set variable
Sys.setenv(DT_API_KEY_TEST = "fake-key-for-demo-only")
cat("Key found:", nchar(safe_get_env("DT_API_KEY_TEST")), "chars\n")
Key found: 22 chars
Sys.unsetenv("DT_API_KEY_TEST")

4 AWS Secrets Manager

For production, AWS Secrets Manager (2) stores secrets encrypted at rest, controls access via IAM, and supports automatic rotation.

# Store a secret (one-time)
aws secretsmanager create-secret \
  --name "dt/prod/db-password" \
  --description "TimescaleDB password for prod environment" \
  --secret-string '{"username":"dtuser","password":"S3cur3P@ss"}'

# Retrieve at runtime (in startup script)
SECRET=$(aws secretsmanager get-secret-value \
  --secret-id "dt/prod/db-password" \
  --query SecretString --output text)

DB_USER=$(echo $SECRET | python3 -c "import sys,json; print(json.load(sys.stdin)['username'])")
DB_PASS=$(echo $SECRET | python3 -c "import sys,json; print(json.load(sys.stdin)['password'])")

export DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:5432/dtdb"

In R:

library(aws.secretsmanager)

get_db_creds <- function(secret_name = "dt/prod/db-password") {
  raw <- get_secret(secret_name)
  creds <- jsonlite::fromJSON(raw$SecretString)
  list(user = creds$username, password = creds$password)
}

5 Automatic rotation

Secrets Manager can rotate the database password automatically every 30–90 days. Set it up once:

aws secretsmanager rotate-secret \
  --secret-id "dt/prod/db-password" \
  --rotation-rules AutomaticallyAfterDays=30

When rotation fires, Secrets Manager calls a Lambda function that updates the RDS user’s password and stores the new value. Your application always calls get_secret() at startup — it never needs to know a rotation happened.

6 Scanning for leaked secrets

Add git-secrets or gitleaks as a pre-commit hook to prevent accidental commits:

# Install gitleaks
brew install gitleaks    # or download binary

# Scan the whole repo history
gitleaks detect --source . --log-opts HEAD

# Add as pre-commit hook
gitleaks protect --staged

7 The secrets audit checklist

Secrets management audit checklist
Item Priority
No secrets in source code (grep for common patterns) Critical
.env files in .gitignore Critical
Secrets passed as environment variables or Secrets Manager Critical
Different credentials per environment (dev ≠ prod) High
Database users have minimum required permissions High
API keys have expiry dates and can be revoked High
Rotation policy configured for long-lived secrets Medium
Audit trail: who accessed which secret and when Medium

Every item marked “Critical” will be raised in a security review by a health department procurement team. Tick them off before your first enterprise client conversation.

8 References

1.
OWASP Foundation. OWASP top ten 2021 [Internet]. 2021. Available from: https://owasp.org/Top10/
2.
Amazon Web Services. AWS secrets manager user guide [Internet]. 2024. Available from: https://docs.aws.amazon.com/secretsmanager/latest/userguide/