From 39e8a220d80917be41c8b23f74589e58da31974c Mon Sep 17 00:00:00 2001 From: Origami404 Date: Sun, 17 May 2026 11:42:51 +0800 Subject: [PATCH] =?UTF-8?q?sops:=20=E5=A2=9E=E5=8A=A0=E5=A4=9A=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E7=AE=A1=E7=90=86=E4=B8=8B=E7=9A=84=E6=A0=87=E5=87=86?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/multi-secrets.md | 538 +++++++++++++++++++++++++++++++++ home/standalone/.zsh.d/nix.zsh | 50 ++- 2 files changed, 584 insertions(+), 4 deletions(-) create mode 100644 docs/multi-secrets.md diff --git a/docs/multi-secrets.md b/docs/multi-secrets.md new file mode 100644 index 0000000..98bd6d7 --- /dev/null +++ b/docs/multi-secrets.md @@ -0,0 +1,538 @@ +# 多机器秘密管理 + +> ChatGPT 回答摘录 + +> 用 sops, 给我一个适合于个人 homelab 的方案. 有一把用户级的密钥, 可以解密和加密任意秘密. 然后每个机器都有一个自己的密钥可以加密解密我在配置文件里想要它们加解密的内容. 我要知道如何为新机器生成密钥, 如何删除机器的密钥, 如何轮转特定机器的密钥, 如何为特定机器生成秘密. 我希望不同机器的秘密是分开多个 yaml 的. + +## 推荐模型 + +使用: + +```text +SOPS + age + sops-nix +``` + +权限模型: + +```text +你的用户 age key + ├─ 能解密 / 编辑所有 secret + └─ 只放在你的管理机 / 笔记本上 + +每台机器自己的 age key + ├─ 只放在该机器上 + ├─ 只被加入该机器对应的 YAML + └─ 只能解密自己的 host secrets +``` + +SOPS 支持 YAML/JSON/ENV/INI/BINARY,并且支持 `age`;`age` recipient 可以写进 `.sops.yaml` 的 `creation_rules`。SOPS 官方文档也明确说明,`creation_rules` 中的 `age` 字段是 age public key 列表。([SOPS: Secrets OPerationS][1]) + +--- + +## 目录结构 + +建议这样放: + +```text +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 +``` + +原则: + +```text +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 + +在你的管理机上: + +```bash +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 +``` + +输出类似: + +```text +age1adminxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +这个 public key 记为: + +```text +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][1]) + +--- + +## 2. 为新机器生成机器 key + +以 `nas` 为例,在 `nas` 机器上执行: + +```bash +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 +``` + +得到: + +```text +age1nasxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +这个 public key 记为: + +```text +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][2]) + +--- + +## 3. 写 `.sops.yaml` + +示例: + +```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 +``` + +注意: + +```text +creation_rules 是顺序匹配; +建议 path_regex 写精确; +不要用一个泛泛的 ^secrets/.*\.yaml$ 把所有 host 都打通。 +``` + +SOPS 文档里的例子也说明了 `creation_rules` 可以按 `path_regex` 匹配不同文件,并给不同文件配置不同的 age recipients。([SOPS: Secrets OPerationS][1]) + +--- + +## 4. 为特定机器生成 secret + +以 `nas` 为例: + +```bash +sops secrets/hosts/nas.yaml +``` + +编辑内容: + +```yaml +restic_password: "example-password" +minio_root_user: "minio" +minio_root_password: "example-password" +``` + +保存后,文件会变成加密形式。SOPS 会保留 YAML 结构,但加密具体值;这正是 SOPS 的主要设计之一。([SOPS: Secrets OPerationS][3]) + +检查谁能解: + +```bash +sops -d secrets/hosts/nas.yaml +``` + +你的 admin key 能解。`nas` 机器的 `/var/lib/sops-nix/key.txt` 也能解。其他机器不能解。 + +--- + +## 5. 在 NixOS 里接入 sops-nix + +`nixos/hosts/nas/secrets.nix`: + +```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` 里: + +```nix +{ + imports = [ + ./secrets.nix + ]; +} +``` + +服务引用 secret: + +```nix +{ + services.restic.backups.main = { + passwordFile = config.sops.secrets."restic_password".path; + }; +} +``` + +`sops-nix` 会在激活阶段解密 SOPS 文件,并把 secret 以单独文件形式放到 `/run/secrets/...`;权限、owner、group 可由 Nix 配置声明。([GitHub][2]) + +--- + +## 6. 新增一台机器 + +假设新增 `router`。 + +### 6.1 在新机器上生成 key + +```bash +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 +``` + +得到: + +```text +age1routerxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 6.2 修改 `.sops.yaml` + +```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 文件 + +```bash +sops secrets/hosts/router.yaml +``` + +### 6.4 写 Nix 配置 + +```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` + +删除: + +```yaml +- &nuc age1nucxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +以及所有引用: + +```yaml +- *nuc +``` + +### 7.2 更新所有受影响文件 + +例如: + +```bash +sops updatekeys -y secrets/hosts/nuc.yaml +sops updatekeys -y secrets/shared/wireguard.yaml +``` + +`updatekeys` 会根据 `.sops.yaml` 更新文件中的 recipients;官方文档说明它就是用 `.sops.yaml` 来添加或移除对应的 key。([sops.pages.dev][4]) + +### 7.3 删除 host 专属文件 + +如果这台机器永久下线: + +```bash +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: + +```bash +sops rotate -i secrets/shared/wireguard.yaml +``` + +SOPS 官方文档说明,`rotate` 会生成新的 data encryption key,并用当前 master keys 重新加密;文档也明确建议移除 key 时轮转 data key。([sops.pages.dev][4]) + +注意:这不能让对方“忘记”历史 Git 版本里已经能解出的旧 secret。真正撤销访问时,应该同时轮转 secret 本身,比如数据库密码、API token、WireGuard private key。 + +--- + +## 8. 轮转某台机器的 key + +假设轮转 `nas` 的机器 key。 + +### 8.1 在 nas 上备份旧 key + +```bash +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 + +```bash +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 +``` + +得到: + +```text +age1nasnewxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 8.3 修改 `.sops.yaml` + +把旧的: + +```yaml +- &nas age1nasoldxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +换成新的: + +```yaml +- &nas age1nasnewxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 8.4 更新该机器相关 secret + +```bash +sops updatekeys -y secrets/hosts/nas.yaml +sops rotate -i secrets/hosts/nas.yaml +``` + +如果 `nas` 也在某些 shared secret 里: + +```bash +sops updatekeys -y secrets/shared/wireguard.yaml +sops rotate -i secrets/shared/wireguard.yaml +``` + +### 8.5 在 nas 上替换 key + +确认 repo 里的文件已经更新后: + +```bash +sudo mv /var/lib/sops-nix/key.txt.new /var/lib/sops-nix/key.txt +sudo chmod 0400 /var/lib/sops-nix/key.txt +``` + +然后: + +```bash +sudo nixos-rebuild switch +``` + +### 8.6 成功后删除旧 key + +```bash +sudo shred -u /var/lib/sops-nix/key.txt.old +``` + +如果文件系统不保证 `shred` 有意义,例如 CoW 文件系统,直接删除即可,但不要保留旧 key 的备份。 + +--- + +## 9. 常用操作速查 + +### 编辑某台机器的 secret + +```bash +sops secrets/hosts/nas.yaml +``` + +### 查看明文,不落盘 + +```bash +sops -d secrets/hosts/nas.yaml +``` + +### 修改 `.sops.yaml` 后同步 recipients + +```bash +sops updatekeys -y secrets/hosts/nas.yaml +``` + +### 移除 key 后轮转 data key + +```bash +sops rotate -i secrets/hosts/nas.yaml +``` + +### 批量更新所有 host secrets + +```bash +find secrets/hosts -name '*.yaml' -print0 | + xargs -0 -n1 sops updatekeys -y +``` + +### 批量 rotate 所有 host secrets + +```bash +find secrets/hosts -name '*.yaml' -print0 | + xargs -0 -n1 sops rotate -i +``` + +--- + +## 10. 实际安全边界 + +这个方案能保证: + +```text +nas 不能解 pve.yaml +pve 不能解 nas.yaml +你的 admin key 能解所有 +Git 里只存加密文件 +每台机器只需要持有自己的 private key +``` + +这个方案不能保证: + +```text +删除某机器 key 后,它无法解历史 Git 版本 +机器被攻破后,攻击者无法读取该机器当前可访问的 secret +admin key 泄露后其他 secret 仍然安全 +``` + +所以额外建议: + +```text +admin age private key 备份到密码管理器或离线介质; +机器 key 不进 Git; +机器 key 不复用; +shared secrets 少用; +删除机器时同时轮转真实业务 secret; +不要给 CI 一个能解所有 secret 的 key,除非 CI 本身就是高信任环境。 +``` + +最终模型可以概括为: + +```text +每个 secret 文件 = admin key + 最小机器 key 集合 +每台机器 = 只能解自己声明需要的 YAML +所有权限变化 = 改 .sops.yaml + updatekeys +撤销访问 = updatekeys + rotate + 轮转真实业务凭证 +``` + +[1]: https://getsops.io/docs/ "SOPS: Secrets OPerationS | SOPS: Secrets OPerationS" +[2]: https://github.com/Mic92/sops-nix/blob/master/README.md "sops-nix/README.md at master · Mic92/sops-nix · GitHub" +[3]: https://getsops.io/?utm_source=chatgpt.com "SOPS: Secrets OPerationS" +[4]: https://sops.pages.dev/ "SOPS Documentation" diff --git a/home/standalone/.zsh.d/nix.zsh b/home/standalone/.zsh.d/nix.zsh index 1e42111..f0bffb1 100644 --- a/home/standalone/.zsh.d/nix.zsh +++ b/home/standalone/.zsh.d/nix.zsh @@ -1,5 +1,7 @@ #!/usr/bin/env zsh +export O4_FLAKES="$HOME/flakes" + if command -v direnv &> /dev/null; then eval "$(direnv hook zsh)" fi @@ -16,7 +18,7 @@ o4-home-switch() { home-manager switch --flake ~/flakes#$(whoami)@$(hostname) } -sops-update-file() { +o4-sops-update-file() { local src_file="$1" local yaml_file="$2" local age_key_file="$HOME/.config/sops/age/keys.txt" @@ -53,12 +55,12 @@ sops-update-file() { return $rc } -sops-update-ssh-config () {( set -e +o4-sops-update-ssh-config () {( set -e local SSH_CONFIG=$HOME/.ssh/config - local FLAKES=$HOME/flakes + local FLAKES=$O4_FLAKES $EDITOR $SSH_CONFIG - sops-update-file $SSH_CONFIG $FLAKES/secrets/ssh-config.yaml + o4-sops-update-file $SSH_CONFIG $FLAKES/secrets/ssh-config.yaml pushd $FLAKES if [[ -z "$(git status --porcelain)" ]]; then git add . @@ -66,3 +68,43 @@ sops-update-ssh-config () {( set -e fi popd )} + +O4_SOPS_MACHINE_KEY_DIR="/var/lib/sops-nix" +O4_SOPS_MACHINE_KEY_FILE="$O4_SOPS_MACHINE_KEY_DIR/key.txt" + +o4-sops-machine-key-init () { + # check sudo + if [[ $EUID -ne 0 ]]; then + echo "require root" >&2 + return 1 + fi + + # check folder and file + local key_dir="$O4_SOPS_MACHINE_KEY_DIR" + local key_file="$O4_SOPS_MACHINE_KEY_FILE" + mkdir -p $key_dir + if [[ -f "$key_file" ]]; then + echo "key exists: $key_file" >&2 + return 1 + fi + + # keygen + install -d -m 0700 -o root -g root $key_dir + age-keygen -o $key_file + chmod 0400 $key_file + age-keygen -y $key_file + + # print pub key + grep "^# public key: " $key_file | cut -d ' ' -f 4 +} + +o4-sops-machine-key-print-pubkey () { + local key_file="$O4_SOPS_MACHINE_KEY_FILE" + if [[ ! -f "$key_file" ]]; then + echo "key file not found: $key_file" >&2 + return 1 + fi + + grep "^# public key: " $key_file | cut -d ' ' -f 4 +} +