Rotate the Django SECRET_KEY Safely

Rotate Django's SECRET_KEY without breaking sessions and password reset links: what the key signs, SECRET_KEY_FALLBACKS, a staged rollout, leak response, and third-party libraries that use the key.

Problem statement

The SECRET_KEY was committed to a repository, pasted into a chat, left in an old CI log, or known to a contractor who has left. Or your security policy requires periodic rotation. Changing it naively invalidates every session (everyone is logged out), every password reset link in flight, and every signed value your app issued.

Django supports graceful rotation with SECRET_KEY_FALLBACKS: new values are signed with the new key while values signed with old keys are still accepted for a transition period. This guide explains what the key protects, rotates it in stages, and covers the different approach needed when the key has actually leaked.

Staged key rotationGenerate new keysecret storeDeploynew key + fallbackTransition periodold values verifyRemove fallbackold key retiredNew values are signed with the new key while fallbacks keep existing sessions and links valid.
  1. Generate a new key: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())".
  2. Deploy with SECRET_KEY = <new> and SECRET_KEY_FALLBACKS = [<old>] on every server and process at the same time.
  3. Wait at least as long as your longest-lived signed values: SESSION_COOKIE_AGE (two weeks by default) and PASSWORD_RESET_TIMEOUT (three days by default).
  4. Remove the fallback and deploy again; destroy the old key.
  5. If the old key leaked, skip the fallback (or keep it very briefly) and accept that users are logged out; that is the point.
  6. Check third-party packages that use SECRET_KEY directly (for example JWT signing) and plan for them separately.

Step-by-step solution

1. Know what the key protects

Django uses SECRET_KEY (through django.core.signing and salted_hmac) for:

  • Sessions: session data is signed; with the signed_cookies engine, the whole session lives in the signed cookie.
  • Authentication: the session auth hash that ties a session to the user's password.
  • Password reset tokens: links in reset emails.
  • Signed cookies and messages: set_signed_cookie(), cookie-based message storage.
  • Anything your code signs with signing.dumps() or TimestampSigner.
what depends on SECRET_KEYWithout a fallbackSessions✕everyone logged outPassword reset links!outstanding links stop workingSigned cookies, messages!cookies rejecteddjango.core.signing!your signed tokens invalidIt is not used to hash passwords; those are unaffecteddjango-deployment.com
Third-party packages may sign data with it too; check them.

It is not used for password hashing: stored password hashes do not depend on the key, so rotation never locks users out of their accounts permanently. CSRF tokens are also independent of it.

2. Generate and store the new key

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Store it in your secret store or environment file, next to the current one:

DJANGO_SECRET_KEY=<new key>
DJANGO_SECRET_KEY_FALLBACKS=<old key>

3. Read fallbacks in settings

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
SECRET_KEY_FALLBACKS = [
    key for key in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",") if key
]
settings for a rotationSECRET_KEY = os.environ["DJANGO_SECRET_KEY"]SECRET_KEY_FALLBACKS= [k for k in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",")if k]1231The new key: signs everything new.2Old keys: only verify existing signatures.3Empty variable means no fallbacks.Needs Django 4.1 or laterdjango-deployment.com
Keys must not contain commas for this parsing to work.

SECRET_KEY_FALLBACKS is available in Django 4.1 and newer. New signatures always use SECRET_KEY; verification tries SECRET_KEY first and then each fallback. Keep the list short, since every verification failure tries each key.

Test a rotation on your Django version before relying on it for sessions: log in, deploy with the new key and fallback on a staging environment, and confirm you stay logged in. Some older releases did not use fallbacks for every signed value.

4. Deploy everywhere at once

Every process that signs or verifies (web servers, Celery workers that generate reset links or signed URLs, scheduled jobs) must switch together. A server still using only the old key rejects values signed by servers with the new key, causing random logouts behind a load balancer.

a routine rotation1Generate a new keyget_random_secret_key()2Old key into FALLBACKSnew key as SECRET_KEY3Deploy everywhere at onceweb, workers, cron4Wait SESSION_COOKIE_AGEsessions re-signed on use5Remove the fallbackrotation completeA process left on the old key cannot verify new signaturesdjango-deployment.com
Default SESSION_COOKIE_AGE is two weeks.
# on each server, after updating the environment file
sudo systemctl restart gunicorn celery-worker celery-beat

