两套模板,供复制使用:
- 配置模板(
.sops.yaml)— sops 客户端加密规则,决定「哪些文件用哪些公钥加密」- 加密模板(
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 的安全编码;复杂秘密宜由应用直接读取凭据文件。这里仅展示片段,不是已部署的完整服务。
相关#
- 配置文件解读 → sops 使用指南
- 换 key 流程 → sops 迁移指南
- 参考:sops 配置文档 | sops-nix README
参考与延伸
根据技术笔记重新整理,示例使用通用名称。