缘起#
在上一篇中,我们介绍了用Home Manager管理个人配置的方法。普通设置可以写进Nix文件,但密码、API令牌和私钥需要另外处理。即使配置仓库是私有的,也不适合把这些内容直接写进配置。
原因是,Nix在构建时会将很多配置和源文件放进store。配置能够重复构建,并不意味着构建结果中的内容只有自己能看到。因此,这里介绍配置与机密的区别,以及使用sops-nix管理机密的方法。
下面使用虚构主机laptop和一个只检查文件能否读取的服务。示例中的密码没有实际用途,不使用现有服务的配置或真实密钥。
Nix store与机密#
Nix store中的文件通常对本机其他用户可读。把密码放进environment.etc.*.text、Home Manager的home.file.*.text,或者构建脚本生成的文件中,都可能让密码出现在store里。不能靠最后安装到目标位置时设置0600,来消除前面已经产生的副本。
另一个容易忽略的地方是flake源文件。本地Git flake会读取被Git跟踪的文件,所以明文即使没有被模块引用,也不应该放进作为flake输入的仓库。可以参考Nix的flake输入说明。
因此,需要区分下面几种内容:
| 内容 | 保存位置 |
|---|---|
| 服务名称、配置结构、机密文件路径 | Nix配置 |
| 加密后的机密文件 | 配置仓库,是否公开还需检查元数据 |
| 解密用的私钥 | 仓库和store以外的受保护位置 |
| 服务使用的明文 | 运行时文件或服务的凭据目录 |
这里的“路径”只是告诉程序去哪里读取文件,和把文件内容读进Nix表达式不同。对运行时机密使用builtins.readFile,会把明文带入求值过程,也会破坏构建机器不需要解密密钥的安排。
运行时文件仍然有访问权限问题。root、获得服务权限的程序和有权限的备份工具,可能读取其中的内容。加密保存解决的是仓库和构建阶段的暴露问题,不能替代系统账户、服务权限和日志管理。
sops-nix简介#
这里使用三个工具:
- age:生成用于加密和解密的密钥;
- SOPS:编辑加密的YAML等文件;
- sops-nix:在NixOS中声明机密的来源、目标位置和访问权限,并在目标系统运行时解密。
SOPS可以使用不同的加密方式,这里只介绍age。age的公钥也称为recipient,可以用来加密;对应私钥用于解密,不应加入Git。相关用法可以参看age文档。
下面采用NixOS模块方式,与上一篇的Home Manager模块不是同一个集成层。示例沿用NixOS 26.05,并使用支持useSystemdActivation的sops-nix版本;具体依赖由自己的flake.lock记录。
配置sops-nix#
添加flake输入#
在已有flake.nix中加入:
| |
让outputs接收sops-nix输入,再在已有主机的模块列表里加入对应模块。例如,下面是输出属性集中的一个片段:
| |
原有模块要保留,不需要为了这个例子重写整个flake。输入使用的分支会继续更新,而本地锁文件记录具体版本;首次添加后要检查flake.lock,以后按计划更新。
创建演示密钥#
下面假设编辑配置和运行演示服务的是同一台电脑。在配置仓库根目录打开一个带工具的Shell:
| |
在这个Shell中创建一份专门用于演示的密钥:
| |
最后一条只输出公钥。私钥保存在仓库之外;如果这个文件已经存在,先检查用途,不要覆盖它。SOPS_AGE_KEY_FILE让后续SOPS命令找到这份演示私钥,换一个Shell后需要重新设置。
实际管理多台电脑时,通常应分别使用管理者的编辑密钥、各台电脑的解密密钥,以及受保护的恢复密钥。这里共用一份演示密钥,是为了先把本机流程说明白,不建议把同一份私钥复制到所有电脑。
创建加密文件#
在仓库根目录创建.sops.yaml:
| |
把<AGE_RECIPIENT>替换成刚才输出的公钥。这是需要替换的占位符,不能直接用于加密。然后创建机密目录并打开编辑器:
| |
在SOPS打开的编辑器里写入:
| |
保存并退出后,磁盘上的secrets/demo.yaml应是加密文件,而不是上面的明文。再次修改时继续用sops edit。不要先把真实密码写入普通YAML,再指望提交前一定能记得加密;编辑器的交换文件、备份和临时文件也需要考虑。
SOPS通常保留YAML键名和加密元数据,所以“加密文件”不等于“可以随意公开的文件”。服务名称、收件人列表和备注仍可能透露配置结构。关于文件编辑和配置规则,可以参看SOPS使用文档。
提供目标系统的私钥#
在这台演示电脑上,把演示私钥安装到root管理的运行位置:
| |
这两条命令用于首次准备演示环境。已有key.txt时不要覆盖,它可能正被其他配置使用。真实的远程电脑需要通过可信的方式准备它自己的私钥,构建结果不会自动包含这份私钥。
/var/lib/sops-nix/key.txt需要持久保存。只保存配置仓库,重装后并不能凭空恢复解密能力。
声明机密与演示服务#
创建modules/secrets-demo.nix:
| |
defaultSopsFile相对于这个模块的位置,指向仓库中的加密文件。机密名称demo/password对应YAML中的嵌套键,默认目标是/run/secrets/demo/password。
这里关闭SSH主机密钥的自动导入,只使用准备好的age私钥。keyFile写成字符串,表示运行时路径,不是把私钥作为Nix源文件复制进去。
例子明确启用了useSystemdActivation,所以服务依赖的是sops-install-secrets.service。旧配置如果使用激活脚本解密,不能直接照搬这个服务依赖;Home Manager的用户服务也有另外的启动方式。相关选项可以参看sops-nix模块源码。
演示服务使用systemd的LoadCredential。systemd读取root拥有的机密文件,并向服务提供自己的凭据目录;服务通过CREDENTIALS_DIRECTORY读取内容,因此不用为了这个检查把原文件开放给普通用户。它只检查非空并尝试读取一个字节,不输出密码。凭据传递方法可以参看NixOS的systemd说明。
实际服务应优先使用它支持的passwordFile、credentialsFile等选项,或对应的凭据接口。不要再用Nix读取运行时文件,拼回一个store中的配置。服务需要完整配置文件时,sops-nix也提供运行时模板,可以在需要时单独了解。
构建与验证#
将配置与加密文件加入Git索引,再构建:
| |
提交前检查暂存的YAML确实是SOPS加密格式,并检查新产生的flake.lock,将它加入索引一起记录。不要把私钥加入索引。构建成功只说明能生成系统,目标机的私钥是否正确、服务是否能读取文件,还要在运行时验证。
按第五篇的流程确认准备应用后,运行:
| |
第二条让演示服务执行检查;如果重建时已经运行过,会再执行一次。正常情况下可以看到Result=success、ExecMainStatus=0,以及原机密文件的400 root root权限。它是一次性服务,成功退出后显示inactive并不表示失败。
这里不需要cat机密内容。真正的服务还应验证实际功能,例如使用凭据完成一次授权请求,同时检查日志是否意外记录了密码。
问题解决#
解密失败#
先区分加密文件不存在、YAML键名不匹配和私钥不能解密这几种情况。新文件是否加入Git、模块路径是否正确,可以在构建前检查;私钥和recipient是否匹配,要在目标电脑检查。
这个例子的解密服务日志可以这样查看:
| |
恢复或复制日志时,先查看内容再决定是否分享。不要为了排错把私钥、解密后的YAML或完整环境变量输出到聊天中。
服务没有使用新密码#
机密文件更新不代表已经运行的程序重新读取了它。systemd提供的凭据也在服务启动时加载。根据实际服务的使用方式,可能需要重启、重载,或者更新服务端保存的密码。
sops-nix的机密选项可以声明restartUnits或reloadUnits,但应先确认相应服务支持哪种方式。对数据库等服务,外部密码文件变化也不一定会自动修改数据库中的账户密码。
用户密码与提前解密#
NixOS用户账户的密码可能要在创建用户前提供,和普通服务机密的顺序不同。sops-nix的neededForUsers用于这种提前解密场景;配合hashedPasswordFile时,文件内容应是密码哈希。提前解密时还不能依赖尚未创建的用户来设置所有者,不要直接照搬上面的普通服务例子。
密钥更新与恢复#
添加一台电脑时,可以将它的公钥加入对应规则,使用仍有解密权限的管理者密钥运行:
| |
这会根据.sops.yaml更新现有文件的收件人。只修改规则不会自动更新之前加密的文件,提交并部署前还需要用新增的解密身份检查结果。
SOPS还可以更换文件的数据加密密钥:
| |
updatekeys与rotate处理的是加密文件,和在服务端更换API令牌或密码是不同操作。相关区别可以参看SOPS密钥管理文档。如果密钥或机密已经泄露,移除一个recipient不能收回旧Git版本和旧密文,更不能让对方忘掉已经知道的密码。需要撤销实际服务端的旧机密。保存新的密码或令牌之前,先移除泄露的recipient,执行updatekeys和rotate,再录入新值并更新使用它的服务。
恢复方面,应另外保存受保护的恢复身份,并确认它能解密必要的文件。只备份密文、却丢失所有私钥,通常无法恢复内容;只有机器上的一份私钥,也容易在磁盘损坏或重装时一起丢失。可以先在独立的临时环境里用无实际用途的机密练习恢复,不必等故障发生后才检查。
例如,在只提供恢复身份的独立环境中,可以检查演示文件是否能解密,丢弃输出以避免把内容留在终端:
| |
这里需要替换恢复私钥路径,并提前将它对应的公钥加入规则、执行updatekeys。确认退出状态为零,并检查测试环境没有其他可用解密身份,避免实际上使用了日常密钥。
系统回滚还可能重新部署旧密文,但已经撤销的令牌不会因此重新有效。恢复系统配置、恢复解密能力和恢复服务账户,需要分别安排。
推荐的使用方法#
我建议先用演示机密确认加密、构建、解密和读取的完整流程,再迁移一个实际服务。每个服务只取得自己需要的机密,多台电脑分别配置解密身份,恢复密钥另外保存。
让AI协助时,可以提供选项结构、占位符和已经检查过的错误信息,要求它说明明文最终出现在哪里,以及谁能读取。录入真实机密和操作私钥时,尽量在自己控制的终端或工具中完成,分享前检查文件、截图和日志。
下一篇将介绍系统状态、备份和恢复,包括哪些内容不能靠重新构建NixOS恢复,以及怎样检查备份确实能用。
