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.

How secrets reach DjangoSecret storemanager or fileService definitioncredentials, envProcessenvironment or filesettings.pyfails if missingRotation is routine when the app reads secrets from one well-defined source.

Quick answer

  1. List every secret, which environment uses it, who can read it, and how to rotate it.
  2. Keep secrets out of Git entirely; scan the repository and its history for leaks.
  3. Use separate credentials per environment.
  4. 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.
  5. Load them in settings with a helper that supports both environment variables and secret files.
  6. Rotate SECRET_KEY with SECRET_KEY_FALLBACKS, and rotate database passwords with a dual-credential window.
  7. 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:

SecretUsed byStored inOwnerRotation
DJANGO_SECRET_KEYweb, workerssecret managerplatform teamyearly, on leak
DB_PASSWORDweb, workers, migrationssecret managerplatform teamyearly, on staff change
STRIPE_API_KEYwebsecret managerbilling owneron leak
AWS credentialsweb (S3)instance role, not storedplatform teamautomatic
know every secret you haveUsed byRotationSECRET_KEYweb, workersfallbacks, yearly or on leakDatabase passwordweb, workers, migrationssecond user, then swapEmail / API keysweb, tasksprovider dashboardCloud credentials✓prefer an identity insteadautomatic with rolesDeploy SSH keyCI onlyon staff changesAn inventory is what makes a leak response fastdjango-deployment.com
Record the owner and the rotation steps next to each entry.

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
prefer the options near the topWorkload identityno stored credential at allSecret managerfetched at start, auditedCredential filessystemd LoadCredential, DockersecretsEnvironment variablessimple, visible to child processesFiles in the repositoryneverMove up one level at a time as the project growsdjango-deployment.com
Environment variables are fine for small projects with care.

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
secrets as private files, not env vars[Service]User=myappLoadCredential=django_secret_key:/etc/myapp/credentials/django_secret_keycredentials_dir = os.environ.get("CREDENTIALS_DIRECTORY")1231Copied into a private, read-only directory.2Source stays root-only.3Only this service can read it.Not in /proc/<pid>/environ, not inherited by child processesdjango-deployment.com
Requires systemd 247 or newer.

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:

  1. Generate a new key.
  2. Set DJANGO_SECRET_KEY_PREVIOUS to the current key and DJANGO_SECRET_KEY to the new key; deploy.
  3. Wait longer than your longest-lived signed data (for example SESSION_COOKIE_AGE, two weeks by default, and PASSWORD_RESET_TIMEOUT, three days by default).
  4. 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:

  1. Create a second database role with the same privileges (or set a new password on a second login role).
  2. Update the secret to the new credential and deploy or restart web and worker processes.
  3. Confirm no connections use the old role: SELECT usename, count(*) FROM pg_stat_activity GROUP BY usename;
  4. Disable or drop the old role.
rotate the database password1Create the new passwordor a second role2Update the secret storeboth values valid3Restart web and workerspick up the new value4Verify connectionspg_stat_activity by user5Revoke the old oneonly after verifyingTwo valid credentials during the switch avoid a hard cut-overdjango-deployment.com
Alternating between two roles makes every rotation the same routine.

Managed databases often support this pattern natively through secret manager rotation integrations.

9. Keep secrets out of logs and reports

  • Never enable DEBUG in production; the debug page shows settings.
  • Django's error reporting filters settings whose names contain API, KEY, PASS, SECRET, SIGNATURE, or TOKEN; name secret settings accordingly.
  • Use @sensitive_variables and @sensitive_post_parameters on 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 echo them 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:

  1. Identify which secret leaked, where (repository, log, ticket, screenshot), and since when.
  2. Revoke or rotate it immediately at the source: provider dashboard for API keys, ALTER ROLE for database passwords, a new SECRET_KEY without the old one in fallbacks.
  3. Deploy the new value to every process that uses it (web, workers, scheduled jobs).
  4. Review access logs for the affected system during the exposure window: provider API logs, database connection logs, cloud audit logs.
  5. Clean up the exposure location (delete the log lines, edit the ticket, rewrite Git history if appropriate), knowing that copies may already exist.
  6. Record what happened and which control would have prevented it, such as a pre-commit scanner or masked CI variables.
a secret leaked: now what?Rotate it firstbefore cleaning historyCheck logs for misusesince the leak timeRemove it from Git historyand ask hosts to purge cachesFind how it leakedand close that pathA secret pushed to a public repo is found by scanners within minutesdjango-deployment.com
Rewriting history does not un-leak it; rotation does.

Rotation is the step that actually removes risk. Everything else limits or documents damage.

Choosing an approach by team size

SituationReasonable approach
One developer, one VPSroot-owned env file with mode 640, or systemd credentials; secrets documented in a password manager
Small team, a few serverssystemd credentials or Docker secrets, values stored in a shared password manager, deploy scripts that write files from it
Cloud-native deploymentplatform secret store (AWS Secrets Manager, Azure Key Vault, Google Secret Manager) with workload identities
Kubernetesexternal secrets operator syncing from a cloud secret manager, encryption at rest for Kubernetes Secrets
Regulated environmentcentralized 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_KEY in the Dockerfile as a build argument, which can persist in image metadata.
  • Printing environment variables in CI for debugging (env or printenv).
  • Sharing one database superuser across applications instead of per-application roles with limited privileges.
  • Relying on .gitignore alone; it does not remove files that were committed before the rule was added.

Next steps

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.

Learning path · step 3 of 15

Hardening a Django deployment

Keep going