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

sops 配置模板与运行时渲染

按字段理解加密规则、sops-nix 配置和运行时占位符。

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

两套模板,供复制使用:

  1. 配置模板(.sops.yaml)— sops 客户端加密规则,决定「哪些文件用哪些公钥加密」
  2. 加密模板(host/sops.nix)— sops-nix NixOS 模块,决定「运行时从哪解密、写到哪里、给谁读」

本文是通用模板说明,依据当前上游 SOPS 与 sops-nix 源码核对;SOPS CLI 版本号不是 sops-nix 模块版本。项目部署前仍需检查 flake.lock 中锁定的 sops-nix revision。

使用前必读 模板中的 AGE_RECIPIENT_PLACEHOLDER_NOT_VALID是占位符。复制使用前必须替换成你自己的真实公钥,否则 sops 解析会报 malformed recipient。带 # 示例 注释的段落同理。


模板一:.sops.yaml(sops 客户端配置)#

放在仓库根目录。当前 SOPS CLI 默认从工作目录向上寻找 .sops.yaml;跨目录调用宜显式 --config。目标文件路径按配置目录参与规则匹配。 规则从上到下匹配,第一条命中的生效——越具体的规则放越前面。

# ============================================================
# .sops.yaml — sops 客户端加密规则模板
# ============================================================

# keys: 变量区(可选)——给公钥起别名,后面用 *别名 引用
# 好处:定义一次、多处引用;改 key 只改这里
keys:
  - &server-key AGE_RECIPIENT_PLACEHOLDER_NOT_VALID   # SSH host key 的 age 公钥
  - &backup AGE_RECIPIENT_PLACEHOLDER_NOT_VALID           # 备用公钥(示例)

creation_rules:
  # ================= 单规则 =================
  # 一条规则 = 「匹配某类文件 → 用这些 key 加密」
  # 规则从上到下,第一条匹配生效
  - path_regex: host/secrets/secrets\.yaml$
    # path_regex: Go 风格正则,匹配相对 .sops.yaml 所在目录的路径
    #   \. = 转义点号(正则的 . 匹配任意字符,必须转义)
    #   $  = 锚定结尾,防止误匹配 secrets.yaml.bak
    key_groups:
      # key_groups: 密钥组列表(高级用)。每个组内可以不同 key,shamir_threshold 控制阈值
      - age:
          - *server-key
        # age: 用 age 公钥加密(数组写法,可以每把 key 一行 + 注释)
        # pgp: 也能混用 PGP 指纹
        # kms: 也能混用 AWS KMS ARN

  # ================= 多接收者(平铺写法) =================
  - path_regex: host/secrets/team\.yaml$
    age: "AGE_RECIPIENT_PLACEHOLDER_NOT_VALID,AGE_RECIPIENT_PLACEHOLDER_NOT_VALID"
    # age: 逗号分隔的字符串写法。任何一把对应私钥都能解。
    # 逗号分隔字符串可以在字符串外加 YAML 注释;需要逐个接收者注释时用 key_groups 数组

  # ================= 不同文件用不同 key =================
  - path_regex: clusters/dev/.*secrets.*\.yaml$
    encrypted_regex: ^(data|stringData)$
    # encrypted_regex: 只加密匹配该正则的 key(K8s Secret 场景,metadata 保持明文)
    age: "AGE_RECIPIENT_PLACEHOLDER_NOT_VALID"

  # ================= 阈值解密(Shamir 门限) =================
  - shamir_threshold: 2
    # shamir_threshold: 需要几个 key_groups 满足才能解密(Shamir 秘密分享)
    path_regex: \.risc\.yaml$
    key_groups:
      - age:
          - "AGE_RECIPIENT_PLACEHOLDER_NOT_VALID"
      - gcp_kms:
          - resource_id: projects/EXAMPLE_PROJECT/locations/global/keyRings/EXAMPLE_RING/cryptoKeys/EXAMPLE_KEY

  # ================= 兜底规则(可选,放最后) =================
  - path_regex: .*
    # 没有 path_regex 或 .* = 匹配所有文件的兜底规则
    age: "AGE_RECIPIENT_PLACEHOLDER_NOT_VALID"

