OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 102

sops 密钥迁移与轮换

以双接收者过渡完成密钥迁移,并区分授权更新与凭据撤销。

所在专题SOPS 加密配置与密钥生命周期 ↗
约 11 分钟阅读开始阅读

本文以从独立 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 数据密钥。


三、前置准备#

  1. 工具与权限:通过项目的声明式工具配置准备 sops、age 与 ssh-to-age,确认版本及管理员权限边界。
  2. 恢复材料:备份旧密钥和密文;确认新 host key 公钥属于目标机器,私钥不需要传出机器。一次性 Nix 工具环境示例为 nix-shell -p ssh-to-age age sops。
  3. 拿到新 key 的 age 公钥:
ssh-to-age -i /etc/ssh/ssh_host_ed25519_key.pub
# → AGE_RECIPIENT_PLACEHOLDER_NOT_VALID(本例 SSH host key 的 age 公钥)
  1. 确认旧 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 保留,回退前还需确认旧身份仍有效、旧密文存在、配置版本和系统恢复路径可用。
  • 清理:确认新链路稳定后:
    1. .sops.yaml 移除旧 anchor + sops updatekeys 删除旧 recipient;按需 sops rotate --in-place 轮换数据密钥
    2. 删除 /persist/secrets/age-key.txt
    3. 更新项目文档、恢复记录和密钥保管记录

清理不可逆 移除 recipient 不能撤销旧 key 对 git 历史、备份或先前获取数据密钥的访问。先验证新身份和运行链路,按政策轮换数据密钥;若旧身份不再可信,还需轮换秘密值及相关凭据。删除旧私钥前确认历史恢复需求,不能以删除文件代替真正的撤销。


七、踩过的坑(经验教训)#

坑 现象 解法
管理员权限 系统私钥无法由普通用户读取 通过明确授权的管理员环境操作,或使用专用编辑身份
权限不足 SOPS_AGE_KEY_FILE 读不到 key 文件 root 所有,需 root 执行
path_regex 不匹配 no matching creation rules found 核对当前目录/--config、配置相对路径与 filename override
临时明文残留 解密中间文件落盘 随机私有目录、umask 077、退出清理;不输出秘密,也不留编辑器备份

相关#


参考与延伸

  1. SOPS 官方文档 ↗
  2. SOPS CLI 源码 ↗
  3. SOPS 配置源码 ↗
  4. sops-nix 官方说明 ↗
  5. sops-nix 模块 ↗
  6. sops-nix 模板模块 ↗
  7. ssh-to-age 官方说明 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

SOPS 加密配置与密钥生命周期

  1. 01sops 原理:混合加密与接收者
  2. 02sops-nix 集成:运行时、时序与 SSH host key
  3. 03sops 使用指南:命令行操作
  4. 04sops 配置模板与运行时渲染
  5. 05sops 密钥迁移与轮换
CONTINUE READING / 下一篇GitHub 基本操作

区分 Git、GitHub 与 gh,并走完登录、Fork、推送和创建 PR 的流程。