Paperclip 密钥管理:主密钥、严格模式与 AWS provider 边界

2026-08-17

在 Paperclip 里配第一个 agent 的时候,几乎所有人都会走同一条捷径:在环境变量字段里直接填 ANTHROPIC_API_KEY=sk-ant-...。能跑,快,五秒钟搞定。问题是这个值从此就以明文躺在配置里,排查问题的时候它可能被贴进 issue。

Paperclip 提供了一套密钥系统来替掉这条捷径:值加密存储,运行时由服务端解析后注入。但真正值得先搞清楚的不是”怎么用”,而是它保护到哪一步为止。官方文档在 secrets 部署指南里专门用一节讲这件事,措辞相当直白,而这一节恰恰最容易被跳过。

这篇按官方文档讲清三件事:托管边界的准确位置、本地默认 provider 的主密钥怎么管、接 AWS Secrets Manager 时那个绕不开的”先有鸡还是先有蛋”问题。

托管边界:注入的那一刻,保护就结束了

文档把 Paperclip 对密钥值的保护拆成三段:

  • 存储:值由当前 provider 加密存放。本地 provider 用一把不离开宿主机的密钥加密。
  • 传输:值在服务端解密,在调用前的最后一刻注入 agent 进程环境、SSH 命令环境、沙箱驱动或 HTTP 请求。Paperclip 不会把解密后的值返回给看板 UI。
  • 审计:每次解析都记录一条不含敏感内容的事件(密钥 id、版本、provider id、消费方、结果),不含值本身,也不含 provider 凭据。

然后是关键的那句:一旦值到达消费进程,Paperclip 就无法再保证保密性。agent(或沙箱、或远端主机)可以读它、把它写进自己的日志或对话记录、传给下游工具。文档给出的心态是:任何绑定给某个 agent 的密钥,就当它对这个 agent 已经暴露了

文档建议的三条控制手段都是围绕它来的:用绑定限制爆炸半径(每个 agent 只绑它真正需要的),在 provider 支持的情况下用短期凭据,以及当 agent 的对话记录或下游系统可能捕获过某个值时执行轮换。

这也是为什么看板 UI 里没有”显示明文”这个能力——设计上就不给。API 层同样如此:所有密钥相关路由都不返回值,提交 accessKeyIdsecretAccessKeytokenpasswordserviceAccountJsonprivateKey 这类凭据形状的字段会在校验阶段被拒。

默认 provider:主密钥文件在哪、丢了会怎样

不做任何配置时,Paperclip 用 local_encrypted,主密钥落在:

~/.paperclip/instances/default/secrets/master.key

这把密钥在 onboarding 时自动创建,永远不离开本机。Paperclip 会尽力(best-effort)在创建或加载该文件时把权限设为 0600paperclipai doctor 和健康接口会在文件对同组或其他用户可读时告警。

备份规则是这一节里最实用的一条,也是最容易在恢复时才发现搞错的:密钥文件必须和数据库备份一起备。只有数据库备份、没有密钥,解不开本地密钥;只有密钥、没有数据库元数据,也不足以还原命名的密钥版本。这条对用户级密钥同样适用——数据库里存的是 user_secret_definitionsuser_secret_declarationscompany_secretsscope = "user" 的行、版本元数据和 owner id,解密它们的本地材料还是那个密钥文件。

配置入口是 CLI:

# onboarding 时写入默认 secrets 配置
pnpm paperclipai onboard

# 修改 secrets 配置
pnpm paperclipai configure --section secrets

# 校验
pnpm paperclipai doctor
npx paperclipai secrets doctor --company-id <company-id>

可用的环境变量覆盖只有三个:

变量说明
PAPERCLIP_SECRETS_MASTER_KEY32 字节密钥,可用 base64、hex 或原始字符串
PAPERCLIP_SECRETS_MASTER_KEY_FILE自定义密钥文件路径
PAPERCLIP_SECRETS_STRICT_MODE设为 true 强制使用密钥引用

关于整体的环境变量组织方式,可以对照看环境变量清单怎么读那篇。

严格模式:把”顺手填明文”这条路堵死

开启严格模式后,匹配 *_API_KEY*_TOKEN*_SECRET 的敏感 env key 必须使用密钥引用,不能再填内联明文值:

PAPERCLIP_SECRETS_STRICT_MODE=true

