sops: 增加多机器管理下的标准密钥流程

This commit is contained in:
2026-05-17 11:42:51 +08:00
parent 6a8c7f3fd0
commit 39e8a220d8
2 changed files with 584 additions and 4 deletions
+538
View File
@@ -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"
+46 -4
View File
@@ -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
}