Django SECRET_KEY generator

A 50-character SECRET_KEY for Django settings, matching the length the framework generates itself.

G
More options
up to 10,000

Constraints

What SECRET_KEY actually signs

SECRET_KEY is not an encryption key. Django uses it as HMAC key material through django.core.signing, so everything the framework signs is derived from it, and everything derived from it stops verifying when it changes. Most developers know that rotating the key logs people out and are surprised by the rest of the list.

The password reset token is the case worth understanding, because it explains the design. Django builds it from SECRET_KEY together with the user's current password hash, their last_login timestamp and a timestamp of its own. Mixing in the password hash is what makes a reset link single use: once the password changes, the hash changes and the token no longer verifies. Nothing has to be stored server side to achieve that.

  • Session data, when the signed_cookies backend is in use, and the signature on the session store's payload otherwise.
  • Password reset tokens, and therefore every reset link currently sitting in a user's inbox.
  • Cookies written with set_signed_cookie, including the messages framework when it falls back to cookie storage.
  • Anything you signed yourself with Signer or TimestampSigner, such as unsubscribe links, invitations and signed download URLs.

Signing is not encryption. A signed cookie is readable by whoever holds it; the signature only proves it was not modified. Do not put anything private into signed data on the assumption that SECRET_KEY hides it.

The 50-character convention

Django ships get_random_secret_key() in django.core.management.utils, and django-admin startproject calls it to write the key into your generated settings file. It returns 50 characters, which is where the convention comes from. Django itself does not enforce a length: it requires the setting to be non-empty and nothing more.

The default here is therefore 50 characters as well, drawn from a 74-symbol alphabet, which is 310.47 bits. That is far more than the framework needs and costs nothing, since the value is pasted once into an environment file. Matching the familiar length also means the value looks right to the next person who opens the settings file, which is a small thing that prevents a wrong question.

Django will tell you when the key is not good enough. Run manage.py check --deploy and security.W009 fires if SECRET_KEY is shorter than 50 characters, has fewer than five distinct characters, or still carries the django-insecure- prefix that startproject writes. That last condition exists precisely because so many projects reach production still using the key the framework generated on day one.

Rotating it, and what breaks when you do

A naive rotation invalidates everything at once. Sessions are dropped, so every user is signed out. Every password reset link already sent stops working, which produces a wave of support requests from people who did nothing wrong. Signed cookies fail, and any signed URL you issued becomes invalid.

Django 4.1 added SECRET_KEY_FALLBACKS to make this survivable. Signing always uses SECRET_KEY; verification tries SECRET_KEY first and then each value in the fallback list. The rotation is therefore three deployments rather than one: move the current key into the fallback list and put the new key in SECRET_KEY, deploy, wait, then remove the old value.

How long to wait is set by the longest-lived thing you sign. SESSION_COOKIE_AGE defaults to 1209600 seconds, which is two weeks, and PASSWORD_RESET_TIMEOUT defaults to 259200 seconds, which is three days. If you have not changed those, a two-week overlap retires the old key with nothing broken. After a compromise you skip the overlap deliberately: the point of rotating a leaked key is that values signed under it must stop being trusted immediately.

Keeping it out of version control

startproject writes the key as a literal string in settings.py, and settings.py is committed on the first commit. That single default is the origin of most leaked Django keys, and it is why the generated value now carries a prefix that the deployment check refuses to pass.

Read the key from the environment with no fallback. os.environ["DJANGO_SECRET_KEY"] raises at import time when the variable is missing, which is exactly what you want: an application that cannot find its secret should refuse to start rather than quietly run on a placeholder. A default value in that line is how a development key ends up serving production traffic.

If a key was ever committed, rotate it. Rewriting history does not undo the exposure, because the old value is already in every clone, every fork, every CI cache and every mirror. The commit is not the secret's location; it is only the first place you noticed it.

Use a different key in every environment. A shared key means a staging compromise forges production sessions, and staging is always the softer target.

Doing this in your own code

Shell
# Django's own helper. Returns 50 characters.
python -c "from django.core.management.utils import get_random_secret_key; \
           print(get_random_secret_key())"

# Without Django installed. 64 bytes as base64url is 86 characters.
python -c "import secrets; print(secrets.token_urlsafe(64))"
settings.py
import os

# No default. A fallback value here means production can quietly run on a
# key that is sitting in your repository.
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# Rotation, Django 4.1 and later. New signatures use SECRET_KEY; anything
# signed under a key still listed here keeps verifying.
SECRET_KEY_FALLBACKS = [
    k for k in [os.environ.get("DJANGO_SECRET_KEY_PREVIOUS")] if k
]

Drop the old key only after the longest-lived thing you sign has expired. SESSION_COOKIE_AGE defaults to 1209600 seconds and PASSWORD_RESET_TIMEOUT to 259200.

What it signs
from django.core.signing import Signer, TimestampSigner
from django.contrib.auth.tokens import default_token_generator

# Both of these are HMAC-SHA-256 keyed by SECRET_KEY plus a per-use salt.
Signer().sign("cart:42")
TimestampSigner().sign("invite")

# The password reset token mixes in the user's current password hash and
# last_login, which is why the link dies once the password changes.
default_token_generator.make_token(user)
Deployment check
python manage.py check --deploy

# security.W009 fires when SECRET_KEY is under 50 characters, has fewer
# than 5 distinct characters, or still carries the "django-insecure-"
# prefix that startproject writes into the generated settings file.

Questions

How long does a Django SECRET_KEY have to be?

Django does not enforce a length. Fifty characters is the convention because that is what get_random_secret_key() produces and what startproject writes, and the deployment check warns below it. Anything shorter than about 32 random characters is worth fixing regardless of what the framework permits.

What happens if I change SECRET_KEY on a live site?

Without SECRET_KEY_FALLBACKS, every user is signed out, every outstanding password reset link stops working, and every signed cookie or signed URL you issued becomes invalid. With the fallback list, nothing breaks: new signatures use the new key while old ones keep verifying until you remove the old value.

Can I reuse one SECRET_KEY across several Django projects?

No. A single leak would then let an attacker forge signed values for all of them, and it makes rotation impossible to scope, because you can no longer say which systems a change affects. One key per project, and a different one per environment.

Is SECRET_KEY used to encrypt anything?

No. It is HMAC key material. Session cookies signed with it are readable by the person holding them; the signature only proves they were not tampered with. If you need stored data to be unreadable, that is a separate encryption key with a separate lifecycle.

Does it matter which characters are in the key?

Only for handling. Django accepts any non-empty string. The practical constraint is the file it lives in: a .env loader may treat an unquoted hash as a comment or expand a dollar sign, and a shell will expand both inside double quotes. Quote the value, or use hex if the loader is one you have not read.

Related generators