sethserver.com
A green padlock with a small key in it sitting on a laptop keyboard next to a Python snake figurine.

Django Secret Key Generator (get_random_secret_key)

By Seth Black • Updated: September 17, 2026

Your Django SECRET_KEY
generating...

.env file

  
settings.py

  

Copy the key, or copy the .env line and the settings.py lines if you are setting this up properly (you are, right?). The key is built in your browser from crypto.getRandomValues and never leaves the page. It is the same shape Django produces, character for character, so nothing downstream can tell the difference.

If you need keys for other frameworks, the secret key generator covers Flask, Express, Laravel, Auth.js, Rails and WordPress formats, and the Flask secret key generator has the Flask specifics.

Exactly what this generates

Here is Django's own function, from django/core/management/utils.py (I checked it against Django 6.1):

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)

get_random_string calls secrets.choice once per character, so each of the 50 positions is an independent, uniform pick from 50 symbols: 26 lowercase letters, 10 digits and 14 punctuation characters. No uppercase, no quotes, no backslash, no comma, no space.

The generator above does the same thing in JavaScript: 50 uniform picks from that exact string, using rejection sampling so no character is favored. That works out to 50 × log2(50), about 282 bits of entropy. For comparison, a 256-bit key is already far beyond anything anyone can brute force, so the lack of uppercase letters is not a problem worth worrying about.

One thing startproject does differently: it writes 'django-insecure-' + get_random_secret_key() into your settings file. The random part is fine. The prefix is a sticky note saying "this key lives in source control," and Django's deployment check looks for it.

What SECRET_KEY protects

According to Django's settings reference, the secret key is used for:

The last one is broader than it looks. Third-party packages sign things with it too: email confirmation links, magic login links, "unsubscribe" URLs. If an attacker has your key, they can produce valid versions of all of them. If you use the signed_cookies session backend, the whole session lives in the cookie and they can write any session they like. Django's docs put it bluntly: running with a known SECRET_KEY "can lead to privilege escalation and remote code execution vulnerabilities."

What it does not touch: user password hashes. Those use the password hasher and per-user salts. Rotating SECRET_KEY will log people out and kill outstanding reset links, but nobody's password stops working.

Also worth knowing: signing is not encryption. signing.dumps({"user": 42}) produces a string anyone can base64-decode and read. The signature stops edits, not eyes.

Put it in the environment, not in settings.py

The generated settings.py has the key as a string literal, which is fine on your laptop and a bad idea anywhere the repository goes. Read it from an environment variable instead:

# settings.py
import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

Use square brackets, not os.environ.get("DJANGO_SECRET_KEY", "something"). A missing key should crash at startup with a KeyError, not quietly boot production with a default that is sitting in your git history. (Django also refuses to start if SECRET_KEY is empty, but a non-empty fallback string sails right past that.)

For local development, a .env file loaded by python-dotenv or django-environ, listed in .gitignore:

# .env
DJANGO_SECRET_KEY='paste-the-key-here'
# settings.py, near the top
from dotenv import load_dotenv
load_dotenv()

In production, set the variable through your platform (container environment, systemd unit, Kubernetes secret, Heroku config var) or pull it from a secrets manager at startup. Use a different key for every environment. A staging key that matches production turns every staging leak into a production incident.

The characters that bite: quoting a Django key

Django's alphabet includes ! @ # $ % ^ & * ( ) - _ = +. Most of those mean something to at least one tool between your key and Django. The good news: the alphabet contains no single quote and no backslash, so single quotes always work. The rest of this section is why you should use them.

WhereProblem charactersWhat happensFix
bash, zsh$, !, &, *, (, ), #Unquoted, & backgrounds the command, * globs, parentheses are a syntax error, and # after a space starts a comment. In double quotes, $abc expands to an (empty) variable and, in an interactive shell, ! can trigger history expansion.export DJANGO_SECRET_KEY='...'
Docker Compose .env or env_file$Unquoted and double-quoted values are interpolated. $abc becomes the value of a variable named abc (usually empty, with a warning), and $$ collapses to a single $. Your key silently changes.Single quotes: DJANGO_SECRET_KEY='...'
environment: in compose.yml$Interpolated the same way.Double every $ as $$, or keep the key in an env file with single quotes.
systemd Environment=%systemd expands % specifiers in Environment= ($ is not special there).Write % as %%, or use EnvironmentFile= with a single-quoted value.
Windows batch files%, ^, &% is variable syntax, ^ is the escape character, & chains commands.Easier to regenerate or set it outside the script. In PowerShell, single quotes are literal: $env:DJANGO_SECRET_KEY = '...'

I tested the Compose behavior rather than trusting my memory: with an unquoted A=a$_b in an env file, docker compose config warns that _b is not set and the value comes out as just a. Single-quoted, it comes through intact.

python-dotenv is more forgiving here. It only expands the ${NAME} form, and Django's alphabet has no curly braces, so a Django key loads intact even unquoted. A # preceded by a space would start a comment, but generated keys never contain spaces. I still quote, because the same .env file tends to get handed to Compose eventually.

