secrets-mcp
Workspace:secrets-core + secrets-mcp(HTTP Streamable MCP + Web)。多租户密钥与元数据存 PostgreSQL;用户通过 Google OAuth 登录,API Key 鉴权 MCP 请求;秘密数据用用户密码短语派生的密钥在客户端加密,服务端不持有原始密钥。
安装
cargo build --release -p secrets-mcp
# 产物: target/release/secrets-mcp
发版产物见 Gitea Release(tag:secrets-mcp-<version>,Linux musl 预编译);其它平台本地 cargo build。
环境变量与本地运行
复制 deploy/.env.example 为项目根目录 .env(已在 .gitignore),或导出同名变量:
| 变量 | 说明 |
|---|---|
SECRETS_DATABASE_URL |
必填。PostgreSQL 连接串(推荐使用域名,例如 db.refining.ltd,避免直连 IP)。 |
SECRETS_DATABASE_SSL_MODE |
可选但强烈建议生产必填。推荐 verify-full(至少 verify-ca),避免回退到弱 TLS 模式。 |
SECRETS_DATABASE_SSL_ROOT_CERT |
可选。私有 CA 或自签链路时指定 CA 根证书路径(如 /etc/secrets/pg-ca.crt)。 |
SECRETS_ENV |
可选。设为 prod / production 时会拒绝弱 PostgreSQL TLS 模式(prefer、disable、allow、require)。 |
BASE_URL |
对外访问基址;OAuth 回调为 {BASE_URL}/auth/google/callback。默认 http://localhost:9315。 |
SECRETS_MCP_BIND |
监听地址,默认 127.0.0.1:9315。容器内或直接对外暴露端口时请改为 0.0.0.0:9315;反代时常为 127.0.0.1:9315。 |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
可选;不配置则无 Google 登录入口。运行时从环境读取,勿写入 CI、勿打入二进制。 |
RUST_LOG |
可选;日志级别,如 secrets_mcp=debug。 |
cargo run -p secrets-mcp
生产推荐示例(PostgreSQL TLS):
SECRETS_DATABASE_URL=postgres://postgres:***@db.refining.ltd:5432/secrets-mcp
SECRETS_DATABASE_SSL_MODE=verify-full
SECRETS_DATABASE_SSL_ROOT_CERT=/etc/secrets/pg-ca.crt
SECRETS_ENV=production
- Web:
BASE_URL(登录、Dashboard、设置密码短语、创建 API Key)。 - MCP:Streamable HTTP 基址
{BASE_URL}/mcp,需Authorization: Bearer <api_key>+X-Encryption-Key: <hex>请求头(读密文工具须带密钥)。
PostgreSQL TLS 加固
- 推荐将数据库域名单独设置为
db.refining.ltd,服务域名保持secrets.refining.app。 - 数据库证书建议使用可校验链路(如 Let's Encrypt 或私有 CA),并保证证书
SAN包含db.refining.ltd。 - PostgreSQL 侧建议使用
hostssl规则限制应用来源(如47.238.146.244/32),逐步移除公网明文host访问。 - 应用端推荐
SECRETS_DATABASE_SSL_MODE=verify-full;仅在过渡阶段可临时用verify-ca。 - 可执行运维步骤见
deploy/postgres-tls-hardening.md。
MCP 与 AI 工作流(v0.3+)
条目在逻辑上以 (folder, name) 在用户内唯一(数据库唯一索引:user_id + folder + name)。同名可在不同 folder 下各存一条(例如 refining/aliyun 与 ricnsmart/aliyun)。
工具列表
| 工具 | 需要加密密钥 | 说明 |
|---|---|---|
secrets_find |
否 | 发现条目(返回含 secret_fields schema),支持 name_query 模糊匹配 |
secrets_search |
否 | 搜索条目,支持 query/folder/type/name 过滤、sort/offset 分页、summary 摘要模式 |
secrets_get |
是 | 按 UUID id 获取单条条目及解密后的 secrets |
secrets_add |
是 | 添加新条目,支持 meta_obj/secrets_obj JSON 对象参数、secret_types 指定密钥类型、link_secret_names 关联已有 secret |
secrets_update |
是 | 更新条目,支持 id 或 name+folder 定位 |
secrets_delete |
否 | 删除条目,支持 id 或 name+folder 定位;dry_run=true 预览删除 |
secrets_history |
否 | 查看条目历史,支持 id 或 name+folder 定位 |
secrets_rollback |
是 | 回滚条目到指定历史版本,支持 id 或 name+folder 定位 |
secrets_export |
是 | 导出条目(含解密明文),支持 JSON/TOML/YAML 格式 |
secrets_env_map |
是 | 将 secrets 转换为环境变量映射(UPPER(entry)_UPPER(field) 格式),支持 prefix |
secrets_overview |
否 | 返回各 folder 和 type 的 entry 计数概览 |
消歧规则
- 按
name定位的工具(secrets_update/secrets_delete/secrets_history/secrets_rollback):若该用户下仅一条匹配则直接执行;若多条(同name、不同folder)则返回错误并提示补全folder。也可直接传id(UUID)跳过消歧。 secrets_get仅支持通过id(UUID)获取。secrets_delete的dry_run=true与真实删除使用相同消歧规则——唯一则预览一条,多条则报错并要求folder。
共享密钥
N:N 关联下,删除 entry 仅解除关联,被共享的 secret 若仍被其他 entry 引用则保留;无引用时自动清理。
加密架构(混合 E2EE)
密钥派生
用户在 Web Dashboard 设置密码短语,浏览器使用 **Web Crypto API(PBKDF2-SHA256,600k 次迭代)**在本地派生 256-bit AES 密钥。
- Salt(32B):首次设置时在浏览器生成,存入服务端
users.key_salt - key_check:派生密钥加密已知常量
"secrets-mcp-key-check",存入users.key_check,用于登录时验证密码短语 - 服务端不存储原始密钥,只存 salt + key_check
跨设备同步:新设备登录 → 输入相同密码短语 → 从服务端取 salt → 同样的 PBKDF2 → 得到相同密钥。
写入与读取流程
flowchart LR
subgraph Web["Web 浏览器(E2E)"]
P["密码短语"] --> K["PBKDF2 → 256-bit key"]
K --> Enc["AES-256-GCM 加密"]
K --> Dec["AES-256-GCM 解密"]
end
subgraph AI["AI 客户端(MCP)"]
HdrKey["X-Encryption-Key: hex"]
end
subgraph Server["secrets-mcp 服务端"]
Middleware["请求中临时持有 key\n请求结束即丢弃"]
DB[(PostgreSQL\nsecrets.encrypted = 密文\nentries.metadata = 明文)]
end
Enc -->|密文| Server
HdrKey -->|key + 请求| Middleware
Middleware <-->|加解密| DB
DB -->|密文| Dec
两种客户端对比
| Web 浏览器 | AI 客户端(MCP) | |
|---|---|---|
| 密钥位置 | 仅在浏览器内存 / sessionStorage | MCP 配置 headers 中 |
| 加解密位置 | 客户端(真正 E2E) | 服务端临时(请求级生命周期) |
| 安全边界 | 服务端零知识 | 依赖 TLS + 服务端内存隔离 |
敏感数据传输
- OAuth
client_secret只存服务端环境变量,不发给浏览器 - API Key 当前存放在
users.api_key,Dashboard 会明文展示并可重置 - X-Encryption-Key 随 MCP 请求经 TLS 传输,服务端仅在请求处理期间持有(不持久化)
- 生产环境必须走 HTTPS/TLS
AI 客户端配置
在 Web Dashboard 设置密码短语后,解锁页面会按客户端格式生成配置。常见客户端示例如下:
Cursor / Claude Desktop 风格:
{
"mcpServers": {
"secrets": {
"url": "https://secrets.example.com/mcp",
"headers": {
"Authorization": "Bearer sk_abc123...",
"X-Encryption-Key": "a1b2c3...(64位hex)"
}
}
}
}
OpenCode 风格:
{
"mcp": {
"secrets": {
"type": "remote",
"enabled": true,
"url": "https://secrets.example.com/mcp",
"headers": {
"Authorization": "Bearer sk_abc123...",
"X-Encryption-Key": "a1b2c3...(64位hex)"
}
}
}
}
数据模型
主表 entries(folder、type、name、notes、tags、metadata,多租户时带 user_id)+ 子表 secrets(每行一个加密字段:name、type、encrypted,通过 entry_secrets 中间表与 entry 建立 N:N 关联)。唯一性:UNIQUE(user_id, folder, name)(user_id 为空时为遗留行唯一 (folder, name))。另有 entries_history、secrets_history、audit_log,以及 users(含 key_salt、key_check、key_params、api_key)、oauth_accounts。首次连库自动迁移建表(secrets-core 的 migrate);已有库在进程启动时亦由同一 migrate() 增量补齐表、索引与 N:N 结构。若需从更早版本对照一次性 SQL,可在 git 历史中检索已移除的 scripts/migrate-v0.3.0.sql。Web 登录会话(tower-sessions)使用同一 SECRETS_DATABASE_URL,进程启动时对会话存储执行迁移(见 secrets-mcp 中 PostgresStore::migrate),无需额外环境变量。
| 位置 | 字段 | 说明 |
|---|---|---|
| entries | folder | 组织/隔离空间,如 refining、ricnsmart;参与唯一键 |
| entries | type | 软分类,用户自定义,如 server、service、account、person、document(不参与唯一键) |
| entries | name | 人类可读标识;与 folder 一起在用户内唯一 |
| entries | notes | 非敏感说明文本 |
| entries | metadata | 明文 JSON(ip、url、subtype 等) |
| secrets | name | 密钥名称(调用方提供) |
| secrets | type | 密钥类型(调用方提供,默认 text) |
| secrets | encrypted | AES-GCM 密文(含 nonce) |
| users | key_salt | PBKDF2 salt(32B),首次设置密码短语时写入 |
| users | key_check | 派生密钥加密已知常量,用于验证密码短语 |
| users | key_params | 派生算法参数,如 {"alg":"pbkdf2-sha256","iterations":600000} |
共享密钥(N:N 关联)
多个条目可共享同一密文字段,通过 entry_secrets 中间表实现 N:N 关联:
- 添加条目时可通过
link_secret_names参数关联已有的 secret(按(user_id, name)精确匹配查找) - 同一 secret 可被多个 entry 引用,删除某 entry 不会级联删除被共享的 secret
- 当 secret 不再被任何 entry 引用时,自动清理(
NOT EXISTS子查询)
类型(Type)
type 字段用于软分类,由用户自由填写,不做任何自动转换或归一化。常见示例:server、service、account、person、document,但任何值均可接受。
审计日志
add、update、delete 等写操作写入 audit_log(操作类型、对象、摘要,不含 secret 明文)。多租户场景下可写 user_id(可空,兼容遗留行)。
业务条目事件使用 folder / type / name;登录类事件使用 folder='auth',此时 type/name 表示认证目标(例如 oauth / google),不表示某条 secrets entry。
SELECT action, folder, type, name, detail, user_id, created_at
FROM audit_log
ORDER BY created_at DESC
LIMIT 20;
项目结构
Cargo.toml
crates/secrets-core/ # db / crypto / models / audit / service
src/
taxonomy.rs # SECRET_TYPE_OPTIONS(secret 字段类型下拉选项)
service/ # 业务逻辑(add, search, update, delete, export, env_map 等)
crates/secrets-mcp/ # MCP HTTP、Web、OAuth、API Key
scripts/
release-check.sh # 发版前 fmt / clippy / test
setup-gitea-actions.sh
sync-test-to-prod.sh # 测试库同步到生产(按需)
deploy/
.env.example # 环境变量模板
secrets-mcp.service # systemd 服务文件(生产部署用)
postgres-tls-hardening.md # PostgreSQL TLS 加固运维手册
CI/CD(Gitea Actions)
见 .gitea/workflows/secrets.yml。
- 触发:任意分支
push,且变更路径包含crates/**、deploy/**、根目录Cargo.toml/Cargo.lock、.gitea/workflows/**。 - 流水线:解析
crates/secrets-mcp/Cargo.toml版本 →cargo fmt/clippy --locked/test --locked→ 交叉编译x86_64-unknown-linux-musl的secrets-mcp→ 构建成功后打 tagsecrets-mcp-<version>(若远端已存在同名 tag,会先删除再于当前提交重建并推送,覆盖式发版)。 - Release(可选):配置仓库 Secret
RELEASE_TOKEN(Gitea PAT,明文勿 base64)时,会通过 API 创建或更新已指向该 tag 的 Release(非 draft)、上传tar.gz与.sha256;未配置则跳过 API Release,仅 tag + 构建结果。 - 部署(可选):仅在
main、feat/mcp或mcp分支且构建成功时,若已配置vars.DEPLOY_HOST、vars.DEPLOY_USER与secrets.DEPLOY_SSH_KEY,则deploy-mcp通过 SCP/SSH 更新目标机二进制并systemctl restart secrets-mcp。 - 通知(可选):
vars.WEBHOOK_URL为飞书 Webhook 时,构建/部署/发布节点会推送简要状态。
./scripts/setup-gitea-actions.sh # 通过 Gitea API 写入 RELEASE_TOKEN、WEBHOOK_URL、部署相关变量等
详见 AGENTS.md(发版规则、代码规范)。