关于 sops-mcp
sops-mcp 是使用 age 加密创建和管理 SOPS 密钥文件的 MCP 服务,面向需要将加密密钥纳入代码库的开发者。明文不会经由 MCP 返回给客户端,且服务没有客户端文件系统访问权限。
主要功能
- 01创建和列出加密密钥
- 02生成、轮换及更新密钥
- 03支持生成、外部和派生密钥
- 04使用命名密钥域管理接收方
- 05重命名、删除和重新加密密钥文件
适用场景
- 01将加密密钥文件提交到代码库
- 02在部署时由 CI/CD 解密密钥
- 03轮换密钥并自动重算派生值
MCP 相关
向 MCP 客户端提供创建和管理 SOPS 加密文件的工具。工具通过文本参数接收和返回内容,不提供解密单个密钥的工具,也不访问客户端文件系统。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx sops-mcpSOPS_MCP_AGE_PUBLIC_KEY密钥Age recipient public key. Required at startup.SOPS_AGE_KEY密钥Age private key. Required at call time for any mutation tool (decrypt + re-encrypt). Read-only tools do not need it.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
sops-mcp
用于创建和管理通过 age 加密的 SOPS 密文文件的 MCP 服务。
专为 Claude Code(或任何 MCP 客户端)设计,可生成加密的 secrets.enc.yaml 文件,同时确保模型始终看不到明文值。所有文件内容都以文本参数的形式传入,并以文本形式返回;该服务无法访问客户端的文件系统。
为什么
设计由两个目标驱动:
1. 将密钥保存在源代码树中,同时避免泄露。 对于小型项目而言,为少数凭据运行完整的密钥管理器(Vault、AWS Secrets Manager 等)有些过度。在 git 中加密静态密钥,并在部署时由 CI/CD 流水线解密,成本低得多:
- 通过此服务创建
secrets.enc.yaml——使用 age 公钥加密,可安全提交。 - 将它与代码一同提交。
- CI/CD 流水线保存 age 私钥,在部署时解密,并将明文作为环境变量注入容器编排器。
age 私钥只会存放在一个地方:CI/CD 密钥库。其他所有地方——你的笔记本电脑、git 远程仓库、容器镜像——看到的都只是密文。请参阅下方的实践示例。
2. 让 AI 编码 Agent 生成它永远无法读取的密钥。 Claude(或任何 MCP 客户端)可以创建密码、轮换密码、派生哈希、重命名和删除密码——但明文值绝不会通过 MCP 边界返回给模型。 服务持有加密密钥;客户端只提交请求并接收元数据。 这里有意不提供“解密单个密钥”的工具。 这样可以防止提示词注入或行为异常的 Agent 窃取密钥。
最简单的配置使用单个 age 接收方(即 CI 私钥)。如果你需要多个接收方——例如 CI 密钥加操作员密钥,或供不同参与方使用的独立密钥组——请参阅密钥域。
密钥来源
每个密钥都属于以下三种来源之一,并记录在 _meta_unencrypted 中:
generated— 通过加密安全随机数生成的值(Pythonsecrets/ OS CSPRNG)。你指定长度和字符集;服务会同时保存这些信息,以便轮换时重新生成。external— 按原样加密的用户提供值(SMTP 凭据、第三方 API 密钥等)。轮换时会保留。通过sops_update_external更新。derived— 通过指定名称的转换,从同一文件中的另一个密钥计算得出。当源密钥轮换(或外部来源更新)时,派生值会按拓扑顺序自动重新计算。适用于 Authelia 的 OIDC 客户端密钥 PBKDF2 哈希等场景。
设计
以下三个理念决定了工具的设计:
- 明文不会通过 MCP 边界传递。 生成的密钥值绝不会返回给客户端。这里有意不提供“解密单个密钥”的工具。如果需要明文,请自行使用 age 私钥运行
sops decrypt。 - 元数据以明文保存。
_meta_unencrypted块与加密值并列存放(使用 SOPS 的unencrypted_suffix功能),记录每个密钥的来源、生成方式、上次轮换时间,以及所属的密钥域。这样服务无需解密即可列出和轮换密钥。默认情况下,SOPS 的 MAC 会覆盖这些值,因此被篡改的块将无法解密。启用mac_only_encrypted关闭此功能的文件会被拒绝。即使不使用密钥读取该块的工具也无法检查 MAC,因此还需要单独验证接收方。 - 不支持原地更新
generated或derived密钥的值。 这些密钥只能通过轮换更改,此时服务和调用方都无法访问明文。external密钥(例如用户掌控的上游 API 密钥)可以通过sops_update_external更新。
转换(用于 derived 秘密)
| 转换 | 用途 | 是否确定性 |
|---|---|---|
pbkdf2_sha512_authelia | Authelia 的 configuration.yml 格式的 PBKDF2-SHA512 哈希($pbkdf2-sha512$310000$...) | 否 — 每次调用都会使用随机盐值 |
sha256_hex | 十六进制编码的 SHA-256 摘要 | 是 |
工具
创建和列出
| 工具 | 功能 |
|---|---|
sops_create_secrets | 创建包含一个或多个秘密的新加密文件(可混合使用任意来源)。 |
sops_list_secrets | 列出文件中的键、来源和描述,不会解密。 |
sops_create_oidc_secret | 便捷工具:一次调用即可将 Authelia OIDC 客户端秘密创建为一对 generated + derived(pbkdf2_sha512_authelia)秘密。哈希会在响应中返回,可粘贴到 configuration.yml 中。 |
sops_create_k8s_secret | 创建 Kubernetes Secret 清单,并将值放在 stringData 下,加密后只有 stringData 是密文。仅支持创建:所有变更工具都会拒绝处理结果。请参阅 Kubernetes Secret 清单。 |
sops_list_domains | 列出已配置的密钥域、其 age 接收方,以及每个域是否可以解密或只能加密。绝不返回私钥材料。 |
变更(需要目标域的私钥)
| 工具 | 功能 |
|---|---|
sops_rotate_generated | 重新生成所有 generated 秘密。其来源已轮换的派生秘密会重新计算;其他秘密保持不变。外部秘密保持不变。 |
sops_add_secrets | 向现有文件添加新秘密。支持全部三种来源。若与现有键冲突则拒绝。 |
sops_update_external | 替换 external 秘密的值。会连带更新任何引用它的派生秘密。拒绝更新 generated 或 derived 秘密。 |
sops_rename_secret | 重命名键,同时保留其值和元数据。更新任何派生秘密中的 from: 引用。 |
sops_delete_secrets | 移除一个或多个键。如果某个秘密仍被另一个派生秘密引用,则拒绝删除(除非在同一次调用中也删除该依赖项)。 |
sops_add_metadata | 为缺少 _meta_unencrypted 的旧版 SOPS 文件补充该字段。支持 generated、external 和 derived 条目。 |
sops_rekey | 使用所属域当前的接收方列表重新加密文件。在域的接收方发生变化后,或要清除“接收方不匹配”的拒绝提示时运行。需要显式指定 domain。不会改变缺少元数据块的旧版文件,因此仍可对其运行 sops_add_metadata。 |
除 sops_list_domains 外,每个工具都需要 domain 参数。除 sops_rekey 外,该参数在所有工具中均为可选;sops_rekey 必须指定该参数,因为调用会更改哪些人可以读取文件。请参阅 密钥域。
设置
前置条件
- Python 3.11+
- sops CLI 二进制文件
- 一对 age 密钥(见下文)
生成一对 age 密钥
如果你还没有密钥,请安装 age 并运行:
age-keygen -o age-key.txt
文件内容如下:
# created: 2026-04-22T12:34:56Z
# public key: age1abc...xyz
AGE-SECRET-KEY-1HH...
- 公钥(
age1...)— 通过SOPS_MCP_AGE_PUBLIC_KEY传给此服务。可安全地分享给任何人。 - 私钥(
AGE-SECRET-KEY-...)— 将其存储为 CI/CD 秘密(通常命名为SOPS_AGE_KEY)。切勿提交到源代码管理中。任何持有此密钥的人都可以解密所有使用匹配公钥加密的secrets.enc.yaml。
请将私钥备份到安全的位置(密码管理器、硬件令牌)。丢失私钥意味着将失去对所有已加密秘密的访问权限。
安装
git clone <repo-url>
cd sops-mcp
python3 -m venv .venv
.venv/bin/pip install -e .
Claude Code 配置
在项目的 .mcp.json 中添加:
{
"mcpServers": {
"sops-mcp": {
"command": "/path/to/sops-mcp/.venv/bin/python",
"args": ["-m", "sops_mcp"],
"env": {
"SOPS_MCP_SOPS_BINARY": "/path/to/sops",
"SOPS_MCP_AGE_PUBLIC_KEY": "<your-age-public-key>"
}
}
}
}
环境变量
| 变量 | 必填 | 用途 |
|---|---|---|
SOPS_MCP_AGE_PUBLIC_KEY | 是* | 用于加密的 Age 公钥 |
SOPS_AGE_RECIPIENTS | 是* | SOPS_MCP_AGE_PUBLIC_KEY 的替代项 |
SOPS_MCP_SOPS_BINARY | 否 | sops 二进制文件的路径(默认值:sops) |
SOPS_MCP_LOG_LEVEL | 否 | 日志级别(默认值:WARNING) |
SOPS_AGE_KEY | 有时 | default 域的 Age 私钥——修改属于该域的文件时必需。命名域从域文件中获取其密钥。 |
SOPS_MCP_DOMAINS_FILE | 否 | 定义命名密钥域的 YAML 文件路径。当一个服务需要多组接收方时使用。 |
SOPS_MCP_DOMAINS | 否 | 以 内联形式提供相同的域文档,格式为 YAML 或紧凑 JSON。仅支持公开的接收方集合——此处的域不能包含 keys 或 key_file。与 SOPS_MCP_DOMAINS_FILE 合并。 |
SOPS_MCP_REQUIRE_DOMAIN | 否 | 设为 1,要求每次工具调用都指定其域。禁用文件中记录的域和 default 回退。 |
SOPS_MCP_TRANSPORT | 否 | stdio(默认值)或 sse |
SOPS_MCP_HOST / SOPS_MCP_PORT | 否 | SSE 传输的绑定主机/端口(默认值:127.0.0.1:55090)。绑定到 0.0.0.0 时必须设置 SOPS_MCP_API_TOKEN,否则服务会拒绝启动。 |
SOPS_MCP_ALLOWED_HOSTS | 否 | SSE Host 标头的逗号分隔允许列表(DNS 重绑定防护)。默认值:127.0.0.1,127.0.0.1:*,localhost,localhost:*。绑定到非环回地址时请显式设置,例如:mcp.example.com,mcp.example.com:*。 |
SOPS_MCP_API_TOKEN | 有时 | SSE 传输绑定到 0.0.0.0 时必需;其他情况下可选。设置后,SSE 要求提供 Authorization: Bearer <token>。 |
* 必须设置 SOPS_MCP_AGE_PUBLIC_KEY 或 SOPS_AGE_RECIPIENTS 其中之一,除非 SOPS_MCP_DOMAINS_FILE 或 SOPS_MCP_DOMAINS 至少提供一个域。没有任何域时,服务会拒绝启动。
两个接收方变量都接受逗号分隔的列表,SOPS_AGE_KEY 则接受多个以换行符分隔的密钥。它们共同构成 default 域。
密钥域
域是一组具名的 Age 接收方,还可以包含服务持有的相应私钥。它是服务用于加密和解密的单位。
你无需配置域。上述环境变量会成为名为 default 的域,单接收方配置无需再提及域。
域的用途
每个变更工具都会先解密,再修改并重新加密。重新加密时会使用服务配置的接收方——因此,如果一个文件同时加密给 CI 密钥和运维人员密钥,而服务只知道 CI 密钥,运维人员的密钥过去会被悄然从文件中移除。域通过将接收方集合设为具名且经过校验的对象来解决这一问题:
- 变更操作会将文件的实际接收方与域中的接收方进行比较。如果两者不同,操作会拒绝执行,而不是悄悄改变文件的可读取对象。
- 一个服务进程可以持有多组相互独立的密钥。
sops_rekey会有意更改接收方,这就是sops updatekeys工作流。
多个接收方,一个域
常见情况无需域文件。只需在常用变量中列出所有接收方:
SOPS_MCP_AGE_PUBLIC_KEY="age1ci...,age1operator..."
SOPS_AGE_KEY="AGE-SECRET-KEY-1CI..."
现在文件会同时加密给两者,变更操作也会保留两者。服务只需要实际用于解密的私钥。
多个域
将 SOPS_MCP_DOMAINS_FILE 指向一个 YAML 文件:
version: 1
domains:
homelab:
recipients:
- age1ci... # CI runner
- age1operator... # operator laptop
keys:
- AGE-SECRET-KEY-1CI...
client-acme:
recipients:
- age1acme...
key_file: /run/secrets/acme.agekey # an age keys file
archive:
recipients:
- age1archive...
# no keys: this domain can encrypt but never decrypt
version: 的含义
version: 是这份配置文档的架构版本——定义域可以包含哪些字段,以及这些字段的排列方式。它不是密钥、接收方或任何轮换内容的版本。轮换某个定义域的接收方不会改变它。它会一直保持为 1,直到某个版本发布更改了文档格式本身。
此字段为可选;省略时默认使用 1。如果存在,则必须完全匹配,因此只支持 1 的服务会拒绝 version: 2 文档,而不会只读取其中一部分。此字段的作用就在于此:如果某个定义域包含无法识别的字段,就会被拒绝;因此,如果不检查版本,旧版服务就会误以为是某个字段名称有问题,而实际问题是整份文档的版本比它支持的更新。
注意,密钥文件 _meta_unencrypted 块中的 version 是另一个数字,用于标记该文件中元数据架构的版本。两者互不相关,而且恰好都是 1。
定义域文件不得允许组或其他用户写入,并且所有者必须是运行服务的用户或 root。无论文件是否包含密钥材料,都会执行此检查,因为该文件决定了所有内容要加密给哪些接收方。
包含私钥的文件——带内联 keys: 的定义域文件,或任何 key_file——还必须禁止其他用户读取。允许组读取,但会发出警告,因此,以 root 所有且运行时组可读的方式挂载容器密钥是可行的。在已发布的镜像中,服务以 uid 65532 运行,因此 Docker 或 compose 密钥需要设置允许该用户读取的 uid:/gid:/mode:;通常的做法是使用 mode: 0640 并设置匹配的组。
启动时会检查每个私钥是否都能解析,并对其公钥部分不在所属定义域接收方列表中的情况发出警告。这是警告而非错误,因为这正是接收方轮换过程中的正常状态:旧密钥仍可打开更改前加密的文件,而 sops_rekey 可用于迁移这些文件。
不使用文件定义定义域
定义域文件是私钥唯一允许存放的位置,因为只有该文件可以进行权限检查。对于完全没有私钥的定义域来说,这并不合适——这类定义域只有要加密给的接收方密钥集合,密钥由其他人持有。对于不方便写入文件的部署环境(没有 shell 的 distroless 镜像,或不支持内联文件功能的编排器),这意味着必须挂载卷来传递两行公钥材料。
SOPS_MCP_DOMAINS 可以通过内联方式接受相同的文档:
SOPS_MCP_DOMAINS='{"version":1,"domains":{"archive":{"recipients":["age1archive..."]}}}'
也可以使用 YAML;JSON 只是更适合不便使用多行值的环境,而且能够解析,因为 YAML 是 JSON 的超集。
此处的 version 是文档架构版本,而非密钥版本——它与文件形式中的字段相同。
此处定义的定义域不得设置 keys 或 key_file,否则服务会拒绝启动。环境变量可通过 docker inspect 和 /proc/<pid>/environ 查看,而且不具备文件所能进行的所有者与模式检查,因此不适合存放私钥。
两个来源会合并,因此部署可以将持有密钥的定义域放在文件中,将公开定义域以内联方式提供。如果多个来源定义了同一个定义域名称,会直接报错,而不会静默解决冲突——这与文件中同时定义 default 和 v1 环境变量时适用的规则相同。
工具使用哪个定义域?
依次为:domain 参数、文件 _meta_unencrypted 块中记录的定义域,然后是 default。设置 SOPS_MCP_REQUIRE_DOMAIN=1 可移除最后一步,并强制每次调用都显式指定。
记录的名称只是提示。检查依据是文件自身 SOPS 信封中的接收方列表,因此标记错误的文件会被拒绝,而不是重新设置密钥。
接收者轮换
添加或移除接收者分两步进行:
- 编辑域的接收者列表并重启服务。
- 对每个受影响的文件运行
sops_rekey,并指定域。
文件重新加密前,对其进行修改的请求都会被拒绝,并显示指向此处的消息。sops_list_secrets 也会报告不匹配,且无需私钥。
sops_rekey 不会将文件移到其他域。记录了域的文件只能使用该域当前的接收者列表重新加密;若指定其他域,则会被拒绝。这在两个域共用一个私钥时尤其重要:否则解密可能成功,并悄悄移除另一个域没有的接收者。在域之间移动机密信息需要手动操作:使用 sops CLI 解密,然后用 sops_create_secrets 将其写入新域。
此服务不会处理的文件
SOPS 可以用此服务无法复现的方式保护文件,因为此服务始终会使用扁平的 age 接收者列表重新加密:
- 除 age 之外还有其他主密钥(
pgp、kms、gcp_kms、azure_kv、hc_vault)。重新加密会移除该密钥持有者。 - Shamir 密钥组(带有
shamir_threshold的key_groups)。重新加密会将 n-of-m 门限扁平化为任何单个持有者都能解密的列表。 mac_only_encrypted。它会使明文元数据块不受身份验证保护,因此任何能够编辑文件的人都可以改写文件记录的域和每个机密的source。- 其他加密模式:
encrypted_regex、unencrypted_regex、encrypted_suffix、注释正则表达式,或值不是_unencrypted的unencrypted_suffix。此服务会使用--unencrypted-suffix _unencrypted重新加密,因此会改变哪些值以明文形式保留。这也包括sops_create_k8s_secret写入的清单;此类清单会显示专属消息并被拒绝。
这些情况都会导致安全性被悄然降低或文件结构被改动,因此所有修改操作都会拒绝处理这些文件,且 sops_list_secrets 会将其标记出来。请使用 sops CLI 管理这些文件。
运行混合版本
此版本写入的文件仍可由 sops-mcp 0.10.1 及更早版本读取和写入。旧版本不了解 domain 字段,因此下次修改文件时会将其删除,使文件不再记录域。在此服务中,这会被解析为 default,无论如何,接收者检查仍会保护该文件。之后调用任何工具时显式传入 domain,或使用 sops_rekey,即可重新记录域。
域并非用于隔离调用方
域用于隔离密钥集,而非调用方。在 SSE 传输上,一个 API 令牌即可访问服务持有的所有域。如果你需要让双方无法读取彼此的机密信息,请分别运行一个服务进程。
安全性
- 不访问文件系统 — 服务绝不会读取或写入客户端文件系统。所有内容都以文本参数的形式传入,并以文本形式返回。
- 常规使用无需私钥 — 加密只使用公钥。仅修改工具需要私钥。
- 响应中不包含明文 — 生成的机密值绝不会返回。派生值会返回,因为这些值旨在粘贴到配置文件中(例如 Authelia PBKDF2 哈希)——请勿派生你不打算发布的值。
- 安全临时文件 — 仅用于调用 sops CLI。创建时权限为 0600,删除前会用零覆盖,并由
finally块确保清理。 - 操作系统级熵源 — 机密生成使用 Python 的
secrets模块(由/dev/urandom提供支持)。 - 默认使用 stdio 传输 — 不暴露到网络;作为客户端子进程运行。
- 输入验证 — 密钥名称必须匹配
^[A-Z][A-Z0-9_]*$。
加密文件格式
DB_PASSWORD: ENC[AES256_GCM,data:...,tag:...,type:str]
SMTP_USER: ENC[AES256_GCM,data:...,tag:...,type:str]
DB_PASSWORD_HASH: ENC[AES256_GCM,data:...,tag:...,type:str]
_meta_unencrypted:
version: 1
domain: default
secrets:
DB_PASSWORD:
source: generated
description: Database password
generation:
length: 32
charset: alphanumeric
last_rotated: "2026-04-21T15:30:00Z"
SMTP_USER:
source: external
description: SMTP username
DB_PASSWORD_HASH:
source: derived
derivation:
transform: sha256_hex
from: DB_PASSWORD
last_rotated: "2026-04-21T15:30:00Z"
sops:
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
unencrypted_suffix: _unencrypted
机密值使用 AES-256-GCM 加密。_meta_unencrypted 块以明文形式存储(使用 SOPS 的 unencrypted_suffix 功能),因此无需解密即可读取元数据。
此服务每次调用 sops 时都会固定使用空的 --config,因此,服务启动目录中的 .sops.yaml 无法改变文件的加密方式。接收方来自域,除此之外别无其他来源。
上面的 sops: 块经过了删节。实际文件还包含 lastmodified、mac、version,以及此服务不会使用的主密钥类型对应的空列表(pgp、kms、gcp_kms、azure_kv、hc_vault)。MAC 也涵盖未加密的值,因此,编辑 _meta_unencrypted 块会导致解密失败。如果这些其他密钥列表中有任何一个非空,此服务就会拒绝修改文件——它只会使用 age 加密,否则会悄悄丢弃该密钥持有者。
不要改变输出结构
此服务返回的加密内容必须原样提交。SOPS 会将每个值的 AES-GCM 标签绑定到该值在文档中的路径,因此,把值移到另一个键下——例如,将根级别的 DB_PASSWORD 嵌套到 stringData: 下以构建 Kubernetes Secret 清单——即使 ENC[...] 字符串本身未变,也会使标签失效:
$ sops decrypt secrets.enc.yaml
Error decrypting tree: Error walking tree: Could not decrypt value: Could not decrypt with AES_GCM: cipher: message authentication failed
此时该值将无法恢复。手动编辑明文部分也会以同样方式失败,但原因不同:MAC 涵盖未加密的值,因此编辑 _meta_unencrypted 块会导致 MAC mismatch。重新缩进、调整键的顺序或重新排版 YAML 都是安全的——SOPS 会解析文档,因此只有键路径和值会产生影响。
如果需要 Kubernetes Secret,请使用 sops_create_k8s_secret(见下文)按这种结构创建。对于其他任何文档结构,请在部署时根据扁平文件构建(使用 Kustomize 的 secretGenerator、helm-secrets、sops-secrets-operator),而不是改变加密文件的结构。
Kubernetes Secret 清单
sops_create_k8s_secret 会写入完整的 Secret 清单,每个值都已放在 stringData 下对应的路径中,因此之后无需再移动:
apiVersion: v1
kind: Secret
metadata:
name: web-app
namespace: prod
labels:
app.kubernetes.io/name: web
annotations:
sops-mcp.privacyplaybook.com/meta: '{"version":1,"domain":"default","secrets":{"DB_PASSWORD":{"source":"generated","generation":{"length":32,"charset":"alphanumeric"},"last_rotated":"..."}}}'
type: Opaque
stringData:
DB_PASSWORD: ENC[AES256_GCM,data:...,tag:...,type:str]
sops:
...
encrypted_regex: ^(data|stringData)$
- 只有
stringData会加密(--encrypted-regex '^(data|stringData)$')。apiVersion、kind、metadata和type保持可读,sops decrypt、ksops 或 Flux 会将文件转换为可直接供kubectl apply接受的清单。 - 所有明文内容都是参数:
name、namespace、labels、annotations和type。MAC 也会覆盖明文,因此之后不能编辑这些内容。标签和注解键data与stringData会被拒绝,因为 sops 也会加密它们。 - 键名遵循 Kubernetes 的规则(
[-._a-zA-Z0-9]+),而不是环境变量的规则,因此类型化的 Secret 可以使用tls.crt或.dockerconfigjson等键。值以字符串形式放入stringData;此工具绝不会写入 base64 编码的data:。 - 元数据位于
sops-mcp.privacyplaybook.com/meta注解中,格式为 JSON,字段与_meta_unencrypted相同:域,以及每个 Secret 的来源、描述、生成方式和派生信息。此处绝不会存放值。它不能放在根级,因为 kubectl 的严格验证会拒绝未知的根字段。它确实会进入集群对象,因此描述中不要填写任何你不愿放入注解的内容。 - 此文件仅支持创建。 每个变更工具(
sops_rotate_generated、sops_add_secrets、sops_update_external、sops_rename_secret、sops_delete_secrets、sops_add_metadata、sops_rekey)都能通过文件的sops信封识别它,无需解密,并会拒绝处理。要更改或轮换任何内容,请再次调用sops_create_k8s_secret并提供完整集合,同时重新提供外部值。sops_list_secrets可以读取此文件:读取stringData下的键名、注解中的来源和域。 - 最低版本。 只有包含此工具的版本及后续版本才会拒绝处理。sops-mcp 0.11.0 及更早版本没有此类保护:在这些版本中使用
sops_add_secrets、sops_rekey或任何其他变更工具,都会将清单重写为扁平文件,加密kind:和metadata:,并添加根级键。值仍可解密,因此不会丢失任何内容,但清单将不再可应用。不要让较旧的服务处理这些文件。
为什么选择这些工具,而非其他工具
逐键读取(decrypt-one-secret): 刻意不提供。通过 MCP 边界返回明文,会让模型在工具调用期间接触 Secret 材料,造成意外泄露风险。如果需要明文值,请自行使用 age 私钥运行 sops decrypt。
原地逐键更新生成型/派生型 Secret: 刻意不提供。sops_rotate_generated 是唯一会更改这些值的途径,因此轮换会留下审计记录(last_rotated 时间戳),并能妥善级联到派生 Secret。
创建 .sops.yaml 规则: 不在范围内。服务无法查看你的文件系统,因此无法应用基于路径的创建规则。接收方改由密钥域决定,而 sops_rekey 可处理 updatekeys 工作流。
SSE 传输上的租户隔离: 尚未实现。域隔离的是密钥集合,而非调用方。单个 SOPS_MCP_API_TOKEN 即可访问服务持有的所有域,因此不要在同一个 SSE 服务后接入两个彼此不信任的参与方。请分别运行独立进程。
其他来源类型(导入、模板化): 不在范围内。这些属于编排工作:从 Vault 获取值,或在部署模板层拼接 URL,然后将结果作为 external Secret 传入此处。
供应链完整性
Docker 构建通过三层验证加固,并由 CI 门禁强制执行。
基础镜像摘要固定
Dockerfile 基于 cgr.dev/chainguard/python(Wolfi)构建,并通过 SHA-256 摘要(@sha256:...)固定,因此 Docker 始终拉取经过审计的确切镜像,而不是当前标签所指向的镜像。两个阶段分别固定:构建阶段使用 :latest-dev,其中包含 apk、shell 和构建工具链;运行阶段使用 :latest,这是一个 distroless 镜像——没有 shell,也没有包管理器。两个摘要及其 cosign 签名状态都记录在 base-images.lock.json 中。
更新基础镜像(当上游发布安全补丁时):
pip install requests # one-time; cosign on PATH is needed for the signature checks
python3 lib/pin_base_images.py
来自 Wolfi 的已签名二进制文件
sops 和 age 二进制文件通过 apk 从 Wolfi 的软件包仓库安装,该仓库的索引由 Chainguard 签名。它们通过基础镜像摘要传递固定,因此重新固定基础镜像时,这些二进制文件也会随之重新固定。
Python 依赖项哈希固定
运行时依赖项通过 pip install --require-hashes 从 requirements.lock.txt 安装,该命令会拒绝任何内容与记录的 SHA-256 哈希不匹配的软件包。这可以防止依赖项劫持和仿冒软件包。
编辑 requirements.in 后更新依赖项:
pip install pip-tools # one-time
lib/compile_requirements.sh
CI 验证
supply-chain.yml 工作流会运行 lib/verify_requirements.py、lib/verify_base_images.py 和 lib/verify_version.py,然后审计锁文件中是否存在已知安全公告。该工作流会在每个针对 main 的拉取请求中运行;当推送到 main 的更改涉及 Dockerfile、锁文件、pyproject.toml、server.json 或 lib/ 时,也会运行。它会检查锁文件格式是否正确、每一行 Dockerfile FROM 是否都固定了摘要、server.json 是否与 pyproject.toml 中的版本一致,以及是否有任何固定的依赖项存在已发布的漏洞。
审计与发布工作流运行的检查相同,因此会阻止发布的锁文件现在也会阻止拉取请求。由于它会查询实时安全公告数据,当新的安全公告涉及某个已固定依赖项时,无关的拉取请求也可能开始失败。这是有意为之的权衡;解决办法是更新锁文件。
请注意,pip-compile 除非收到指示,否则会保留现有固定版本,因此在安全公告发布后重新生成时,需要使用升级标志:
lib/compile_requirements.sh --upgrade
验证已发布版本
发布到 GHCR 的每个容器镜像都由此仓库的发布工作流通过 sigstore 使用无密钥 cosign 签名,并附带 SLSA v1.0 构建来源证明和 SPDX SBOM,作为 OCI 工件。main 上的每个提交和每个发布标签都使用 SSH 签名。无需持有任何长期密钥材料,即可验证所有这些内容——签名锚定在 sigstore 的公共透明日志中。
安装 cosign:https://docs.sigstore.dev/system_config/installation
验证镜像签名。 验证成功可证明该镜像由此仓库的 publish.yml 工作流构建,而不只是其摘要与别人发给你的引用相匹配。
cosign verify \
--certificate-identity-regexp 'https://github.com/privacyplaybook/sops-mcp/\.github/workflows/publish\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/privacyplaybook/sops-mcp:<tag>
检查证明。
# List all artifacts attached to the image
cosign tree ghcr.io/privacyplaybook/sops-mcp:<tag>
# Download the SBOM (SPDX JSON)
cosign download sbom ghcr.io/privacyplaybook/sops-mcp:<tag>
# Verify the SLSA build provenance
cosign verify-attestation --type slsaprovenance1 \
--certificate-identity-regexp 'https://github.com/privacyplaybook/sops-mcp/\.github/workflows/publish\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/privacyplaybook/sops-mcp:<tag>
验证 git 标签和提交。 最简单的检查方式是查看 github.com 上 commits 和 tags 页面上的绿色 Verified 标记。
开发
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
pytest tests/ -v # unit tests plus end-to-end sops round-trips
ruff check src/ tests/
示例:与 CI/CD 部署流水线集成
一种常见模式:使用此服务生成 secrets.enc.yaml 文件并提交到基础设施仓库,然后在 CI 中解密,并将明文值作为环境变量注入容器编排器(Portainer、Kubernetes、Nomad)。
- 一次性生成 age 密钥对。将私钥作为机密提供给 CI(例如
SOPS_AGE_KEY),将公钥提供给 Claude Code,设置为SOPS_MCP_AGE_PUBLIC_KEY。 - 让模型使用
sops_create_secrets生成secrets.enc.yaml。 - 提交加密文件。
- 在部署工作流中,运行
sops decrypt secrets.enc.yaml > .env(或等效命令),并将结果传递给编排器。 - 需要轮换机密时,让模型运行
sops_rotate_generated或sops_update_external;提交并重新部署。
_meta_unencrypted 块可让你的工具在向编排器推送值时过滤掉元数据(以 _ 开头的键),避免元数据泄露到环境变量中。
许可证
Apache-2.0。请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。