Manage Django Secrets in Production
Store, load, and rotate Django secrets: inventories, file-based secrets, systemd credentials, cloud secret managers, and SECRET_KEY_FALLBACKS.
Problem statement
Every production Django app holds secrets: the SECRET_KEY, database passwords, API keys for payment and email providers, cloud storage credentials, and error-tracking DSNs. Most leaks do not come from sophisticated attacks. They come from ordinary engineering habits:
- a key committed to Git "just for testing" and still in history years later
- secrets pasted into CI logs, tickets, or chat while debugging
- one shared credential used by every environment, so staging access means production access
- no rotation plan, so a leaked key stays valid indefinitely
- a debug page or crash report that dumps environment variables
This guide covers a practical secrets setup for Django deployments of different sizes: what to inventory, where to store secrets, how Django should load them, and how to rotate the SECRET_KEY and database password without downtime.
Quick answer
- List every secret, which environment uses it, who can read it, and how to rotate it.
- Keep secrets out of Git entirely; scan the repository and its history for leaks.
- Use separate credentials per environment.
- Store secrets in the platform's secret store (systemd credentials, Docker or Kubernetes secrets, a cloud secret manager), with access limited to the app's identity.
- Load them in settings with a helper that supports both environment variables and secret files.
- Rotate
SECRET_KEYwithSECRET_KEY_FALLBACKS, and rotate database passwords with a dual-credential window. - Keep secrets out of logs and error reports.
Step-by-step solution
1. Build a secret inventory
A simple table in your operations docs is enough:
| Secret | Used by | Stored in | Owner | Rotation |
|---|---|---|---|---|
DJANGO_SECRET_KEY | web, workers | secret manager | platform team | yearly, on leak |
DB_PASSWORD | web, workers, migrations | secret manager | platform team | yearly, on staff change |
STRIPE_API_KEY | web | secret manager | billing owner | on leak |
AWS credentials | web (S3) | instance role, not stored | platform team | automatic |
The inventory answers the questions you will be asked during an incident: what leaked, what it can access, and how quickly you can revoke it.
2. Prefer identities over stored credentials
The safest secret is one you do not have to store. On cloud platforms:
- give EC2 instances, ECS tasks, or Kubernetes service accounts an IAM role for S3 access instead of access keys
- use managed identities on Azure and workload identity on Google Cloud
- use IAM database authentication where your database supports it
boto3 and django-storages pick up role credentials automatically when no keys are configured.
3. Load secrets from environment variables or files
Many secret systems deliver secrets as files: Docker and Kubernetes secrets, systemd credentials, and Vault agents. Support both sources with one helper:
# settings.py
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
def read_secret(name, required=True):
"""Read NAME from the environment, or from the file in NAME_FILE."""
file_path = os.environ.get(f"{name}_FILE")
if file_path:
return Path(file_path).read_text().strip()
credentials_dir = os.environ.get("CREDENTIALS_DIRECTORY")
if credentials_dir:
candidate = Path(credentials_dir) / name.lower()
if candidate.exists():
return candidate.read_text().strip()
value = os.environ.get(name)
if required and not value:
raise ImproperlyConfigured(f"Secret {name} is not configured")
return value
SECRET_KEY = read_secret("DJANGO_SECRET_KEY")
DB_PASSWORD = read_secret("DB_PASSWORD")
The NAME_FILE convention is widely used by container images. CREDENTIALS_DIRECTORY is set by systemd when the service uses credentials (step 4).
4. systemd credentials on a VM
Store the secret in a root-only file:
sudo install -d -m 700 /etc/myapp/credentials
python3 -c "import secrets; print(secrets.token_urlsafe(50))" | sudo tee /etc/myapp/credentials/django_secret_key >/dev/null
sudo chmod 600 /etc/myapp/credentials/django_secret_key
Pass it to the service:
[Service]
User=myapp
LoadCredential=django_secret_key:/etc/myapp/credentials/django_secret_key
LoadCredential=db_password:/etc/myapp/credentials/db_password
systemd copies each file into a private, read-only directory exposed as $CREDENTIALS_DIRECTORY that only this service can read, even though the source files are owned by root. The secret does not appear in the process environment, which keeps it out of /proc/<pid>/environ and out of child processes. LoadCredential= requires systemd 247 or newer; check with systemctl --version.
5. Docker secrets
With Docker Compose:
services:
web:
image: myorg/myapp:1.4.2
environment:
DJANGO_SECRET_KEY_FILE: /run/secrets/django_secret_key
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- django_secret_key
- db_password
secrets:
django_secret_key:
file: /etc/myapp/secrets/django_secret_key
db_password:
file: /etc/myapp/secrets/db_password
Compose mounts each secret as a file under /run/secrets/. The helper in step 3 reads them through the _FILE variables. Keep the source files outside the project directory so they are never copied into an image build context.
6. Cloud secret managers
On AWS, ECS and EKS can inject values from Secrets Manager at task start. For VMs or custom loading, fetch at startup:
import json
import boto3
def load_aws_secret(secret_id, region):
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_id)
return json.loads(response["SecretString"])
if os.environ.get("AWS_SECRET_ID"):
_secrets = load_aws_secret(os.environ["AWS_SECRET_ID"], os.environ["AWS_REGION"])
os.environ.setdefault("DJANGO_SECRET_KEY", _secrets["DJANGO_SECRET_KEY"])
os.environ.setdefault("DB_PASSWORD", _secrets["DB_PASSWORD"])
Grant the app's IAM role secretsmanager:GetSecretValue on only the secrets it needs. Loading once at startup avoids a network call per request; rotation then requires a restart, which fits normal deploy workflows.
7. Rotate SECRET_KEY without logging everyone out
Django uses SECRET_KEY to sign sessions (with signed cookie sessions), password reset tokens, messages, and other signed data. Changing it abruptly invalidates all of those. Since Django 4.1, SECRET_KEY_FALLBACKS lets old signatures remain valid while new ones use the new key:
SECRET_KEY = read_secret("DJANGO_SECRET_KEY")
SECRET_KEY_FALLBACKS = [
key for key in [read_secret("DJANGO_SECRET_KEY_PREVIOUS", required=False)] if key
]
Rotation procedure:
- Generate a new key.
- Set
DJANGO_SECRET_KEY_PREVIOUSto the current key andDJANGO_SECRET_KEYto the new key; deploy. - Wait longer than your longest-lived signed data (for example
SESSION_COOKIE_AGE, two weeks by default, andPASSWORD_RESET_TIMEOUT, three days by default). - Remove the previous key; deploy again.
If the key leaked, skip the waiting period: an attacker with the old key can forge signed data until it is removed from the fallbacks. Session authentication hashes also depend on the key, so users may need to log in again.
8. Rotate the database password
Use two credentials to avoid a window where running processes hold an invalid password:
- Create a second database role with the same privileges (or set a new password on a second login role).
- Update the secret to the new credential and deploy or restart web and worker processes.
- Confirm no connections use the old role:
SELECT usename, count(*) FROM pg_stat_activity GROUP BY usename; - Disable or drop the old role.
Managed databases often support this pattern natively through secret manager rotation integrations.
9. Keep secrets out of logs and reports
- Never enable
DEBUGin production; the debug page shows settings. - Django's error reporting filters settings whose names contain
API,KEY,PASS,SECRET,SIGNATURE, orTOKEN; name secret settings accordingly. - Use
@sensitive_variablesand@sensitive_post_parameterson views that handle credentials. - Configure your error tracker's data scrubbing, and avoid sending local variables from code that handles secrets.
- In CI, mark variables as masked, and never
echothem for debugging.
10. Prevent and detect leaks in Git
Add a secret scanner to pre-commit or CI, for example gitleaks:
gitleaks detect --source . --redact
If a secret was ever committed, rotate it. Rewriting history is useful cleanup, but anyone who cloned or forked the repository still has the old commits.
Verify
# Secret is not in the process environment when using credentials
sudo tr '\0' '\n' < /proc/$(systemctl show -p MainPID --value myapp)/environ | grep -c SECRET_KEY
# Django fails fast if the secret source is missing
sudo systemctl edit myapp # temporarily point LoadCredential at a missing file, then restore
# Repository has no committed secrets
gitleaks detect --source . --redact
Verification check:
- the running process loads its secrets from the intended source
- missing secrets cause a startup failure, not a fallback
- staging and production use different values for every secret
- the scanner reports no findings
Explanation
Secret management is mostly about narrowing who and what can read each value, and making rotation routine. Environment variables are a reasonable baseline; file-based delivery through systemd credentials, container secrets, or a secret manager reduces accidental exposure through process environments and child processes. Django's SECRET_KEY_FALLBACKS turns key rotation from a disruptive event into a normal deploy.
Edge cases / notes
Celery and other processes
Workers, beat schedulers, and one-off management commands need the same secrets. Use the same loading helper and delivery mechanism for every process type.
Secrets needed at build time
Avoid them. If collectstatic needs settings to import, use a throwaway build-only key, never the production key, and keep runtime secrets out of image layers.
Incident response: a secret leaked
Have the steps written down before you need them:
- Identify which secret leaked, where (repository, log, ticket, screenshot), and since when.
- Revoke or rotate it immediately at the source: provider dashboard for API keys,
ALTER ROLEfor database passwords, a newSECRET_KEYwithout the old one in fallbacks. - Deploy the new value to every process that uses it (web, workers, scheduled jobs).
- Review access logs for the affected system during the exposure window: provider API logs, database connection logs, cloud audit logs.
- Clean up the exposure location (delete the log lines, edit the ticket, rewrite Git history if appropriate), knowing that copies may already exist.
- Record what happened and which control would have prevented it, such as a pre-commit scanner or masked CI variables.
Rotation is the step that actually removes risk. Everything else limits or documents damage.
Choosing an approach by team size
| Situation | Reasonable approach |
|---|---|
| One developer, one VPS | root-owned env file with mode 640, or systemd credentials; secrets documented in a password manager |
| Small team, a few servers | systemd credentials or Docker secrets, values stored in a shared password manager, deploy scripts that write files from it |
| Cloud-native deployment | platform secret store (AWS Secrets Manager, Azure Key Vault, Google Secret Manager) with workload identities |
| Kubernetes | external secrets operator syncing from a cloud secret manager, encryption at rest for Kubernetes Secrets |
| Regulated environment | centralized secret manager with audit logs, automatic rotation, and access reviews |
Moving up this table adds operational overhead. Choose the lowest level that meets your risk and compliance needs, and make rotation and access review routine at whatever level you pick.
Common mistakes
- Storing the production
SECRET_KEYin the Dockerfile as a build argument, which can persist in image metadata. - Printing environment variables in CI for debugging (
envorprintenv). - Sharing one database superuser across applications instead of per-application roles with limited privileges.
- Relying on
.gitignorealone; it does not remove files that were committed before the rule was added.
Next steps
- Start with typed configuration in Configure Django Environment Variables for Production.
- Review broader controls in Django Deployment Security Basics You Should Not Skip.
- Harden the host that stores secrets with the Linux Server Hardening Checklist for Django.
FAQ
Is it safe to store Django secrets in environment variables?
It is a common, acceptable baseline when the values are set from protected files or a platform secret store. File-based secrets reduce exposure through process listings, child processes, and crash reports.
How do I rotate the Django SECRET_KEY safely?
Set the new key as SECRET_KEY and move the old key into SECRET_KEY_FALLBACKS. After sessions and tokens signed with the old key have expired, remove it from the fallbacks.
What happens if my Django SECRET_KEY leaks?
An attacker can forge data Django signs with it, such as password reset tokens and, with signed cookie sessions, session data. Rotate immediately without keeping the leaked key in fallbacks.
Should staging and production share secrets?
No. Separate credentials limit the impact of a staging compromise and make it clear which environment a leaked value belongs to.