14 KiB
14 KiB
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 等远程服务器,并留存在聊天历史中。
设计目标
- Secret 不进入 LLM 上下文:AI Agent 只知道环境变量名(
$VAR_NAME),不知道值 - 跨设备:多台电脑共享同一套 secrets(云端 PostgreSQL)
- 面向 AI Agent:通过 MCP 协议无缝集成 Cursor / OpenCode
- 轻量:核心 500-800 行代码,不依赖重型框架
- 安全:客户端加密,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 共用加密库,可编译为同一个二进制(子命令区分):
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 同步
采用 密码派生 方案:
# 新设备初始化(只需一次)
secrets init
# 输入 master password → Argon2id 派生 Master Key → 存入 OS Keychain
# 之后自动从 Keychain 获取,无需再次输入
所有设备使用相同密码,派生出相同的 Master Key。Argon2id 的 salt 存储在 PostgreSQL 中。
4. MCP Tool 接口
// 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
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 命令设计
# ===== 初始化 =====
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)
{
"mcpServers": {
"secrets": {
"command": "secrets",
"args": ["mcp"]
}
}
}
OpenCode
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_secretsMCP tool
参考
- MCP 协议规范
- Infisical - 开源 Secret Manager
- age - 文件加密工具
- SOPS - Mozilla 的加密文件编辑器
- Rust
aes-gcm- AEAD 加密 - Rust
argon2- 密码哈希 - Rust
keyring- 跨平台 Keychain