OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 101

sops configuration templates and runtime rendering

Understand encryption rules, sops-nix configuration, and runtime placeholders field by field.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSSOPS encrypted configuration and key lifecycles ↗
About 6 min readStart reading

Two sets of templates to copy and use:

  1. Configuration template (.sops.yaml): sops client encryption rules, specifying “which files are encrypted with which public keys”
  2. Encryption template (host/sops.nix): the sops-nix NixOS module, specifying “what to decrypt at runtime, where to write it, and who can read it”

This article explains general-purpose templates checked against current upstream SOPS and sops-nix source code. The SOPS CLI version is not the sops-nix module version. Before deploying a project, still check the sops-nix revision pinned in flake.lock.

Read before use AGE_RECIPIENT_PLACEHOLDER_NOT_VALID in the templates is a placeholder. Before copying and using a template, replace it with your own real public key; otherwise, sops parsing will fail with malformed recipient. The same applies to sections marked with # 示例 comments.


Template one: .sops.yaml (sops client configuration)#

Place it at the repository root. By default, the current SOPS CLI searches upward from the working directory for .sops.yaml; when invoking it across directories, specify --config explicitly. The target file path is matched against rules relative to the configuration directory. Rules are matched from top to bottom, and the first match wins, so put more specific rules first.

# ============================================================
# .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 common fields: quick reference#

Field Purpose Type
path_regex Match file paths (relative to the .sops.yaml directory) Regular expression string
age age recipients; comma-separated string at the top level, array within key_groups Depends on the level
pgp PGP fingerprints; comma-separated string at the top level, array within key_groups Depends on the level
kms AWS KMS ARNs; string at the top level, array of objects in key_groups Depends on the level
aws_profile AWS profile name str
gcp_kms GCP KMS resource IDs; array of resource_id objects in key_groups Depends on the level
azure_keyvault Azure Key Vault URL str
hc_vault_transit_uri HashiCorp Vault transit URI str
key_groups Key groups (one key per line allows comments) list
shamir_threshold Number of groups required for decryption int
encrypted_regex Encrypt only matching keys Regular expression
unencrypted_regex Keys to leave in plaintext Regular expression
encrypted_suffix Encrypt keys ending with this suffix str
unencrypted_suffix Leave keys ending with this suffix in plaintext (default: _unencrypted) str
encrypted_comment_regex Encrypt matching comments Regular expression
unencrypted_comment_regex Comments to leave in plaintext Regular expression
mac_only_encrypted MAC covers encrypted values only bool

When to use each form

  • One key / one file: a flat string such as age: "..." is simplest
  • Multiple keys with ownership comments: use key_groups + arrays, with a comment on each key's line
  • Multiple environments / teams: one rule per environment, specific rules first and fallback last

Template two: host/sops.nix (sops-nix NixOS module)#

System-level configuration specifying the runtime “decryption source, output location, file ownership, and permissions.”

{ 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 common options: quick reference#

Option Type Default Description
sops.defaultSopsFile path — Default encrypted file
sops.defaultSopsFormat str yaml Default format
sops.defaultSopsKey nullOr str null Default key name to retrieve
sops.useSystemdActivation bool Depends on sysusers/userborn true = decrypt through a systemd service; the default also depends on options such as userborn
sops.validationPackage package sops-install-secrets Validation tool
sops.age.keyFile nullOr path null Separate age private key
sops.age.generateKey bool false Generate if keyFile does not exist
sops.age.sshKeyPaths listOf path hostKeys Decrypt with an SSH key
sops.secrets.<n>.path str /run/secrets/<n> Output path
sops.secrets.<n>.owner nullOr str null Owner
sops.secrets.<n>.group nullOr str The owner's group Group
sops.secrets.<n>.mode str 0400 Permissions
sops.secrets.<n>.sopsFile path defaultSopsFile Override the encrypted file
sops.secrets.<n>.format enum defaultSopsFormat Override the format
sops.secrets.<n>.neededForUsers bool false Decrypt during the user stage
sops.secrets.<n>.restartUnits listOf str [ ] Restart after decryption
sops.secrets.<n>.reloadUnits listOf str [ ] Reload after decryption

Decryption sources can coexist age.keyFile and age.sshKeyPaths are not mutually exclusive, and age and GnuPG sources can also be declared together. Choosing sources is a permissions and maintenance decision; do not misrepresent mechanisms that can coexist as a type constraint. Key locations must be readable at the selected decryption stage, and private keys must never enter the Nix store.

Template three: sops-nix runtime configuration rendering#

The first two templates are examples of CLI and module configuration, a different concept from actual sops.templates. The latter contains only placeholders during Nix evaluation; after secrets are decrypted, the installer substitutes the actual contents, keeping plaintext out of the 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;
  };
}

Templates normally output to /run/secrets/rendered/<name>; referring to them through .path is more robust. You can set content/file, path, owner/group, mode, and restartUnits/reloadUnits to run on changes. Placeholder replacement does not automatically escape JSON, YAML, shell syntax, or EnvironmentFile contents. If a secret contains newlines, quotes, or special characters, design around the consuming format. The example above requires values safely encoded for EnvironmentFile; for complex secrets, it is preferable for the application to read credential files directly. This is only a snippet, not a complete deployed service.

Related#


References & further reading

  1. Official SOPS documentation ↗
  2. SOPS CLI source code ↗
  3. SOPS configuration source code ↗
  4. Official sops-nix documentation ↗
  5. sops-nix module ↗
  6. sops-nix templates module ↗
  7. Official ssh-to-age documentation ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

SOPS encrypted configuration and key lifecycles

  1. 01sops principles: hybrid encryption and recipients
  2. 02sops-nix integration: runtime, ordering, and SSH host keys
  3. 03sops usage guide: command-line operations
  4. 04sops configuration templates and runtime rendering
  5. 05sops key migration and rotation
CONTINUE READING / NEXT ARTICLEsops key migration and rotation

Migrate keys through a two-recipient transition, distinguishing authorization updates from credential revocation.