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.
- Generate a new key:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())". - Deploy with
SECRET_KEY = <new>andSECRET_KEY_FALLBACKS = [<old>]on every server and process at the same time. - Wait at least as long as your longest-lived signed values:
SESSION_COOKIE_AGE(two weeks by default) andPASSWORD_RESET_TIMEOUT(three days by default). - Remove the fallback and deploy again; destroy the old key.
- If the old key leaked, skip the fallback (or keep it very briefly) and accept that users are logged out; that is the point.
- Check third-party packages that use
SECRET_KEYdirectly (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_cookiesengine, 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()orTimestampSigner.
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
]
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.
# 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_ageyou 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:
- 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).
- Accept that all sessions and reset links become invalid.
- Consider forcing password resets if there is any sign the key was used.
- 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.
- Check other secrets exposed in the same place (database passwords, API tokens) and rotate them too.
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 --deploypasses (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
- Keep keys out of code with Manage Django Secrets in Production.
- If a key change already logged everyone out, see Fix Users Logged Out After a Django Deploy.
- If the key is missing at start-up, see Fix ImproperlyConfigured: SECRET_KEY Must Not Be Empty.
- Review related defaults in Django Deployment Security Basics You Should Not Skip.
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.