creation_rules 常用字段速查#

字段 作用 类型
path_regex 匹配文件路径(相对 .sops.yaml 目录) 正则字符串
age age recipients;顶层逗号字符串,key_groups 内数组 按所在层级
pgp PGP 指纹;顶层逗号字符串,key_groups 内数组 按所在层级
kms AWS KMS ARN;顶层字符串,key_groups 为对象数组 按所在层级
aws_profile AWS 配置档名 str
gcp_kms GCP KMS 资源 ID;key_groups 为 resource_id 对象数组 按所在层级
azure_keyvault Azure Key Vault URL str
hc_vault_transit_uri HashiCorp Vault transit URI str
key_groups 密钥组(每把 key 一行可注释) list
shamir_threshold 需要几个组满足才可解密 int
encrypted_regex 只加密匹配的 key 正则
unencrypted_regex 保持明文的 key 正则
encrypted_suffix 以此后缀结尾的 key 加密 str
unencrypted_suffix 以此后缀结尾的 key 明文(默认 _unencrypted) str
encrypted_comment_regex 加密匹配的注释 正则
unencrypted_comment_regex 保持明文的注释 正则
mac_only_encrypted MAC 只覆盖加密值 bool

何时用哪种写法

  • 一把 key / 一个文件:平铺字符串 age: "..." 最简
  • 多把 key 要注释归属:用 key_groups + 数组,每把一行加注释
  • 多环境 / 多团队:一条规则一个环境,具体放前面、兜底放最后

模板二:host/sops.nix(sops-nix NixOS 模块)#

系统级配置,决定运行时「解密来源、写入位置、文件属主权限」。

{ config, pkgs, lib, ... }:
{
  sops = {
    # ============ 顶层 ============
    defaultSopsFile = ./secrets/secrets.yaml;
    #   type: path | 默认密文文件(所有 secret 的默认来源)
    defaultSopsFormat = "yaml";
    #   type: str | 默认加密文件格式
    #   enum: yaml | json | dotenv | ini | binary
    defaultSopsKey = null;
    #   null = 默认按 secret 名选择;空字符串 "" 才表示整个文件
    useSystemdActivation = true;
    #   type: bool | true=用 systemd service 解密;false=NixOS activation script
    #   按实际用户管理、解密时序与挂载依赖选择
    # validationPackage 通常保留模块默认的 sops-install-secrets 验证器
    #   type: package | 此处不是任意 SOPS CLI 包

    # ============ age(解密密钥来源) ============
    age = {
      keyFile = null;
      #   type: nullOr path | 独立 age 私钥路径(null = 不用独立 key)
      #   可与 sshKeyPaths 共存;仅在希望单一来源时主动关闭其他来源
      generateKey = false;
      #   type: bool | true=若 keyFile 不存在则自动生成
      sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
      #   type: listOf path | 从 SSH key 导入作为 age 解密密钥
      #   复用机器身份;仍需保管、备份并保证部署时可读
      #   默认值: openssh hostKeys 里的 ed25519 key
    };

    # ============ secrets(每个机密单独配置) ============
    secrets = {
      app_env = {
        sopsFile = ./secrets/secrets.yaml;
        #   type: path | 覆盖默认文件;不填写则继承 defaultSopsFile
        format = "yaml";
        #   type: enum | 覆盖格式(yaml/json/dotenv/ini/binary)
        #   默认: 继承 defaultSopsFormat;不是按扩展名推断
        path = "/run/secrets/app_env";
        #   type: str | 解密后写到哪
        #   默认: /run/secrets/<name>(普通)/ /run/secrets-for-users/<name>(neededForUsers)
        owner = null;
        #   type: nullOr str | 文件属主(null = root)
        group = null;
        #   type: nullOr str | 文件属组(默认 = owner 的 group)
        mode = "0400";
        #   type: str | 文件权限(默认 0400,仅属主可读)
        neededForUsers = false;
        #   type: bool | true=在 setupSecretsForUsers 阶段解密(早于用户就绪)
        #   该阶段在用户创建前;owner/group 必须为 root,不能指定尚未存在的应用用户
        restartUnits = [ ];
        #   type: listOf str | 解密后要重启的 systemd 服务
        reloadUnits = [ ];
        #   type: listOf str | 解密后要 reload 的 systemd 服务
      };
    };

    # ============ gnupg(可选,可与 age 来源共存) ============
    gnupg = {
      home = null;        # GnuPG home 目录
      sshKeyPaths = [ ];  # 用 SSH key 作 GPG 密钥
    };
  };

  # ============ 消费方示例 ============
  # 服务用 EnvironmentFile 读解密后的环境变量
  systemd.services.example = {
    after = [ "sops-install-secrets.service" ];
    requires = [ "sops-install-secrets.service" ];
    serviceConfig.EnvironmentFile = "/run/secrets/app_env";
  };
}

