sops 3.13.3 常用 CLI 命令与工具详解

sops 3.13.3 常用 CLI 命令详解:encrypt/decrypt/edit/set/rotate/updatekeys/groups/exec-env/exec-file 等 14 个子命令逐条实测,覆盖 .sops.yaml 规则、encrypted-regex 局部加密、git 明文 diff 集成与 MAC mismatch 等八类报错排障。

sopssops2026/10/18

sops 3.13.3 常用 CLI 命令与工具详解

目标:系统梳理 sops 3.13.3 全部 14 个子命令与核心全局参数,按「加密/解密 → 编辑/修改 → 密钥管理 → 运行时注入 → git 集成 → 状态辅助」逐条给出可独立执行的命令与实测回显,覆盖 .sops.yaml 规则、局部加密控制、MAC 校验行为与典型报错排障,已在 Ubuntu 22.04 实测通过。

一、环境信息

本章命令均在以下环境实测,读者环境一致即可逐条复现。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • sops:3.13.3(官方发布件安装)
  • 加密后端 age:1.3.2(age --version → v1.3.2)
  • 密钥文件:/root/.config/sops/age/keys.txt(age-keygen 生成,权限 600,sops 默认自动加载该路径,无需额外环境变量)
  • 演示目录:/opt/demo(本文所有示例文件的所在目录)
  • 演示公钥(与密钥文件配套,实测值):age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
  • 演示备份公钥(第二把钥匙,用于密钥轮换实测):age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc

版本自检回显(环境就绪的判定标准):

sops --version --check-for-updates && age --version
sops 3.13.3 (latest)
v1.3.2

二、工具概览

sops 是单一二进制的多子命令工具,无常驻服务。sops --help 的官方自述(3.13.3 实测原文):"sops - encrypted file editor with AWS KMS, GCP KMS, HuaweiCloud KMS, Azure Key Vault, age, and GPG support"。全部子命令清单(sops --help 实测 COMMANDS 节选):

子命令作用(官方描述 + 实测归纳)
encrypt加密文件,结果输出到 stdout(或 -i 原地覆盖)
decrypt解密文件,结果输出到 stdout(或 -i 原地覆盖)
edit解密到安全临时目录并调起编辑器,保存后重加密
set / unset按 JSONPath 风格路径修改/删除密文中的字段(非交互)
rotate重新生成数据密钥并重加密全部值(密钥轮换)
updatekeys按 .sops.yaml 配置同步文件的主密钥
groups管理 sops 文件上的密钥分组(add/delete)
exec-env解密后把值注入子进程环境变量并执行命令
exec-file解密内容放进临时文件(默认 FIFO)供命令读取
filestatus检查文件加密状态
publish把 sops 文件/目录发布到配置的目标位置
keyservice启动密钥服务(本地/远程解密能力托管)
completion生成 bash/zsh 补全脚本

💡 技术拓展:与「先解密成明文文件再操作」的一次性脚本相比,sops 的命令形态始终围绕「密文落盘、明文只在管道/内存/临时文件中瞬时存在」设计:decrypt 输出到 stdout 而不是默认写文件,edit 用的是解密即删的安全临时目录,exec-env/exec-file 用 FIFO 让明文不落盘。这是它与「手写 openssl 加密脚本」的本质区别。本文全部按 sops 原生形态实测。

核心工作流只有两条:加密(sops encrypt --age <公钥> --in-place <file>)与消费(decrypt/edit/exec-*)。其余子命令都是围绕「多把钥匙怎么管」「明文怎么安全地交给程序」展开的工程化能力。

三、前置条件

本章从零准备演示文件与密钥,保证后文每条命令都能独立执行(不依赖阅读本系列其他篇目)。

  1. 安装 sops 3.13.3 与 age(完整安装步骤见 《Ubuntu 22.04 安装 sops 3.13.3 指南》,此处仅做就位检查):
which sops age age-keygen
/usr/local/bin/sops
/usr/local/bin/age
/usr/local/bin/age-keygen
  1. 准备 age 密钥对(若已有 keys.txt 可跳过;-o 输出的 Public key: 行即演示公钥):
mkdir -p /root/.config/sops/age
age-keygen -o /root/.config/sops/age/keys.txt
Public key: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
  1. 创建演示 YAML(本文主线文件,含数据库密码与 API Key 两类敏感字段):
mkdir -p /opt/demo && cd /opt/demo
cat > secrets.yaml <<'EOF'
# 应用配置文件(演示)
app:
  name: order-service
  port: 8080
database:
  host: 192.168.1.100
  port: 3306
  username: admin
  password: q1w2e3r4
  dbname: orders
api:
  payment_key: sk-test-1234567890abcdef
  webhook_secret: whsec-abcdefgh12345678
EOF
cat secrets.yaml
# 应用配置文件(演示)
app:
  name: order-service
  port: 8080
database:
  host: 192.168.1.100
  port: 3306
  username: admin
  password: q1w2e3r4
  dbname: orders
api:
  payment_key: sk-test-1234567890abcdef
  webhook_secret: whsec-abcdefgh12345678

四、加密与解密命令

4.1 sops encrypt:加密文件