Verify the fingerprint of the active key matches across processes without printing it:

python -c "import hashlib, os; print(hashlib.sha256(os.environ['DJANGO_SECRET_KEY'].encode()).hexdigest()[:12])"

5. Wait, then remove the fallback

Keep the fallback until values signed with the old key have naturally expired:

  • sessions: SESSION_COOKIE_AGE (1,209,600 seconds by default);
  • reset links: PASSWORD_RESET_TIMEOUT (259,200 seconds by default);
  • your own signed values: whatever max_age you verify with.

Then remove DJANGO_SECRET_KEY_FALLBACKS, restart everything, and delete the old key from the secret store and any backups of environment files.

6. When the key has leaked

A leaked key lets an attacker forge signed values: session data for signed-cookie sessions, password reset tokens (combined with knowledge of a user's state), and anything your code trusts because it is signed. In that case:

  1. Generate a new key and deploy it without the leaked key as a fallback (or with it only for minutes if an immediate mass logout is unacceptable).
  2. Accept that all sessions and reset links become invalid.
  3. Consider forcing password resets if there is any sign the key was used.
  4. Remove the key from wherever it leaked; for a Git repository, rewriting history does not help if it was ever public, so rotation is what matters.
  5. Check other secrets exposed in the same place (database passwords, API tokens) and rotate them too.
routine rotation versus a leakRoutine✓old key in fallbacks✓nobody notices✓remove fallback laterLeaked key✕no fallback: attacker could signeveryone logged out, on purposereview what was signed with itAfter a leak, keeping the old key as a fallback keeps the attacker indjango-deployment.com
Also rotate anything else that leaked with it.

7. Libraries that use SECRET_KEY directly

Some packages use settings.SECRET_KEY as their own signing key and ignore SECRET_KEY_FALLBACKS. A common example is JWT authentication: djangorestframework-simplejwt uses SECRET_KEY as its default SIGNING_KEY, so rotation invalidates all issued tokens. Give such libraries their own key setting where possible, so rotating Django's key does not force a global token reset and vice versa:

SIMPLE_JWT = {"SIGNING_KEY": os.environ["JWT_SIGNING_KEY"]}

Verification check:

  • Before the switch, log in on a test account; after deploying the new key with fallback, confirm you are still logged in.
  • Request a password reset after the switch and confirm the link works.
  • After removing the fallback, confirm new logins work and python manage.py check --deploy passes (it warns about weak or short keys).

Explanation

Signing in Django computes an HMAC over the data with a key derived from SECRET_KEY and a salt. Verification recomputes it and compares. With fallbacks, verification also tries HMACs derived from each old key, while signing only ever uses the current one. That makes rotation a two-step migration: first make every process sign with the new key while still accepting the old, then, once nothing signed with the old key remains valid, stop accepting it.

Edge cases / notes

Multiple environments

Each environment (staging, production) should have its own key. A shared key lets values signed in staging be accepted in production.

Key format

Use the output of get_random_secret_key() or another long random string. check --deploy warns when the key is short, has few unique characters, or starts with django-insecure-, the prefix used in generated development settings.

When to automate this

Store keys in a secrets manager with versioning, make the deploy read both the current and previous version, and schedule rotation (for example yearly) as a runbook with the two deploys and the waiting period. Combine it with a check that every process reports the same key fingerprint.

Next steps

FAQ

What happens if I change Django's SECRET_KEY?

Existing sessions, password reset links, and other signed values become invalid unless the old key is listed in SECRET_KEY_FALLBACKS. Passwords themselves keep working.

How long should I keep the old key in SECRET_KEY_FALLBACKS?

Until values signed with it expire: at least SESSION_COOKIE_AGE and PASSWORD_RESET_TIMEOUT, plus any custom signed values.

Should I use fallbacks if my SECRET_KEY leaked?

No, or only very briefly. A leaked key should stop being trusted, even though that logs users out.

Does rotating SECRET_KEY invalidate JWT tokens?

If your JWT library signs with SECRET_KEY, yes. Configure a separate signing key for tokens.

Keep going