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

14 KiB
Raw Blame History

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 共用加密库,可编译为同一个二进制(子命令区分):

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 Serverstdio 模式)

MCP 集成配置

Cursor (.cursor/mcp.json)

{
  "mcpServers": {
    "secrets": {
      "command": "secrets",
      "args": ["mcp"]
    }
  }
}

OpenCode

mcpServers:
  secrets:
    command: secrets
    args: [mcp]

配置后AI Agent 在需要凭证时会自动调用 inject_secretslist_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 workspacecli + 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

参考