NixOS 密钥管理实战:sops-nix、agenix 与声明式秘钥分发

NixOS 密钥管理实战:为什么配置里不能写明文密钥、sops-nix(SOPS+age)工作流、agenix 声明式秘钥、多主机密钥分发、CI 与拉取密钥、密钥轮换与审计、配合 systemd 服务的秘钥注入。

引言

把数据库密码、API Key 写进 configuration.nix 是最大的安全隐患——配置会进 git、会分享、会在 history 里裸奔。NixOS 生态给出声明式密钥管理:密钥加密存储、部署时解密到内存/临时文件、可多主机分发、可轮换。本文以 sops-nix(现代主流)为主线,对比 agenix,讲清从加密到注入服务的完整链路。

前置:/nixos-configuration/(配置基础)、/nixos-services-containers/(服务与秘钥注入)、/nix-deployment-tools/(远程部署)。


目录


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-nixagenix
加密文件格式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 -e
声明密钥sops.secrets / age.secrets
解密位置/run/secrets 或 /run/agenix
注入服务serviceConfig.EnvironmentFile
多主机多个 recipient
CIGitHub Secrets 注入私钥
轮换sops -e -i 重新加密

一句话记忆:NixOS 密钥管理 = 用 age 公钥加密、私钥解密——sops-nix(现代默认)让加密文件安全进 git、声明在 sops.secrets 里、部署时解密到 /run/secrets(内存盘);服务用 config.age.secrets..path 引用、owner/group/mode 限权;多主机把各自公钥都加为 recipient、CI 里用平台 Secrets 注入私钥现场解密;定期轮换、明文不打日志、服务只取所需密钥。


延伸阅读

  • /nixos-services-containers/ — 服务部署与秘钥注入
  • /nix-deployment-tools/ — 远程部署与密钥同步
  • /nixos-network-firewall/ — 网络与安全
  • /nix-flakes-best-practices/ — Flake 工程化
  • [[linux]] — 系统安全加固
  • [[security]] — 安全工程实践
  • [[devops]] — 基础设施即代码
  • sops-nix 仓库
  • agenix 仓库

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nix」更多文章

  1. NixOS 运维实战:升级、回滚、GC 与日常维护
  2. NixOS 网络与防火墙配置:声明式网络管理实战
  3. NixOS 服务管理实战:systemd 声明、NixOS 容器与常用服务部署