Files
secrets/ai-secrets-manager-design.md

545 lines
21 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 管理工具。通过 CLI 提供安全的环境变量注入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. **跨设备、跨平台**macOS / Windows / Linux 多台电脑共享同一套 secrets
3. **面向 AI Agent**AI 通过 CLI 进行 CRUD 和环境变量注入
4. **人性化管理**Web UI 提供可视化的 secret 管理界面
5. **轻量安全**客户端加密CLI + WASMServer 端永远不接触敏感明文
### 市场定位
| 工具 | 定位 | AI Agent 支持 | 客户端加密 | 自托管 |
|------|------|--------------|-----------|--------|
| HashiCorp Vault | 企业级 Secret 管理 | 无 | 否 | 是 |
| Infisical | 开发团队 Secret 管理 | 无 | 有 | 是 |
| 1Password MCP | 消费级密码管理 + AI | 有SaaS | 有 | 否 |
| SOPS / age | 文件加密 | 无 | 是 | - |
| **本项目** | **AI Agent 的 Secret 注入** | **核心功能** | **是** | **是** |
## 架构
### 整体架构
```
Rust crypto crate加密核心
┌──────────────────────────┐
│ AES-256-GCM / Argon2id │
│ DEK 管理 / 信封加密 │
└──────┬──────────┬─────────┘
│ │
编译为 native │ │ 编译为 WASM
│ │
┌────────────────────┐ │ ┌────┴───────────────┐
│ CLI (Rust 跨平台) │◄──────┘ │ Web UI (浏览器) │
│ + OS Keychain │ │ + WASM 加密模块 │
│ macOS / Win / Linux│ │ + wasm-bindgen │
└─────────┬──────────┘ └─────────┬──────────┘
│ HTTPS │ HTTPS
└──────────────┬───────────────────┘
┌─────────┴──────────┐
│ Remote Server │
│ (Rust / Axum) │
│ ├─ HTTPS API │
│ └─ 托管 Web UI + │
│ WASM 静态文件 │
└─────────┬──────────┘
┌─────────┴──────────┐
│ PostgreSQL 18 │
└────────────────────┘
```
### 组件
| 组件 | 职责 | 技术选型 |
|------|------|---------|
| **crypto** | 加密核心AES-256-GCM + Argon2id + 信封加密) | Rust crate编译为 native + wasm32 |
| **server** | HTTPS API、数据库 CRUD、认证、托管静态文件 | Rust / Axum |
| **cli** | 客户端加解密、OS Keychain、环境变量注入、CRUD | Rust / clap |
| **web (WASM)** | 浏览器端加解密绑定 | Rust / wasm-bindgen |
| **web-ui** | 可视化 Secret 管理界面 | HTML / JS / CSS |
| **PostgreSQL 18** | 持久化存储(密文 + 明文元数据) | 云端 PG |
### Cargo Workspace 结构
```
secrets/
├── crates/
│ ├── crypto/ ← 加密核心(编译目标: native + wasm32
│ ├── server/ ← Axum HTTPS Server
│ ├── cli/ ← CLI 客户端(链接 crypto native
│ └── web/ ← WASM 绑定wasm-bindgen链接 crypto wasm
├── web-ui/ ← 前端代码(调用 WASM 模块)
└── Cargo.toml ← workspace 根配置
```
## 核心设计
### 1. 安全模型:分层暴露策略
Secret 在不同生命周期阶段有不同的暴露等级:
| 阶段 | Secret 暴露情况 | 说明 |
|------|----------------|------|
| **创建** | 可能经过 LLM | 用户通过聊天告知 AI或 AI 执行 `secrets set` 命令。一次性暴露,可接受 |
| **使用** | 不经过 LLM | AI 只引用 `$VAR_NAME`,本地 shell 展开后执行。反复使用,始终安全 |
| **generate** | 完全不经过 LLM | CLI 本地生成随机密码 → 本地加密 → 密文发送 Server。值不经过 AI |
**核心价值**:一个 secret 可能被使用几十上百次,只有创建时可能暴露一次,之后的所有使用都通过环境变量引用,安全。
工作流示例:
```
# 创建阶段一次性secret 可能经过 LLM
AI Agent: secrets set ricnsmart/aliyun.ak "LTAI5t..."
# 使用阶段反复使用secret 不经过 LLM
AI Agent: eval $(secrets inject ricnsmart --export)
AI Agent: ssh -i "$RICNSMART_SSH_KEY" "$RICNSMART_SSH_USER@$RICNSMART_HOST_PRIMARY" "..."
↑ AI 只写变量名,本地 shell 展开后执行
# 生成阶段secret 完全不经过 LLM
AI Agent: secrets generate ricnsmart/db.password --length 32
→ CLI 本地生成 → 本地加密 → 密文存 Server
→ 返回: "已生成并存储,环境变量名: RICNSMART_DB_PASSWORD"
```
### SSH 远程命令场景
环境变量注入天然支持 SSH 远程命令场景:
```bash
eval $(secrets inject ricnsmart --export)
ssh user@server "mysql -u root -p'$DB_PASSWORD' -e 'CREATE DATABASE app'"
```
执行过程:
1. `eval` 将 secrets 注入当前 shell 环境变量
2. AI 写命令只引用 `$DB_PASSWORD`(变量名)
3. **本地 shell 展开** `$DB_PASSWORD` 为真实值
4. 展开后的命令通过 **SSH 加密通道**传输到远程执行
5. AI 始终只看到变量名,从不看到实际值
### 2. 加密模型:客户端加密 + 混合存储
采用**混合加密模型**:用户决定每条 secret 是否加密。敏感数据客户端加密后存密文,非敏感数据明文存储以支持搜索。
#### 敏感数据:信封加密
```
secret_value
→ AES-256-GCM(DEK) → 密文 → 存入 PostgreSQL (encrypted_value)
DEK (Data Encryption Key)
→ AES-256-GCM(Master Key) → 加密 DEK → 存入 PostgreSQL (encrypted_dek)
Master Key
→ Argon2id(password, salt) → 派生 → 存入 OS Keychain
```
**每条加密的 secret 使用独立的 DEK**,限制单个密钥泄露的影响范围。
PostgreSQL 只存密文和加密后的 DEKServer 被攻破也无法获取明文。
#### 非敏感数据:明文存储
用户选择不加密的 secret如 URL、用户名等以明文存储支持服务端搜索和索引。
#### 元数据JSONB 灵活字段
每条 secret 附带 JSONB 元数据字段,始终为明文,用于描述、标签、备注等。支持 GIN 索引高效搜索。
### 3. 跨设备 Master Key 同步
采用**密码派生**方案,跨平台支持:
```bash
# 新设备初始化(只需一次)
secrets init --server "https://secrets.example.com"
# 输入 master password
# → 从 Server 获取 Argon2id salt
# → 本地派生 Master Key
# → 存入 OS Keychain
# 之后自动从 Keychain 获取,无需再次输入
```
所有设备使用相同密码,派生出相同的 Master Key。Argon2id salt 存储在 Server 端 PostgreSQL 中。
| 平台 | Keychain 实现 |
|------|--------------|
| macOS | Keychain Services |
| Windows | Credential Manager |
| Linux | libsecret (Secret Service API) |
Rust `keyring` crate 统一封装三个平台。
### 4. Web UI 浏览器端加密Rust WASM
Web UI 通过 Rust 编译为 WebAssembly 实现浏览器端加密,与 CLI **共享完全相同的加密逻辑**
```
用户打开 Web UI
→ 输入 master password
→ WASM 模块中 Argon2id 派生 master keysalt 从 Server API 获取)
→ master key 仅存于浏览器内存
创建敏感 secret:
→ WASM 加密 → 密文通过 HTTPS API 发送到 Server
查看敏感 secret:
→ Server 返回密文 → WASM 解密 → 浏览器中显示
页面关闭:
→ master key 从内存消失
```
**Server 端全程不接触明文**,无论是 CLI 还是 Web UI 访问,安全模型一致。
使用 WASM 而非 Web Crypto API 的原因:
- Web Crypto API 不支持 Argon2id只有 PBKDF2
- WASM 直接复用 CLI 的 Rust 加密代码,保证一致性
- Rust 内存安全 + 成熟加密 crate优于 JS 实现
### 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(".", "_"))
```
## Server API
### 认证
API Token 认证。首次 `secrets init` 时生成 Token存入客户端 OS Keychain。所有 API 请求通过 `Authorization: Bearer <token>` 头认证。
### 接口列表
```
# 配置
GET /api/config/:key 获取配置(如 argon2_salt
POST /api/config 设置配置
# 项目
GET /api/projects 列出所有项目
POST /api/projects 创建项目
DELETE /api/projects/:id 删除项目
# Secrets
GET /api/secrets 列出 secrets支持过滤不含加密值
?project=ricnsmart
&pattern=ssh.*
&kind=file
POST /api/secrets 创建 secret接收密文或明文
GET /api/secrets/:id 获取 secret 详情(含加密值密文,客户端解密)
PUT /api/secrets/:id 更新 secret
DELETE /api/secrets/:id 删除 secret
# 静态文件
GET / Web UI 入口
GET /assets/* Web UI 静态资源 + WASM 文件
```
### 请求/响应示例
**创建加密 secret**
```json
POST /api/secrets
{
"project": "ricnsmart",
"key": "aliyun.access_key_id",
"is_encrypted": true,
"kind": "text",
"encrypted_value": "<base64 密文>",
"encrypted_dek": "<base64 加密后的 DEK>",
"nonce": "<base64>",
"dek_nonce": "<base64>",
"metadata": {
"description": "阿里云主账号 AccessKey",
"tags": ["cloud", "aliyun", "production"],
"env_var": "RICNSMART_ALIYUN_ACCESS_KEY_ID"
}
}
```
**创建非加密 secret**
```json
POST /api/secrets
{
"project": "ricnsmart",
"key": "gitea.url",
"is_encrypted": false,
"kind": "text",
"plaintext_value": "https://gitea.refining.dev",
"metadata": {
"description": "Gitea 服务地址",
"tags": ["service", "gitea"]
}
}
```
**列出 secrets 响应**(不含加密值):
```json
[
{
"id": "uuid-...",
"project": "ricnsmart",
"key": "aliyun.access_key_id",
"is_encrypted": true,
"kind": "text",
"metadata": {
"description": "阿里云主账号 AccessKey",
"tags": ["cloud", "aliyun", "production"]
},
"created_at": "2025-03-06T10:00:00Z",
"updated_at": "2025-03-06T10:00:00Z"
}
]
```
## 数据模型
### PostgreSQL 18 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, -- AES-256-GCM 密文
encrypted_dek BYTEA, -- 加密后的 DEK
nonce BYTEA, -- GCM nonce
dek_nonce BYTEA, -- DEK 加密的 nonce
-- 非敏感数据(明文存储,可搜索)
plaintext_value TEXT, -- 用户选择不加密时存储明文
-- 灵活元数据(明文 JSONB可搜索
metadata JSONB DEFAULT '{}', -- 描述、标签、备注等
is_encrypted BOOLEAN NOT NULL DEFAULT true,
kind TEXT NOT NULL DEFAULT 'text', -- "text", "ssh-key", "json", "file"
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(project_id, key)
);
-- JSONB GIN 索引,支持高效搜索
CREATE INDEX idx_secrets_metadata ON secrets USING GIN (metadata);
-- 按 kind 过滤
CREATE INDEX idx_secrets_kind ON secrets (kind);
-- 全局配置Argon2id salt 等)
CREATE TABLE config (
key TEXT PRIMARY KEY,
value BYTEA NOT NULL
);
-- 审计日志
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", "generate"
device TEXT, -- 设备标识
detail TEXT, -- 额外信息
created_at TIMESTAMPTZ DEFAULT now()
);
```
### 从现有 TOML 配置迁移的映射
```
# ricnsmart/config.toml
[[servers]] → ricnsmart/server.tianchu-primary (kind=json, encrypted)
hostname = "..." encrypted_value: {...}
public_ip = "8.153.204.96" metadata: {"description": "天储主服务器"}
[services.gitea] → ricnsmart/gitea.url (kind=text, 不加密)
url = "https://gitea.refining.dev" plaintext_value: "https://gitea.refining.dev"
ricnsmart/gitea.token (kind=text, 加密)
token = "92a627..." encrypted_value: <密文>
# SSH 密钥 → ricnsmart/ssh-key.ricnsmart-sh (kind=file, 加密)
~/.../keys/ricnsmart-sh.pem encrypted_value: <密文>
metadata: {"original_path": "ricnsmart-sh.pem"}
```
## CLI 命令设计
CLI 是 AI Agent 和人共用的主要接口。
```bash
# ===== 初始化 =====
secrets init --server "https://secrets.example.com"
# 输入 master password → 从 Server 获取 salt → Argon2id 派生 Master Key
# → Master Key + API Token 存入 OS Keychain
# ===== 项目管理 =====
secrets project list # 列出所有项目
secrets project create ricnsmart # 创建项目
secrets project delete ricnsmart # 删除项目(需确认)
# ===== Secret CRUD =====
secrets set ricnsmart/aliyun.ak "LTAI5t..." # 创建(默认加密)
secrets set ricnsmart/gitea.url "https://..." --no-encrypt # 创建(不加密)
secrets set ricnsmart/ssh.key --file ~/.ssh/ricnsmart.pem # 从文件创建(加密)
secrets set ricnsmart/server.primary --json '{"host":...}' # JSON 类型(加密)
secrets get ricnsmart/aliyun.ak # 获取值(本地解密,输出到 stdout
secrets list # 列出所有(不显示值)
secrets list ricnsmart/ # 按项目过滤
secrets list --pattern "ssh.*" # 模糊匹配 key
secrets list --kind file # 按类型过滤
secrets delete ricnsmart/aliyun.ak # 删除
# ===== 生成随机密码 =====
secrets generate ricnsmart/db.password --length 32 # 默认字符集
secrets generate ricnsmart/api.key --length 64 --charset hex # 指定字符集
# → 本地生成 → 本地加密 → 密文存 Server
# → 输出: "已生成并存储,环境变量名: RICNSMART_DB_PASSWORD"
# ===== 环境变量注入 =====
# macOS / Linux (bash/zsh)
eval $(secrets inject ricnsmart --export)
# Windows PowerShell
secrets inject ricnsmart --export-ps | Invoke-Expression
# 通用方式(所有平台,启动子进程)
secrets inject ricnsmart -- ./app
# 选择性注入
secrets inject ricnsmart --keys "aliyun.*,ssh.*" --export
# ===== 导入 =====
secrets import config.toml --project ricnsmart # 从 TOML 导入
secrets import config.toml --project ricnsmart --dry-run # 预览
```
## 安全注意事项
### Secret 生命周期
```
创建: CLI set / generate → 本地加密 → HTTPS → Server 存密文到 PG
Web UI 输入 → WASM 加密 → HTTPS → Server 存密文到 PG
读取: CLI inject → HTTPS 拉取密文 → 本地解密 → 注入环境变量
Web UI 查看 → HTTPS 拉取密文 → WASM 解密 → 浏览器显示
使用: AI 写 shell 命令引用 $VAR → 本地 shell 展开为真实值 → 执行
清理: 进程退出后环境变量消失Web UI 关闭后 master key 消失
```
### 威胁模型
| 威胁 | 防御措施 |
|------|---------|
| PostgreSQL 被攻破 | 客户端加密PG 只有密文Server 不持有 Master Key |
| Server 被攻破 | 敏感数据为客户端密文,非敏感数据暴露但不含凭证 |
| 设备丢失 | Master Key 在 OS Keychain需要设备密码/生物识别) |
| LLM 提供商窥探 | 使用阶段 secret 不进入 LLM context只有变量名 |
| 命令输出泄露 secret | 可选输出脱敏Phase 2替换已知 secret 值为 `***` |
| Master Password 弱 | Argon2id 高计算成本推荐m=64MB, t=3, p=4 |
| 传输层攻击 | HTTPS/TLS 加密传输 |
| WASM 模块被篡改 | Server 托管 WASM可通过 SRI (Subresource Integrity) 校验 |
### 不防御的场景
- 设备上的 root 权限攻击者(可直接读取进程环境变量或内存)
- 用户主动将 secret 粘贴到聊天中(行为问题,非技术问题)
- AI 通过 `echo $VAR` 读取环境变量值并返回到 LLM 上下文已知弱点Phase 2 输出脱敏可缓解)
## 技术选型
| 组件 | 选择 | 理由 |
|------|------|------|
| 语言 | Rust | 单二进制分发、加密库成熟、性能好、编译为 native + WASM |
| 加密 | `aes-gcm` crate | AEAD业界标准 |
| 密钥派生 | `argon2` crate | 抗 GPU/ASICOWASP 推荐,支持 WASM 编译 |
| OS Keychain | `keyring` crate | 跨平台macOS Keychain / Windows Credential Manager / Linux libsecret |
| 数据库 | PostgreSQL 18 + `sqlx` crate | async、编译期 SQL 检查、JSONB 原生支持 |
| HTTP Server | `axum` crate | Rust 生态主流、tokio 驱动、tower 中间件 |
| WASM 绑定 | `wasm-bindgen` + `wasm-pack` | Rust → WASM 标准工具链 |
| CLI 框架 | `clap` crate | Rust 生态标准 |
| 序列化 | `serde` + `serde_json` | JSON 处理标准库 |
## 开发计划
### Phase 1: MVPServer + CLI
- [ ] Cargo workspace 脚手架(`crypto` + `server` + `cli`
- [ ] `crypto` crateAES-256-GCM + Argon2id + 信封加密
- [ ] PostgreSQL 18 schema + 迁移脚本
- [ ] `server` crateAxum HTTPS APICRUD + config + 认证)
- [ ] `cli` crateinit / set / get / list / delete / generate / inject
- [ ] 跨平台 OS Keychain 集成macOS / Windows / Linux
- [ ] 跨平台 inject 命令bash/zsh + PowerShell
- [ ] `secrets import config.toml` 从现有配置导入
### Phase 2: Web UI + 增强
- [ ] `web` cratecrypto 编译为 WASM + wasm-bindgen 绑定
- [ ] Web UI 前端CRUD 管理界面 + WASM 浏览器端加解密
- [ ] Server 托管 Web UI + WASM 静态文件
- [ ] TOTP 支持(存储 TOTP seed生成一次性验证码
- [ ] 输出脱敏(可选,替换已知 secret 值为 `***`
- [ ] 审计日志
### Phase 3: 扩展
- [ ] MCP Server可选 AI 增强stdio 模式)
- [ ] 模板渲染(`secrets template config.tpl > config.yaml`
- [ ] 团队共享(多用户 + RBAC
- [ ] Secret 轮换提醒
- [ ] CI/CD 集成Gitea Actions 中使用)
## 参考
- [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
- [Rust `axum`](https://docs.rs/axum/) — HTTP Server 框架
- [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/) — Rust WASM 绑定
- [wasm-pack](https://rustwasm.github.io/wasm-pack/) — Rust WASM 打包工具