Enterprise Secrets Management & Rotation
A configuration value can sit in a .env file; a secret cannot. Secrets must be fetched from a managed store at runtime, held in memory as SecretStr, refreshed on a rotation schedule, and never serialized to logs or disk. This section covers the three managers Python teams actually deploy — Vault, AWS Secrets Manager, and Doppler — and the rotation patterns that keep credentials short-lived.
What this section covers
| Topic | Why it matters | Go deeper |
|---|---|---|
| AWS Secrets Manager | Native AWS secret storage with IAM-scoped access and rotation | AWS Secrets Manager Integration |
| HashiCorp Vault | Cloud-agnostic secrets with dynamic, short-lived credentials | HashiCorp Vault Python SDK |
| Doppler | Developer-friendly multi-cloud secret sync | Doppler for Multi-Cloud Secrets |
| Rotation patterns | Replacing credentials with zero downtime on a schedule | Automated Secret Rotation Patterns |
Fetch at runtime, hold as SecretStr
The defining rule of secrets management: the credential is never in the source tree, the image, or an environment variable baked into a manifest. It is fetched at startup and wrapped so it cannot leak through a stray print.
# secrets/aws.py
import boto3
from pydantic import SecretStr
def fetch_secret(name: str) -> SecretStr:
client = boto3.client("secretsmanager")
value = client.get_secret_value(SecretId=name)["SecretString"]
return SecretStr(value) # masked in repr(), logs, and model_dump()
db_password = fetch_secret("prod/db/password")
Key rule: a secret only ever exists as a SecretStr in application memory. Native AWS integration is covered in AWS Secrets Manager Integration.
Prefer dynamic, short-lived credentials
A static password that lives for a year is a liability. Vault can mint a database credential that exists for an hour, scoped to one role, then expires automatically.
# secrets/vault.py
import hvac
client = hvac.Client(url="https://vault.internal:8200")
client.auth.approle.login(role_id=ROLE_ID, secret_id=SECRET_ID.get_secret_value())
lease = client.secrets.database.generate_credentials(name="app-readonly")
# lease["data"]["username"], lease["data"]["password"] — valid for the lease TTL only
Key rule: the shorter the lifetime, the smaller the blast radius. AppRole and dynamic credentials are detailed in HashiCorp Vault Python SDK.
Sync secrets across clouds with Doppler
When a team runs across providers, Doppler centralizes secret definition and injects them into each environment without per-cloud glue — see Doppler for Multi-Cloud Secrets.
Rotate on a schedule, not after a breach
Rotation must be routine and automated. The pattern is dual-write: provision the new credential, let both old and new work during an overlap window, switch traffic, then revoke the old — described in Automated Secret Rotation Patterns.
Anti-patterns & common mistakes
- Secrets in environment variables on the manifest — readable by anyone with cluster access and baked into image history.
- Caching a secret to disk — survives the process and ends up in backups.
- Logging the config object — leaks any credential not wrapped in
SecretStr. - Static, never-expiring credentials — one leak compromises the system indefinitely.
- Per-service copies of the same secret — rotation has to find every copy; centralize instead.
- Fetching the secret on every request — hammers the manager’s API and rate limits; cache with a TTL.
Decision flow: which secret manager?
Are you all-in on AWS?
├── Yes → AWS Secrets Manager: IAM-native, built-in RDS rotation.
└── No → Do you need dynamic, short-lived credentials across many backends?
├── Yes → HashiCorp Vault: cloud-agnostic, dynamic secrets engine.
└── No → Is developer ergonomics and multi-cloud sync the priority?
├── Yes → Doppler: simple, fast, good local-dev story.
└── No → start with your cloud's native store and revisit at scale.
CI/CD integration checklist
- Grant the pipeline a short-lived, narrowly-scoped identity (OIDC, AppRole) — never a long-lived key.
- Inject secrets at deploy time from the manager; never store them in CI variables in plaintext.
- Scan the repo and image layers for committed secrets on every build.
- Verify rotation works in staging by forcing a rotation and asserting the app reconnects.
- Alert on secrets approaching their TTL so rotation never runs late.
Bringing it together
Secrets management is configuration with a sharper edge: fetched from AWS Secrets Manager, Vault, or Doppler at runtime, held as SecretStr inside a validated settings model, and rotated automatically before expiry. Combine that with the configuration patterns and the credential never touches disk, git, or a log line.