Skip to content

SECRET_KEY strength and rotation

Severity: criticalBehaviour changed in Django 4.1Applies to: Django 4.1+ (SECRET_KEY_FALLBACKS)Facts last verified 2026-07-26 against Django 6.0 (main)

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.

the fix
settings.py
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # KeyError at boot if unset — deliberate

Generate a value with Django’s own helper, which produces 50 characters:

Terminal window
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.

verify it workedRun this in: framework cli
Terminal window
python manage.py check --deploy
if you see this
Your 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.W009

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.

settings.py
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # the new key
SECRET_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.

before you ship this

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.