引言
把数据库密码、API Key 写进 configuration.nix 是最大的安全隐患——配置会进 git、会分享、会在 history 里裸奔。NixOS 生态给出声明式密钥管理:密钥加密存储、部署时解密到内存/临时文件、可多主机分发、可轮换。本文以 sops-nix(现代主流)为主线,对比 agenix,讲清从加密到注入服务的完整链路。
前置:/nixos-configuration/(配置基础)、/nixos-services-containers/(服务与秘钥注入)、/nix-deployment-tools/(远程部署)。
目录
- 1. 为什么不能明文存密钥
- 2. age 密钥体系:声明式密钥的基础
- 3. sops-nix 工作流:加密文件进 git
- 4. 声明密钥与部署解密
- 5. 注入 systemd 服务
- 6. agenix:另一种选择
- 7. 多主机密钥分发
- 8. CI 与拉取密钥
- 9. 密钥轮换与审计
- 10. 速查表与一句话记忆
- 延伸阅读
1. 为什么不能明文存密钥
1.1 明文密钥的后果
| 风险 | 说明 |
|---|---|
| 进 git 历史 | 一次泄露永久存在,无法删除干净 |
| 权限难控 | 所有能读配置的人都能看到 |
| 审计失效 | 不知道谁用什么密钥 |
1.2 声明式密钥的目标
密钥 = 加密文件(可安全进 git)
解密 = 仅部署时按需进行(不落盘明文或落 /run 内存盘)
分发 = 多主机用各自公钥可解同一密文
记忆:声明式密钥管理让密钥以密文进 git、部署时按需解密、多主机用各自公钥解同一密文——明文永不落盘。
2. age 密钥体系:声明式密钥的基础
2.1 什么是 age
现代加密工具(替代 PGP,更简单)。一对密钥:公钥加密、私钥解密。
# 生成 age 密钥对
mkdir -p ~/.config/sops/age
nix run nixpkgs#age -- --generate-key -o ~/.config/sops/age/keys.txt
# 查看公钥
nix run nixpkgs#age-keygen -- -y ~/.config/sops/age/keys.txt
输出形如 age1xxxxxxxx...,公钥可写入 NixOS 配置。
2.2 密钥存储位置
私钥放不入 git 的路径(~/.config/sops/age/keys.txt);公钥放配置里公开。
记忆:age 是一对密钥:公钥加密、私钥解密;私钥不入 git,公钥公开写进配置;私钥文件是 NixOS 部署时的解密凭据。
3. sops-nix 工作流:加密文件进 git
3.1 加密一个文件
# 用 sops 加密 secrets.yaml,age 公钥为接收者
nix shell nixpkgs#sops -c sops --age <age1...> \
--encrypt secrets.yaml > secrets.enc.yaml
# 或先创建空白加密文件再编辑
nix shell nixpkgs#sops -c sops --age <age1...> -e -i secrets.yaml
3.2 加密文件的元信息
SOPS 在文件头记录:使用的密钥、各接收者的加密块。同一文件可对多个公钥加密(多主机分发的基础)。
# SOPS 头部
sops:
age:
- recipient: age1abc...
enc: '...'
记忆:sops 用 age 公钥加密文件,头部记录所有接收者;同一密文可对多个公钥加密 → 多主机可解。
4. 声明密钥与部署解密
4.1 安装 sops-nix
{ config, pkgs, inputs, ... }:
{
imports = [ inputs.sops-nix.nixosModules.sops ];
sops = {
defaultSopsFile = ./secrets.enc.yaml; # 加密文件
age.keyFile = "/home/user/.config/sops/age/keys.txt";
secrets = {
"db_password" = { }; # 声明一个密钥
"api_key" = { owner = "myapp"; };
};
};
}
4.2 解密结果
部署时 sops-nix 把密钥解密到 /run/secrets/(内存盘):
cat /run/secrets/db_password
记忆:sops-nix 在 sops.defaultSopsFile 指向加密文件、secrets 声明密钥名、age.keyFile 指向私钥;解密结果在 /run/secrets/(内存盘不落盘)。
5. 注入 systemd 服务
5.1 作为环境变量/文件给服务
systemd.services.myapp = {
# 方式一:环境变量(注意:会暴露在进程环境中)
serviceConfig.EnvironmentFile = [
config.age.secrets.myapp_env.path
];
# 方式二:读文件路径(推荐)
serviceConfig.ExecStart = ''
${pkgs.bash}/bin/bash -c \
'DB_PASS=$(cat ${config.age.secrets.db_password.path}) \
exec ${pkgs.myapp}/bin/myapp'
'';
};
5.2 权限控制
sops.secrets.db_password = {
owner = "myapp";
group = "myapp";
mode = "0440"; # 仅属主组可读
};
记忆:服务里用 config.age.secrets.
.path 引用 /run/secrets 解密路径;用 owner/group/mode 限制谁能读 。
6. agenix:另一种选择
6.1 agenix 用法
{ config, pkgs, ... }:
{
imports = [ "${inputs.agenix}/modules/age.nix" ];
age.secrets.db_password = {
file = ./db_password.age; # 用 agenix 加密的文件
owner = "myapp";
};
}
systemd.services.myapp = {
# agenix 解密到 /run/agenix/<name>
serviceConfig.ExecStart = "${pkgs.myapp}/bin/myapp --pw ${config.age.secrets.db_password.path}";
};
6.2 sops-nix vs agenix
| 维度 | sops-nix | agenix |
|---|---|---|
| 加密文件格式 | SOPS(支持 yaml/json) | 单值 age 加密 |
| 一个文件多密钥 | 支持(多个 age recipient) | 需生成多个文件 |
| 编辑体验 | sops 直接编辑加密文件 | 加密前明文编辑 |
| 生态热度 | 高(现代默认) | 成熟简单 |
记忆:agenix 更简单(单文件单密钥)、sops-nix 更强大(SOPS 格式、一个文件多接收者);现代项目默认 sops-nix。
7. 多主机密钥分发
7.1 同一密文、多主机可解
# 加密时把多台主机的公钥都作为 recipient
sops --age age1host1...,age1host2... -e -i secrets.yaml
7.2 每台主机用各自私钥解密
# 每台机器配置自己的私钥路径
sops.age.keyFile = "/var/lib/secrets/keys.txt"; # 各主机不同
记忆:多主机分发 = 加密时把每台主机的 age 公钥都加入 recipient,每台用各自私钥解密同一密文。
8. CI 与拉取密钥
8.1 CI 里解密(不把私钥放 CI)
# 用 GitHub Actions + sops + age
steps:
- uses: actions/checkout@v4
- uses: nix-community/setup-nix-action@v6
- name: Decrypt secrets
run: |
echo "$AGE_SECRET_KEY" > ~/.config/sops/age/keys.txt
sops -d secrets.enc.yaml > /tmp/secrets.yaml
env:
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }} # 存于 GitHub Secrets
8.2 私钥放哪安全
| 位置 | 建议 |
|---|---|
| GitHub/CI Secrets | 可接受(平台托管) |
| 构建机本地 | 更优(不入第三方) |
| git 仓库 | 禁止 |
记忆:CI 里把私钥作为平台 Secrets 注入环境、现场解密,私钥永不进 git 仓库。
9. 密钥轮换与审计
9.1 轮换密钥
# 重新加密(换公钥或轮换数据密钥)
sops --age <new-age-key> -e -i secrets.yaml
# 部署后旧私钥可作废
9.2 审计与纪律
| 纪律 | 做法 |
|---|---|
| 权限最小化 | 服务只拿自己需要的密钥 |
| 日志 | 明文密钥不打日志 |
| 定期轮换 | 关键密钥定期重新加密 |
| 目录检查 | 配置里搜明文密码(grep 检查) |
记忆:轮换 = sops 重新加密并替换接收者;纪律上服务只取所需密钥、明文不打日志、定期重加密、部署前 grep 检查明文。
10. 速查表与一句话记忆
| 环节 | 工具/做法 |
|---|---|
| 生成密钥 | age-keygen |
| 加密文件 | sops –age |
| 声明密钥 | sops.secrets / age.secrets |
| 解密位置 | /run/secrets 或 /run/agenix |
| 注入服务 | serviceConfig.EnvironmentFile |
| 多主机 | 多个 recipient |
| CI | GitHub Secrets 注入私钥 |
| 轮换 | sops -e -i 重新加密 |
一句话记忆:NixOS 密钥管理 = 用 age 公钥加密、私钥解密——sops-nix(现代默认)让加密文件安全进 git、声明在 sops.secrets 里、部署时解密到 /run/secrets(内存盘);服务用 config.age.secrets.
延伸阅读
- /nixos-services-containers/ — 服务部署与秘钥注入
- /nix-deployment-tools/ — 远程部署与密钥同步
- /nixos-network-firewall/ — 网络与安全
- /nix-flakes-best-practices/ — Flake 工程化
- [[linux]] — 系统安全加固
- [[security]] — 安全工程实践
- [[devops]] — 基础设施即代码
- sops-nix 仓库
- agenix 仓库
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。