[Fixed]-Django `SECRET_KEY` settings

12đź‘Ť

Let’s break this question into two parts:

  1. Key storage and rotation
  2. Key length and allowed characters

1) Key storage and key rotation

Key storage

Putting the key into the database is a really bad idea. Don’t do it.

Ideally, you don’t want the key hardcoded anywhere in your application, either. This means you should find a way to replace the hardcoded key in settings.py with a reference to (usually) an environment variable (e.g., SECRET_KEY = os.getenv('SECRET_KEY')).

At a minimum, you should at least be loading the key into your environment variables from a .env file. There are multiple approaches to this in python.

Ideally, however, you don’t want to store the key anywhere in your environment. The better solution is to use a secrets management service (e.g., Hashicorp Vault, Doppler, etc., or the secrets manager provided by your IaaS/PaaS), fetch the key at runtime, and only ever store it in memory.

Key rotation

Rotating the SECRET_KEY in Django<4.1 will immediately log out all users, invalidate password reset and email verification links, etc.

In most cases, this means you really only want to rotate the key if there’s a breach.

For Django>=4.1, however, a SECRET_KEY_FALLBACKS setting has been introduced.

Now, rotating keys is generally good practice, so you may want to start doing this. However, as noted in the documentation, falling back to old keys is expensive, so you will also want to limit the number of fallback keys.

Ultimately, the answer will come down to your particular security model and the tradeoffs you’re willing to make in terms of computational costs (and user incovenience in the case of Django<4.1).

2) Key length and allowed characters

While Jontas CD correctly identifies how Django automagically generates a secret key, the answer is incorrect.

The correct answer is that, with the exception of a small handful of constraints, your Django SECRET_KEY can be any arbitrary str or bytes of any arbitrary length.

Constraints

Length, forbidden prefix, and unique characters

The following check (from the django.core.checks.security.base module) defines a small handful of contraints on length, prefixes, and unique characters:

SECRET_KEY_INSECURE_PREFIX = "django-insecure-"
SECRET_KEY_MIN_LENGTH = 50
SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5

def _check_secret_key(secret_key):
    return (
        len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
        and len(secret_key) >= SECRET_KEY_MIN_LENGTH
        and not secret_key.startswith(SECRET_KEY_INSECURE_PREFIX)
    )

To put that into plain English, your SECRET_KEY must:

  1. Be a minimum of 50 characters in length
  2. Contain a minimum of 5 unique characters
  3. Not be prefixed with "django-insecure-"

Source encoding

As mentioned in the Django documentation on SECRET_KEY, developers should not assume the key to be either str or bytes. Therefore, each use should pass the SECRET_KEY through either force_str() or force_bytes(), depending on the desired type.

This implicitly says that the key must either be provided as either a valid string or, if bytes are provided, those bytes must decode to a valid string.

As for what constitutes a valid string, since Python 3.0 introduced PEP3120, the default source encoding for strings has been UTF-8.

This is further enforced within Django. For example, django.utils.encoding.force_str() explicitly sets UTF-8 as the source encoding.

def force_str(s, encoding="utf-8", strings_only=False, errors="strict"):

    ...

    try:
        if isinstance(s, bytes):
            s = str(s, encoding, errors)
        else:
            s = str(s)
    except UnicodeDecodeError as e:
        raise DjangoUnicodeDecodeError(s, *e.args)
    return s

If for some reason you’re still stuck on older versions of Python, then any ASCII character is, presumably, fair game. This would include whitespace.

Why is there no maximum length?

In cryptography, we generally use fixed-length, binary keys. The way we go from variable-length strings (e.g. passwords, SECRET_KEY, etc.) to a fixed-length binary key is through a key derivation function.

In practical terms, this means that we "hash" the SECRET_KEY before using it in any cryptographic function.

How you hash the key is largely a product of your desired key length is (e.g. 128-bit, 256-bit, 512-bit, etc.), and how much security against brute force attacks you want to buy with your key derivation function. It’s more or less like password hashing, and, indeed, common password hashing algorithms are also commonly used as key derivation functions.

To see how this looks in Django, we can look at django.crypto.utils.salted_hmac()

