Load Django Secrets From HashiCorp Vault
Use HashiCorp Vault for Django secrets: read KV secrets with hvac using AppRole or Kubernetes auth, render them with Vault Agent or the Vault Secrets Operator, issue dynamic PostgreSQL credentials, and handle lease renewal and restarts.
Problem statement
Organisations that run HashiCorp Vault (or its open-source fork OpenBao) want every application, including Django, to get secrets from it: no secrets in environment files, images, or CI variables, short-lived credentials, and an audit trail. Vault offers several integration styles, from reading a key-value secret at startup to issuing a unique, expiring PostgreSQL user for each app instance. The right choice depends on your platform and how much the app should know about Vault.
- Store static secrets (
SECRET_KEY, API keys) in the KV v2 engine at a path such assecret/data/myapp/prod. - Give the app an identity: Kubernetes auth (service account) on Kubernetes, AppRole on VMs.
- Prefer a sidecar or operator (Vault Agent, Vault Secrets Operator) that renders secrets to files or Kubernetes Secrets, so Django reads files or env vars.
- If the app calls Vault itself, use
hvaconce at startup and cache the values. - For PostgreSQL, consider the database secrets engine: dynamic users with a TTL, renewed by the agent.
- Write a Vault policy that grants read on exactly the app's paths.
Step-by-step solution
1. Store secrets and write a policy
vault kv put secret/myapp/prod SECRET_KEY="$(python -c 'import secrets;print(secrets.token_urlsafe(50))')" STRIPE_API_KEY=sk_live_...
# myapp-prod.hcl
path "secret/data/myapp/prod" { capabilities = ["read"] }
path "database/creds/myapp-prod" { capabilities = ["read"] }
vault policy write myapp-prod myapp-prod.hcl
2. Authenticate the app
On Kubernetes, bind a role to the app's service account:
vault write auth/kubernetes/role/myapp-prod \
bound_service_account_names=django bound_service_account_namespaces=app \
policies=myapp-prod ttl=1h
On VMs, use AppRole: the deploy tooling delivers a role_id and a wrapped, short-lived secret_id, which the agent exchanges for a token.
3. Render secrets with Vault Agent
Vault Agent runs beside Django, authenticates, and renders templates to files, refreshing them when leases change:
template {
destination = "/vault/secrets/django.env"
contents = <<EOT
{{ with secret "secret/data/myapp/prod" }}SECRET_KEY={{ .Data.data.SECRET_KEY }}
STRIPE_API_KEY={{ .Data.data.STRIPE_API_KEY }}{{ end }}
{{ with secret "database/creds/myapp-prod" }}DATABASE_URL=postgres://{{ .Data.username }}:{{ .Data.password }}@db.internal:5432/app?sslmode=require{{ end }}
EOT
}
Django loads the file at startup (django-environ's read_env("/vault/secrets/django.env")). On Kubernetes, the Agent Injector adds this sidecar from pod annotations, or the Vault Secrets Operator syncs values into native Kubernetes Secrets that pods consume with envFrom.
4. Calling Vault from settings with hvac
When no agent is available:
import hvac, os
from pathlib import Path
client = hvac.Client(url=os.environ["VAULT_ADDR"])
jwt = Path("/var/run/secrets/kubernetes.io/serviceaccount/token").read_text()
client.auth.kubernetes.login(role="myapp-prod", jwt=jwt)
data = client.secrets.kv.v2.read_secret_version(path="myapp/prod", mount_point="secret")["data"]["data"]
SECRET_KEY = data["SECRET_KEY"]
This runs once per process at import time. The app now depends on Vault availability at startup and must handle token expiry if it reads secrets later; keep later reads to a minimum.
5. Dynamic database credentials
The database secrets engine creates a PostgreSQL user per lease with a TTL (for example 24 hours). The agent renews the lease while the pod runs; when it ends, Vault drops the user. Django holds connections opened with those credentials; when the agent renders new credentials after a lease is not renewable, restart workers (the agent can run a command after rendering) so new connections use the new user. Grant the dynamic users a role that owns or can access the app's tables, and run migrations with a separate, more privileged role.
Verification check: the running pod has no secrets in its spec or image, vault audit logs show reads from the app's role only, and python manage.py check --deploy passes using values from the rendered file or Secret.
Explanation
Vault separates identity from secrets: workloads authenticate with something they already have (a Kubernetes service account token, a cloud instance identity, an AppRole), receive a short-lived token bound to policies, and read only what those policies allow. Agents and operators move authentication and renewal out of the application, so Django keeps reading environment variables or files. Dynamic secrets go further by generating credentials on demand with expiry, which limits the damage of a leak and makes revocation routine.
Edge cases / notes
- Startup ordering. With the agent injector, use the init container mode so secrets exist before Django starts.
- OpenBao is API-compatible for these use cases.
- SECRET_KEY rotation in Vault should use
SECRET_KEY_FALLBACKSto avoid logging users out. - High availability. If Vault is down, new pods cannot start; run Vault in HA mode and monitor it.
Next steps
- Compare with a managed option: Load Django Secrets From AWS Secrets Manager.
- Rotate the key safely with Rotate Django SECRET_KEY Safely.
FAQ
How does Django read secrets from Vault?
Usually through Vault Agent or the Vault Secrets Operator, which provide files or Kubernetes Secrets. Direct hvac calls also work.
What auth method should Django use with Vault?
Kubernetes auth on Kubernetes, AppRole or a cloud auth method on VMs.
Can Vault give Django temporary database passwords?
Yes, with the database secrets engine, which creates users with a TTL.
Does Vault need to be up for Django to run?
For startup, yes, unless secrets are already synced. Running pods keep working with cached values.