mcpField
S
sops-mcp

sops-mcp

本地服务
github.com/privacyplaybook安全01 周前更新

关于 sops-mcp

sops-mcp 是使用 age 加密创建和管理 SOPS 密钥文件的 MCP 服务,面向需要将加密密钥纳入代码库的开发者。明文不会经由 MCP 返回给客户端,且服务没有客户端文件系统访问权限。

主要功能

  1. 01创建和列出加密密钥
  2. 02生成、轮换及更新密钥
  3. 03支持生成、外部和派生密钥
  4. 04使用命名密钥域管理接收方
  5. 05重命名、删除和重新加密密钥文件

适用场景

  1. 01将加密密钥文件提交到代码库
  2. 02在部署时由 CI/CD 解密密钥
  3. 03轮换密钥并自动重算派生值

MCP 相关

向 MCP 客户端提供创建和管理 SOPS 加密文件的工具。工具通过文本参数接收和返回内容,不提供解密单个密钥的工具,也不访问客户端文件系统。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"privacyplaybook-sops": {
"command": "uvx",
"args": [
"sops-mcp"
],
"env": {
"SOPS_MCP_AGE_PUBLIC_KEY": "<SOPS_MCP_AGE_PUBLIC_KEY>",
"SOPS_AGE_KEY": "<SOPS_AGE_KEY>"
}
}
}
}
启动命令uvx sops-mcp
需要配置的环境变量
SOPS_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+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypisops-mcp0.12.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

sops-mcp

用于创建和管理通过 age 加密的 SOPS 密文文件的 MCP 服务。

专为 Claude Code(或任何 MCP 客户端)设计,可生成加密的 secrets.enc.yaml 文件,同时确保模型始终看不到明文值。所有文件内容都以文本参数的形式传入,并以文本形式返回;该服务无法访问客户端的文件系统。

为什么

设计由两个目标驱动:

1. 将密钥保存在源代码树中,同时避免泄露。 对于小型项目而言,为少数凭据运行完整的密钥管理器(Vault、AWS Secrets Manager 等)有些过度。在 git 中加密静态密钥,并在部署时由 CI/CD 流水线解密,成本低得多:

  1. 通过此服务创建 secrets.enc.yaml——使用 age 公钥加密,可安全提交。
  2. 将它与代码一同提交。
  3. CI/CD 流水线保存 age 私钥,在部署时解密,并将明文作为环境变量注入容器编排器。

age 私钥只会存放在一个地方:CI/CD 密钥库。其他所有地方——你的笔记本电脑、git 远程仓库、容器镜像——看到的都只是密文。请参阅下方的实践示例。

2. 让 AI 编码 Agent 生成它永远无法读取的密钥。 Claude(或任何 MCP 客户端)可以创建密码、轮换密码、派生哈希、重命名和删除密码——但明文值绝不会通过 MCP 边界返回给模型。 服务持有加密密钥;客户端只提交请求并接收元数据。 这里有意不提供“解密单个密钥”的工具。 这样可以防止提示词注入或行为异常的 Agent 窃取密钥。

最简单的配置使用单个 age 接收方(即 CI 私钥)。如果你需要多个接收方——例如 CI 密钥加操作员密钥,或供不同参与方使用的独立密钥组——请参阅密钥域。

密钥来源

每个密钥都属于以下三种来源之一,并记录在 _meta_unencrypted 中:

  • generated — 通过加密安全随机数生成的值(Python secrets / OS CSPRNG)。你指定长度和字符集;服务会同时保存这些信息,以便轮换时重新生成。
  • external — 按原样加密的用户提供值(SMTP 凭据、第三方 API 密钥等)。轮换时会保留。通过 sops_update_external 更新。
  • derived — 通过指定名称的转换,从同一文件中的另一个密钥计算得出。当源密钥轮换(或外部来源更新)时,派生值会按拓扑顺序自动重新计算。适用于 Authelia 的 OIDC 客户端密钥 PBKDF2 哈希等场景。

设计

以下三个理念决定了工具的设计:

  1. 明文不会通过 MCP 边界传递。 生成的密钥值绝不会返回给客户端。这里有意不提供“解密单个密钥”的工具。如果需要明文,请自行使用 age 私钥运行 sops decrypt。
  2. 元数据以明文保存。 _meta_unencrypted 块与加密值并列存放(使用 SOPS 的 unencrypted_suffix 功能),记录每个密钥的来源、生成方式、上次轮换时间,以及所属的密钥域。这样服务无需解密即可列出和轮换密钥。默认情况下,SOPS 的 MAC 会覆盖这些值,因此被篡改的块将无法解密。启用 mac_only_encrypted 关闭此功能的文件会被拒绝。即使不使用密钥读取该块的工具也无法检查 MAC,因此还需要单独验证接收方。
  3. 不支持原地更新 generated 或 derived 密钥的值。 这些密钥只能通过轮换更改,此时服务和调用方都无法访问明文。external 密钥(例如用户掌控的上游 API 密钥)可以通过 sops_update_external 更新。
转换(用于 derived 秘密)
转换用途是否确定性
pbkdf2_sha512_autheliaAuthelia 的 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 信封中的接收方列表,因此标记错误的文件会被拒绝,而不是重新设置密钥。

接收者轮换

添加或移除接收者分两步进行:

  1. 编辑域的接收者列表并重启服务。
  2. 对每个受影响的文件运行 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)。

  1. 一次性生成 age 密钥对。将私钥作为机密提供给 CI(例如 SOPS_AGE_KEY),将公钥提供给 Claude Code,设置为 SOPS_MCP_AGE_PUBLIC_KEY。
  2. 让模型使用 sops_create_secrets 生成 secrets.enc.yaml。
  3. 提交加密文件。
  4. 在部署工作流中,运行 sops decrypt secrets.enc.yaml > .env(或等效命令),并将结果传递给编排器。
  5. 需要轮换机密时,让模型运行 sops_rotate_generated 或 sops_update_external;提交并重新部署。

_meta_unencrypted 块可让你的工具在向编排器推送值时过滤掉元数据(以 _ 开头的键),避免元数据泄露到环境变量中。

许可证

Apache-2.0。请参阅 LICENSE。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。