本文以从独立 age key 迁移到 SSH host key 的通用流程说明:给 secrets 换解密密钥、迁移机器或新增解密身份。示例路径与接收者均为占位,不代表实际系统。
一、为什么切换#
原方案用独立 age key(/persist/secrets/age-key.txt)作为 sops 解密私钥:
secrets.yaml(git 内,密文)
│ 解密
▼
/persist/secrets/age-key.txt ← 独立管理的私钥,要手动备份/迁移问题:
- 多一把要保管的密钥
- 迁移机器时容易忘记带,导致 secrets 解不开
- 若没有其他有效接收者或备份,私钥丢失可能无法恢复密文
新方案复用系统已有的 SSH host key:
secrets.yaml(git 内,密文)
│ 解密
▼
/etc/ssh/ssh_host_ed25519_key ← 由系统生成或恢复;迁移需明确备份与恢复收益是少维护一把独立密钥,但 host key 依然需要备份与恢复;重装或重新生成会改变机器身份,不能认为迁移问题自然消失。
代价 SSH host key 泄露的影响范围从「仅 secrets」扩大到「SSH + secrets」。是否接受取决于威胁模型、备份和权限管理;独立解密 key 可将两类能力分开。
二、核心概念:双 recipient#
sops 支持一个文件登记多个 recipient(多个解密者)。这是迁移的关键技巧——先加新 key,保留旧 key,验证通过后再移除旧 key,全程有兜底:
sops:
age:
- enc: <AES 密钥用旧 key 加密的块>
recipient: AGE_RECIPIENT_PLACEHOLDER_NOT_VALID # 旧独立 age key(保留兜底)
- enc: <同一把 AES 密钥用新 key 加密的块>
recipient: AGE_RECIPIENT_PLACEHOLDER_NOT_VALID # 新 SSH host key每把私钥只能解开"属于自己那一份"的加密块,但拿到的是同一把 AES 数据密钥。
三、前置准备#
- 工具与权限:通过项目的声明式工具配置准备
sops、age与ssh-to-age,确认版本及管理员权限边界。 - 恢复材料:备份旧密钥和密文;确认新 host key 公钥属于目标机器,私钥不需要传出机器。一次性 Nix 工具环境示例为
nix-shell -p ssh-to-age age sops。 - 拿到新 key 的 age 公钥:
ssh-to-age -i /etc/ssh/ssh_host_ed25519_key.pub
# → AGE_RECIPIENT_PLACEHOLDER_NOT_VALID(本例 SSH host key 的 age 公钥)- 确认旧 key 能解密(迁移前基线):
SOPS_AGE_KEY_FILE=/persist/secrets/age-key.txt sops decrypt host/secrets/secrets.yaml > /dev/null权限与验证 私钥应仅对授权读取者开放。若需 root,在已有授权的受控管理员 shell 内执行,不依赖无密码工具包装规避权限设计。基线解密检查退出码,不打印秘密;记录工具版本和恢复位置。
四、迁移步骤#
第 1 步:更新 .sops.yaml(声明新 recipient)
.sops.yaml 是 sops 的 creation rules,用 YAML anchor 定义 key:
keys:
- &admin-key AGE_RECIPIENT_PLACEHOLDER_NOT_VALID # 旧独立 age key
- &server-key AGE_RECIPIENT_PLACEHOLDER_NOT_VALID # 新 SSH host key
creation_rules:
- path_regex: host/secrets/secrets\.yaml$
key_groups:
- age:
- *admin-key
- *server-key第 2 步:sops updatekeys 同步 recipient 到 secrets.yaml
cd /path/to/config-repo # 替换为目标配置仓库
SOPS_AGE_KEY_FILE=/persist/secrets/age-key.txt \
sops updatekeys host/secrets/secrets.yaml输出会显示 +++ AGE_RECIPIENT_PLACEHOLDER_NOT_VALID(新增)和保留的旧 key。updatekeys 更新数据密钥的接收者封装,不是数据密钥轮换,需要旧 key 权限。
第 3 步:改 host/sops.nix 切换解密来源
# 改之前
sops = {
age.keyFile = "/persist/secrets/age-key.txt";
};
# 过渡后(原 keyFile 仍可保留到验证完成)
sops = {
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
};双保险 此时 secrets.yaml 仍是双 recipient(旧 + 新),即使新方案有问题,旧 key 还能解。等 rebuild 验证通过后再考虑清理旧 key。
第 4 步:验证新 key 能解密
# 在目标机器已有授权的管理员 shell 内;工具已准备
umask 077
task_key_dir=$(mktemp -d /run/sops-migrate.XXXXXXXX)
trap 'rm -rf -- "$task_key_dir"' EXIT
ssh-to-age -private-key -i /etc/ssh/ssh_host_ed25519_key > "$task_key_dir/new-identity.txt" &&
SOPS_AGE_KEY_FILE="$task_key_dir/new-identity.txt" \
sops decrypt host/secrets/secrets.yaml > /dev/null解密退出码成功只证明 CLI 可读密文;要证明新 key,需使用只包含新身份的受控环境,禁用额外默认 key/KMS 等来源。之后仍需验证 sops-nix 的实际部署链路。
第 5 步:分层验证与经授权应用
# 在已准备好恢复方案的配置仓库内,用实际 flake target 替换 workstation
nix flake check --no-build
nixos-rebuild dry-build --flake .#workstation
# 经明确授权后,由管理员应用;这会修改运行系统
sudo nixos-rebuild switch --flake .#workstation五、rebuild 后验证#
systemctl status sops-install-secrets.service # 仅适用于 systemd 解密路径
systemctl status example-app.service # 检查消费方,不显示 secret 内容
ls -la /run/secrets/app_env # 仅证明产物存在;结合解密日志和应用行为检查六、回滚与清理#
- 回滚:
sops.nix改回age.keyFile,rebuild。旧 key 一直在/persist/secrets/age-key.txt,且 secrets.yaml 双 recipient 保留,回退前还需确认旧身份仍有效、旧密文存在、配置版本和系统恢复路径可用。 - 清理:确认新链路稳定后:
.sops.yaml移除旧 anchor +sops updatekeys删除旧 recipient;按需sops rotate --in-place轮换数据密钥- 删除
/persist/secrets/age-key.txt - 更新项目文档、恢复记录和密钥保管记录
清理不可逆 移除 recipient 不能撤销旧 key 对 git 历史、备份或先前获取数据密钥的访问。先验证新身份和运行链路,按政策轮换数据密钥;若旧身份不再可信,还需轮换秘密值及相关凭据。删除旧私钥前确认历史恢复需求,不能以删除文件代替真正的撤销。
七、踩过的坑(经验教训)#
| 坑 | 现象 | 解法 |
|---|---|---|
| 管理员权限 | 系统私钥无法由普通用户读取 | 通过明确授权的管理员环境操作,或使用专用编辑身份 |
| 权限不足 | SOPS_AGE_KEY_FILE 读不到 |
key 文件 root 所有,需 root 执行 |
| path_regex 不匹配 | no matching creation rules found |
核对当前目录/--config、配置相对路径与 filename override |
| 临时明文残留 | 解密中间文件落盘 | 随机私有目录、umask 077、退出清理;不输出秘密,也不留编辑器备份 |
相关#
- NixOS 更新机制笔记
- 配置仓库:
/path/to/config-repo/host/sops.nix、.sops.yaml、host/secrets/secrets.yaml
参考与延伸
根据技术笔记重新整理,示例使用通用名称。