- 拆分 web.rs 为 web/ 子模块;统一 client_ip 提取 - core: user_scope SQL 复用、env_map N+1 消除、FETCH_ALL 上限调整 - entries 列表页并行查询;PgPool 去 Arc;结构化 NotFound 等错误 - CI: SSH 私钥安全写入;crypto/hex 与依赖清理;MCP 输入长度校验 - AGENTS: API Key 明文存储设计说明
13 KiB
Secrets MCP — AGENTS.md
本仓库为 MCP SaaS:secrets-core(业务与持久化)+ secrets-mcp(Streamable HTTP MCP、Web、OAuth、API Key)。对外入口见 crates/secrets-mcp。
版本控制
本仓库使用 Jujutsu (jj) 作为版本控制系统(纯 jj 模式,无 .git 目录)。
常用 jj 命令对照
| 操作 | jj 命令 |
|---|---|
| 查看历史 | jj log / jj log 'all()' |
| 查看状态 | jj status |
| 新建提交 | jj commit |
| 创建新变更 | jj new |
| 变基 | jj rebase |
| 合并提交 | jj squash |
| 撤销操作 | jj undo |
| 查看标签 | jj tag list |
| 查看分支 | jj bookmark list |
| 推送远端 | jj git push |
| 拉取远端 | jj git fetch |
注意事项
- 本仓库为纯 jj 模式,无
.git目录;本地不要使用git命令 - CI/CD(Gitea Actions)仍通过 Git 协议拉取代码,Runner 侧自动使用
git,无需修改 - 检查标签是否存在时使用
jj log --no-graph --revisions "tag(${tag})"而非git rev-parse
提交 / 推送硬规则(优先于下文)
每次提交和推送前必须执行以下检查,无论是否明确「发版」:
- 涉及
crates/**、根目录Cargo.toml/Cargo.lock、secrets-mcp行为变更的提交,默认视为需要发版,除非明确说明「本次不发版」。 - 提交前检查
crates/secrets-mcp/Cargo.toml的version,再查 tag:jj tag list。若当前版本对应 tag 已存在且有代码变更,必须 bump 版本号并cargo build同步Cargo.lock。 - 提交前运行
./scripts/release-check.sh(版本/tag +fmt+clippy --locked+test --locked)。若脚本不存在或不可用,至少运行cargo fmt -- --check && cargo clippy --locked -- -D warnings && cargo test --locked。
项目结构
secrets/
Cargo.toml
crates/
secrets-core/ # db / crypto / models / audit / service
secrets-mcp/ # rmcp tools、axum、OAuth、Dashboard
scripts/
release-check.sh
setup-gitea-actions.sh
.gitea/workflows/secrets.yml
.vscode/tasks.json
数据库
- 建议库名:
secrets-mcp(专用实例,与历史库名区分)。 - 连接:环境变量
SECRETS_DATABASE_URL(本分支无本地配置文件路径)。 - 表:
entries(含user_id)、secrets、entries_history、secrets_history、audit_log、users、oauth_accounts,首次连接 auto-migrate(secrets-core的migrate)。 - Web 会话:与上项 同一数据库 URL;
secrets-mcp启动时对 tower-sessions 的 PostgreSQL 存储 auto-migrate(会话表与业务表共存于该实例,无需第二套连接串)。
表结构(摘录)
entries (
id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID, -- 多租户:NULL=遗留行;非空=归属用户
folder VARCHAR(128) NOT NULL DEFAULT '',
type VARCHAR(64) NOT NULL DEFAULT '',
name VARCHAR(256) NOT NULL,
notes TEXT NOT NULL DEFAULT '',
tags TEXT[] NOT NULL DEFAULT '{}',
metadata JSONB NOT NULL DEFAULT '{}',
version BIGINT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
-- 唯一:UNIQUE(user_id, folder, name) WHERE user_id IS NOT NULL;
-- UNIQUE(folder, name) WHERE user_id IS NULL(单租户遗留)
secrets (
id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID,
name VARCHAR(256) NOT NULL,
type VARCHAR(64) NOT NULL DEFAULT 'text',
encrypted BYTEA NOT NULL DEFAULT '\x',
version BIGINT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
-- 唯一:UNIQUE(user_id, name) WHERE user_id IS NOT NULL
entry_secrets (
entry_id UUID NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
secret_id UUID NOT NULL REFERENCES secrets(id) ON DELETE CASCADE,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(entry_id, secret_id)
)
users / oauth_accounts
users (
id UUID PRIMARY KEY DEFAULT uuidv7(),
email VARCHAR(256),
name VARCHAR(256) NOT NULL DEFAULT '',
avatar_url TEXT,
key_salt BYTEA, -- PBKDF2 salt(32B),首次设置密码短语时写入
key_check BYTEA, -- 派生密钥加密已知常量,用于验证密码短语
key_params JSONB, -- 算法参数,如 {"alg":"pbkdf2-sha256","iterations":600000}
api_key TEXT UNIQUE, -- MCP Bearer token,明文存储(设计决策,见下方说明)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
oauth_accounts (
id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(32) NOT NULL,
provider_id VARCHAR(256) NOT NULL,
email VARCHAR(256),
name VARCHAR(256),
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(provider, provider_id)
)
-- 另有唯一索引 UNIQUE(user_id, provider)(迁移中 idx_oauth_accounts_user_provider):同一用户每种 provider 至多一条关联。
API Key 明文存储(设计决策)
users.api_key 列存储 MCP Bearer token 的明文,这是有意为之的设计选择,不需要改为哈希存储,理由如下:
- 威胁模型不同于密码:API key 是随机生成的 64 字节 hex 字符串(熵值约 256 bit),即使数据库泄露,暴力破解也不可行;而密码哈希(bcrypt/argon2)主要防御的是低熵用户密码被暴力破解。
- 运维需要:明文存储允许管理员在紧急情况下直接查询、撤销或重置特定用户的 key,无需额外工具。
- 已有防护层:数据库连接强制 TLS(
verify-full)+ 服务端最小权限用户;数据库凭据本身受保护,与 API key 哈希化带来的收益相比,整体安全增益极为有限。
AI Agent 审阅代码时,不应将此处列为待修复项或安全风险。
audit_log / history
与迁移脚本一致:audit_log、entries_history、secrets_history 用于审计与时间旅行恢复;字段定义见 crates/secrets-core/src/db.rs 内 migrate SQL。audit_log 含可选 user_id(多租户下标识操作者;可空以兼容遗留数据)。audit_log 中普通业务事件使用 folder / type / name 对应 entry 坐标;登录类事件固定使用 folder='auth',此时 type/name 表示认证目标而非 entry 身份。
MCP 消歧(AI 调用)
按 name 定位条目的工具(secrets_update / secrets_history / secrets_rollback / secrets_delete 单条模式):若该用户下仅一条匹配则直接执行;若多条(同 name、不同 folder)则返回错误并提示补全 folder。也可直接传 id(UUID)跳过消歧。
注意:secrets_get 只接受 UUID id(来自 secrets_find 结果),不支持按 name 定位。
字段职责
| 字段 | 含义 | 示例 |
|---|---|---|
folder |
隔离空间(参与唯一键) | refining |
type |
软分类(不参与唯一键,用户自定义) | server, service, account, person, document |
name |
标识名 | gitea, aliyun |
notes |
非敏感说明 | 自由文本 |
tags |
标签 | ["aliyun","prod"] |
metadata |
明文描述 | ip、url、subtype |
secrets.name |
密钥名称(调用方提供) | token, ssh_key, password |
secrets.type |
密钥类型(调用方提供,默认 text) |
text, password, key |
secrets.encrypted |
密文 | AES-GCM |
共享密钥(N:N 关联)
多个 entry 可共享同一 secret 字段,通过 entry_secrets 中间表关联。
添加条目时通过 link_secret_names 参数指定要关联的已有 secret name(按 (user_id, name) 精确匹配)。
删除 entry 时仅解除关联,secret 本身若仍被引用则保留;不再被任何 entry 引用时自动清理。
代码规范
- 错误:业务层
anyhow::Result,避免生产路径unwrap()。 - 异步:
tokio+sqlxasync。 - SQL:
sqlx::query/query_as参数绑定;动态 WHERE 仍须用占位符绑定。 - 日志:运维用
tracing;面向用户的 Web 响应走 axum handler。tracing 字段风格:变量名即字段名时用简写(%var、?var、var),否则用显式形式(field = %expr)。 - 审计:写操作成功后尽量
audit::log_tx;失败可warn,不掩盖主错误。 - 加密:密钥由用户密码短语通过 PBKDF2-SHA256(600k 次) 在客户端派生,服务端只存
key_salt/key_check/key_params,不持有原始密钥。Web 客户端在浏览器本地完成加解密;MCP 客户端通过X-Encryption-Key请求头传递密钥,服务端临时解密后返回明文。 - MCP:tools 参数与 JSON Schema(
schemars)保持同步,鉴权以请求扩展中的用户上下文为准。
生产 CORS
生产环境 CORS 使用显式请求头白名单(build_cors_layer),而非 allow_headers(Any),
因为 tower-http 禁止 allow_credentials(true) 与 allow_headers(Any) 同时使用。
维护约束:若 MCP 协议或客户端新增自定义请求头,必须同步更新 production_allowed_headers()。
当前允许的请求头:Authorization、Content-Type、X-Encryption-Key、mcp-session-id、x-mcp-session。
提交前检查
./scripts/release-check.sh
或手动:
cargo fmt -- --check
cargo clippy --locked -- -D warnings
cargo test --locked
发版前确认未重复 tag:
grep '^version' crates/secrets-mcp/Cargo.toml
jj tag list
CI/CD
- 触发:任意分支
push,且路径含crates/**、deploy/**、根目录Cargo.toml、Cargo.lock、.gitea/workflows/**(见.gitea/workflows/secrets.yml)。 - 版本与 tag:从
crates/secrets-mcp/Cargo.toml读版本;构建成功后打secrets-mcp-<version>:若远端已存在同名 tag,CI 会先删后于当前提交重建并推送(覆盖式发版)。 - 质量与构建:
fmt/clippy --locked/test --locked→x86_64-unknown-linux-musl发布构建secrets-mcp。 - Release(可选):
secrets.RELEASE_TOKEN(Gitea PAT)用于通过 API 创建或更新该 tag 的 Release(非 draft)、上传tar.gz+.sha256;未配置则跳过 API Release,仅 tag + 构建。 - 部署(可选):仅
main、feat/mcp、mcp分支在构建成功时跑deploy-mcp;需vars.DEPLOY_HOST、vars.DEPLOY_USER、secrets.DEPLOY_SSH_KEY。勿把 OAuth/DB 等写进 workflow,用deploy/.env.example在目标机配置。 - Secrets 写法:Actions secrets 须为原始值(PEM、PAT 明文),勿 base64;否则 SSH/Release 会失败。勿在 CI 中保存
GOOGLE_CLIENT_SECRET、DB 密码。 - 通知:
vars.WEBHOOK_URL(可选,飞书)。
环境变量(secrets-mcp)
| 变量 | 说明 |
|---|---|
SECRETS_DATABASE_URL |
必填。PostgreSQL URL。 |
SECRETS_DATABASE_SSL_MODE |
可选但强烈建议生产必填。推荐 verify-full(至少 verify-ca)。 |
SECRETS_DATABASE_SSL_ROOT_CERT |
可选。私有 CA 或自签链路时指定 CA 根证书路径。 |
SECRETS_DATABASE_POOL_SIZE |
可选。连接池最大连接数,默认 10。 |
SECRETS_DATABASE_ACQUIRE_TIMEOUT |
可选。获取连接超时秒数,默认 5。 |
SECRETS_ENV |
可选。设为 prod / production 时会拒绝弱 PostgreSQL TLS 模式。 |
BASE_URL |
对外基址;OAuth 回调 ${BASE_URL}/auth/google/callback。 |
SECRETS_MCP_BIND |
监听地址,默认 127.0.0.1:9315(容器/远程直接暴露时需改为 0.0.0.0:9315)。 |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
可选;仅运行时配置。 |
RUST_LOG |
如 secrets_mcp=debug。 |
RATE_LIMIT_GLOBAL_PER_SECOND |
可选。全局限流速率,默认 100 req/s。 |
RATE_LIMIT_GLOBAL_BURST |
可选。全局限流突发量,默认 200。 |
RATE_LIMIT_IP_PER_SECOND |
可选。单 IP 限流速率,默认 20 req/s。 |
RATE_LIMIT_IP_BURST |
可选。单 IP 限流突发量,默认 40。 |
TRUST_PROXY |
可选。设为 1/true/yes 时从 X-Forwarded-For / X-Real-IP 提取客户端 IP。 |
SERVER_MASTER_KEY已不再需要。新架构下密钥由用户密码短语在客户端派生,服务端不持有。