基础形态:--age 指定接收者公钥,-i(--in-place)原地覆盖。执行后敏感值全部变成 ENC[AES256_GCM,...],文件尾部追加 sops: 元数据块:

cd /opt/demo
sops --encrypt --age age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt --in-place secrets.yaml

加密后的密文形态(实测全文照抄,末尾为元数据块):

#ENC[AES256_GCM,data:L+qWbUUKwdl08YSpCRLCPPhdJR9sC1M6rzNnusvEqA==,iv:446hUFIsA9LFKCpCbP8ITtr9Rai704tE/qwGuepz/3w=,tag:xArzdWDqSDmt8BRZMDI2Bw==,type:comment]
app:
    name: ENC[AES256_GCM,data:79/afZiDvMQ2YFuwpA==,iv:5RBxIYn2AsaMPZhVYJM9srjXg1D2108nE4A4+PkDPKI=,tag:PwRCuTmO3Rv7ACEEn+Rn6w==,type:str]
    port: ENC[AES256_GCM,data:IUsAKQ==,iv:7VfUJrHeSbs17hTnN6mxjsJkMb1l62gG1vkqs+HJ5Rc=,tag:4OrBQ0f7IrtgKI6VGVVo/g==,type:int]
database:
    host: ENC[AES256_GCM,data:1TlCvn600IZH2/qckA==,iv:lUlcnf+2KgaKbjxfLu3Fye+N3m10pRN+RJ7GGpICDxE=,tag:FpfcGJtGytPQ8XXuqwKtSg==,type:str]
    port: ENC[AES256_GCM,data:u51vSw==,iv:Nhc4HkyLgeS1oivwH8DE2D0yZP1ENIKuiz1Td7lMuns=,tag:ykt+Jknck8/JSyjvnOjAdw==,type:int]
    username: ENC[AES256_GCM,data:uvbykdY=,iv:9F73ypaXb4eacorAURK0GWJ1+wQpTw9GV6vd+MOXKzk=,tag:0QkyuEUBeLtgQmCTfGheUg==,type:str]
    password: ENC[AES256_GCM,data:Hnsqug7yIOI=,iv:Jb3iEvv4vkJwOUR5ekud4zM4PmscCUpgY6EOloXI9+g=,tag:83lmvKDSzh2LO/WzhpV9Qw==,type:str]
    dbname: ENC[AES256_GCM,data:HKFLUKFC,iv:irlZUoPOnawNG4jvzJMSWr7GgRsS2tj8+Um/ILiiZRE=,tag:uZ8edJiSeMH1vOEsAHUv2A==,type:str]
api:
    payment_key: ENC[AES256_GCM,data:yObF8mDBAgVPfVx0nMtx882eFSqZDSOU,iv:tAgjK6IvvcqcPU+T5fz5OAmTwgmarNUhFJPhBrwtzS4=,tag:eV5J3ooJdKS+TeX/YhLtJw==,type:str]
    webhook_secret: ENC[AES256_GCM,data:/WrgOEV10MZp/Rz9Ir85qLl3YGP2AA==,iv:HWtZLJTWEarcM9+ZTBcvNM0PpgkKX9v6cwr3murXfXI=,tag:fxVp7v1+11zMFFJDar1aqw==,type:str]
sops:
    age:
        - enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMTU5XM3J0T3hpS1hBTnFi
            dGhyUmY4LzFJYndGNVl0WGpsbVNZckNrcEdBCjdvbHFnMTkrcUYwcHNROXFmdlZl
            SnNOaEVtdnM2cUhGcWJYaWx4QlpjREkKLS0tIFRqQ2gzaXFKMTNpRHRpMWNLSy95
            T21IMm5QZkdMWC91M0dCNlpud0twVzgKz/X0XL5Z/bB3B1gA6DpofVwAVycEMwcJ
            XrvZR+rm03XJ3MmMACc43VXTC/ObPSJuYBfgYiJdtZ+bMbWhGGTt2w==
            -----END AGE ENCRYPTED FILE-----
          recipient: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
    lastmodified: "2026-10-18T08:13:36Z"
    mac: ENC[AES256_GCM,data:EDrxf82sc5sHzobsT1mIw6bHIhhqMACYnWQL/vQLZEQhjXnoEPIRryqRkrlKLCqDQvfpzE1JfxSOYtyYkP4Y82zZvCEFhLUPg8924c45t0S7DbCxIcamy7tEPOAqUJJ7/Rfib2D5p75L9gLh+GmdKjKv85XQyUcCPNqWvn8pPlE=,iv:Q8eW4n/qK2tl/wSLOWFw4LqXnbrYVlVVaScnig7/Rsw=,tag:cIXG0L7L1HBWYb8yQaR/TA==,type:str]
    unencrypted_suffix: _unencrypted
    version: 3.13.3