文档的建议是:只要不是本地可信环境都开。有一条默认行为值得注意——已启用认证的部署默认开启严格模式,除非配置或 PAPERCLIP_SECRETS_STRICT_MODE=false 显式覆盖。如果你在带认证的部署上突然发现内联 key 存不进去了,多半不是 bug,是这条默认在生效。

已经有一堆内联 key 的存量 agent,官方给了迁移命令:

npx paperclipai secrets migrate-inline-env --company-id <company-id>
npx paperclipai secrets migrate-inline-env --company-id <company-id> --apply

# 直接操作数据库的低层脚本
pnpm secrets:migrate-inline-env         # 空跑
pnpm secrets:migrate-inline-env --apply # 执行

日常运维该用 CLI 而不是底层脚本:CLI 走 Paperclip API,会创建或轮换密钥记录、更新 agent 的 env 绑定,并且带审计日志。

密钥怎么到 agent 手里:绑定不是自动的

一个容易踩的认知偏差:创建公司密钥不会自动产生环境变量。密钥要通过”绑定”进入 agent、项目、环境或插件里某个支持密钥引用的配置字段才生效。

agent 和项目环境变量的流程是四步:在 Company Settings > Secrets 里创建或链接密钥;打开 agent 的 Environment variables 或项目的 Env 字段;填入进程实际期望的 env key(比如 GH_TOKENOPENAI_API_KEY);把该行来源设为 Secret,选中密钥并选择 latest 或钉死某个版本。存储的密钥名可以是人类可读的,真正传给进程的是绑定行里的 key。

项目 env 对该项目下每次 issue 运行都生效;项目 env key 与 agent env key 撞名时项目值优先,之后 Paperclip 才注入自己的 PAPERCLIP_* 运行时变量。

agent 配置里的引用长这样:

{
  "env": {
    "ANTHROPIC_API_KEY": {
      "type": "secret_ref",
      "secretId": "8f884973-c29b-44e4-8ea3-6413437f8081",
      "version": "latest"
    }
  }
}

还有一个独立于 env 绑定的特例:服务端自己会按名字消费一个叫 GITHUB_TOKENGH_TOKENPAPERCLIP_GITHUB_TOKEN 的公司密钥(不需要绑定),用于克隆私有仓库做 repo-only 项目工作区、刷新 worktree 的 base ref。

除了注入,agent 还能主动取。文档给了两条 API:

GET  /api/agents/me/secrets
POST /api/agents/me/secrets/github_token/value

列表接口不返回值、不返回密钥 ID、不返回绑定 ID 和配置路径,只给 key、name、delivery、版本等元数据;取值接口无请求体,响应带 Cache-Control: no-store。这两条路由要求当前运行绑定的 agent JWT,长期 agent key、低信任审查 agent、task-bridge key、skill-test token 都用不了。

两种方式怎么选,文档口径很清楚:适配器或其子进程每次运行都要用的值走 env 注入;只在部分运行里用到的值、体积大或有结构的值、以及不继承适配器 env 的 skill 与工具,走按需取值。

delivery 字段有 envapiboth 三种。env.* 绑定隐含了通过该 API 的读取权限,access.* 绑定则只给 API 访问、不做环境注入。每次取值——成功或失败——都会同时记入 secret_access_eventsactivity_log,并且规则写死:agent 不得把取到的值记录或粘贴进 issue、评论、文档。审计这条线的读法可以参考活动日志与审计追溯

用户级密钥:共享 agent 用各人自己的 token

共享的 agent 或项目可以声明一个”槽位”,比如 github_api_token,运行时按这次运行的责任人解析出对应的值。绑定里只存定义 key,不存具体 secretId:

{
  "env": {
    "GITHUB_TOKEN": {
      "type": "user_secret_ref",
      "key": "github_api_token",
      "required": true,
      "allowMissingOverride": false
    }
  }
}

required 默认 trueallowMissingOverride 默认 false。失败语义写得很硬:必填的用户密钥引用在责任人缺失、定义缺失、或责任人没有有效值时一律 fail closed——在适配器分发之前就失败。可选引用可以省略该环境变量,但不允许注入空字符串,也不允许回退到另一个用户的值

