Undocumented Django: Generating a SECRET_KEY

Updated Security, Errors, bestpractices

Table of Contents

Django has some of the best documentation out there, a point of pride from the beginning that continues today thanks to the heroic efforts of many volunteers. But for all that is documented, quite a few “hidden” features still lurk in the codebase. Here’s one that came up in my recent update to the Django for Beginners book.

Every Django project ships with a unique SECRET_KEY, and startproject prepends a django-insecure- prefix to it so you know to replace it before going to production. Really, this should live in an environment variable safe from Git and any accidental posting online. But how do you generate a new key? You can use Django’s code to generate one.

Why does SECRET_KEY matter?

In Django, the SECRET_KEY signs data, proving it hasn't been tampered with when Django interacts with the outside world.

That includes:

  • Sessions, including the hash that ties a logged-in session to a user
  • Password reset tokens, the links in "forgot your password?" emails
  • Messages stored in cookies by the messages framework
  • Anything using django.core.signing, including set_signed_cookie() and your own signed values

The SECRET_KEY does not hash passwords; passwords are salted per user and have nothing to do with the 'SECRET_KEY'.

This is why a leaked SECRET_KEY is serious. Anyone who has it can forge session data, reset tokens, and signed cookies that your site will accept as genuine.

The One-Liner

If you need a new SECRET_KEY here it is in one line:

(.venv) $ python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
<new secret key here....>

You get a different value every time you run it.

The function lives in django/core/management/utils.py, which is easy to access in the Django codebase. If you’re not already, you should take the time to look at Django's source code. It is eminently readable, documented, and a great way to learn what “good” code looks like.

The Code

Under the hood, this function picks 50 characters from a 50-character alphabet. get_random_string uses Python’s secrets module, so the result is cryptographically secure.

def get_random_secret_key():
    """
    Return a 50 character random string usable as a SECRET_KEY setting value.
    """
    chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
    return get_random_string(50, chars)

When you run django-admin startproject, Django generates your initial key like this:

options["secret_key"] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key()

SECRET_KEY_INSECURE_PREFIX is the string django-insecure-, and it's why every new project's key looks like this:

SECRET_KEY = "django-insecure-!8fi&^9#b9um&a@$b6jkp-5ru&enmg=ssynu0y_tuakarxlk@k"

The randomness is just as strong as any other key, so the prefix doesn’t make it weaker. It’s a label.

A key created by startproject sits in a settings.py file that almost certainly ended up in version control, pasted into a tutorial, or shared with a teammate. The prefix makes such keys easy to spot, both for you and for Django’s deployment checklist:

(.venv) $ python manage.py check --deploy
System check identified some issues:

WARNINGS:
...
?: (security.W009) 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.
...

A fresh project will show several other deployment warnings too; we're only concerned with W009 here. Generating a fresh key with get_random_secret_key() and loading it from an environment variable clears it.

Gotcha: Special Characters

The characters !@#$%^&*(-_=+) often mean something to your shell. If you paste an unquoted SECRET_KEY into an export statement, $ will expand variables, & will send the command to the background, and ( will cause a syntax error. Double quotes aren’t enough either: $ still expands inside them, and so does ! in an interactive shell. Use single quotes:

$ export SECRET_KEY='e-t&d)p%x8&-rwrl&186fy!wf=lv9gq-b#u)u9r7g^_3%tucfd'

A .env file is a different story. Most .env parsers, including python-dotenv (which environs uses), handle these keys fine unquoted. Still, the safest habit is to wrap the value in double quotes:

SECRET_KEY="e-t&d)p%x8&-rwrl&186fy!wf=lv9gq-b#u)u9r7g^_3%tucfd"

Or you can avoid the problem entirely with the next approach.

The Documented Alternative: secrets

Python's standard library can produce an equally strong key using only URL-safe characters:

(.venv) $ python -c "import secrets; print(secrets.token_urlsafe(38))"
dDJSY1BlB9U1S7W9nHHdKQVGMgyyAUhfichIR3ZiEy3aOF5HhjI

token_urlsafe(38) takes 38 random bytes (304 bits) and Base64-encodes them into a 51-character string of letters, digits, - and _. It passes the W009 check, and you never have to quote it.

Either one is fine. Choose get_random_secret_key() if you want exactly what Django itself uses, or secrets if you want a documented API and no quoting headaches. If you're writing a script or tool that others will rely on, lean toward secrets: get_random_secret_key() isn't part of Django's public API, so it could in theory be renamed or removed, though startproject depends on it and it has been stable for many releases.

Rotating Without Logging Everyone Out: SECRET_KEY_FALLBACKS

If your SECRET_KEY ever leaks, immediately replace it. For best security, routine rotation is the rule. Django 4.1 added a SECRET_KEY_FALLBACKS option: a list of old keys that Django still accepts when verifying signatures but never uses to create new ones:

# django_project/settings.py
SECRET_KEY = env.str("SECRET_KEY")
SECRET_KEY_FALLBACKS = env.list("SECRET_KEY_FALLBACKS", default=[])

Fallback keys are checked by the same W009 rules as warning security.W025, so an old django-insecure- key will show up in check --deploy until you remove it.

Conclusion

Keep your SECRET_KEY well and truly secret. Generate it with get_random_secret_key() or secrets.token_urlsafe(), and rotate it with SECRET_KEY_FALLBACKS when the time comes.

Django has plenty of other useful features like this one that never made it into the docs. We’ll dig into more of them in the next post in the series.

Related Tutorials

  • Django Best Practices: Security

    Updated

    Django security features for any production website.

    Security, Best Practices
    View Tutorial
  • NameError: name 'os' is not defined

    Updated

    How to fix this common error for Django 3.0 and earlier.

    Errors
    View Tutorial