refactor: workspace secrets-core + secrets-mcp MCP SaaS

- Split library (db/crypto/service) and MCP/Web/OAuth binary
- Add deploy examples and CI/docs updates

Made-with: Cursor
This commit is contained in:
voson
2026-03-20 17:36:00 +08:00
parent ff9767ff95
commit 49fb7430a8
56 changed files with 5531 additions and 5456 deletions

401
README.md
View File

@@ -1,308 +1,149 @@
# secrets
# secrets-mcp
跨设备密钥与配置管理 CLI基于 Rust + PostgreSQL 18
将服务器信息、服务凭据统一存入数据库,供本地工具和 AI 读取上下文。每个敏感字段单独行存储(`secrets` 子表),字段名、类型、长度以明文保存便于 AI 理解,仅值本身使用 AES-256-GCM 加密;主密钥由 Argon2id 从主密码派生并存入系统钥匙串。
Workspace**`secrets-core`** + **`secrets-mcp`**HTTP Streamable MCP + Web。多租户密钥与元数据存 PostgreSQL用户通过 **Google OAuth** 登录,**API Key** 鉴权 MCP 请求;秘密数据用**用户密码短语派生的密钥**在客户端加密,服务端不持有原始密钥
## 安装
```bash
cargo build --release
# 或从 Release 页面下载预编译二进制
cargo build --release -p secrets-mcp
# 产物: target/release/secrets-mcp
```
已有旧版本时,可执行 `secrets upgrade` 自动下载最新版并替换。该命令会校验 Release 附带的 `.sha256` 摘要后再安装
发版产物见 Gitea Releasetag`secrets-mcp-<version>`Linux musl 预编译);其它平台本地 `cargo build`
## 首次使用(每台设备各执行一次)
## 环境变量与本地运行
复制 `deploy/.env.example` 为项目根目录 `.env`(已在 `.gitignore`),或导出同名变量:
| 变量 | 说明 |
|------|------|
| `SECRETS_DATABASE_URL` | **必填**。PostgreSQL 连接串(建议专用库,如 `secrets-mcp`)。 |
| `BASE_URL` | 对外访问基址OAuth 回调为 `{BASE_URL}/auth/google/callback`。默认 `http://localhost:9315`。 |
| `SECRETS_MCP_BIND` | 监听地址,默认 `0.0.0.0:9315`。反代时常为 `127.0.0.1:9315`。 |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | 可选;不配置则无 Google 登录入口。运行时从环境读取,勿写入 CI、勿打入二进制。 |
```bash
# 1. 配置数据库连接(会先验证连接可用再写入)
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
# 2. 初始化主密钥(提示输入至少 8 位的主密码,派生后存入 OS 钥匙串)
secrets init
cargo run -p secrets-mcp
```
主密码不会存储,仅用于派生主密钥,且至少需 8 位。同一主密码在所有设备上会得到相同主密钥salt 存于数据库,首台设备生成后共享)。
- **Web**`BASE_URL`登录、Dashboard、设置密码短语、创建 API Key)。
- **MCP**Streamable HTTP 基址 `{BASE_URL}/mcp`,需 `Authorization: Bearer <api_key>` + `X-Encryption-Key: <hex>` 请求头。
**主密钥存储**macOS → KeychainWindows → Credential ManagerLinux → keyutils会话级重启后需再次 `secrets init`)。
## 加密架构(混合 E2EE
**从旧版(明文存储)升级**:升级后首次运行需执行 `secrets init` 即可(明文记录需手动重新 add 或通过 update 更新)。
### 密钥派生
## AI Agent 快速指南
用户在 Web Dashboard 设置**密码短语**,浏览器使用 **Web Crypto APIPBKDF2-SHA256600k 次迭代)**在本地派生 256-bit AES 密钥。
这个 CLI 以 AI 使用优先设计。核心路径只有一条:**读取用 `search`,写入用 `add` / `update`**。
- **Salt32B**:首次设置时在浏览器生成,存入服务端 `users.key_salt`
- **key_check**:派生密钥加密已知常量 `"secrets-mcp-key-check"`,存入 `users.key_check`,用于登录时验证密码短语
- **服务端不存储原始密钥**,只存 salt + key_check
### 第一步:发现有哪些数据
跨设备同步:新设备登录 → 输入相同密码短语 → 从服务端取 salt → 同样的 PBKDF2 → 得到相同密钥。
```bash
# 列出所有记录摘要(默认最多 50 条,安全起步)
secrets search --summary --limit 20
### 写入与读取流程
# 按 namespace 过滤
secrets search -n refining --summary --limit 20
```mermaid
flowchart LR
subgraph Web["Web 浏览器E2E"]
P["密码短语"] --> K["PBKDF2 → 256-bit key"]
K --> Enc["AES-256-GCM 加密"]
K --> Dec["AES-256-GCM 解密"]
end
# 按最近更新排序
secrets search --sort updated --limit 10 --summary
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
```
`--summary` 只返回轻量字段namespace、kind、name、tags、desc、updated_at不含完整 metadata 和 secrets。
### 两种客户端对比
### 第二步:精确读取单条记录
| | Web 浏览器 | AI 客户端MCP |
|---|---|---|
| 密钥位置 | 仅在浏览器内存 / sessionStorage | MCP 配置 headers 中 |
| 加解密位置 | 客户端(真正 E2E | 服务端临时(请求级生命周期) |
| 安全边界 | 服务端零知识 | 依赖 TLS + 服务端内存隔离 |
```bash
# 精确定位namespace + kind + name 三元组)
secrets search -n refining --kind service --name gitea
### 敏感数据传输
# 获取完整记录(含 secrets 字段名,无需 master_key
secrets search -n refining --kind service --name gitea -o json
- **OAuth `client_secret`** 只存服务端环境变量,不发给浏览器
- **API Key** 创建时原始 key 仅展示一次,库中只存 SHA-256 哈希
- **X-Encryption-Key** 随 MCP 请求经 TLS 传输,服务端仅在请求处理期间持有(不持久化)
- **生产环境必须走 HTTPS/TLS**
# 直接提取单个 metadata 字段值(最短路径)
secrets search -n refining --kind service --name gitea -f metadata.url
## AI 客户端配置
# 同时提取多个 metadata 字段
secrets search -n refining --kind service --name gitea \
-f metadata.url -f metadata.default_org
在 Web Dashboard 设置密码短语后,解锁页面会按客户端格式生成配置。常见客户端示例如下:
# 需要 secrets 时,改用 run只注入 token 字段到子进程)
secrets run -n refining --kind service --name gitea -s token -- ./deploy.sh
`Cursor / Claude Desktop` 风格:
# 预览 run 会注入哪些变量(不执行命令)
secrets run -n refining --kind service --name gitea --dry-run
```json
{
"mcpServers": {
"secrets": {
"url": "https://secrets.example.com/mcp",
"headers": {
"Authorization": "Bearer sk_abc123...",
"X-Encryption-Key": "a1b2c3...64位hex"
}
}
}
}
```
`search` 展示 metadata 与 secrets 的字段名,不展示 secret 值本身;需要 secret 值时用 `run`(仅注入加密字段到子进程,不含 metadata。用 `-s` 指定只注入特定字段,最小化注入范围。
`OpenCode` 风格:
### 输出格式
| 场景 | 推荐命令 |
|------|----------|
| AI 解析 / 管道处理(默认) | jsonpretty-printed |
| 管道紧凑格式 | `-o json-compact` |
| 注入 secrets 到子进程环境 | `run` |
| 人类查看 | `-o text` |
默认始终输出 JSON无论是 TTY 还是管道。`text` 输出中时间按本地时区显示;`json/json-compact` 使用 UTCRFC3339
```bash
# 默认 JSON 输出,直接可 jq 解析
secrets search -n refining --kind service | jq '.[].name'
# 需要 secrets 时,使用 run-s 指定只注入特定字段)
secrets run -n refining --kind service --name gitea -s token -- ./deploy.sh
# 预览 run 会注入哪些变量(不执行命令)
secrets run -n refining --kind service --name gitea --dry-run
```
## 完整命令参考
```bash
# 查看帮助(包含各子命令 EXAMPLES
secrets --help
secrets init --help # 主密钥初始化
secrets search --help
secrets add --help
secrets update --help
secrets delete --help
secrets config --help
secrets upgrade --help # 检查并更新 CLI 版本
secrets export --help # 批量导出JSON/TOML/YAML
secrets import --help # 批量导入JSON/TOML/YAML
# ── search ──────────────────────────────────────────────────────────────────
secrets search --summary --limit 20 # 发现概览
secrets search -n refining --kind service # 按 namespace + kind
secrets search -n refining --kind service --name gitea # 精确查找
secrets search -q mqtt # 关键词模糊搜索
secrets search --tag hongkong # 按 tag 过滤
secrets search -n refining --kind service --name gitea -f metadata.url # 提取 metadata 字段
secrets search -n refining --kind service --name gitea -o json # 完整记录(含 secrets schema
secrets search --sort updated --limit 10 --summary # 最近改动
secrets search -n refining --summary --limit 10 --offset 10 # 翻页
# ── add ──────────────────────────────────────────────────────────────────────
secrets add -n refining --kind server --name my-server \
--tag aliyun --tag shanghai \
-m ip=10.0.0.1 -m desc="Example ECS" \
-s username=root -s ssh_key=@./keys/server.pem
# 多行文件直接写入嵌套 secret 字段
secrets add -n refining --kind server --name my-server \
-s credentials:content@./keys/server.pem
# 使用 typed JSON 写入 secret布尔、数字、数组、对象
secrets add -n refining --kind service --name deploy-bot \
-s enabled:=true \
-s retry_count:=3 \
-s scopes:='["repo","workflow"]' \
-s extra:='{"region":"ap-east-1","verify_tls":true}'
secrets add -n refining --kind service --name gitea \
--tag gitea \
-m url=https://code.example.com -m default_org=myorg \
-s token=<token>
# ── update ───────────────────────────────────────────────────────────────────
secrets update -n refining --kind server --name my-server -m ip=10.0.0.1
secrets update -n refining --kind service --name gitea --add-tag production -s token=<new>
secrets update -n refining --kind service --name mqtt --remove-meta old_port --remove-secret old_key
secrets update -n refining --kind server --name my-server --remove-secret credentials:content
# ── delete ───────────────────────────────────────────────────────────────────
secrets delete -n refining --kind service --name legacy-mqtt # 精确删除单条(--kind 必填)
secrets delete -n refining --dry-run # 预览批量删除(不写入)
secrets delete -n ricnsmart # 批量删除整个 namespace
secrets delete -n ricnsmart --kind server # 批量删除指定 kind
# ── init ─────────────────────────────────────────────────────────────────────
secrets init # 主密钥初始化(每台设备一次,主密码至少 8 位,派生后存钥匙串)
# ── config ───────────────────────────────────────────────────────────────────
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets" # 先验证再写入
secrets config show # 密码脱敏展示
secrets config path # 打印配置文件路径
# ── upgrade ──────────────────────────────────────────────────────────────────
secrets upgrade --check # 仅检查是否有新版本
secrets upgrade # 下载、校验 SHA-256 并安装最新版(可通过 SECRETS_UPGRADE_URL 自托管)
# ── export ────────────────────────────────────────────────────────────────────
secrets export --file backup.json # 全量导出到 JSON
secrets export -n refining --file refining.toml # 按 namespace 导出为 TOML
secrets export -n refining --kind service --file svc.yaml # 按 kind 导出为 YAML
secrets export --tag production --file prod.json # 按 tag 过滤
secrets export -q mqtt --file mqtt.json # 模糊搜索导出
secrets export --no-secrets --file schema.json # 仅导出 schema无需主密钥
secrets export -n refining --format yaml # 输出到 stdout指定格式
# ── import ────────────────────────────────────────────────────────────────────
secrets import backup.json # 导入(冲突时报错)
secrets import --force refining.toml # 冲突时覆盖已有记录
secrets import --dry-run backup.yaml # 预览将要执行的操作(不写入)
# ── run ───────────────────────────────────────────────────────────────────────
secrets run -n refining --kind service --name gitea -- ./deploy.sh # 注入全部 secrets
secrets run -n refining --kind service --name gitea -s token -- ./deploy.sh # 只注入 token 字段
secrets run -n refining --kind service --name aliyun \
-s access_key_id -s access_key_secret -- aliyun ecs DescribeInstances # 只注入指定字段
secrets run --tag production -- env # 按 tag 批量注入
secrets run -n refining --kind service --name gitea --dry-run # 预览变量映射
secrets run -n refining --kind service --name gitea -s token --dry-run # 过滤后预览
secrets run -n refining --kind service --name gitea --dry-run -o text # 人类可读预览
# ── 调试 ──────────────────────────────────────────────────────────────────────
secrets --verbose search -q mqtt
RUST_LOG=secrets=trace secrets search
```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`namespace、kind、name、tags、metadata+ 子表 `secrets`(每个加密字段一行,含 field_nameencrypted)。首次连接自动建表;同时创建 `audit_log``entries_history``secrets_history`表。
主表 **`entries`**`namespace``kind``name``tags``metadata`,多租户时带 `user_id`+ 子表 **`secrets`**(每行一个加密字段:`field_name``encrypted`)。另有 `entries_history``secrets_history``audit_log`,以及 **`users`**(含 `key_salt``key_check``key_params`)、**`oauth_accounts`**、**`api_keys`**。首次连库自动迁移建表。
| 位置 | 字段 | 说明 |
|------|------|------|
| entries | namespace | 一级隔离,如 `refining``ricnsmart` |
| entries | kind | 记录类型,如 `server``service``key`(可自由扩展) |
| entries | name | 人类可读唯一标识 |
| entries | tags | 多维标签,如 `["aliyun","hongkong"]` |
| entries | metadata | 明文描述ip、desc、domains、key_ref 等) |
| secrets | field_name | 明文search 可见AI 可推断 run 会注入哪些变量 |
| secrets | encrypted | 仅加密值本身AES-256-GCM |
| entries | kind | `server``service``key`(可扩展) |
| entries | name | 人类可读标识 |
| entries | metadata | 明文 JSONip、url、`key_ref` 等) |
| secrets | field_name | 明文字段名,便于 schema 展示 |
| secrets | encrypted | AES-GCM 密文(含 nonce |
| users | key_salt | PBKDF2 salt32B首次设置密码短语时写入 |
| users | key_check | 派生密钥加密已知常量,用于验证密码短语 |
| users | key_params | 派生算法参数,如 `{"alg":"pbkdf2-sha256","iterations":600000}` |
`-m` / `--meta` 写入 `metadata``-s` / `--secret` 写入 `secrets` 表的独立行。支持 `key=value``key=@file``key:=<json>`,也支持 `credentials:content@./key.pem` 这类嵌套字段文件写入;删除时支持 `--remove-secret credentials:content`。加解密使用主密钥(由 `secrets init` 设置)。
### PEM 共享(`key_ref`
**PEM 共享**:同一 PEM 被多台服务器共享时,可存为 `kind=key` 记录,服务器通过 `metadata.key_ref` 引用;轮换只需 update 一条 key 记录,所有引用自动生效。详见 [AGENTS.md](AGENTS.md)
### `-m` / `--meta` JSON 语法速查
`-m``-s` 走的是同一套解析规则,只是写入位置不同:`-m` 写到明文 `metadata`,适合端口、开关、标签、描述性配置等非敏感信息。
| 目标值 | 写法示例 | 实际存入 |
|------|------|------|
| 普通字符串 | `-m url=https://code.example.com` | `"https://code.example.com"` |
| 文件内容字符串 | `-m notes=@./service-notes.txt` | `"..."` |
| 布尔值 | `-m enabled:=true` | `true` |
| 数字 | `-m port:=3000` | `3000` |
| `null` | `-m deprecated_at:=null` | `null` |
| 数组 | `-m domains:='["code.example.com","git.example.com"]'` | `["code.example.com","git.example.com"]` |
| 对象 | `-m tls:='{"enabled":true,"redirect_http":true}'` | `{"enabled":true,"redirect_http":true}` |
| 嵌套路径 + JSON | `-m deploy:strategy:='{"type":"rolling","batch":2}'` | `{"deploy":{"strategy":{"type":"rolling","batch":2}}}` |
常见规则:
- `=` 表示按字符串存储。
- `:=` 表示按 JSON 解析。
- shell 中数组和对象建议整体用单引号包住。
- 嵌套字段继续用冒号分隔:`-m runtime:max_open_conns:=20`
示例:新增一条带 typed metadata 的记录
```bash
secrets add -n refining --kind service --name gitea \
-m url=https://code.example.com \
-m port:=3000 \
-m enabled:=true \
-m domains:='["code.example.com","git.example.com"]' \
-m tls:='{"enabled":true,"redirect_http":true}'
```
示例:更新已有记录中的嵌套 metadata
```bash
secrets update -n refining --kind service --name gitea \
-m deploy:strategy:='{"type":"rolling","batch":2}' \
-m runtime:max_open_conns:=20
```
### `-s` / `--secret` JSON 语法速查
当你希望写入的不是普通字符串,而是 `true``123``null`、数组或对象时,用 `:=`,右侧按 JSON 解析。
| 目标值 | 写法示例 | 实际存入 |
|------|------|------|
| 普通字符串 | `-s token=abc123` | `"abc123"` |
| 文件内容字符串 | `-s ssh_key=@./id_ed25519` | `"-----BEGIN ..."` |
| 布尔值 | `-s enabled:=true` | `true` |
| 数字 | `-s retry_count:=3` | `3` |
| `null` | `-s deprecated_at:=null` | `null` |
| 数组 | `-s scopes:='["repo","workflow"]'` | `["repo","workflow"]` |
| 对象 | `-s extra:='{"region":"ap-east-1","verify_tls":true}'` | `{"region":"ap-east-1","verify_tls":true}` |
| 嵌套路径 + JSON | `-s auth:policy:='{"mfa":true,"ttl":3600}'` | `{"auth":{"policy":{"mfa":true,"ttl":3600}}}` |
常见规则:
- `=` 表示按字符串存储,不做 JSON 解析。
- `:=` 表示按 JSON 解析,适合布尔、数字、数组、对象、`null`
- shell 里对象和数组通常要整体加引号,推荐单引号:`-s flags:='["a","b"]'`
- 嵌套字段继续用冒号分隔:`-s credentials:enabled:=true`
- 如果你就是想存一个“JSON 字符串字面量”,可以写成 `-s note:='"hello"'`,但大多数字符串场景直接用 `=` 更直观。
示例:新增一条同时包含字符串、文件、布尔、数组、对象的记录
```bash
secrets add -n refining --kind service --name deploy-bot \
-s token=abc123 \
-s ssh_key=@./keys/deploy-bot.pem \
-s enabled:=true \
-s scopes:='["repo","workflow"]' \
-s policy:='{"ttl":3600,"mfa":true}'
```
示例:更新已有记录中的嵌套 JSON 字段
```bash
secrets update -n refining --kind service --name deploy-bot \
-s auth:config:='{"issuer":"gitea","rotate":true}' \
-s auth:retry:=5
```
同一 PEM 可被多条 `server` 记录引用:将 PEM 存为 `kind=key` 的 entry在服务器条目的 `metadata.key_ref` 中写 key 的名称;轮换时只更新 key 对应记录即可
## 审计日志
`add``update``delete` 操作成功后自动向 `audit_log` 表写入一条记录,包含操作类型、操作对象和变更摘要不含 secret 值)。操作者取自 `$USER` 环境变量
`add``update``delete` 等写操作写入 **`audit_log`**(操作类型、对象、摘要不含 secret 明文)
```sql
-- 查看最近 20 条审计记录
SELECT action, namespace, kind, name, actor, detail, created_at
FROM audit_log
ORDER BY created_at DESC
@@ -312,49 +153,19 @@ LIMIT 20;
## 项目结构
```
src/
main.rs # CLI 入口clap含各子命令 after_help 示例
output.rs # OutputMode 枚举 + TTY 检测
config.rs # 配置读写(~/.config/secrets/config.toml
db.rs # 连接池 + auto-migrateentries + secrets + entries_history + secrets_history + audit_log + kv_config
crypto.rs # AES-256-GCM 加解密、Argon2id 派生、OS 钥匙串
models.rs # Entry + SecretField 结构体
audit.rs # 审计日志写入audit_log 表)
commands/
init.rs # 主密钥初始化(首次/新设备)
add.rs # upsert entries + secrets 行,支持 -o json
config.rs # config set-db/show/path
search.rs # 多条件查询,展示 secrets schema-f/-o/--summary/--limit/--offset/--sort
delete.rs # 删除CASCADE 删除 secrets
update.rs # 增量更新tags/metadata + secrets 行级 UPSERT/DELETE
rollback.rs # rollback / history按 entry_version 恢复
run.rs # run仅 secrets 逐字段解密 + key_ref 引用解析(不含 metadata
upgrade.rs # 从 Gitea Release 自更新
export_cmd.rs # export批量导出支持 JSON/TOML/YAML含解密明文
import_cmd.rs # import批量导入冲突检测dry-run重新加密写入
Cargo.toml
crates/secrets-core/ # db / crypto / models / audit / service
crates/secrets-mcp/ # MCP HTTP、Web、OAuth、API Key
scripts/
setup-gitea-actions.sh # 配置 Gitea Actions 变量与 Secrets
deploy/ # systemd、.env 示例
```
## CI/CDGitea Actions
推送 `main` 分支时自动fmt/clippy/test 检查 → Linux/macOS/Windows 构建 → 上传二进制与 `.sha256` 摘要 → 所有平台成功后发布 Release
**首次使用需配置 Actions 变量和 Secrets**
见 [`.gitea/workflows/secrets.yml`](.gitea/workflows/secrets.yml)。变更 `crates/**``deploy/**`、根目录 `Cargo.toml`/`Cargo.lock` 并推送到配置的分支时fmt / clippy / test → 构建 `x86_64-unknown-linux-musl` → tag `secrets-mcp-<version>` 与 Release 产物 → 可选 SSH 部署
```bash
# 需有 ~/.config/gitea/config.envGITEA_URL、GITEA_TOKEN、GITEA_WEBHOOK_URL
./scripts/setup-gitea-actions.sh
./scripts/setup-gitea-actions.sh # 配置 Gitea 变量与 Secrets
```
- `RELEASE_TOKEN`SecretGitea PAT用于创建 Release 上传二进制
- `WEBHOOK_URL`Variable飞书通知可选
- **注意**Secret/Variable 的 `data`/`value` 字段需传入原始值,不要 base64 编码
当前 Release 预编译产物覆盖:
- Linux `x86_64-unknown-linux-musl`
- macOS Apple Silicon `aarch64-apple-darwin`
- macOS Intel `x86_64-apple-darwin`(由 ARM mac runner 交叉编译)
- Windows `x86_64-pc-windows-msvc`
详见 [AGENTS.md](AGENTS.md)。
详见 [AGENTS.md](AGENTS.md)(发版规则、代码规范)。