权限划分是:看板/管理员管定义和覆盖率,用户各自管自己的值。覆盖率视图必须保持”仅元数据”——可以显示缺失、已配置、未激活、provider、vault 状态,但不能显示明文值、原始外部引用、provider 凭据或 provider 错误载荷。

外部引用路径的命名,文档推荐 paperclip-ext/{environment}/{company-id}/user-secrets/{definition-key}/{opaque-owner-id},owner 段用不透明的稳定用户 id 或单向映射的 subject id,避免在 provider 路径里出现邮箱、人名、客户名、OAuth scope 或工单号——路径、ARN、版本、别名、标签这些元数据虽然不是明文值,但被文档归为”secret-adjacent”。

provider vault:一家公司可以有多个金库

provider vault 是公司级的具名配置,把密钥材料指向某个受支持的 provider 后端。一家公司可以配多个 vault,同一 provider 家族也能有多个,并为每个家族指定默认。入口在 Company Settings → SecretsProvider vaults 页,自动化接口是 /api/companies/{companyId}/secret-provider-configs

vault 有四种状态,决定了运行时能拿它做什么:

状态含义
ready可用于创建/轮换/解析,可被设为默认
warning配置已存,但健康检查需要关注(例如缺 AWS 环境变量),仍可选
coming_soon可见、可编辑为草稿元数据,但被锁在所有运行时操作之外
disabled软删除,从创建/轮换流程里隐藏,但保留审计历史

gcp_secret_managervault(HashiCorp Vault)这两个 provider 官方标注为计划中,目前尚未提供运行时支持——它们被钉在 coming_soon,可以先存 projectIdlocationnamespaceaddressmountPath 这些草稿配置,但任何指向 coming-soon vault 的创建、轮换、解析调用都会以 runtime-locked 错误失败。Vault 的 address 必须是 origin-only 的 http(s)://host[:port],带凭据、路径、查询串或 fragment 的地址会被拒。

还有一条向后兼容的设计:创建密钥时没带 providerConfigId(一个 vault 都没配,或操作者清空了选择器),运行时解析会回落到部署级 provider 配置——也就是老装机一直在走的那条路,所以配 vault 之前创建的密钥不需要迁移。

AWS provider:先有鸡还是先有蛋

接 AWS Secrets Manager 时,第一个要理解的不是 IAM,而是一条边界:Paperclip 不能用 company_secrets 去解锁那个存放 company_secrets 的 AWS provider。初始的 AWS 信任必须在 Paperclip 服务端启动之前就存在。

文档允许的引导(bootstrap)位置只有这几类:挂在服务端运行时上的基础设施 IAM 或工作负载身份(实例 profile、ECS task role、EKS IRSA/OIDC web identity);启动服务端所用的进程环境或编排器密钥库;本地 AWS SDK 来源(AWS_PROFILE、AWS SSO/共享配置、容器或实例元数据);以及仅限本地开发的短期 shell 凭据。

明确禁止的是:把 AWS 根凭据或长期 IAM user access key 粘进看板 UI,或把这类引导材料存进 company_secrets。临时的 AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY 只能当本地破窗或短期测试来源,不该写进 Paperclip 配置、不该提交进 .env,更不该当作 Paperclip Cloud 的默认引导路径。

部署所需的非敏感环境变量:

PAPERCLIP_SECRETS_PROVIDER=aws_secrets_manager
PAPERCLIP_SECRETS_AWS_REGION=us-east-1
PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID=prod-us-1
PAPERCLIP_SECRETS_AWS_KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-...

可选的还有 PAPERCLIP_SECRETS_AWS_PREFIXPAPERCLIP_SECRETS_AWS_ENVIRONMENTPAPERCLIP_SECRETS_AWS_PROVIDER_OWNERPAPERCLIP_SECRETS_AWS_ENDPOINTPAPERCLIP_SECRETS_AWS_DELETE_RECOVERY_DAYS

Paperclip 托管密钥的命名约定是固定的:

paperclip/{deploymentId}/{companyId}/{secretKey}