密文形态逐项解读:

  • 每个值独立为一段 ENC[AES256_GCM,data:密文,iv:随机向量,tag:认证标签,type:类型];type:str/int/bool/comment 记录原值类型,解密时还原。
  • 注释行也被加密(首行 #ENC[...]),保证注释里的敏感信息不泄露。
  • sops: 元数据块:age[].enc 是被公钥包裹的数据密钥(recipient 标注公钥),mac 是全文件完整性校验值,unencrypted_suffix 记录默认豁免后缀,version 记录加密时的 sops 版本。
  • 实测细节:sops 重写文件时统一了缩进(原文 2 空格变为 4 空格),首次加密后 diff 会有缩进变化,属正常行为。

encrypt 常用参数实测(3.13.3 sops encrypt --help 原文参数,标注实测默认值):

参数说明默认值
--in-place, -i原地覆盖而不是输出 stdout关闭
--output加密结果另存到指定文件未设置
--age, -a逗号分隔的 age 接收者列表未设置(也可用环境变量 SOPS_AGE_RECIPIENTS)
--encrypted-regex仅加密键名匹配该正则的字段未设置(全量加密)
--unencrypted-regex仅键名匹配的豁免加密未设置
--encrypted-suffix / --unencrypted-suffix按键名后缀圈定加密/豁免范围unencrypted 后缀默认 _unencrypted
--input-type / --output-type指定格式:json/yaml/dotenv/ini/binary按文件扩展名自动识别
--indentYAML/JSON 输出缩进空格数0(sops 内部默认风格)

局部加密:只加密敏感字段。配置里有非敏感字段(服务名、副本数)又不想整文件加密时,用 --encrypted-regex 圈定(新建 mixed.yaml 演示):

cd /opt/demo
cat > mixed.yaml <<'EOF'
service: payment-gateway
region: cn-north-1
replicas: 3
credentials:
  access_key: AKIAIOSFODNN7EXAMPLE
  secret_key: wJalrXUtnFEMIexampleKEY
  session_token: FwoGZXIvYXdzEXAMPLE
EOF
sops --encrypt --age age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt --encrypted-regex '^(access_key|secret_key|session_token|password|payment_key|webhook_secret)$' --in-place mixed.yaml

加密结果(实测):

service: payment-gateway
region: cn-north-1
replicas: 3
credentials:
    access_key: ENC[AES256_GCM,data:Bw7i3FIpNAltPF9/VeCHdUBaDHE=,iv:EWMzgV5BaPwZsJ57wKdzQMcTdmEOLbWA1GMy+MbXo9s=,tag:vIMvSabrAYLi4Wdnqb20zw==,type:str]
    secret_key: ENC[AES256_GCM,data:V+5Fb32UD9AG9uQWAQ2nWKZNDgDhtgU=,iv:IVaGRT63kKy6xtcY0jS+J07LUu0UHwsXVgDnJQO0NGo=,tag:RTClkPZ8LdVex9bvuifemg==,type:str]
    session_token: ENC[AES256_GCM,data:vSU03bLHt9E2uGUTelk+JmlPhg==,iv:FjLplk/v4jc1SniUsOxtzssKhBpA4XbGBZG6fqiLZFc=,tag:JT/vLsFkYYQKmO9IcM4siA==,type:str]
sops:
    age:
        - enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6dmdCNzNWYXl1K1BFcVJs
            bnFPaDhsQzYxWmxRQVVicks5ZzhBVG5HOTFrCi9xMmxzN2svWWJ4M2g4OU1tSFhG
            Rzc0SE10bW5rMHNhVm1CM2tjaFptb0UKLS0tIG5MZzJRRkxsaDYyNkkxTVF3SmRV
            Q0R3VmZ2QUxOckFUczAvd1l0RDd5N2cKcci/vtKcE7IgiHil4grK7MRO31mTEJ17
            kvUIDDjoJM4e1YpXgVLQdeRbfOKIRxitaSZk74dbKCGNW1aRgvv4Vg==
            -----END AGE ENCRYPTED FILE-----
          recipient: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
    encrypted_regex: ^(access_key|secret_key|session_token|password|payment_key|webhook_secret)$
    lastmodified: "2026-10-18T08:13:49Z"
    mac: ENC[AES256_GCM,data:GSnMNAXGKeA784eOULbgbKtd/ZMlSIIwJEQiXJAhJJUc6zeroTU8QPu3do8uQt4aFhsWAU2Q51KOtbb3Wi9n0tBRhZpk+AK5QIl34im69sbUnYms0XGEL3JOxBsMRXZsyz7QsDVpx6FkFL5BtQ+mPBMCz9tc1Y1Z5Fv4ILvOjgc=,iv:HhAkWF5IQ9YvqzgJdmvVheaoj/KYw28TrDtzHotdeZ4=,tag:6CMmnwsOYtpFLiio/pOJNw==,type:str]
    version: 3.13.3

效果验证:service/region/replicas 保持明文可直接 review,credentials 三个键加密,且加密范围被记进元数据 encrypted_regex 字段(解密/再加密时沿用同一规则)。混合格式推荐配合 --mac-only-encrypted:MAC 只对加密值计算,避免改明文字段触发 MAC 报错(见 10.1 注意事项)。

JSON 与 ENV(dotenv)格式实测:sops 按扩展名自动识别格式,同一把公钥加密 JSON:

cd /opt/demo
cat > secrets.json <<'EOF'
{
  "app": {"name": "order-service", "port": 8080},
  "database": {"host": "192.168.1.100", "username": "admin", "password": "q1w2e3r4"},
  "api": {"payment_key": "sk-test-1234567890abcdef"}
}
EOF
sops --encrypt --age age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt --in-place secrets.json
head -10 secrets.json

实测回显(JSON 键保持明文、值变密文,元数据并入 sops 键;另注意实测 sops 重写 JSON 默认使用 tab 缩进,与原始文件的空格缩进不同,属正常行为):

{
	"app": {
		"name": "ENC[AES256_GCM,data:D5950xZ/p5gJpvppPg==,iv:2t8xqajw/sj3cKnik8EY+Jg9lPX0G2JUWw4FI3g+pWM=,tag:yxu3KqH+UGqY0HhQHgbxPA==,type:str]",
		"port": "ENC[AES256_GCM,data:d5QwCA==,iv:cuOHA3B0Xt5bn3lzpBdKSn3KHdaU4W7kPwj3lQOr94M=,tag:zEqUTNFk0GhJuGh/K2J+vA==,type:int]"
	},
	"database": {
		"host": "ENC[AES256_GCM,data:+oTWYfNXUg6GAJMOTA==,iv:UZOAMVFhduaDBix52etnmwI59NvbhzN2PPUxsngegvs=,tag:1jG4lGHYp5rPUWaCn7BWVg==,type:str]",
		"username": "ENC[AES256_GCM,data:8OkLp1c=,iv:aZUpBXJKgb1cjpNKqwg8+d3lrsOKkjiAHo7qwm+YclM=,tag:Ho6HSND/CwB3LcXGRJzvWQ==,type:str]",
		"password": "ENC[AES256_GCM,data:pFuhDSWKzAs=,iv:gkp0FczKgSHYUVvU5M0oYaikW0OfkqGo2OpoFcjYttU=,tag:AUTxtlOeyQgFQ6iT1h1v6w==,type:str]"
	},

ENV 格式(.env 文件,无层级结构,逐行加密):

cd /opt/demo
cat > demo.env <<'EOF'
DB_HOST=192.168.1.100
DB_USER=admin
DB_PASSWORD=q1w2e3r4
API_PAYMENT_KEY=sk-test-1234567890abcdef
EOF
sops --encrypt --age age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt --in-place demo.env
cat demo.env

实测回显:

DB_HOST=ENC[AES256_GCM,data:tnUpoo1dBDfvANz6bw==,iv:DWd/9vTh/ApimgSFEP1X0J6WXY00qzIqQ9QR8OQSuY8=,tag:cQeLov37AoowqwWbBCzCFQ==,type:str]
DB_USER=ENC[AES256_GCM,data:IuIng78=,iv:u/CtwyHeZe3d+BzsLbd+k7DE6i9mYiD5csgAp9D9s6A=,tag:913aJJuyGgyeUWMPQkAZkA==,type:str]
DB_PASSWORD=ENC[AES256_GCM,data:qTK8xzBuEqM=,iv:XfzvV5tuWoF1c1NDEmFRxcj/+CgyGQtq53dcSF/4b/M=,tag:f0733s1Z0NjzceoMxP35eA==,type:str]
API_PAYMENT_KEY=ENC[AES256_GCM,data:bWdUT5KFIyTV219u6NXm8WUagjSkN0FX,iv:zSCkU34/ArQmkRq/EP5TiGfoGQOGlskkRH+SZeOMJJQ=,tag:h+l+clq23aSuiK7mujeZDQ==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDTjA1ekMwYi9PWnlFWU00\nOWd3Mm1TRUdnQXRGc1NRQit5MTR2YkpPMDB3Ci9SNVpwSTFVZUtqWWxLKzU0SHB2\ncG1rMlFFUDRxODBWTjA3WU9ONVh6Y0EKLS0tIDJjRjd4NWV6WGFJMFNKWWtETXpw\nbFZpd3lWSGo2UTV2T2VwandUcFBFdDgKLDwnPAZqnEfngedpErJfqOVsS81gwX94\nHUKaNOcWW7zg/kI6OaDU3OUeaEmHvFhu/czX+IIKzR4lGtr+oxnqjQ==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
sops_lastmodified=2026-10-18T08:13:49Z
sops_mac=ENC[AES256_GCM,data:oD2CaAAarZNx+DA8APAeXU516Wtf/+wG3AM3eqb92R3N++OnuD4R9oNrOpKqWK51z27rFMqBJcqu1YmHp/CJ+7AgqQqw4TirZ9Y5zza8XUKYl7Y5Os81Gl2VQTRe4AfRR1pBVW8WZrnkkXF6usfrv7pwPbZIoWmh9dfmevKhpog=,iv:p+cJMIHZIoy3JHvvQTZKl45ftcuogflnMVAIsGAvBtY=,tag:Jr5pgdedn5828ZFZXj2jcw==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.13.3

ENV 格式把元数据扁平化为 sops_* 变量,密钥数据同样全程加密。三种格式闭环验证完毕。

4.2 sops decrypt:解密文件

输出到 stdout,不改动密文文件:

cd /opt/demo
sops --decrypt secrets.yaml

实测回显(明文完整还原,注意缩进按 sops 风格为 4 空格):

# 应用配置文件(演示)
app:
    name: order-service
    port: 8080
database:
    host: 192.168.1.100
    port: 3306
    username: admin
    password: q1w2e3r4
    dbname: orders
api:
    payment_key: sk-test-1234567890abcdef
    webhook_secret: whsec-abcdefgh12345678

extract:只取某个字段的值(脚本里取密码喂给其他程序的常见姿势):

sops --decrypt --extract '["database"]["password"]' secrets.yaml
q1w2e3r4

ignore-mac:跳过完整性校验(仅在明确知道文件被合法改动过、且能接受跳过校验时使用):

sops --decrypt --ignore-mac secrets.yaml | grep password
    password: q1w2e3r4

decrypt 常用参数(sops decrypt --help 实测):--extract(按路径取值)、--ignore-mac(忽略 MAC)、--output(另存明文文件)、--decryption-order(逗号分隔的解密密钥类型顺序,如 age,kms)、--filename-override(从 stdin 读取时用于确定格式的文件名占位)。

4.3 sops rotate:轮换数据密钥

rotate 做两件事:重新随机生成数据密钥并重加密全部值;配合 --add-age/--rm-age 调整包裹数据密钥的主密钥。给 mixed.yaml 换上双钥匙(实测命令与回显):

cd /opt/demo
sops rotate --in-place --add-age age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt --add-age age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc mixed.yaml
grep recipient mixed.yaml

实测回显(文件里现在有两个接收者):

          recipient: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
          recipient: age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc

轮换后解密验证(两把钥匙中任一把私钥在手都可解):

sops --decrypt mixed.yaml | head -8
service: payment-gateway
region: cn-north-1
replicas: 3
credentials:
    access_key: AKIAIOSFODNN7EXAMPLE
    secret_key: wJalrXUtnFEMIexampleKEY
    session_token: FwoGZXIvYXdzEXAMPLE

⚠️ 注意:rotate 子命令的密钥参数是 --add-age/--rm-age 等动词形式,不支持全局的 -a/--age 简写——实测用错会报 flag provided but not defined: -a(完整报错与排障见第十一章 FAQ Q4)。

五、编辑与修改命令

5.1 sops edit:编辑器式修改

sops edit <file> 的内部流程:解密到安全临时目录(实测为 /tmp/<随机数字>/ 形态,仅属主可读写)→ 调起编辑器 → 保存退出后校验 MAC 并重加密写回。编辑器默认 vim,可用 SOPS_EDITOR/EDITOR 覆盖。

非交互演示一:SOPS_EDITOR=true(true 命令什么都不改直接成功退出,用于验证流程):

cd /opt/demo
SOPS_EDITOR=true sops edit notes.yaml

(notes.yaml 为按 --encrypted-regex api_key 局部加密的演示文件,先创建并加密:)

cat > notes.yaml <<'EOF'
title: deploy notes
note: 密钥在保险柜
api_key: sk-abc123
EOF
sops --encrypt --age age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt --encrypted-regex api_key --in-place notes.yaml
head -3 notes.yaml

(加密结果实测:)

title: deploy notes
note: 密钥在保险柜
api_key: ENC[AES256_GCM,data:wWCQM51fjnRK,iv:XRiJ9vqW4tBLyiq8DnZT75VKeJpzKcVqBIAqTTbsV9o=,tag:DWgyXFN8aNorxi2cgjp9zg==,type:str]

再执行 edit 演示:

SOPS_EDITOR=true sops edit notes.yaml
File has not changed, exiting.

退出码 0。内容未变化时 sops 不重写文件。非交互演示二:用 vim 直接保存退出(vim -c wq 打开即存退):

SOPS_EDITOR='vim -c wq' sops edit notes.yaml

实测回显(vim 的打开与写盘提示,随后 sops 检测无变化退出):

"/tmp/3161746643/notes.yaml" 3L, 64B written
File has not changed, exiting.

edit 常用参数(sops edit --help 实测):--show-master-keys, -s(编辑界面顶部显示主密钥,便于确认用哪把钥匙)、--ignore-mac、加密范围四件套 --encrypted-regex/--unencrypted-regex/--encrypted-suffix/--unencrypted-suffix、--age(给已有文件追加接收者)。

5.2 sops set:非交互修改密文字段

set 按 JSONPath 风格路径直接改密文文件,不打开编辑器,适合脚本。给 secrets.yaml 加一个 api.timeout 字段(值为 JSON 字符串 "30"):

cd /opt/demo
sops set secrets.yaml '["api"]["timeout"]' '"30"'
sops --decrypt secrets.yaml | grep timeout

实测回显:

    timeout: "30"

再删除该字段:

sops unset secrets.yaml '["api"]["timeout"]'
sops --decrypt secrets.yaml | grep -c timeout

实测回显(0 表示已删除):

0

要点说明:

  • 路径参数必须写成 ["顶层键"]["子键"] 的 JSON 形态;值参数必须是合法 JSON(字符串带引号、数字不带)。
  • set --value-file <file> / --value-stdin:把值从文件或 stdin 读入,避免敏感值出现在 ps 进程列表里(sops set --help 实测参数,安全场景推荐)。
  • unset --idempotent:删除不存在的路径也不报错(幂等,脚本友好)。

六、密钥管理命令

6.1 .sops.yaml 规则文件

在目录(通常是 Git 仓库根)放置 .sops.yaml 后,加密新文件不用再敲 --age,sops 按 creation_rules 从上到下匹配 path_regex。在 /opt/demo 创建:

cd /opt/demo
cat > .sops.yaml <<'EOF'
# 全局默认规则:所有文件用 age 公钥加密
creation_rules:
  - path_regex: \.dev\.yaml$
    age: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
  - age: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
EOF

规则逐项说明:creation_rules 是有序列表,逐条向下匹配;path_regex 用正则限定该条规则适用的文件路径(本例匹配 .dev.yaml 结尾的文件);不带 path_regex 的条目是兜底规则,匹配其余所有文件;age 字段可写多个接收者(逗号分隔),也可用 stpgp/kms/... 等其他后端字段。

实测:新建文件免参数自动加密。创建 app-dev.yaml(明文),只敲 sops --encrypt --in-place,不传 --age:

cat > app-dev.yaml <<'EOF'
app: order-service
debug: true
db_password: q1w2e3r4
EOF
sops --encrypt --in-place app-dev.yaml
head -8 app-dev.yaml

实测回显(自动按规则加密,文件里出现对应 recipient):

app: ENC[AES256_GCM,data:D1WUmBLmpH1Nr9dI0w==,iv:UblodXu7D92otBNdbSi1worWjtPND4LbBo8kWr2xpsQ=,tag:10CL7Vwq3iWkjo5dGA8Ktg==,type:str]
debug: ENC[AES256_GCM,data:rB1Zjg==,iv:U38kCXbs7XMQY2eUDJ5uKJkWJrUg7NAN0VZe6i5+gdI=,tag:gKpYwwnHm7ZbviMB5sAvAA==,type:bool]
db_password: ENC[AES256_GCM,data:O96M1cejQMU=,iv:U+udRRRsv5rSNgiZ5j0wt1BhttSZdHIGm+e6wk4YTAw=,tag:ko+AjkU9iprO4g62rXwzXA==,type:str]
sops:
    age:
        - enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2NWFzZDBXN2RrN3dGOVJv

--config 参数可显式指定规则文件路径(默认自动向上层目录搜索,SOPS_CONFIG 环境变量同效)。

6.2 sops updatekeys:按规则同步密钥

先准备第二把 age 密钥(模拟"新增了一台有解密权限的机器/一个新同事"):

age-keygen -o /root/backup-keys.txt
Public key: age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc

把第二把公钥加进 .sops.yaml 的两个规则(age 值改为逗号分隔双公钥),然后对已有文件跑 updatekeys:

cat > .sops.yaml <<'EOF'
creation_rules:
  - path_regex: \.dev\.yaml$
    age: >-
      age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt,
      age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc
  - age: >-
      age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt,
      age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc
EOF
sops updatekeys --yes secrets.yaml

实测回显(+++ 标记新增的密钥):

2026/10/18 08:14:22 Syncing keys for file /opt/demo/secrets.yaml
The following changes will be made to the file's groups:
Group 1
    age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
+++ age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc
2026/10/18 08:14:22 File /opt/demo/secrets.yaml synced with new keys

验证文件已带两个接收者:

grep recipient secrets.yaml
          recipient: age1kyu22mnpkeyf64r64zc5v2pwxsyv3ew0pz434w2mne2qstsa4urqgdk3dt
          recipient: age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc

无差异时实测输出(重复执行第二次):

2026/10/18 07:52:53 Syncing keys for file /opt/demo/secrets.yaml
2026/10/18 07:52:53 File /opt/demo/secrets.yaml already up to date

参数说明:--yes, -y 跳过确认直接执行(脚本必需);--input-type 覆盖格式识别。删除密钥同理:从规则里移除某公钥后再 updatekeys,文件对应 recipient 会被移除(变更输出用 --- 标记)。密钥轮换的完整姿势:updatekeys 调整主密钥 → sops rotate -i <file> 重生成数据密钥。

6.3 sops groups add / delete:密钥分组

groups 直接在文件上增删密钥组(每个组持有各自的"被组内钥匙包裹的数据密钥")。给 mixed.yaml 追加一个只含备份公钥的组:

cd /opt/demo
sops groups add --file mixed.yaml --age age1le62ap47f99xsrxlc3hwa30sllqgjy3hcy02nyrkdrek2v9qqswsp30ntc --in-place
grep -c recipient mixed.yaml

实测回显(现在共 2 个组、3 个 recipient:组1 双公钥 + 组2 备份公钥):

3

删除组(按组序号,从 0 起):

sops groups delete --file mixed.yaml 1 --in-place

配合 --shamir-secret-sharing-threshold N(encrypt/edit/groups add 均支持,实测默认 0 关闭):开启后数据密钥被 Shamir 拆分给 N 个组分别包裹,必须凑齐 N 个组才能复原数据密钥,用于"两把钥匙同时在场才解得开"的强管控场景。

七、运行时注入命令

7.1 sops exec-env:环境变量注入

把 dotenv 文件解密后注入子进程环境变量,命令里直接 $变量名 引用(密文文件、明文只在子进程内存中):

cd /opt/demo
sops exec-env demo.env 'sh -c "echo DB_HOST=$DB_HOST; echo DB_USER=$DB_USER; echo DB_PASSWORD=$DB_PASSWORD; echo API_PAYMENT_KEY=$API_PAYMENT_KEY"'

实测回显(四个敏感值全部还原为明文环境变量):

DB_HOST=192.168.1.100
DB_USER=admin
DB_PASSWORD=q1w2e3r4
API_PAYMENT_KEY=sk-test-1234567890abcdef

参数(sops exec-env --help 实测):--pristine(只注入解密值、不透传现有环境变量,防泄漏用)、--user(以指定用户运行命令)、--same-process(当前进程内执行而非子进程)、--background(已标记 DEPRECATED,勿用)。

7.2 sops exec-file:临时文件注入

解密内容放进临时文件(默认 FIFO 命名管道,命令退出即消失)再执行命令,{} 占位符代表临时文件路径。把解密后的 YAML 喂给 cat:

cd /opt/demo
sops exec-file secrets.yaml 'cat {}'

实测回显(等价于解密输出):

# 应用配置文件(演示)
app:
    name: order-service
    port: 8080
database:
    host: 192.168.1.100
    port: 3306
    username: admin
    password: q1w2e3r4
    dbname: orders
api:
    payment_key: sk-test-1234567890abcdef
    webhook_secret: whsec-abcdefgh12345678

参数(sops exec-file --help 实测):--no-fifo(用普通文件代替 FIFO——目标程序需要 seek/多次读取时必须加)、--filename(指定临时文件名,默认 tmp-file)、--user、--input-type/--output-type。典型用途:把解密配置喂给只认文件路径的老程序(如 sops exec-file config.yaml 'myapp --config {}')。

八、状态检查与辅助命令

8.1 sops filestatus

检查文件是否为 sops 加密格式(CI 里判断"该文件加密了没"的最轻量手段):

cd /opt/demo
sops filestatus secrets.yaml
{"encrypted":true}

对明文文件实测输出 {"encrypted":false}。参数仅有 --input-type。

8.2 sops completion

生成 shell 补全脚本(3.13.3 支持 bash/zsh):

sops completion bash | head -5
sops completion bash | wc -l

实测回显:

#! /bin/bash

_cli_bash_autocomplete() {
  if [[ "${COMP_WORDS[0]}" != "source" ]]; then
    local cur opts base
17

加载方式 source <(sops completion bash)(写入 ~/.bashrc 持久化)。

8.3 sops keyservice 与 publish

  • sops keyservice:启动本地密钥服务进程(--network tcp/unix,默认监听 127.0.0.1:5000,--prompt 对每个请求人工确认)。客户端用 --keyservice tcp://地址:5000 把解密请求转发给它,适合把 KMS 权限集中在跳板机的架构。
  • sops publish:把文件(--recursive 支持目录)解密或原样发布到配置目标(.sops.yaml 的 destinations 段配置),--yes 免确认、--omit-extensions 去掉目标路径后缀。两者本文按官方参数说明收录,未做端到端实测。

九、与 git 集成:密文仓库的明文 diff

目标:git diff 直接显示解密后的明文变化,让 code review 像普通文件一样进行。两步配置 + 实测验证。

  1. 仓库内 .gitattributes 声明哪些文件用 sops diff 驱动:
cd /opt/demo/gitdemo
cat > .gitattributes <<'EOF'
*.yaml diff=sops
*.json diff=sops
*.env diff=sops
EOF
  1. 注册 textconv(容错版:密文用 sops 解密,明文/临时态直接 cat,避免 "sops metadata not found" 中断 diff):
git config diff.sops.textconv 'sops --decrypt "$@" 2>/dev/null || cat "$@"'
git config diff.sops.textconv
sops --decrypt "$@" 2>/dev/null || cat "$@"
  1. 实测:提交一版加密文件,修改后 diff。首次提交(把加密后的 app-dev.yaml 入库):
git init -q && git config user.email admin@example.com && git config user.name admin
git add .gitattributes app-dev.yaml && git commit -qm "init"

把 debug 字段从 true 改为 false 后重新加密,再看 diff:

sops --encrypt --in-place app-dev.yaml
git diff app-dev.yaml

实测回显(密文差异被还原为明文差异,这正是集成的价值):

diff --git a/app-dev.yaml b/app-dev.yaml
index 310b7b4..b051a91 100644
--- a/app-dev.yaml
+++ b/app-dev.yaml
@@ -1,3 +1,3 @@
 app: order-service
-debug: true
+debug: false
 db_password: q1w2e3r4

要点说明:diff=sops 声明驱动名;diff.sops.textconv 定义"把任意版本还原成可读形态"的命令,git 把新旧两版分别经 textconv 再对比;"$@" 是 git 调用 textconv 时传入文件参数的引用写法。⚠️ 若 textconv 不加容错(只写 sops --decrypt),对明文文件执行 diff 会报 sops metadata not found 与 fatal: unable to read files to diff(实测踩坑,务必带 || cat "$@" 兜底)。

十、注意事项

  1. 改了未加密字段必须重加密:MAC 覆盖整个文件(含明文键名),手工编辑密文文件的任何内容后直接解密会报 MAC mismatch(实测报错见 FAQ Q1)。合规修改路径只有两条:sops edit 或 sops set/unset。使用 --encrypted-regex 局部加密且允许脚本改明文字段的文件,加密时加 --mac-only-encrypted 让 MAC 只覆盖加密值(sops --help 全局参数,实测存在)。
  2. 接收者参数只在加密新文件时生效:--age/-k/-p 等主密钥参数对「已加密文件」的解密无效(解密自动使用文件元数据里的密钥);改已有文件的密钥集合用 updatekeys/groups/rotate。官方 help 原文亦提示 "The -p, -k, ... flags are only used to encrypt new documents"。
  3. 参数必须写在文件名之前:官方 help 明确 "flags must always be provided before the filename to operate on. Otherwise, they will be ignored."——sops file --decrypt 这种写法参数会被静默忽略。
  4. stdin 读入要配 --filename-override:从管道加密/解密时 sops 无法从扩展名判断格式,需显式 --input-type 或 --filename-override demo.yaml(模拟一个文件名)。
  5. 私钥文件权限:age 私钥保持 600;经 SOPS_AGE_KEY_FILE 指向的路径不要放在会被 Git 提交的目录里。

十一、常见问题(FAQ)

Q1:解密报 "MAC mismatch. File has ..., computed ..."(实测原文):

MAC mismatch. File has F03015B455A4E1B3D102AE23651BAD941A1E109D1DD7F08C77757F7BED404347111227F7EA76C0434407196641BFD9235C9A182749C8DF04D3626812FC77DEF5, computed 8630AD07DD37D3F298FBAE0588432180CDC8A6275A108868412C0682993CF3D1E37BF4FDF0E2BBCD29AA7FFE03F2E6C2BF9A603E107942E3BFDE76D0B15C7AAA

原因:文件在加密后被改动过(实测复现:对局部加密的 mixed.yaml 手工把明文字段 region: cn-north-1 改成 cn-south-1 后解密即报此错)。处理:确认改动是否本人所为——是,则用 sops --decrypt --ignore-mac 临时读取(实测可绕过并取到改动后的值),随后立即用 sops edit/sops set 正式修改并重加密;不是,则文件可能被篡改,走安全事件流程。

Q2:解密报 "Could not decrypt with AES_GCM: cipher: message authentication failed"(实测原文):

Error decrypting tree: Error walking tree: Could not decrypt value: Could not decrypt with AES_GCM: cipher: message authentication failed

原因:某个值的密文体(iv/data/tag)被改动,GCM 认证失败(实测复现:篡改密文中的 iv 字段任意一个字符即触发)。这与 Q1 不同:Q1 是"内容与 MAC 记录不一致"(可 ignore),本错误是"密文本身解不开"(不可恢复)。处理:从 Git 历史或备份恢复该文件版本。

Q3:解密报 "Recovery failed because no master key was able to decrypt the file"(实测原文):

Recovery failed because no master key was able to decrypt the file. In
order for SOPS to recover the file, at least one key has to be successful,
but none were.

原因与处理见安装篇 FAQ Q5:私钥缺失/路径不对/不是该文件的接收者。实测验证:把 /root/.config/sops/age/keys.txt 移走即复现,移回即恢复;多接收者文件只需任意一把私钥匹配即可解密。

Q4:sops rotate -a <公钥> 报 "flag provided but not defined: -a"? rotate 子命令不支持全局的 -a/--age 简写,要用动词形式 --add-age <公钥>(添加)/--rm-age <公钥>(移除),可多次出现(实测正确命令见 4.3 节)。

Q5:sops encrypt --encrypted-only 报 "flag provided but not defined: -encrypted-only"? --encrypted-only 是 age 命令的参数,不是 sops 的。sops 的加密范围控制参数是 --encrypted-regex/--encrypted-suffix(圈定加密)与 --mac-only-encrypted(MAC 只算加密值),实测参数清单见 4.1 节表格。

Q6:sops set secrets.yaml '["api"]["timeout"]' 报 "no file specified, or index and value are missing"? set 的参数顺序是 sops set <文件> <路径> <值> 三段,文件名必须在最前(实测报错后按此顺序成功,见 5.2 节);路径引号建议用单引号包整段 ["api"]["timeout"],避免 shell 吞掉双引号。

Q7:git diff 报 "sops metadata not found" 和 "fatal: unable to read files to diff"? textconv 对工作区里的明文文件(还没加密,或刚生成)执行 sops --decrypt 失败导致。用容错版 textconv(第九章第 2 步的 ... || cat "$@" 写法)即可同时兼容密文与明文。

Q8:加密后的 YAML 缩进从 2 空格变成了 4 空格,能控制吗? 能。sops 重写文件时使用自己的序列化风格,--indent 参数(encrypt/edit 全局参数,默认 0)控制 YAML/JSON 缩进空格数;对缩进敏感的流水线建议首次加密时显式指定并与团队约定统一。

十二、参考链接

  1. sops 官方仓库与 README:https://github.com/getsops/sops
  2. sops v3.13.3 Release 页:https://github.com/getsops/sops/releases/tag/v3.13.3
  3. age 官方仓库(密钥后端):https://github.com/FiloSottile/age
  4. 本系列配套文档:《sops 3.13.3 介绍与选型指南》、《Ubuntu 22.04 安装 sops 3.13.3 指南》