12đź‘Ť
Let’s break this question into two parts:
- Key storage and rotation
- 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:
- Be a minimum of 50 characters in length
- Contain a minimum of 5 unique characters
- 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'
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.
- Subclassing Django ModelForms
- How passing string on filter keyword to Django Objects Model?
- Django-allauth, JWT, Oauth
- Django admin GenericForeignKey inline
- Django runserver error when specifying port