SECRET_KEY strength and rotation
SECRET_KEY is not one secret. It is the root of a key-derivation scheme that signs
session cookies, password-reset tokens, django.core.signing values, message-framework
cookies and any signed cookie your own code sets.
That is what makes disclosure severe rather than embarrassing: an attacker with the key does not need to guess a password, because they can mint a valid password-reset token or forge a session cookie for any user.
Its default in global_settings.py is the empty string. startproject writes a real
key into your settings file prefixed with django-insecure-, which is Django
telling you, in the value itself, that this key was generated for development and is
sitting in a file you are about to commit.
The check tests three conditions, and names none of them
Section titled “The check tests three conditions, and names none of them”security.W009 fires if the key is shorter than 50 characters, has fewer than
5 unique characters, or starts with django-insecure-. The message lists all
three regardless of which one failed, so read it as “one of these” and check the
prefix first.
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # KeyError at boot if unset — deliberateGenerate a value with Django’s own helper, which produces 50 characters:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"Index the environment rather than using .get(). A missing key should stop the
process at startup, not silently fall back to a default that signs real sessions.
python manage.py check --deployYour SECRET_KEY has less than 50 characters, less than 5 unique characters, or it's prefixed with 'django-insecure-' indicating that it was generated automatically by Django. Please generate a long and random value, otherwise many of Django's security-critical features will be vulnerable to attack.One of three separate conditions failed, and the message does not tell you which. The most common is the `django-insecure-` prefix that `startproject` writes.
Read in: django/core/checks/security/base.py — SECRET_KEY_WARNING_MSG, id security.W009Rotating without logging everyone out
Section titled “Rotating without logging everyone out”Before Django 4.1, changing SECRET_KEY invalidated every session and every
outstanding password-reset link at once. SECRET_KEY_FALLBACKS fixes that: the new
key signs, and the old key still verifies.
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # the new keySECRET_KEY_FALLBACKS = [os.environ["DJANGO_SECRET_KEY_OLD"]]Deploy that, wait longer than SESSION_COOKIE_AGE (two weeks by default), then remove
the fallback. Leaving the old key in the list forever defeats the point of rotating —
it stays valid for as long as it is listed.
Rotation invalidates anything signed with the old key the moment you drop the fallback, which includes password-reset emails already in someone’s inbox and any long-lived signed URLs your code issues. If you rotate because the key leaked, that is exactly what you want and you should not use a fallback at all — a leaked key must stop verifying immediately.
SECRET_KEY_FALLBACKS is for planned rotation. A leak is not planned rotation.
Related
Section titled “Related”- DEBUG in production — the debug page is one way the key gets out
- Session and CSRF cookie flags — what the key is signing