体验 Age + SOPS 的轻量化配置文件加密方案

前两天从 AI 那里听说 age + SOPS,不了解,学习看看。

安装 age & SOPS

age 是使用 go 编写的现代、简单的文件加密工具,主打易用和安全,支持用 age 原生密钥、SSH 密钥等对文件进行加密解密,它常作为 SOPS 的底层加密方式之一,负责真正完成密钥加密和解密。

$ brew install age sops

查看版本

$ age --version
v1.3.1

$ sops --version
sops 3.13.3 (latest)

使用 age 加解密文件

生成加解密用到公钥、私钥。

$ age-keygen -o key.txt
Public key: age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru

打印出来的 age14t32rzna4... 即公钥,可以分发,私钥存放在 key.txt 中,应该妥善保管,不能泄露

以下是 key.txt 文件内容的示例:

# created: 2026-08-29T08:55:01+08:00
# public key: age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru
AGE-SECRET-KEY-1LHSCMTGXMWC5229EXTMELTVQPQ0C05ADC5W5FWE6VH8CSMFGTRXS0RM98M

加密

$ echo 'hello world' > hello.txt

$ age -r age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru hello.txt > hello.txt.age

在当前目录会生成 hello.txt.age 加密后的文件,内容如下:

age-encryption.org/v1
-> X25519 tTkSP4qyuUXckkhXBHAqn7eEew6PBH4bRwpwouxccko
OtWK+IwbXZy26BAyv/pdHxjyl3pN2UdpqfBkXwtbayQ
--- Z7PzqnySRHiVm3wQoylR3Fn2SYForQgz1C11PuWq0pQ
ú$<8c>æ<9b>;¹<9d>U-SCz+Ó<91>[¦FB¿<83>ÄBQ<9d>©zM%½¯<90>`ù/Û^K¸a^^4Ár

解密

$ age --decrypt -i key.txt hello.txt.age > hello.dec.txt

解密后的 hello.dec.txt 内容等同于 hello.txt

使用上很简单,对 age 了解这么多就好,另外可以思考下:

  1. age 相较于传统强大的 GPG/OpenPGP 解决了什么问题?使用简单、密钥对简洁、更少的历史包袱、更适合脚本和 DevOps 工具
  2. age 能使用 ~/.ssh 目录下已存在的密钥对吗?格式上能兼容使用,但不推荐不同用途的密钥对混用

SOPS:Secrets OPerationS

它是专门用来安全管理配置文件中敏感字段的加密工具,没有它的保护,硬编码密钥的配置文件看起来是这样的:

config.yaml

s3:
  endpoint: https://s3.example.com
  bucket: my-app-backup
  access_key_id: AKIAEXAMPLE123456
  secret_access_key: example-secret-access-key

postgresql:
  dsn: postgresql://app_user:example_password@db.example.com:5432/app_db?sslmode=require

配置文件有 N 种可能被泄露的方式,接下来看使用 SOPS 加密后的配置文件:

$ sops --encrypt \
    --age age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru \
    config.yaml > config.enc.yaml

config.enc.yaml

s3:
    endpoint: ENC[AES256_GCM,data:oDPGD9bCSlNAaOtXCprzFhTQfkUleA==,iv:wyyPhrb+xhIe6mtR0yovlEtjZu8PYhay+XJmbavTF8A=,tag:m6jCarHl9+qxNHzS+X1OMw==,type:str]
    bucket: ENC[AES256_GCM,data:hE+oekVYvoedDx1fNw==,iv:IVcGNeOQeZzZmsmvXI8t3QLtV0bdGioctx9bDRAeujo=,tag:579Jhc/rJ4Au+HeynwVlCg==,type:str]
    access_key_id: ENC[AES256_GCM,data:cPax5qWeGuCEVDps7aCTuIU=,iv:8nAjb1/ObTWw0AxGc88dxGF/F6/o4Le6jXmFJNN+vo0=,tag:FKi8FVthEGb4tzKBZXAU7Q==,type:str]
    secret_access_key: ENC[AES256_GCM,data:tzH26GxxIPmuIqQz7ubmMjeeVMhtt9pp6w==,iv:sDl1HoPiprQv5iklT7ayKxyZwoECOlQzyiG7h95kpdU=,tag:p485EjV9fOIG8IOZiJFjNQ==,type:str]
postgresql:
    dsn: ENC[AES256_GCM,data:biJkveus/ptrJYqpHVGLayn+s6kaYZeztuxjjlzEhh/qaByea81tIgXHVsd/yqATAaWIMeI9G947fHfBZwmHMv53c59noIbk/G6uhGFw7ZPb,iv:3e2HFFJbEzWfpXgFsAUR8OdMhj4GPncy6m73Yh8iXZc=,tag:3oanfjNud5lRw94pf9P3qA==,type:str]
sops:
    age:
        - enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBna1dPaDNnYzFpbUx0RmJF
            RW9vcUZwUnA0aDBvTXVzZlViWmdnUWkvbmxjCkVPN29HeklvU0VEUXRYOUdtbFlT
            Y3kvdVRYbXRXdHlJMGJnSytwOUd0WncKLS0tIFJXTlRMRnFUcWh6MVk5TzVJekZZ
            TTg3UU5rQVQ2S29vMWdMemtKZHY1M1kKSUFPo0k+vPCOktIyDYAr+G7iAndE2yGO
            Ks7i2EIevHNBeWZJP5fOVR0jB0iHICbYlaYkAv3a0vZ7inHYQ90X8g==
            -----END AGE ENCRYPTED FILE-----
          recipient: age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru
    lastmodified: "2026-08-29T01:34:08Z"
    mac: ENC[AES256_GCM,data:eC5Ix0QYRQUe3s2IVq21e4ZQDgig5vfIKfqYhlBhBN0hhMn93K5WU9MjEUSqlq3VbUNXkBw1MlhOGDw2sLC3mkvcah+G08V2rTB9InIy74M8RxiorFaBmUKsgq4tzRrUxSX0/k9FMo5UtJwOFZlr+J/SxmPie9haJD/e1YYf0/4=,iv:839fq/lqbjguY5ehJuAU53QhsyJeohuXk1NjJ46yxoo=,tag:5uQxMofwIhi4qhYlAWcqsw==,type:str]
    unencrypted_suffix: _unencrypted
    version: 3.13.3

可以安全感满满的提交到 Git 仓库中了,解密需要先将私钥文件设置环境变量

$ export SOPS_AGE_KEY_FILE="$PWD/key.txt"

# 或直接设置 KEY 内容
$ export SOPS_AGE_KEY="$(cat key.txt)"

解密

# 打印到终端
$ sops --decrypt config.enc.yaml

# 输出到文件
$ sops --decrypt config.enc.yaml > config.dec.yaml

以上的命令是默认的全配置文件加密,实际使用上只会对特定字段进行保护。使用正则表达式:

$ sops --encrypt \
    --age age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru \
    --encrypted-regex '^(secret_access_key|dsn)$' \
    config.yaml > config.enc.yaml

密文变为

s3:
    endpoint: https://s3.example.com
    bucket: my-app-backup
    access_key_id: AKIAEXAMPLE123456
    secret_access_key: ENC[AES256_GCM,data:iqeYDj2XeR1Nen1amnnX4CHly7ecdvDFJw==,iv:KuitwA6pFNVGmcXpebm5ww2gmvCg48fMA8Ly5YoYOss=,tag:Hvpe8VhjXWEUKeNpfZn3nA==,type:str]
postgresql:
    dsn: ENC[AES256_GCM,data:cXhMxahcKvzzVCljQoAwDkZAn6kosuKKajz+hCurOK4oObgLNDe7lWzPSZyCOgCyvokh/ndmTzxszQJmuwX181FRgGDf6A6OWhuiBAoo2J6y,iv:rEBpzK9EIz/qhKwEeRsiPfKsCRnjqjfbdqZZgftWb6M=,tag:gWiXCVHTPERQBhR/1gdcnA==,type:str]
sops:
    age:
        - enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqb0IvbFdiRjE0UXNCTUUx
            WnA1R1lyTFVkMVdma3ZGS2V6QUhpanhjcHk4CnBmdVI0S2d3c1BzaVZ1NlhzaU91
            ZHVZdk95SVdNQUZIeGRBTWdjU1YvOG8KLS0tIHp0Vmh3bjFCMkVzT3dKUWFIZ2ln
            UUZ0aUwvazRpVWttNFRWT0JBSUNuYjAK7z+A8k1ieZS9T5jgsL08Qh9xxYBdhDC3
            7lYjTrbHAS0Ln5/H4jaLsjznVaa+COAgiUP1aRll5ttrxSarYaVPkg==
            -----END AGE ENCRYPTED FILE-----
          recipient: age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru
    encrypted_regex: ^(secret_access_key|dsn)$
    lastmodified: "2026-08-29T01:44:08Z"
    mac: ENC[AES256_GCM,data:MEPLH/+uflaeYsbklr4eEqV3g64jf94LnsSShPoZH++FPy3YuYMaWQDHFFJSXcwrfXa1WlzZN2LWycSGPpkjrtQA31D4CDn8DR2tzV9gJyfIupQa0ZQV+6SVft1Y4vRy7AahZ39WjD/78LZVmr8LkDn+1Z/77IrHHLgTdnQWqrY=,iv:aWF4zNVqpVoPrCGHcUHR7/MZ2jydg5mbgpjpjYuwAfQ=,tag:UZ5rTYwegNShXTwgKvc5Tw==,type:str]
    version: 3.13.3

这样,配置文件仍适合人类阅读,仅密钥相关字段被加密,在项目中使用,可以把加密字段的配置添加到 .sops.yaml 中

creation_rules:
  - path_regex: config\.yaml$
    age: age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru
    encrypted_regex: '^(secret_access_key|dsn)$'

实际使用也会遇到另外的问题:

  1. Git 仓库中提交的是加密的配置文件,是 config.yaml(SOPS 加密)还是 config.enc.yaml 呢?SOPS 推荐 config.yaml
  2. 本地电脑启动服务的时候,需要将 config.yaml(SOPS 加密)解密到临时文件供程序读取,或者 sops -d config.yaml | ./app 然后程序从 Stdin 读取,我更倾向于前者

SOPS 轮换密钥对

假如你的私钥 key.txt 不小心被 AI 读取提交到了远端,你担心泄露,那么可以生成新的密钥对替换。

根据建议,SOPS 加密文件本身就叫 config.yaml,不用额外加 .enc。先做一点调整,用之前生成的 config.enc.yaml 覆盖 config.yaml

$ mv config.enc.yaml config.yaml

生成新的密钥对

$ age-keygen -o new-key.txt
Public key: age1qz0sk8u6xktawtxdrve3le4mga89y8qvztgg53p24wkq8mz3hvesqv0udp

修改 .sops.yaml 配置替换公钥(如果你在之前的步骤有创建)

$ sops updatekeys config.yaml

输出

2026/08/29 10:15:20 Syncing keys for file /Users/Username/config.yaml
The following changes will be made to the file's groups:
Group 1
+++ age1qz0sk8u6xktawtxdrve3le4mga89y8qvztgg53p24wkq8mz3hvesqv0udp
--- age14t32rzna4zfd4wen58k4cdeqfe4c34xn826626s8dkd4evhkha3su4wqru
Is this okay? (y/n):

这里提示是否替换,输入 ‘y’

接下来直接执行 sops --decrypt config.yaml 预计会失败,因为配置文件已更新,环境变量使用的仍是旧的私钥。

Failed to get the data key required to decrypt the SOPS file.

Group 0: FAILED
  age1qz0sk8u6xktawtxdrve3le4mga89y8qvztgg53p24wkq8mz3hvesqv0udp: FAILED
    - | failed to create reader for decrypting sops data key with
      | age: no identity matched any of the recipients. Did not find
      | keys in locations 'SOPS_AGE_SSH_PRIVATE_KEY_FILE',
      | 'SOPS_AGE_SSH_PRIVATE_KEY_CMD', 'SOPS_AGE_KEY', and
      | 'SOPS_AGE_KEY_CMD'.

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.

设置新的私钥:export SOPS_AGE_KEY_FILE="$PWD/new-key.txt"

然后再次解密:

$ sops --decrypt config.yaml

打印出了我们原始的配置文件

s3:
    endpoint: https://s3.example.com
    bucket: my-app-backup
    access_key_id: AKIAEXAMPLE123456
    secret_access_key: example-secret-access-key
postgresql:
    dsn: postgresql://app_user:example_password@db.example.com:5432/app_db?sslmode=require

打开加密后的配置文件,可以看到这样一段:

sops:
  age:
    - enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
      recipient: age1qz0...

这里的 enc 块是用 age 公钥加密后的 SOPS data key

SOPS 随机生成 data key

 data key 加密 secret_access_key / dsn

再用 age recipient(公钥) 加密 data key

 “加密后的 data key” 存到 sops.age[].enc

说了这么多,就是说变更密钥对后,data key 也是建议轮换的,我们可以执行以下命令获取到原始的 data key

$ yq -r '.sops.age[0].enc' config.yaml \
    | age --decrypt -i new-key.txt \
    | xxd -p -c 256

执行后输出:98a1a5c148e19199d511ddf93f2d0c3c60cc0bc36edaf2305b040e03ef33c1a5 (256-bit data key)

执行以下命令生成新的 data key 并重新加密字段,执行后再次执行上方命令会获得新的 data key

sops rotate --in-place config.yaml

配置文件字段的加密算法

上方我们获取到 data key,了解 SOPS 是用这个 key 来进行实际的加解密,那猜测大概率是 AES 加密,翻到示例配置文件得到被加密的字段

secret_access_key: ENC[AES256_GCM,data:85KAPsMp1aaqIOM7L1xnLvOys6Dm+Pub3w==,iv:KuitwA6pFNVGmcXpebm5ww2gmvCg48fMA8Ly5YoYOss=,tag:x+ESVatZQIJk2UEWH9SInA==,type:str]

AES256_GCM 表明是 AES,GCM 是 AES 的一种工作模式,全称是 Galois/Counter Mode(伽罗瓦/计数器模式),256 是使用 256-bit 密钥,跟上方获取的 data key 长度一致。

我们有了数据字段,有了 data key,可以自己写脚本解密数据:

package main

import (
    "encoding/hex"
    "fmt"
    "log"

    sopsAES "github.com/getsops/sops/v3/aes"
)

func main() {
    // SOPS data key:32 bytes / 256 bits
    dataKeyHex := "98a1a5c148e19199d511ddf93f2d0c3c60cc0bc36edaf2305b040e03ef33c1a5"

    // 最新的 SOPS 加密字段
    encryptedValue := `ENC[AES256_GCM,data:85KAPsMp1aaqIOM7L1xnLvOys6Dm+Pub3w==,iv:KuitwA6pFNVGmcXpebm5ww2gmvCg48fMA8Ly5YoYOss=,tag:x+ESVatZQIJk2UEWH9SInA==,type:str]`

    // YAML 路径:
    //
    // s3:
    //   secret_access_key: ENC[...]
    //
    // SOPS 将路径作为 AES-GCM 的 AAD
    additionalData := "s3:secret_access_key:"

    dataKey, err := hex.DecodeString(dataKeyHex)
    if err != nil {
        log.Fatalf("decode data key failed: %v", err)
    }

    if len(dataKey) != 32 {
        log.Fatalf("invalid data key length: %d bytes", len(dataKey))
    }

    cipher := sopsAES.NewCipher()

    plaintext, err := cipher.Decrypt(
        encryptedValue,
        dataKey,
        additionalData,
    )
    if err != nil {
        log.Fatalf("decrypt failed: %v", err)
    }

    fmt.Printf("plaintext: %v\n", plaintext)
}

执行输出

$ go run main.go
plaintext: example-secret-access-key

打印出的内容正是上方配置文件 secret_access_key 字段值,所以 data key 也不能泄露。

其它 sops 特性和使用方法

直接编辑加密文件

$ sops config.yaml

获取部分解密后字段值

$ sops -d config.yaml | yq '.postgresql.dsn'

SOPS 不仅支持 age 还支持其它加密平台和方案、多人协作的仓库如何管理,都值得实践,等有使用到再探索。

本文只是了解和体验,就先到这里。