def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
    ...
    key_salt = force_bytes(key_salt)
    secret = force_bytes(secret)
    # snip the wrapping try/except block for brevity
    hasher = getattr(hashlib, algorithm)  # returns hashlib.sha1
    key = hasher(key_salt + secret).digest()
    return hmac.new(key, msg=force_bytes(value), digestmod=hasher)

Here, your SECRET_KEY (along with a random salt) is either stretched or reduced to a 160-bit SHA1 hash.

Said another way, it doesn’t matter how long your SECRET_KEY is; it will always be stretched/reduced to the correct number of bits when it actually comes time to use it.

Will a really long key be wasted?

As we’ve seen above, Django defaults to hashing the key down to a 160-bit sha1 hash when signing messages.

However, the stock-standard get_random_secret_key() spits out a key that’s 50 characters long, and randomly chooses from a set of 50 characters. This gives it an entropy of log2(50^50), or approximately 282 bits.

So are these extra bits just getting wasted? Well, yes, and no.

Speaking from a computational cost standpoint, the time and memory cost differences in binary encoding and hashing, let’s say, a 50-character string vs. a 500-character string are negligible. Yes, if you need to do it thousands of times over on a bunch of random strings, you’ll start to see a difference. However, it will make no measurable difference in Django performance.

In terms of wasted entropy (i.e., hashing a SECRET_KEY with 282 bits of entropy down to a 160-bit hash), then yes, a super-long key is a waste if it is truly random. However, I suppose there could be some additional safety in generating some extra bits of entropy in the case of a pseudo-random method of generating your key.

tl;dr

Chars/length

Django SECRET_KEY may be any str (or bytes that decode to a UTF-8 str) that is of len(str) > 49, contains at least 5 unique characters, and does not have "django-insecure-" as a prefix.

Concretely, this means:

# Valid
SECRET_KEY = b'\x20\x20\x20\x63\x6f\x52\x72\x45\x43\x54\x42\x41\x54\x74\x65\x52\x79\x68\x6f\x52\x53\x65\x73\x54\x41\x70\x6c\x65\xf0\x9f\x92\xa9\xf0\x9f\x8d\x86\xf0\x9f\x92\xa6\xf0\x9f\xa6\xb4\xf0\x9f\x8d\x92\xf0\x9f\x8d\x91 \xc2\xb6\xc2\xbc\xc3\x8b\xc3\x9f\xc3\xb1\xc4\x86\xc4\x9c\xc5\x94\xc7\xb8\xc8\x8f\xc8\xa4\xd0\x82\xf0\x9f\x91\x89\xf0\x9f\x91\x8c\xf0\x9f\x91\x85 \xf0\x9f\x91\x84\x21\x20\x61\x6e\x64\x20\x6f\x6e\x20\x61\x6e\x64\x20\x6f\x6e\x20\x61\x6e\x64\x20\x6f\x6e\x2e\x2e\x2e'

# Valid
SECRET_KEY = "   coRrECTBATteRyhoRSesTAple💩🍆💦🦴🍒🍑 ¶¼ËßñĆĜŔǸȏȤЂ👉👌👅 👄! and on and on and on..."

# Invalid: the first byte (0xFF) is not a valid UTF-8 code unit
# Note: Django never checks this, so Django will seem okay at first. However,
# this will throw a UnicodeDecodeError as soon as anything attempts to
# force_str() it.
SECRET_KEY = b'\xFF68(*ctf1e2r=##e+nl=7(&w*v5z!%h=dej)ypjv&p=n9is4d04'

# Invalid: To short
SECRET_KEY = 'jfieo0w'

# Invalid: Not enough unique characters
SECRET_KEY = 'abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdab'
👤ben

5đź‘Ť

Django generates a SECRET_KEY every time start a project, so, no, you can’t leave it blank.

SECRET_KEY has always 50 characters of length.

No whitespaces. Here is the method Django uses to generate it.

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)

Yes, you can store it somewhere else, like with an env settings approach, e. g.

Of course, you can always look at documentation about SECRET_KEY or at code snippets like:

Beyond that, there’s also the alternative of creating a ticket to improve SECRET_KEY documentation – if you think it’s the case.

👤Jonatas CD

Leave a comment