And if a key keeps fighting some tool you cannot fix, regenerating until you get one without $ costs you nothing meaningful. Every key from this alphabet has entropy to spare.

What manage.py check --deploy looks for

Run this against your production settings before every deploy that touches configuration:

DJANGO_SETTINGS_MODULE=mysite.settings_production python manage.py check --deploy

The secret key check (security.W009) lives in django/core/checks/security/base.py. A key passes only if all three are true:

Otherwise you get "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." Every key from this page passes, and I verified that by running check --deploy against generated keys in a throwaway project. The same rules are applied to each entry in SECRET_KEY_FALLBACKS under security.W025.

Note what the check cannot see: a 50 character key that was pasted from a tutorial passes just fine. Length is not secrecy.

Rotating the key with SECRET_KEY_FALLBACKS

Since Django 4.1 you can rotate without logging everyone out. Django signs with SECRET_KEY and accepts anything signed by a key in SECRET_KEY_FALLBACKS. The procedure from the settings reference:

  1. Generate a new key.
  2. Set it as SECRET_KEY and move the previous value to the beginning of SECRET_KEY_FALLBACKS.
  3. Deploy. Existing sessions and reset links keep working, and new ones use the new key.
  4. After your session lifetime and reset token timeout have passed, remove the old key from the end of the list.

Django's alphabet has no commas, so a comma separated environment variable is a clean way to feed the list:

# settings.py
import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
SECRET_KEY_FALLBACKS = [
    k for k in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",") if k
]

Do not leave fallbacks in forever. Django's docs point out that every extra key adds overhead to signature checks that do not match earlier keys, and a key you meant to retire is still a key that works.

If the old key leaked, skip the fallback step. Put in a new key with no fallbacks and accept that everyone logs in again. That is the part that kicks the attacker out.

Generate a Django secret key offline

With Django installed, use Django's own function:

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Without Django, the Python standard library does the same thing in one line:

python3 -c 'import secrets; print("".join(secrets.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for _ in range(50)))'

Notice the whole Python program sits in single quotes. In double quotes, an interactive bash would read !@ as a history reference and refuse to run the command. See, the quoting problem is real. If all you need is "long and random" rather than Django's exact alphabet, python3 -c "import secrets; print(secrets.token_urlsafe(50))" gives you a key with no shell-hostile characters at all, and it passes check --deploy too.

Common Django SECRET_KEY mistakes

More of my opinions on this sort of thing: Seth's opinionated security best practices.

Django Secret Key FAQ

Is this the same as Django's get_random_secret_key()?

Yes, in format. Django's get_random_secret_key() returns 50 characters picked uniformly from abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+). This page uses the same 50 character alphabet and the same length, picking each character with crypto.getRandomValues and rejection sampling so every character is equally likely.

How long is a Django SECRET_KEY?

Django generates 50 characters, and manage.py check --deploy warns (security.W009) if your SECRET_KEY has fewer than 50 characters, fewer than 5 unique characters, or starts with django-insecure-.

Why does my new Django project have django-insecure- in the SECRET_KEY?

startproject prefixes the generated key with django-insecure- on purpose. It marks the key as a development key that was written into settings.py, and check --deploy warns about it. Generate a fresh key for production and load it from the environment.

Can I use uppercase letters or other characters in a Django SECRET_KEY?

Yes. Django accepts any string, and the check only looks at length, unique characters and the insecure prefix. The generator here matches Django's own alphabet, which happens to have no uppercase letters. A 50 character key from that alphabet still carries about 282 bits of entropy, far more than an attacker can search.

What happens if I change SECRET_KEY?

Everything signed with the old key stops validating: sessions (unless you use the cache-only session backend), cookie-based messages, password reset links and anything using django.core.signing. User passwords are not affected. To avoid logging everyone out, move the old key into SECRET_KEY_FALLBACKS for a while.

Does SECRET_KEY encrypt my database or user passwords?

No. Password hashes use their own salts and the password hasher, and Django's docs state secret keys are not used for user passwords. SECRET_KEY is used for signing, so rotating it will not lock anyone out of their account.

Why does my Django SECRET_KEY break in docker compose or bash?

The key can contain $, #, !, %, &, * and parentheses. Bash expands $ inside double quotes, Docker Compose treats $name in .env files as a variable, and systemd treats % as a specifier. Django's alphabet has no single quote or backslash, so wrapping the key in single quotes works in bash, .env files and PowerShell.

Related tools: secret key generator (Flask, Express, Laravel, Auth.js, Rails, WordPress), Flask secret key generator, JWT secret generator, API key generator, encryption key generator, and the PostgreSQL password generator for your database user.

-Sethers

Newsletter

One email, once a week.

Notes on databases, systems, and the occasional strong opinion about Python. No spam, unsubscribe anytime.

More to read

All tools →

My Password Generators Had 53 Bits of Entropy

Sep 21, 2026

API Key Generator with Prefix, Checksum and SHA-256

Sep 17, 2026

Bcrypt Generator: Hash and Verify Passwords in Your Browser

Sep 17, 2026