Files
secrets/README.md
2026-04-04 17:58:26 +08:00

11 KiB
Raw Blame History

secrets-mcp

Workspacesecrets-core + secrets-mcpHTTP Streamable MCP + Web。多租户密钥与元数据存 PostgreSQL用户通过 Google OAuth 登录,API Key 鉴权 MCP 请求;秘密数据用用户密码短语派生的密钥在客户端加密,服务端不持有原始密钥。

安装

cargo build --release -p secrets-mcp
# 产物: target/release/secrets-mcp

发版产物见 Gitea Releasetagsecrets-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 模式(preferdisableallowrequire)。
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
  • WebBASE_URL登录、Dashboard、设置密码短语、创建 API Key
  • MCPStreamable 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/aliyunricnsmart/aliyun)。

  • secrets_search:发现条目(可按 query / folder / type / name 过滤);不要求加密头。
  • secrets_get / secrets_update / secrets_delete(按 name/ secrets_history / secrets_rollback:仅 name 且全局唯一则直接命中;若多条同名,返回消歧错误,需在参数中补 folder
  • secrets_deletedry_run=true 时与真实删除相同的消歧规则——唯一则预览一条,多条则报错并要求 folder
  • 共享密钥N:N 关联下,删除 entry 仅解除关联,被共享的 secret 若仍被其他 entry 引用则保留;无引用时自动清理。

加密架构(混合 E2EE

密钥派生

用户在 Web Dashboard 设置密码短语,浏览器使用 **Web Crypto APIPBKDF2-SHA256600k 次迭代)**在本地派生 256-bit AES 密钥。

  • Salt32B:首次设置时在浏览器生成,存入服务端 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_keyDashboard 会明文展示并可重置
  • 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"
      }
    }
  }
}

数据模型

主表 entriesfoldertypenamenotestagsmetadata,多租户时带 user_id+ 子表 secrets(每行一个加密字段:nametypeencrypted,通过 entry_secrets 中间表与 entry 建立 N:N 关联)。唯一性UNIQUE(user_id, folder, name)user_id 为空时为遗留行唯一 (folder, name))。另有 entries_historysecrets_historyaudit_log,以及 users(含 key_saltkey_checkkey_paramsapi_key)、oauth_accounts。首次连库自动迁移建表(secrets-coremigrate);已有库在进程启动时亦由同一 migrate() 增量补齐表、索引与 N:N 结构。若需从更早版本对照一次性 SQL可在 git 历史中检索已移除的 scripts/migrate-v0.3.0.sqlWeb 登录会话tower-sessions使用同一 SECRETS_DATABASE_URL,进程启动时对会话存储执行迁移(见 secrets-mcpPostgresStore::migrate),无需额外环境变量。

位置 字段 说明
entries folder 组织/隔离空间,如 refiningricnsmart;参与唯一键
entries type 软分类,如 serverservicepersondocument(可扩展,不参与唯一键)
entries name 人类可读标识;与 folder 一起在用户内唯一
entries notes 非敏感说明文本
entries metadata 明文 JSONip、url、subtype 等)
secrets name 密钥名称(调用方提供)
secrets type 密钥类型(调用方提供,默认 text
secrets encrypted AES-GCM 密文(含 nonce
users key_salt PBKDF2 salt32B首次设置密码短语时写入
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 子查询)

审计日志

addupdatedelete 等写操作写入 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
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/                 # systemd、.env 示例

CI/CDGitea 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-muslsecrets-mcp → 构建成功后打 tag secrets-mcp-<version>(若远端已存在同名 tag会先删除再于当前提交重建并推送,覆盖式发版)。
  • Release可选:配置仓库 Secret RELEASE_TOKENGitea PAT明文勿 base64会通过 API 创建或更新已指向该 tag 的 Release非 draft、上传 tar.gz.sha256;未配置则跳过 API Release仅 tag + 构建结果。
  • 部署(可选):仅在 mainfeat/mcpmcp 分支且构建成功时,若已配置 vars.DEPLOY_HOSTvars.DEPLOY_USERsecrets.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(发版规则、代码规范)。