Django: rotate your secret key, fast or slow

A key, ready for rotation.

Django’s SECRET_KEY setting is used for cryptographic signing in various places, such as for session storage and password reset tokens. This makes keeping it secure a high priority since an attacker with the key could forge things like password reset tokens.

If you have leaked your secret key, you should rotate it. Committing it to your source code repository should count as a leak because it puts it in the hands of anyone who gains repository access in the future.

Two options for rotating Django’s secret key are fast and forceful or slow and soft. Let’s look at them now after a quick tip.

Generate a new secret key

To make yourself a new secret key, you can use this undocumented-but-stable function from Django:

In [1]: from django.core.management.utils import get_random_secret_key

In [2]: get_random_secret_key()
Out[2]: 'e4t-tdx$+(+d%-jy@d47&+gmohi8)3uwvjcc(fp4%n(jt4ur6v'

This function is what Django uses to generate a secret key when you run startproject.

Fast and forceful: change SECRET_KEY

Use this path if you don’t mind these side effects:

These are generally not a concern for small or internal sites or those with seasonal traffic.

If you’re okay with all the above, change the SECRET_KEY setting to read your new key from a secret store. Many deployment platforms use environment variables, per The Twelve-Factor App pattern.

If your code already reads from an environment variable, you only need to change the value there.

If you’re moving from a committed key to reading from environment variables, you could change from this:

SECRET_KEY = "django-insecure-!%_&gh8fm2g7*p4g^q8rlh!ookh55)d5v2p%z7cu"

…to this:

SECRET_KEY = os.environ.get(
    "DJANGO_SECRET_KEY",
    "django-insecure-)&$m#1xam9#efjeg&s32!v-+rk#u^4*)o^(t#=-g",
)

The pattern still uses a default committed key for local development, which would be insecure for production. To be extra safe, this example uses a new secret key for that default, though that’s not strictly necessary.

Deploy your code change, add the secret key to your secret store, and ensure your servers reload to pick up the change. (Many deployment platforms automatically reload after changing an environment variable.) The side effects apply as soon as the new secret key is loaded.

Slow and soft: use SECRET_KEY_FALLBACKS

Use this path to mitigate side effects, such as allowing users to stay logged in. That is often important for large sites, where forcing users to log out loses business.

The SECRET_KEY_FALLBACKS setting is a list of alternative secret keys used only when reading signed data. It was added in Django 4.1 (August 2022) to allow rotation to a new key whilst accepting data signed with the old one. This path requires several changes spaced out over time.

First, make SECRET_KEY read your new secret key, and add the old one to SECRET_KEY_FALLBACKS.

If you read the secret key from an environment variable, put the key into a new one:

SECRET_KEY = os.environ.get(
    "DJANGO_SECRET_KEY_2",
    "django-insecure-)&$m#1xam9#efjeg&s32!v-+rk#u^4*)o^(t#=-g",
)

if "DJANGO_SECRET_KEY" in os.environ:
    SECRET_KEY_FALLBACKS = [os.environ["DJANGO_SECRET_KEY"]]

If you’re moving from a committed key to reading from environment variables, move the old one to SECRET_KEY_FALLBACKS:

SECRET_KEY = os.environ.get(
    "DJANGO_SECRET_KEY",
    "django-insecure-)&$m#1xam9#efjeg&s32!v-+rk#u^4*)o^(t#=-g",
)

SECRET_KEY_FALLBACKS = [
    "django-insecure-!%_&gh8fm2g7*p4g^q8rlh!ookh55)d5v2p%z7cu",
]

Deploy this change, including setting the new key on your deployment platform.

Second, wait a while. How long is up to you.

You might pick two weeks, Django’s default session age, as set in the SESSION_COOKIE_AGE setting. If you’ve increased this setting, you may wait longer. You may also have business or security constraints that require you to extend or limit this window.

Third, remove the old key from SECRET_KEY_FALLBACKS. In the usual case, with only one fallback, you can remove the setting entirely:

-if "DJANGO_SECRET_KEY" in os.environ:
-    SECRET_KEY_FALLBACKS = [os.environ["DJANGO_SECRET_KEY"]]

And there you go, all rotated!

Fin

May you keep it secret, keep it safe,

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: