Files
flake/docs/multi-secrets.md
T

12 KiB
Raw Blame History

多机器秘密管理

ChatGPT 回答摘录

用 sops, 给我一个适合于个人 homelab 的方案. 有一把用户级的密钥, 可以解密和加密任意秘密. 然后每个机器都有一个自己的密钥可以加密解密我在配置文件里想要它们加解密的内容. 我要知道如何为新机器生成密钥, 如何删除机器的密钥, 如何轮转特定机器的密钥, 如何为特定机器生成秘密. 我希望不同机器的秘密是分开多个 yaml 的.

推荐模型

使用:

SOPS + age + sops-nix

权限模型:

你的用户 age key
  ├─ 能解密 / 编辑所有 secret
  └─ 只放在你的管理机 / 笔记本上

每台机器自己的 age key
  ├─ 只放在该机器上
  ├─ 只被加入该机器对应的 YAML
  └─ 只能解密自己的 host secrets

SOPS 支持 YAML/JSON/ENV/INI/BINARY,并且支持 ageage recipient 可以写进 .sops.yamlcreation_rules。SOPS 官方文档也明确说明,creation_rules 中的 age 字段是 age public key 列表。(SOPS: Secrets OPerationS)


目录结构

建议这样放:

infra/
  .sops.yaml

  secrets/
    hosts/
      nas.yaml
      pve.yaml
      nuc.yaml

    shared/
      wireguard.yaml
      tailscale.yaml

  nixos/
    hosts/
      nas/
        configuration.nix
        secrets.nix
      pve/
        configuration.nix
        secrets.nix
      nuc/
        configuration.nix
        secrets.nix

原则:

