配置中不应包含秘密
摘要
本文认为秘密不应存储在配置文件中,通过对NixOS模块的审计显示,四分之一的模块需要变通方法才能将秘密与配置分离。它提倡为秘密使用专门的运行时通道。
<p><a href="https://lobste.rs/s/iypcjj/secrets_don_t_belong_config">评论</a></p>
查看缓存全文
缓存时间: 2026/07/21 01:31
# 秘密不属于配置文件
来源:https://secretspec.dev/blog/secrets-dont-belong-in-config/
应用程序不应在配置文件中要求提供密码、API 密钥或令牌。
配置描述行为,它属于 Git、代码审查、错误报告和开发者的电脑。
秘密赋予权限,它需要受限访问和独立轮换。
将两者放在同一个文件中,会耦合不同的生命周期和受众。如果轮换密码需要重新生成应用程序配置,那说明接口耦合过于紧密。
## NixOS 为此提供了 110 个变通方案
标题:“NixOS 包含了 110 个变通方案”(https://secretspec.dev/blog/secrets-dont-belong-in-config/#nixos-contains-110-workarounds-for-this)
我们审核了 nixpkgs 中 commit `141f212` 下所有处理真实秘密(https://github.com/NixOS/nixpkgs/issues/24288#issuecomment-5024009774)的 445 个 NixOS 模块,按秘密值的最终去向进行了分类。
| 秘密值最终去向 | 模块数 | 占比 |
| --- | --- | --- |
| 运行时合并到配置文件中 | 110 | 25% |
| 直接内联到 `/nix/store` 中的配置 | 42 | 9% |
| 作为环境变量传递 | 161 | 36% |
| 保留在由应用程序打开专用文件中 | 58 | 13% |
| 通过 systemd 凭据加载 | 53 | 12% |
| 作为命令行参数传递 | 19 | 4% |
| 分类不确定 | 2 | — |
最引人注目的数字是 110。四分之一的模块安全地获取秘密,然后将其复制到配置中,因为这是应用程序接受的唯一接口。
这些模块使用 `envsubst`、`replace-secret`、`jq`、`yq`、`sed` 或自定义代码,在启动时组装一个受限文件。结果可能是安全的,但每个模块现在都拥有了特定于应用程序的、与安全相关的胶水代码,仅仅是为了组合本应保持分离的两个输入。
这种情况并非 NixOS 独有。在其他平台上,同样的变通方案表现为入口点脚本、Helm 模板、初始化容器或 CI 插值步骤。
顺便提一下,42 个模块可以将秘密内联到世界可读的 `/nix/store` 中。这个直接的安全问题在 nixpkgs 问题 #24288(https://github.com/NixOS/nixpkgs/issues/24288)中有所跟踪。而那 110 个运行时合并器则指出了一个更广泛的论点:即使部署作者避免了泄漏,缺少分离仍然会产生工作量。
## 为秘密提供自己的接口
标题:“为秘密提供自己的接口”(https://secretspec.dev/blog/secrets-dont-belong-in-config/#give-secrets-their-own-interface)
应用程序应通过专用的运行时通道接受秘密值,例如:
- `password_file` 或 `token_file` 设置;
- 系统凭据(systemd credential);
- 范围受限的环境变量;
- 或外部秘密提供者。
这些机制的安全性不尽相同:环境变量可能被继承,参数可能出现在进程列表中,文件仍需正确的权限设置。但分离能确保的是,部署者不再需要制造另一个包含秘密的配置版本。
原则很简单,但在不同环境中实现却并不容易。本地开发可能使用系统密钥环,CI 使用环境变量,生产环境使用 1Password 或 Vault。如果没有共享的抽象层,每个环境都需要自己的命名、查找、验证和注入胶水代码。
## 我在 Cachix 中犯的错误
标题:“我在 Cachix 中犯的错误”(https://secretspec.dev/blog/secrets-dont-belong-in-config/#how-i-got-it-wrong-in-cachix)
Cachix 历史上将其认证令牌和每个缓存的签名密钥存储在 `~/.config/cachix/cachix.dhall` 中,与缓存名称和其他配置放在一起。虽然方便,但该文件必须被视为秘密,即使其中大部分是普通配置。
典型的文件直接将它们混合在一起:
```
{ authToken = "XXX-AUTH-TOKEN", binaryCaches = [ { name = "mycache" , secretKey = "XXX-SIGNING-KEY" } ]}
```
缓存名称是配置;认证令牌和签名密钥是秘密。你不能在不共享凭证的情况下共享缓存配置。
devenv 2.2 通过 SecretSpec(https://devenv.sh/binary-caching/#setup-with-secretspec-recommended)分离了令牌。项目声明 `CACHIX_AUTH_TOKEN`,devenv 从配置的提供者处解析该值,并将值传递给 Cachix,而不会将其添加到 devenv 的配置中。
Cachix PR #737(https://github.com/cachix/cachix/pull/737)通过 SecretSpec Haskell SDK 将同样的边界引入客户端。它从 SecretSpec 解析 `CACHIX_AUTH_TOKEN` 和 `CACHIX_SIGNING_KEY`,并将其存储在用户选择的提供者中,而不是 `cachix.dhall` 中。现有的环境变量和配置文件作为兼容性考虑,仍然保持更高的优先级作为回退。该 PR 仍未合并,因此尚未在发布的 Cachix 版本中可用。
这就是 SecretSpec 旨在解决的问题:配置声明需求,而每个环境选择值的位置。
## 声明一次,随处解析
标题:“声明一次,随处解析”(https://secretspec.dev/blog/secrets-dont-belong-in-config/#declare-once-resolve-anywhere)
SecretSpec 通过让 `secretspec.toml` 成为应用程序所需的声明(不存储实际值)来应用这种分离:
```
[project]
name = "myapp"
[profiles.production]
DATABASE_URL = { description = "Postgres connection string" }
STRIPE_API_KEY = { description = "Stripe secret key" }
```
提供者(https://secretspec.dev/concepts/providers/)决定值所在的位置。开发者可以使用系统密钥环,CI 可以使用环境变量,生产环境可以使用 1Password、Vault/OpenBao 或云密钥管理服务,而无需更改声明。
现有应用程序可以在启动时接收解析后的值:
```
secretspec run -- ./myapp
```
应用程序也可以通过 SecretSpec SDK(https://secretspec.dev/sdk/overview/)直接解析这些值,SDK 支持 Rust、Python、Go、Ruby、Node.js/TypeScript、Haskell、PHP 和 C#,所有 SDK 共享同一个解析器,以确保跨语言行为一致。
提供者负责秘密值的来源。SDK 为应用程序提供了一种惯用的消费方式。配置则保持为可共享的需求声明。
## 让边界变得实用
标题:“让边界变得实用”(https://secretspec.dev/blog/secrets-dont-belong-in-config/#making-the-boundary-practical)
如果你维护一个应用程序,请停止将密码和令牌添加到普通的配置模式中。改为接受文件引用、凭据、环境变量或提供者。
对于 NixOS,SecretSpec 问题 #65(https://github.com/cachix/secretspec/issues/65)跟踪了如何通过官方集成来声明和解析秘密,而无需每个模块的替换胶水代码。
在开发者机器、CI 和生产环境之间实现一致的秘密处理,过去需要只有专门的平台团队才能构建的基础设施。任何规模的项目都应该能够在不先构建自己的秘密平台的前提下,将秘密与配置分离。
相似文章
NixOS 与密钥管理
教程介绍 NixOS 的密钥管理选项,比较 sops-nix、agenix 和 ragenix 工具,并提供使用 sops-nix 进行加密密钥管理的实际示例。
使用sops-nix在NixOS上管理机密
使用sops-nix在NixOS配置中管理机密的指南,涵盖设置、加密以及与Samba等服务的集成。
我的Nix配置很私密
作者分享了对自己全面的Nix配置的自豪,该配置能高效管理所有机器,同时解释了保持其私密性的个人和安全原因。
部分密钥管理应交给 HTTP 代理
博客文章建议将 API 密钥注入工作卸载到内部 HTTP 代理,使应用和代理程序永远接触不到密钥,从而简化轮换并降低外泄风险。
赋予编程代理 shell 访问权限感觉太疯狂了。大家是如何处理机密的?
作者对授予编程代理 shell 访问权限表示担忧,指出它们可以读取 .env、凭据等敏感文件,并希望在让代理接触真实仓库之前,向社区请教实用的机密处理模式。