配置中不应包含秘密

Lobsters Hottest 新闻

摘要

本文认为秘密不应存储在配置文件中,通过对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 与密钥管理

Lobsters Hottest

教程介绍 NixOS 的密钥管理选项,比较 sops-nix、agenix 和 ragenix 工具,并提供使用 sops-nix 进行加密密钥管理的实际示例。

我的Nix配置很私密

Lobsters Hottest

作者分享了对自己全面的Nix配置的自豪,该配置能高效管理所有机器,同时解释了保持其私密性的个人和安全原因。

部分密钥管理应交给 HTTP 代理

Hacker News Top

博客文章建议将 API 密钥注入工作卸载到内部 HTTP 代理,使应用和代理程序永远接触不到密钥,从而简化轮换并降低外泄风险。