secrets/hosts/nas.yaml 只给 admin + nas 加密
secrets/hosts/pve.yaml 只给 admin + pve 加密
secrets/hosts/nuc.yaml 只给 admin + nuc 加密
secrets/shared/*.yaml 只在确实需要多机器共享时使用

不要把所有机器的 secret 塞进一个大 secrets.yaml,否则任意一台机器只要能解这个文件,就能看到别的机器的秘密。


1. 生成你的用户级 admin key

在你的管理机上:

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt

输出类似:

age1adminxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

这个 public key 记为:

ADMIN_AGE_PUB

SOPS 默认会在 Linux 的 $XDG_CONFIG_HOME/sops/age/keys.txt~/.config/sops/age/keys.txt 找 age private key;也可以用 SOPS_AGE_KEY_FILE 显式指定。(SOPS: Secrets OPerationS)


2. 为新机器生成机器 key

nas 为例,在 nas 机器上执行:

sudo install -d -m 0700 -o root -g root /var/lib/sops-nix
sudo age-keygen -o /var/lib/sops-nix/key.txt
sudo chmod 0400 /var/lib/sops-nix/key.txt
sudo age-keygen -y /var/lib/sops-nix/key.txt

得到:

age1nasxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

这个 public key 记为:

NAS_AGE_PUB

sops-nix 支持 sops.age.keyFile = "/var/lib/sops-nix/key.txt",也可以设置 sops.age.generateKey = true 自动生成 key;不过个人 homelab 我更建议手动生成一次,然后把 private key 持久化。sops-nix 文档里也提示,如果用 Impermanence,这个 key 必须放在持久化目录并且启动早期可用。(GitHub)


3. 写 .sops.yaml

示例:

keys:
  - &admin age1adminxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

  - &nas   age1nasxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - &pve   age1pvexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - &nuc   age1nucxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

creation_rules:
  - path_regex: ^secrets/hosts/nas\.yaml$
    age:
      - *admin
      - *nas

  - path_regex: ^secrets/hosts/pve\.yaml$
    age:
      - *admin
      - *pve

  - path_regex: ^secrets/hosts/nuc\.yaml$
    age:
      - *admin
      - *nuc

  - path_regex: ^secrets/shared/wireguard\.yaml$
    age:
      - *admin
      - *nas
      - *pve
      - *nuc

注意:

creation_rules 是顺序匹配;
建议 path_regex 写精确;
不要用一个泛泛的 ^secrets/.*\.yaml$ 把所有 host 都打通。

SOPS 文档里的例子也说明了 creation_rules 可以按 path_regex 匹配不同文件,并给不同文件配置不同的 age recipients。(SOPS: Secrets OPerationS)


4. 为特定机器生成 secret

nas 为例:

sops secrets/hosts/nas.yaml

编辑内容:

restic_password: "example-password"
minio_root_user: "minio"
minio_root_password: "example-password"

保存后,文件会变成加密形式。SOPS 会保留 YAML 结构,但加密具体值;这正是 SOPS 的主要设计之一。(SOPS: Secrets OPerationS)

检查谁能解:

sops -d secrets/hosts/nas.yaml

你的 admin key 能解。nas 机器的 /var/lib/sops-nix/key.txt 也能解。其他机器不能解。


5. 在 NixOS 里接入 sops-nix

nixos/hosts/nas/secrets.nix

{ config, ... }:

{
  sops.age.keyFile = "/var/lib/sops-nix/key.txt";

  sops.secrets."restic_password" = {
    sopsFile = ../../../secrets/hosts/nas.yaml;
    owner = "root";
    group = "root";
    mode = "0400";
  };

  sops.secrets."minio_root_user" = {
    sopsFile = ../../../secrets/hosts/nas.yaml;
    owner = "minio";
    group = "minio";
    mode = "0400";
  };

  sops.secrets."minio_root_password" = {
    sopsFile = ../../../secrets/hosts/nas.yaml;
    owner = "minio";
    group = "minio";
    mode = "0400";
  };
}

configuration.nix 里:

{
  imports = [
    ./secrets.nix
  ];
}

服务引用 secret

{
  services.restic.backups.main = {
    passwordFile = config.sops.secrets."restic_password".path;
  };
}

sops-nix 会在激活阶段解密 SOPS 文件,并把 secret 以单独文件形式放到 /run/secrets/...;权限、owner、group 可由 Nix 配置声明。(GitHub)


6. 新增一台机器

假设新增 router

6.1 在新机器上生成 key

sudo install -d -m 0700 -o root -g root /var/lib/sops-nix
sudo age-keygen -o /var/lib/sops-nix/key.txt
sudo chmod 0400 /var/lib/sops-nix/key.txt
sudo age-keygen -y /var/lib/sops-nix/key.txt

得到:

age1routerxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

6.2 修改 .sops.yaml

keys:
  - &admin age1adminxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

  - &nas    age1nasxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - &pve    age1pvexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - &nuc    age1nucxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  - &router age1routerxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

creation_rules:
  - path_regex: ^secrets/hosts/router\.yaml$
    age:
      - *admin
      - *router

6.3 创建该机器的 secret 文件

sops secrets/hosts/router.yaml

6.4 写 Nix 配置

{
  sops.age.keyFile = "/var/lib/sops-nix/key.txt";

  sops.secrets."some_secret" = {
    sopsFile = ../../../secrets/hosts/router.yaml;
    owner = "root";
    group = "root";
    mode = "0400";
  };
}

7. 删除一台机器的密钥

假设要删除 nuc

7.1 从 .sops.yaml 删除 nuc

删除:

- &nuc age1nucxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

以及所有引用:

- *nuc

7.2 更新所有受影响文件

例如:

sops updatekeys -y secrets/hosts/nuc.yaml
sops updatekeys -y secrets/shared/wireguard.yaml

updatekeys 会根据 .sops.yaml 更新文件中的 recipients;官方文档说明它就是用 .sops.yaml 来添加或移除对应的 key。(sops.pages.dev)

7.3 删除 host 专属文件

如果这台机器永久下线:

git rm secrets/hosts/nuc.yaml
git rm -r nixos/hosts/nuc

7.4 对共享 secret 做 rotate

如果 nuc 曾经能解某些共享文件,例如 secrets/shared/wireguard.yaml,删除 recipient 后建议 rotate data key

sops rotate -i secrets/shared/wireguard.yaml

SOPS 官方文档说明,rotate 会生成新的 data encryption key,并用当前 master keys 重新加密;文档也明确建议移除 key 时轮转 data key。(sops.pages.dev)

注意:这不能让对方“忘记”历史 Git 版本里已经能解出的旧 secret。真正撤销访问时,应该同时轮转 secret 本身,比如数据库密码、API token、WireGuard private key。


8. 轮转某台机器的 key

假设轮转 nas 的机器 key。

8.1 在 nas 上备份旧 key

sudo cp /var/lib/sops-nix/key.txt /var/lib/sops-nix/key.txt.old
sudo chmod 0400 /var/lib/sops-nix/key.txt.old

8.2 生成新 key

sudo age-keygen -o /var/lib/sops-nix/key.txt.new
sudo chmod 0400 /var/lib/sops-nix/key.txt.new
sudo age-keygen -y /var/lib/sops-nix/key.txt.new

得到:

age1nasnewxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

8.3 修改 .sops.yaml

把旧的:

- &nas age1nasoldxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

换成新的:

- &nas age1nasnewxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

8.4 更新该机器相关 secret

sops updatekeys -y secrets/hosts/nas.yaml
sops rotate -i secrets/hosts/nas.yaml

如果 nas 也在某些 shared secret 里:

sops updatekeys -y secrets/shared/wireguard.yaml
sops rotate -i secrets/shared/wireguard.yaml

8.5 在 nas 上替换 key

确认 repo 里的文件已经更新后:

sudo mv /var/lib/sops-nix/key.txt.new /var/lib/sops-nix/key.txt
sudo chmod 0400 /var/lib/sops-nix/key.txt

然后:

sudo nixos-rebuild switch

8.6 成功后删除旧 key

sudo shred -u /var/lib/sops-nix/key.txt.old

如果文件系统不保证 shred 有意义,例如 CoW 文件系统,直接删除即可,但不要保留旧 key 的备份。


9. 常用操作速查

编辑某台机器的 secret

sops secrets/hosts/nas.yaml

查看明文,不落盘

sops -d secrets/hosts/nas.yaml

修改 .sops.yaml 后同步 recipients

sops updatekeys -y secrets/hosts/nas.yaml

移除 key 后轮转 data key

sops rotate -i secrets/hosts/nas.yaml

批量更新所有 host secrets

find secrets/hosts -name '*.yaml' -print0 |
  xargs -0 -n1 sops updatekeys -y

批量 rotate 所有 host secrets

find secrets/hosts -name '*.yaml' -print0 |
  xargs -0 -n1 sops rotate -i

10. 实际安全边界

这个方案能保证:

nas 不能解 pve.yaml
pve 不能解 nas.yaml
你的 admin key 能解所有
Git 里只存加密文件
每台机器只需要持有自己的 private key

这个方案不能保证:

删除某机器 key 后,它无法解历史 Git 版本
机器被攻破后,攻击者无法读取该机器当前可访问的 secret
admin key 泄露后其他 secret 仍然安全

所以额外建议:

admin age private key 备份到密码管理器或离线介质;
机器 key 不进 Git
机器 key 不复用;
shared secrets 少用;
删除机器时同时轮转真实业务 secret;
不要给 CI 一个能解所有 secret 的 key,除非 CI 本身就是高信任环境。

最终模型可以概括为:

每个 secret 文件 = admin key + 最小机器 key 集合
每台机器 = 只能解自己声明需要的 YAML
所有权限变化 = 改 .sops.yaml + updatekeys
撤销访问 = updatekeys + rotate + 轮转真实业务凭证