Files
secrets/ai-secrets-manager-design.md
2026-03-05 16:53:52 +08:00

382 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI-Native Secret Manager 设计文档
面向 AI AgentCursor / OpenCode的轻量级 Secret 管理工具,通过 MCP 协议提供安全的环境变量注入secret 永远不进入 LLM 上下文。
## 背景与动机
### 当前痛点
在使用 AI 编码助手Cursor、OpenCode进行服务器运维、部署等操作时经常需要提供敏感凭证
```
# 当前做法:明文存储 + 聊天中粘贴
~/.../ricnsmart/config.toml ← 明文 TOMLiCloud 同步
~/.../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** | 管理 secretsCRUD、初始化、导入 | 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 Serverstdio 模式)
```
## 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/ASICOWASP 推荐 |
| 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 Serverinject_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