skip to content

Your Django SECRET_KEY was committed to a public repository; how do you rotate it with SECRET_KEY_FALLBACKS without logging everyone out, and what does the fallback cost you?

level: seniorimportance: nice to knowfreq 30%

answer

  1. new key first, old key second
  2. verify with fallbacks, sign with current
  3. sessions re-signed on activity
  4. the attacker's signatures verify too
  5. a short window, then remove

basics

~20 s

Set a freshly generated SECRET_KEY and move the leaked key into SECRET_KEY_FALLBACKS briefly, so active sessions verify and get re-signed, then remove it. While it stays there, anything the attacker signs with it also verifies.

solid answer

~50 s

Since Django 4.1, verification tries `SECRET_KEY` and then each entry of `SECRET_KEY_FALLBACKS`, while all new signatures use `SECRET_KEY`. So you deploy a new key from `get_random_secret_key()` with the leaked key as the first fallback. Active users keep working: `get_user()` matches their session auth hash against a fallback, cycles the session key and stores the hash made with the new key; signed sessions, cookie messages and outstanding reset tokens verify through the fallback until they expire or are rewritten. The cost is that a fallback accepts **every** signature made with the leaked key, including forgeries of signed cookies, cookie-based sessions and your own `signing.dumps()` links. So for a leak you keep the window short, hours rather than weeks, then remove the key and accept that users idle through the window log in again; if the key is known to be exploited, skip the fallback entirely.

code

python · 9 lines
python
# settings/production.py during the rotation window
import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # freshly generated

# Leaked key, first in the list; remove it after a short window.
SECRET_KEY_FALLBACKS = [
    key for key in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",") if key
]

go deeper

for a junior

Know that a leaked SECRET_KEY must be replaced and that SECRET_KEY_FALLBACKS lets old signatures keep working for a while.

for a middle

Explain that fallbacks only verify, that new signatures use SECRET_KEY, and how the session auth hash is upgraded on the next request.

for a senior

Run the leak playbook: short fallback window, synchronised deploy, removal, and the judgement of when to skip the fallback and accept logout.

for a principal

Set a rotation policy that separates routine rotation from incident response, and limit what one leaked key can forge by giving critical flows their own keys.

## What a leaked key lets an attacker do `SECRET_KEY` is the default HMAC key for Django's signing. Anyone holding it can produce signatures Django will accept for data signed with the key alone: - **signed cookies** set with `set_signed_cookie()` and read with `get_signed_cookie()`; - the **session contents** on the `signed_cookies` backend, which live entirely in the cookie; - **cookie-stored messages**; - any **`signing.dumps()` payload** your app trusts, such as email-confirmation, invitation or unsubscribe links. Session auth hashes and password-reset tokens also mix in the user's password hash, so forging them takes more than the key, but they are still keyed by it and should be treated as compromised. The documentation warns that a known key can lead to privilege escalation and remote code execution; the historical `PickleSerializer` for sessions, removed in Django 5.0, made a forged session cookie a code-execution bug. ## How SECRET_KEY_FALLBACKS works Added in **Django 4.1**, **`SECRET_KEY_FALLBACKS`** is a list of previous keys (default `[]`). | Operation | Key used | |---|---| | creating a signature | only `SECRET_KEY` | | verifying a signature | `SECRET_KEY`, then each fallback in order | | session auth hash check | current hash first, then `get_session_auth_fallback_hash()` | | password-reset token check | `PasswordResetTokenGenerator` tries the secret, then each fallback | When a logged-in user's auth hash matches only a fallback, `get_user()` **cycles the session key** and stores the hash computed with the new key. Active users are therefore migrated to the new key on their next request, without noticing. ## The rotation, step by step 1. **Generate** a new key with `get_random_secret_key()` and store it in the secret store. 2. **Deploy** with `SECRET_KEY = <new>` and `SECRET_KEY_FALLBACKS = [<leaked>]`, the documented order: the new key in `SECRET_KEY`, the previous key at the **beginning** of the fallbacks. 3. **Wait a short window** while active users make requests and their sessions are re-signed. Outstanding reset links and signed cookies still verify during this time. 4. **Remove** the leaked key from the fallbacks and deploy again. From now on the leaked key verifies nothing, and anyone who did not return during the window logs in again. 5. **Clean up** the rest: remove the key from the repository history as far as possible, check your own code for other keys derived from it, and confirm `check --deploy` raises no `security.W025` for the fallback list. ## Routine rotation versus a leak The same setting serves two different situations: | | Routine rotation | Leaked key | |---|---|---| | Is the old key known to others? | no | assume yes | | Fallback window | as long as your longest-lived token, often days | as short as you can tolerate, often hours or none | | Cost of the fallback | a little CPU per failed first check | forged signatures keep verifying | | Accepting mass logout | avoidable | often the right call | The honest answer to "without logging everyone out" is that you trade exposure time for convenience. If there is any sign the key is being used, skip step 3 and deploy without a fallback: everyone logs in again, and nothing signed with the leaked key verifies. ## Details that trip people up - **Order matters for cost, not correctness**: each fallback that does not match costs another HMAC, so the documentation asks you to remove old values after an appropriate period. - **All instances must switch together**: a server still signing with the old key keeps producing signatures the new servers only accept via the fallback. - **Fallbacks are keys too**: they must be as strong as the main key, and the deploy check flags weak entries. - **Rotation does not touch passwords**: stored password hashes do not depend on the key.

  • Why do active users not notice the rotation?
    When `get_user()` finds that the session auth hash matches a fallback rather than the current key, it cycles the session key and stores the hash made with the new `SECRET_KEY`. Session data is re-signed with the new key when the session is saved. By the time the fallback is removed, users who were active in the window already carry new-key signatures.
  • Where should the leaked key go in SECRET_KEY_FALLBACKS if there are already older keys there?
    At the beginning. The documentation says to move the previous key to the start of the list and remove old values from the end, so verification tries the most likely match first and each extra non-matching entry costs another HMAC. For a leak, also drop older fallbacks you no longer need.
  • Does removing the key from the Git history make the rotation unnecessary?
    No. Once a key has been public you must assume it was copied, and rewriting history does not reach clones, forks or caches. Rotation is what makes the leaked key worthless; history cleanup only limits further exposure.

Rekeying a building but leaving the old key working at one side door for a few days so staff can swap keys: convenient, but anyone who copied the old key walks in through that door until you finally change its lock.

saying these in an interview costs you the question

  • Keeping the leaked key in SECRET_KEY_FALLBACKS indefinitely so nobody is ever logged out
  • Putting the new key in SECRET_KEY_FALLBACKS and leaving the leaked key as SECRET_KEY
  • Rewriting Git history instead of rotating the key
  • Forcing every user to reset their password because the key leaked
  • Believing fallbacks are used to create new signatures as well as verify them