sops.nix 常用选项速查#

选项 类型 默认 说明
sops.defaultSopsFile path — 默认密文文件
sops.defaultSopsFormat str yaml 默认格式
sops.defaultSopsKey nullOr str null 默认取的 key 名
sops.useSystemdActivation bool 依 sysusers/userborn true=systemd service 解密;默认还依 userborn 等选项
sops.validationPackage package sops-install-secrets 验证工具
sops.age.keyFile nullOr path null 独立 age 私钥
sops.age.generateKey bool false keyFile 不存在则生成
sops.age.sshKeyPaths listOf path hostKeys 用 SSH key 解密
sops.secrets.<n>.path str /run/secrets/<n> 输出路径
sops.secrets.<n>.owner nullOr str null 属主
sops.secrets.<n>.group nullOr str owner 的组 属组
sops.secrets.<n>.mode str 0400 权限
sops.secrets.<n>.sopsFile path defaultSopsFile 覆盖密文文件
sops.secrets.<n>.format enum defaultSopsFormat 覆盖格式
sops.secrets.<n>.neededForUsers bool false 用户阶段解密
sops.secrets.<n>.restartUnits listOf str [ ] 解密后重启
sops.secrets.<n>.reloadUnits listOf str [ ] 解密后 reload

解密来源可以共存 age.keyFile 与 age.sshKeyPaths 不互斥,age 与 GnuPG 来源也可同时声明。选择哪些来源是权限和维护决策;不要把可共存的机制误写为类型约束。密钥位置必须在所选解密阶段可读,并且不能把私钥送进 Nix store。

模板三:sops-nix 运行时配置渲染#

前两套是 CLI 和模块的配置范例,和真正的 sops.templates 是不同概念。后者在 Nix 求值时只放占位符,秘密解密后由安装器替换成实际内容,从而避免明文进入 store。

{ config, ... }:
{
  sops.secrets.api_token = { };
  sops.templates."example-app.env" = {
    content = ''
      API_TOKEN=${config.sops.placeholder.api_token}
    '';
    mode = "0400";
  };
  systemd.services.example-app = {
    after = [ "sops-install-secrets.service" ];
    requires = [ "sops-install-secrets.service" ];
    serviceConfig.EnvironmentFile = config.sops.templates."example-app.env".path;
  };
}

模板通常输出到 /run/secrets/rendered/<name>,通过 .path 引用更稳妥;可设置 content/file、path、owner/group、mode 和变更时的 restartUnits/reloadUnits。占位符替换不自动完成 JSON、YAML、Shell 或 EnvironmentFile 的转义,秘密含换行、引号或特殊字符时必须根据消费格式设计。上例要求值符合 EnvironmentFile 的安全编码;复杂秘密宜由应用直接读取凭据文件。这里仅展示片段,不是已部署的完整服务。

相关#


参考与延伸

  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 / 下一篇sops 密钥迁移与轮换

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