commit 54a513f8454be68e066cf552d29f9eabc8108761 Author: voson Date: Thu Mar 5 16:53:52 2026 +0800 Initial commit diff --git a/ai-secrets-manager-design.md b/ai-secrets-manager-design.md new file mode 100644 index 0000000..7fcebf2 --- /dev/null +++ b/ai-secrets-manager-design.md @@ -0,0 +1,381 @@ +# AI-Native Secret Manager 设计文档 + +面向 AI Agent(Cursor / OpenCode)的轻量级 Secret 管理工具,通过 MCP 协议提供安全的环境变量注入,secret 永远不进入 LLM 上下文。 + +## 背景与动机 + +### 当前痛点 + +在使用 AI 编码助手(Cursor、OpenCode)进行服务器运维、部署等操作时,经常需要提供敏感凭证: + +``` +# 当前做法:明文存储 + 聊天中粘贴 +~/.../ricnsmart/config.toml ← 明文 TOML,iCloud 同步 +~/.../refining/config.toml ← 包含 AK/SK、密码、API Key +~/.../*/keys/*.pem ← SSH 私钥 +聊天中直接粘贴 AccessKey ← 明文进入 LLM context → 发送到云端 API +``` + +**核心安全问题**:Secret 明文进入 LLM 的上下文窗口,被发送到 Anthropic/OpenAI 等远程服务器,并留存在聊天历史中。 + +### 设计目标 + +1. **Secret 不进入 LLM 上下文**:AI Agent 只知道环境变量名(`$VAR_NAME`),不知道值 +2. **跨设备**:多台电脑共享同一套 secrets(云端 PostgreSQL) +3. **面向 AI Agent**:通过 MCP 协议无缝集成 Cursor / OpenCode +4. **轻量**:核心 500-800 行代码,不依赖重型框架 +5. **安全**:客户端加密,PostgreSQL 只存密文,Master Key 存本地 OS Keychain + +### 市场定位 + +| 工具 | 定位 | AI Agent 支持 | 自托管 | +|------|------|--------------|--------| +| HashiCorp Vault | 企业级 Secret 管理 | 无 | 是 | +| Infisical | 开发团队 Secret 管理 | 无 | 是 | +| 1Password MCP | 消费级密码管理 + AI | 有(SaaS) | 否 | +| SOPS / age | 文件加密 | 无 | - | +| **本项目** | **AI Agent 的 Secret 注入** | **核心功能** | **是** | + +"AI-native Secret Manager" 这个定位目前市场几乎空白。 + +## 架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 设备 A (MacBook) 设备 B (其他电脑) │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Cursor/OC │ │ Cursor/OC │ │ +│ │ ↓ MCP │ │ ↓ MCP │ │ +│ │ secrets-mcp │ │ secrets-mcp │ │ +│ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ +│ ┌──────┴───────┐ ┌──────┴───────┐ │ +│ │ OS Keychain │ │ OS Keychain │ │ +│ │ (master key) │ │ (master key) │ │ +│ └──────────────┘ └──────────────┘ │ +│ │ │ │ +└─────────┼───────────────────────────────┼───────────────────────┘ + │ TLS + Auth │ + └───────────────┬───────────────┘ + │ + ┌─────────┴─────────┐ + │ Cloud PostgreSQL │ + │ (加密存储 secrets) │ + └───────────────────┘ +``` + +### 组件 + +| 组件 | 职责 | 技术选型 | +|------|------|---------| +| **CLI** | 管理 secrets(CRUD)、初始化、导入 | Rust | +| **MCP Server** | AI Agent 接口、环境变量注入 | Rust (stdio) | +| **PostgreSQL** | 持久化存储(密文) | 云端 PG | +| **OS Keychain** | 存储 Master Key | macOS Keychain / Linux secret-service | + +CLI 和 MCP Server 共用加密库,可编译为同一个二进制(子命令区分): + +```bash +secrets set ricnsmart/aliyun.ak "LTAI5t..." # CLI 模式 +secrets mcp # MCP Server 模式(stdio) +``` + +## 核心设计 + +### 1. 安全模型:Secret 不进入 LLM 上下文 + +**两种模式对比**: + +| | 模式 A: AI 拿明文 | 模式 B: 环境变量注入 | +|---|---|---| +| AI 看到什么 | `"LTAI5t79fLxc..."` | `"$ACS_ACCESS_KEY_ID"` | +| LLM context 暴露 | secret 明文 | 只有变量名 | +| 云端 API 风险 | 明文发到 Anthropic | 只发变量名 | +| 聊天历史 | 明文留存 | 安全 | + +**本项目采用模式 B**。 + +工作流: + +``` +AI Agent: 调用 MCP tool inject_secrets("ricnsmart") +MCP Server: (本地解密,注入环境变量) + → 返回给 AI: "已注入: $SSH_KEY_RICNSMART, $HOST_TIANCHU_PRIMARY, ..." +AI Agent: 执行 shell + ssh -i "$SSH_KEY_RICNSMART" "$SSH_USER@$HOST_TIANCHU_PRIMARY" "..." + ← shell 进程拿到真实值,LLM 只写了变量名 +``` + +### 2. 加密模型:客户端加密 + 信封加密 + +``` +存储路径: + secret_value + → AES-256-GCM(DEK) → 密文 → 存入 PostgreSQL + DEK (Data Encryption Key) + → AES-256-GCM(Master Key) → 加密 DEK → 存入 PostgreSQL + Master Key + → Argon2id(password) → 派生 → 存入 OS Keychain +``` + +PostgreSQL 只存密文和加密后的 DEK,被攻破也无法获取明文。 + +**每条 secret 使用独立的 DEK**,限制单个密钥泄露的影响范围。 + +### 3. 跨设备 Master Key 同步 + +采用 **密码派生** 方案: + +```bash +# 新设备初始化(只需一次) +secrets init +# 输入 master password → Argon2id 派生 Master Key → 存入 OS Keychain +# 之后自动从 Keychain 获取,无需再次输入 +``` + +所有设备使用相同密码,派生出相同的 Master Key。Argon2id 的 salt 存储在 PostgreSQL 中。 + +### 4. MCP Tool 接口 + +```typescript +// Tool 1: 注入 secrets 到当前 shell 环境(核心功能) +// AI Agent 调用后,后续 shell 命令可通过 $VAR 引用 +inject_secrets(project: string) +→ { injected: string[], hint: string } +// 示例返回: { injected: ["TIANCHU_HOST_PRIMARY", "TIANCHU_SSH_KEY", ...], +// hint: "使用 $TIANCHU_HOST_PRIMARY 引用天储主服务器" } + +// Tool 2: 列出可用 projects 和 keys(不含值) +list_secrets(project?: string) +→ string[] +// 示例返回: ["ricnsmart/ssh.tianchu-primary", "ricnsmart/aliyun.ak", ...] + +// Tool 3: 用 secret 执行命令(secret 不经过 LLM,可选) +exec_with_secrets(project: string, command: string) +→ { stdout: string, stderr: string, exit_code: number } +// MCP Server 本地替换 $VAR 后执行,输出可脱敏 +``` + +### 5. 环境变量命名规则 + +``` +project: ricnsmart, key: aliyun.access_key_id +→ 环境变量: RICNSMART_ALIYUN_ACCESS_KEY_ID + +project: refining, key: grafana.password +→ 环境变量: REFINING_GRAFANA_PASSWORD + +规则: upper(project) + "_" + upper(key.replace(".", "_")) +``` + +## 数据模型 + +### PostgreSQL Schema + +```sql +CREATE TABLE projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT UNIQUE NOT NULL, -- "ricnsmart", "refining" + description TEXT, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE TABLE secrets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES projects(id) ON DELETE CASCADE, + key TEXT NOT NULL, -- "aliyun.access_key_id" + encrypted_value BYTEA NOT NULL, -- AES-256-GCM 密文 + encrypted_dek BYTEA NOT NULL, -- 加密后的 DEK + nonce BYTEA NOT NULL, -- GCM nonce + dek_nonce BYTEA NOT NULL, -- DEK 加密的 nonce + kind TEXT NOT NULL DEFAULT 'text', -- "text", "ssh-key", "json", "file" + description TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(project_id, key) +); + +-- Argon2id salt,跨设备共享 +CREATE TABLE config ( + key TEXT PRIMARY KEY, + value BYTEA NOT NULL +); +-- INSERT INTO config (key, value) VALUES ('argon2_salt', ...); + +-- 审计日志(可选) +CREATE TABLE audit_log ( + id BIGSERIAL PRIMARY KEY, + secret_id UUID REFERENCES secrets(id) ON DELETE SET NULL, + action TEXT NOT NULL, -- "read", "write", "delete", "inject" + device TEXT, -- 设备标识 + detail TEXT, -- 额外信息 + created_at TIMESTAMPTZ DEFAULT now() +); +``` + +### 从现有 TOML 配置迁移的映射 + +``` +# ricnsmart/config.toml + +[[servers]] → ricnsmart/server.tianchu-primary (kind=json) +hostname = "..." { "hostname": "...", "ip": "8.153.204.96", +public_ip = "8.153.204.96" "key": "ricnsmart-sh.pem", "user": "ecs-user" } +key = "ricnsmart-sh.pem" + +[services.gitea] → ricnsmart/gitea.url = "https://gitea.refining.dev" +url = "https://gitea.refining.dev" ricnsmart/gitea.token = "92a627..." +token = "92a627..." + +# refining/config.toml + +[services.grafana] → refining/grafana.url = "https://grafana.refining.dev" +url = "..." refining/grafana.username = "voson" +username = "voson" refining/grafana.password = "ZXG-..." +password = "ZXG-..." + +[services.cloudflare] → refining/cloudflare.api_key = "57a99d..." +global_api_key = "57a99d..." refining/cloudflare.email = "voson..." + +[services.aliyun] → digitevents/aliyun.access_key_id = "LTAI5t..." +AccessKeyId = "..." digitevents/aliyun.access_key_secret = "ZKgl..." +AccessKeySecret = "..." + +# SSH 密钥 → ricnsmart/ssh-key.ricnsmart-sh (kind=file) +~/.../keys/ricnsmart-sh.pem refining/ssh-key.ricn-hk (kind=file) +``` + +## CLI 命令设计 + +```bash +# ===== 初始化 ===== +secrets init # 输入 master password,派生密钥,存入 Keychain +secrets init --db "postgres://..." # 指定 PG 连接串(也存入 Keychain) + +# ===== 项目管理 ===== +secrets project list # 列出所有项目 +secrets project create ricnsmart # 创建项目 +secrets project delete ricnsmart # 删除项目(需确认) + +# ===== Secret 管理 ===== +secrets set ricnsmart/aliyun.ak "LTAI5t..." # 设置 text 类型 +secrets set ricnsmart/ssh.key --file ~/.ssh/ricnsmart.pem # 设置 file 类型 +secrets set ricnsmart/server.primary --json '{"host":...}' # 设置 json 类型 + +secrets get ricnsmart/aliyun.ak # 获取值(输出到 stdout) +secrets list # 列出所有(不显示值) +secrets list ricnsmart/ # 列出项目下的(不显示值) +secrets delete ricnsmart/aliyun.ak # 删除 + +# ===== 导入 ===== +secrets import config.toml # 从现有 TOML 导入 +secrets import config.toml --project ricnsmart --dry-run # 预览 + +# ===== 环境变量注入 ===== +secrets inject ricnsmart -- ./app # 注入后启动子进程 +secrets inject ricnsmart --export # 输出 export 语句 + +# ===== MCP Server ===== +secrets mcp # 启动 MCP Server(stdio 模式) +``` + +## MCP 集成配置 + +### Cursor (.cursor/mcp.json) + +```json +{ + "mcpServers": { + "secrets": { + "command": "secrets", + "args": ["mcp"] + } + } +} +``` + +### OpenCode + +```yaml +mcpServers: + secrets: + command: secrets + args: [mcp] +``` + +配置后,AI Agent 在需要凭证时会自动调用 `inject_secrets` 或 `list_secrets`,而非要求用户粘贴。 + +## 安全注意事项 + +### Secret 生命周期 + +``` +创建: CLI set → 本地加密 → 密文存 PG +读取: MCP inject → PG 拉取密文 → 本地解密 → 注入环境变量 → 返回变量名给 AI +使用: AI 写 shell 命令引用 $VAR → shell 进程替换为真实值 → 执行 +清理: 进程退出后环境变量消失 +``` + +### 威胁模型 + +| 威胁 | 防御措施 | +|------|---------| +| PostgreSQL 被攻破 | 客户端加密,PG 只有密文 | +| 设备丢失 | Master Key 在 OS Keychain(需要设备密码) | +| LLM 提供商窥探 | Secret 不进入 LLM context,只有变量名 | +| 命令输出泄露 secret | MCP Server 可选输出脱敏(替换已知 secret 值为 `***`) | +| Master Password 弱 | Argon2id 高计算成本(推荐配置:m=64MB, t=3, p=4) | + +### 不防御的场景 + +- 设备上的 root 权限攻击者(可直接读取进程环境变量) +- 用户主动将 secret 粘贴到聊天中(行为问题,非技术问题) + +## 技术选型 + +| 组件 | 选择 | 理由 | +|------|------|------| +| 语言 | Rust | 单二进制分发、加密库成熟、性能好 | +| 加密 | `aes-gcm` crate | AEAD,业界标准 | +| 密钥派生 | `argon2` crate | 抗 GPU/ASIC,OWASP 推荐 | +| OS Keychain | `keyring` crate | 跨平台(macOS/Linux/Windows) | +| 数据库 | `sqlx` crate + PostgreSQL | async、编译期 SQL 检查 | +| MCP 协议 | `rmcp` 或手动实现 JSON-RPC over stdio | MCP 协议本身很简单 | +| CLI 框架 | `clap` | Rust 生态标准 | + +## 开发计划 + +### Phase 1: MVP(核心功能) + +- [ ] 项目脚手架(Cargo workspace:`cli` + `core` + `mcp`) +- [ ] 加密模块(AES-256-GCM + Argon2id) +- [ ] PostgreSQL 存储层(schema + CRUD) +- [ ] OS Keychain 集成 +- [ ] CLI 基本命令(init / set / get / list / delete) +- [ ] MCP Server(inject_secrets / list_secrets) +- [ ] Cursor 集成测试 + +### Phase 2: 完善 + +- [ ] `secrets import config.toml` 导入现有配置 +- [ ] `secrets inject -- ./app` 环境变量注入启动子进程 +- [ ] 审计日志 +- [ ] 输出脱敏(可选) +- [ ] CI/CD 集成(Gitea Actions 中使用) + +### Phase 3: 扩展 + +- [ ] Web UI(查看/管理 secrets) +- [ ] Secret 轮换提醒 +- [ ] 团队共享(多用户 + RBAC) +- [ ] `exec_with_secrets` MCP tool + +## 参考 + +- [MCP 协议规范](https://modelcontextprotocol.io/) +- [Infisical](https://github.com/Infisical/infisical) - 开源 Secret Manager +- [age](https://github.com/FiloSottile/age) - 文件加密工具 +- [SOPS](https://github.com/getsops/sops) - Mozilla 的加密文件编辑器 +- [Rust `aes-gcm`](https://docs.rs/aes-gcm/) - AEAD 加密 +- [Rust `argon2`](https://docs.rs/argon2/) - 密码哈希 +- [Rust `keyring`](https://docs.rs/keyring/) - 跨平台 Keychain