最小 IAM 边界:允许 secretsmanager:CreateSecretPutSecretValueGetSecretValueDeleteSecret,资源收到 arn:aws:secretsmanager:<region>:<account-id>:secret:paperclip/<deployment-id>/*;KMS 侧允许 kms:Encryptkms:Decryptkms:GenerateDataKeykms:DescribeKey,限定到那把部署 CMK;拒绝部署前缀之外的通配访问。一个部署一个 app role、一个部署级 KMS key。

还有一条容易被忽略的补充:Paperclip 强制执行公司范围隔离、责任人推导、声明策略、脱敏和访问事件元数据,但它不替代外部金库的 IAM 策略。一个拥有宽泛外部金库读权限的运行时角色,如果被别的代码路径调用,照样能读到 IAM 允许它读的东西。要做 provider 侧的强隔离,得按 vault、AWS 账号、Region、前缀或运行时角色去拆。部署形态相关的取舍,可以顺带看看Tailscale 私有访问与 AWS ECS 部署

远程导入:只搬元数据,不搬值

AWS vault 可以把已有的 Secrets Manager 条目导入成 Paperclip 的 external_reference 密钥。这是纯元数据链接:Paperclip 存 ARN/路径、指纹或版本引用、绑定元数据,预览和导入过程中不读取、不复制、不存储、不记录、不显示远端明文。

预览只用 ListSecrets,不得调用 GetSecretValueBatchGetSecretValue,不得请求 SecretString。而 ListSecrets 有个 AWS 侧的硬约束:它是账号/Region 级的清点动作,AWS 不支持按单个密钥 ARN 或 tag 约束它,策略只能写 Resource: "*",所以要不要开取决于你能否接受这个账号和 Region 的清单被看到。tag 和名称过滤只是搜索体验,不是权限边界。

预览失败的四类对号入座:

现象官方给的判断
AccessDenied / not authorized运行时角色缺 secretsmanager:ListSecrets;只在该 vault 确实要开远程导入时才加这条可选清单语句
限流(Throttling)稍等重试并收窄搜索条件再翻页,避免全账号枚举
游标无效刷新预览;AWS 的 NextToken 是不透明的,会过期或变陈旧
导入后运行时解析失败核对所选外部密钥的 GetSecretValue 与 KMS 解密范围——能在清单里看到,不等于运行时角色能读到值

导入结果按行返回 imported / skipped / error。有一条守卫规则值得记住:Paperclip 自己托管命名空间下的引用禁止作为外部引用导入,那部分资源该走托管流程。

轮换方面,V1 是显式创建新版本加受控发布:走 Paperclip 的轮换流程写入新值,Paperclip 用 PutSecretValue 创建新的 AWS 版本并把 providerVersionRef 记进 company_secret_versions,然后重跑消费 latest 的工作负载。高风险发布建议先把消费方钉到具体版本。provider 原生的自动轮换被定位为后续增强项。

什么时候这套机制帮不上你

几条边界值得先说清楚:

它挡不住 agent 自己泄密。 值一旦注入进程,agent 可以把它写进日志、对话记录、下游系统。如果你的威胁模型里包含”agent 可能不受控地转发凭据”,这套系统解决不了,只能靠最小化绑定、短期凭据和事后轮换。绑定给谁、绑几个,本质上是Agent 增删改与配置那一层的决策。

GCP Secret Manager 和 HashiCorp Vault 现在不能用。 官方标注为计划中,目前尚未提供运行时支持,只能存草稿配置。内置的 AWS、GCP、Vault provider ID 都接受外部引用元数据,但运行时解析要求部署里配好该 provider,未配置前健康检查会一直报 warning。

没有”接管已有 AWS 密钥”的流程。 V1 把已有的 AWS 条目保持为链接的外部引用,而不是收编成 Paperclip 托管资源。想让 Paperclip 负责创建和轮换,就得走托管命名的那条路。要加”adopt existing”能力,官方说需要显式的确认 UX、范围校验、预期 tag 和安全评审。

导出不带密钥。 公司导出只包含环境声明,不含密钥 ID、provider 引用、加密材料或明文值。跨实例搬包之前,得先用 npx paperclipai secrets declarations --company-id <company-id> --kind secret 把声明拉出来,在目标部署里创建本地值或链接托管 provider 引用。这步没做,包搬过去 agent 跑不起来。

local_encrypted 的备份纪律靠人保证。 vault 行里只记路径和一个”已备份”的确认,不记密钥字节。系统能提醒,但没法替你备份,这条落到运维手册上比落到配置里更靠谱。

延伸阅读


本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档 与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。 我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感; 部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。 请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。