修复 OAuth 解绑时非法聚合 FOR UPDATE,Web OAuth 审计 IP 与 TRUST_PROXY 对齐并校验 IP,账号绑定写入 oauth_state 失败时回滚 bind 标记。回滚条目时恢复 folder/type,导入冲突检查在 DB 失败时传播错误,MCP delete/history 要求已登录用户,全局请求体 10MiB 限制。CI 部署支持 DEPLOY_KNOWN_HOSTS,默认 accept-new;文档与 deploy 示例补充连接池、限流、TRUST_PROXY。移除含明文凭据的 sync-test-to-prod 脚本。
255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
# secrets-mcp
|
||
|
||
Workspace:**`secrets-core`** + **`secrets-mcp`**(HTTP Streamable MCP + Web)。多租户密钥与元数据存 PostgreSQL;用户通过 **Google OAuth** 登录,**API Key** 鉴权 MCP 请求;秘密数据用**用户密码短语派生的密钥**在客户端加密,服务端不持有原始密钥。
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
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`。 |
|
||
| `SECRETS_DATABASE_POOL_SIZE` | 可选。连接池最大连接数,默认 `10`。 |
|
||
| `SECRETS_DATABASE_ACQUIRE_TIMEOUT` | 可选。获取连接超时秒数,默认 `5`。 |
|
||
| `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;仅在反代环境下启用。 |
|
||
|
||
```bash
|
||
cargo run -p secrets-mcp
|
||
```
|
||
|
||
生产推荐示例(PostgreSQL TLS):
|
||
|
||
```bash
|
||
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`](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 → 得到相同密钥。
|
||
|
||
### 写入与读取流程
|
||
|
||
```mermaid
|
||
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` 风格:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"secrets": {
|
||
"url": "https://secrets.example.com/mcp",
|
||
"headers": {
|
||
"Authorization": "Bearer sk_abc123...",
|
||
"X-Encryption-Key": "a1b2c3...(64位hex)"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`OpenCode` 风格:
|
||
|
||
```json
|
||
{
|
||
"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。
|
||
|
||
```sql
|
||
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`](.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` → 构建成功后打 tag `secrets-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 时,构建/部署/发布节点会推送简要状态。
|
||
|
||
```bash
|
||
./scripts/setup-gitea-actions.sh # 通过 Gitea API 写入 RELEASE_TOKEN、WEBHOOK_URL、部署相关变量等
|
||
```
|
||
|
||
详见 [AGENTS.md](AGENTS.md)(发版规则、代码规范)。
|