Django `SECRET_KEY`设置

14

我有几个关于Django中SECRET_KEY设置的“简单”问题:

  • 它的最小、最大和推荐长度是多少?

    • 我可以将其留空吗?
    • 如果我使用非常长的SECRET_KEY,会有一些“浪费”吗?
  • 允许使用哪些字符?

    • 它允许使用不可打印字符(包括但不限于空格)吗?
  • 何时应该更改它?

    • 我知道当我更改它时,所有现有的cookies/sessionssignature等都将失效。
  • 从数据库(或其他来源)检索它是否可以,而不是直接在settings.py中编写它?

2个回答

12
让我们将这个问题分成两个部分:
1)密钥存储和轮换
2)密钥长度和允许的字符
1)密钥存储和密钥轮换
密钥存储在数据库中是一个非常糟糕的想法,不要这样做。
理想情况下,您不希望在应用程序中任何地方硬编码密钥。这意味着您应该找到一种方法来替换settings.py中硬编码的密钥,通常是使用环境变量的引用(例如:SECRET_KEY = os.getenv('SECRET_KEY'))。
至少,您应该将密钥从.env文件加载到环境变量中。Python 有多种方法实现此操作。
然而,理想情况下,您不希望在环境中存储密钥。更好的解决方案是使用密码管理服务(例如:Hashicorp Vault、Doppler 等,或者由您的 IaaS/PaaS 提供的密码管理器),在运行时获取密钥,并仅将其存储在内存中。
2)密钥轮换

Django<4.1中旋转SECRET_KEY将立即注销所有用户,使密码重置和电子邮件验证链接无效等。

在大多数情况下,这意味着只有在发生违规事件时才需要旋转密钥。

然而,对于Django>=4.1,引入了一个SECRET_KEY_FALLBACKS设置。

现在,旋转密钥通常是良好的做法,因此您可能希望开始这样做。然而,正如文档中所指出的那样,回退到旧密钥是昂贵的,因此您还需要限制回退密钥的数量。

最终的答案将取决于您特定的安全模型以及在计算成本(以及在Django<4.1的用户不便的情况下)上愿意做出的权衡。

2) 密钥长度和允许的字符

虽然Jontas CD正确地识别了Django自动生成秘密密钥的方式,但该答案是不正确的。

正确的答案是,除了一小部分约束条件外,您的Django SECRET_KEY可以是任意长度的任意strbytes

约束条件

长度、禁止前缀和唯一字符

以下检查(来自{{link1:django.core.checks.security.base}}模块)对长度、前缀和唯一字符定义了一小部分约束条件:

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)
    )

为了简单易懂地表达,你的SECRET_KEY必须满足以下要求:
  1. 长度不少于50个字符
  2. 至少包含5个独特字符
  3. 不能以“django-insecure-”为前缀

源编码

正如Django文档中SECRET_KEY所述,开发人员不应假设密钥是strbytes类型。因此,每个使用者都应该通过force_str()force_bytes()SECRET_KEY传递,具体取决于所需类型。
这暗示着密钥必须作为有效字符串提供,或者如果提供bytes,则这些字节必须解码为有效字符串。
关于什么构成有效字符串的问题,自从Python 3.0引入PEP3120以来,字符串的默认源编码为UTF-8。
在Django中进一步强制执行此规定。例如,django.utils.encoding.force_str() 明确将UTF-8设置为源编码。
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

如果出于某些原因您仍然停留在较旧版本的Python上,那么任何ASCII字符都可以使用,包括空格。
为什么没有最大长度?
在密码学中,我们通常使用固定长度的二进制密钥。我们从可变长度字符串(例如密码、SECRET_KEY等)转换为固定长度二进制密钥的方法是通过密钥派生函数。
实际上,这意味着我们在使用任何加密功能之前对SECRET_KEY进行“哈希”。
如何哈希密钥在很大程度上取决于所需的密钥长度(例如128位、256位、512位等)以及您想要通过密钥派生函数购买多少防止暴力攻击的安全性。这更像是密码哈希,事实上,常用的密码哈希算法也常用作密钥派生函数。
要查看Django中的示例,请参见{{link1: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)

在这里,你的SECRET_KEY(连同一个随机的salt)被拉伸或缩小为160位SHA1哈希值。
换句话说,无论你的SECRET_KEY有多长,在实际使用时它总是会被拉伸/缩小到正确的比特数。
一个非常长的密钥会浪费吗?
正如我们上面所看到的,Django默认将密钥散列成160位sha1哈希值来签署消息。
然而,标准的get_random_secret_key()生成一个50个字符长的密钥,并从一组50个字符中随机选择。这给它一个log2(50^50)的熵,大约是282位。
那么这些额外的比特只是被浪费了吗?嗯,是和不是。
从计算成本的角度来看,二进制编码和哈希在时间和内存成本上的差异,比如对于一个50个字符的字符串和一个500个字符的字符串,是可以忽略不计的。是的,如果你需要在一堆随机字符串上执行数千次,你会开始看到差异。然而,在Django性能方面,这将不会产生任何可测量的影响。
就浪费的熵(即使用282位熵的SECRET_KEY进行哈希到160位哈希),那么,如果它真正是随机的,超长密钥是浪费的。然而,我认为在生成密钥的伪随机方法中,生成一些额外的熵可能会增加一些额外的安全性。
总之:
字符/长度
Django SECRET_KEY 可以是任何len(str)>49的str(或解码为UTF-8 str的bytes),包含至少5个唯一字符,并且没有“django-insecure-”作为前缀。
# 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'

1
很奇怪,我一直在使用从urandom获取的32个随机字节作为密钥,但从未出现过关于非utf8或<50个字符的错误。 - Cristiano Coelho
1
关于解码错误的缺失,这并不让我感到惊讶——我无法想象为什么有人需要在秘密密钥上调用force_string()。但是,文档确实暗示了这种可能性。至于_check_secret_key(),经过仔细检查,发现它唯一的引用是在check_secret_key()check_secret_key_fallbacks()中,这两者只在测试套件中被引用。除此之外,看起来它们唯一被调用的时候是如果你使用--deploy标志运行check管理命令。 - ben
1
你说得对,如果密钥是二进制的话,运行 check --deploy 会出错。这是 Django 的一个可怕疏忽,因为文档中明确提到了密钥可以是二进制的。我真的希望他们能修补自己的秘密密钥处理方式 :( - Cristiano Coelho

5
Django在启动项目时会生成一个SECRET_KEY,因此,不能留空(链接1)

SECRET_KEY始终具有50个字符的长度。

没有空格。这是Django用于生成它的方法。

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)

是的,你可以将它存储在其他地方,比如使用env设置方法,例如:

当然,你也可以查看文档关于SECRET_KEY或者代码片段,例如:

除此之外,还有一种选择,就是创建一个改进SECRET_KEY文档的票务 - 如果你认为有必要的话。


2
我知道django在项目创建期间会自动生成SECRET_KEY,但是当然,我也可以手动编辑它。 - someone
3
我找不到任何关于SECRET_KEY始终为50个字符的参考资料。文档说它是一个字符串,没有额外的说明,所以我认为它可以是任意长度。 - texnic
1
@texnic 你有检查过Django源代码的链接吗?它是https://github.com/django/django/blob/master/django/core/management/utils.py#L76 - Jonatas CD
这个答案并不完全正确。虽然Django确实会通过这种方式生成初始的密钥,但这并不是对允许的SECRET_KEY设置的限制。请参考我的答案了解详情。 - ben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接