skip to content

How should a Django project load SECRET_KEY in production, and how do you generate a strong new value for it?

level: juniorimportance: should knowfreq 45%

answer

  1. not in the repository
  2. fail loudly when missing
  3. the insecure prefix from startproject
  4. a management utility helper

basics

~10 s

Read SECRET_KEY from the environment or a secrets file with no hard-coded default, so a missing value fails loudly. Generate one with django.core.management.utils.get_random_secret_key(), which returns 50 random characters; never ship startproject's 'django-insecure-' key.

solid answer

~40 s

`startproject` writes a random key into `settings.py`, but prefixes it with `django-insecure-` to mark it as unfit for production, because it is now in source control. In production load it from outside the code, for example `SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]`, so a missing variable raises at startup instead of silently falling back. Avoid `os.environ.get("DJANGO_SECRET_KEY", "dev-key")`: the committed default becomes the production key the day the variable is missing. Generate values with `get_random_secret_key()` from `django.core.management.utils`, 50 characters from a mixed alphabet via `get_random_string()`. `check --deploy` warns (`security.W009`) when the key is shorter than 50 characters, has fewer than 5 unique characters, or still carries the `django-insecure-` prefix; an empty key raises `ImproperlyConfigured`.

code

python · 10 lines
python
# settings/production.py
import os

# KeyError at startup if the variable is missing: no silent default.
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# Comma-separated previous keys during a rotation; empty otherwise.
SECRET_KEY_FALLBACKS = [
    key for key in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",") if key
]

go deeper

for a junior

Keep SECRET_KEY out of the repository, load it from the environment, and generate it with get_random_secret_key().

for a middle

Explain the django-insecure- prefix, why os.environ[...] beats .get() with a default, and what W009 checks.

for a senior

Enforce one key per environment and the same key per instance, review for silent defaults, and prepare the rotation path in advance.

for a principal

Own how secrets reach Django across services and environments, so key hygiene is a platform guarantee rather than per-project discipline.

## Where the first key comes from `django-admin startproject` generates a **random `SECRET_KEY`** and writes it into `settings.py`. Since the key is then committed with the rest of the project, Django deliberately prefixes it with **`django-insecure-`**. The prefix is a label, not a weakness in the randomness: it tells you, and the deployment checks, that this value has been exposed in source control and must be replaced before production. ## Generating a proper key Django ships a helper for exactly this job: - **`get_random_secret_key()`** in `django.core.management.utils` returns a **50-character** string drawn from lowercase letters, digits and punctuation, using `get_random_string()`, which is built on the `secrets` module. - It is the same function `startproject` uses, minus the insecure prefix. A one-liner from a shell with Django installed: ```bash python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" ``` Paste the output into your secret store, not into a file in the repository. ## Loading it without a silent default The common failure is not a weak key but a **committed fallback**. Compare three patterns: | Pattern | What happens when the variable is missing | |---|---| | `SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]` | `KeyError` at import, the process does not start | | `SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")` | `None`; the first access to `settings.SECRET_KEY` raises `ImproperlyConfigured` | | `SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-key")` | the committed `dev-key` silently becomes the production key | The third row is the one to spot in code review. Anyone who can read the repository can then forge sessions, signed cookies and signed links for production. A separate development settings module, or a local `.env` file that is never committed, gives developers a key without putting one in code that production also runs. ## What Django checks for you 1. **Empty key.** `settings.SECRET_KEY` raises `ImproperlyConfigured("The SECRET_KEY setting must not be empty.")` when the value is empty, so Django refuses to run without one. 2. **Weak key.** `manage.py check --deploy` runs `check_secret_key`, which emits **`security.W009`** unless the key has at least **50 characters**, at least **5 unique characters**, and does not start with `django-insecure-`. 3. **Weak fallbacks.** The same criteria apply to each entry of `SECRET_KEY_FALLBACKS`, reported as **`security.W025`**. These checks catch mistakes; they cannot tell whether a strong-looking key has leaked. ## Keys in tests and local development Development and test runs need a key too, but not the production one: - A **test settings module** can set any fixed, non-empty value; tests never need a secret that matters, and `override_settings(SECRET_KEY=...)` lets a single test prove that a signature fails after a key change. - A developer's **local key** can live in an uncommitted `.env` file or in a development-only settings module that production never imports. - **Continuous integration** should use its own throwaway value rather than a copy of production's key, because build logs and caches are read by more people than production is. The principle is the same everywhere: a key's value should be known only to the environment whose signatures it protects. ## Good habits - One key **per environment**: staging must not be able to forge production's cookies. - The **same** key on every instance of one environment, or users are logged out as requests hop between servers. - Keep the key out of logs, error pages and container image layers; `DEBUG = False` matters here because the debug page is where settings tend to leak, although Django masks settings whose names contain `SECRET` or `KEY` in that page. - Plan how you will **rotate** it before you need to, using `SECRET_KEY_FALLBACKS`.

  • Is a key starting with django-insecure- actually weak cryptographically?
    Not necessarily: the characters after the prefix come from the same `get_random_secret_key()` generator. The prefix marks that the value was written into `settings.py` by `startproject` and is therefore in source control. Its exposure, not its randomness, is the problem, which is why `check --deploy` flags it.
  • Does the Django debug page leak SECRET_KEY when DEBUG is on?
    The technical 500 page lists settings, but Django's exception reporter replaces values whose names match sensitive patterns, including `SECRET` and `KEY`, with asterisks. That masking is a safety net, not a reason to run `DEBUG = True` in production, where the page exposes plenty of other information.

saying these in an interview costs you the question

  • Keeping the key startproject generated because it is already random
  • Using os.environ.get with a hard-coded default key for convenience
  • Giving every environment the same key so sessions work everywhere
  • Generating the key with uuid4 or a short word because length does not matter
  • Believing Django runs happily with an empty SECRET_KEY in production