refactor: workspace secrets-core + secrets-mcp MCP SaaS
- Split library (db/crypto/service) and MCP/Web/OAuth binary - Add deploy examples and CI/docs updates Made-with: Cursor
This commit is contained in:
702
AGENTS.md
702
AGENTS.md
@@ -1,68 +1,49 @@
|
||||
# Secrets CLI — AGENTS.md
|
||||
# Secrets MCP — AGENTS.md
|
||||
|
||||
## 提交 / 发版硬规则(优先于下文其他说明)
|
||||
本仓库为 **MCP SaaS**:`secrets-core`(业务与持久化)+ `secrets-mcp`(Streamable HTTP MCP、Web、OAuth、API Key)。对外入口见 `crates/secrets-mcp`。
|
||||
|
||||
1. 涉及 `src/**`、`Cargo.toml`、`Cargo.lock`、CLI 行为变更的提交,默认视为**需要发版**,除非用户明确说明“本次不发版”。
|
||||
2. 发版前必须先检查 `Cargo.toml` 中的 `version`,再检查是否已存在对应 tag:`git tag -l 'secrets-*'`。
|
||||
3. 若当前版本对应 tag 已存在,必须先 bump `Cargo.toml` 的 `version`,再执行 `cargo build` 同步 `Cargo.lock`,然后才能提交。
|
||||
4. 提交前优先运行 `./scripts/release-check.sh`;该脚本会检查重复版本并执行 `cargo fmt -- --check && cargo clippy --locked -- -D warnings && cargo test --locked`。
|
||||
## 提交 / 发版硬规则(优先于下文)
|
||||
|
||||
跨设备密钥与配置管理 CLI 工具,将服务器信息、服务凭据等存储到 PostgreSQL 18,供 AI 工具读取上下文。每个加密字段单独行存储(`secrets` 子表),字段名、类型、长度以明文保存,主密钥由 Argon2id 从主密码派生并存入平台安全存储(macOS Keychain / Windows Credential Manager / Linux keyutils)。
|
||||
1. 涉及 `crates/**`、根目录 `Cargo.toml`/`Cargo.lock`、`secrets-mcp` 行为变更的提交,默认视为**需要发版**,除非明确说明「本次不发版」。
|
||||
2. 发版前检查 `crates/secrets-mcp/Cargo.toml` 的 `version`,再查 tag:`git tag -l 'secrets-mcp-*'`。
|
||||
3. 若当前版本对应 tag 已存在,须先 bump `version`,再 `cargo build` 同步 `Cargo.lock` 后提交。
|
||||
4. 提交前优先运行 `./scripts/release-check.sh`(版本/tag + `fmt` + `clippy --locked` + `test --locked`)。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
secrets/
|
||||
src/
|
||||
main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数
|
||||
output.rs # OutputMode 枚举(默认 json,-o text 供人类使用)
|
||||
config.rs # 配置读写:~/.config/secrets/config.toml(database_url)
|
||||
db.rs # PgPool 创建 + 建表/索引(DROP+CREATE,含所有表)
|
||||
crypto.rs # AES-256-GCM 加解密、Argon2id 派生、OS 钥匙串
|
||||
models.rs # Entry + SecretField 结构体(sqlx::FromRow + serde)
|
||||
audit.rs # 审计写入:log_tx(事务内)
|
||||
commands/
|
||||
init.rs # init 命令:主密钥初始化(每台设备一次)
|
||||
add.rs # add 命令:upsert entries + 逐字段写入 secrets,含历史快照
|
||||
config.rs # config 命令:set-db / show / path(持久化 database_url)
|
||||
search.rs # search 命令:多条件查询,展示 secrets 字段 schema(无需 master_key)
|
||||
delete.rs # delete 命令:事务化,CASCADE 删除 secrets,含历史快照
|
||||
update.rs # update 命令:增量更新,secrets 行级 UPSERT/DELETE,CAS 并发保护
|
||||
rollback.rs # rollback 命令:按 entry_version 恢复 entry + secrets
|
||||
history.rs # history 命令:查看 entry 变更历史列表
|
||||
run.rs # run 命令:仅 secrets 逐字段解密 + key_ref 引用解析(不含 metadata)
|
||||
upgrade.rs # upgrade 命令:检查、校验摘要并下载最新版本,自动替换二进制
|
||||
export_cmd.rs # export 命令:批量导出记录,支持 JSON/TOML/YAML,含解密明文
|
||||
import_cmd.rs # import 命令:批量导入记录,冲突检测,dry-run,重新加密写入
|
||||
Cargo.toml
|
||||
crates/
|
||||
secrets-core/ # db / crypto / models / audit / service
|
||||
secrets-mcp/ # rmcp tools、axum、OAuth、Dashboard
|
||||
scripts/
|
||||
release-check.sh # 发版前检查版本号/tag 是否重复,并执行 fmt/clippy/test
|
||||
setup-gitea-actions.sh # 配置 Gitea Actions 变量与 Secrets
|
||||
.gitea/workflows/
|
||||
secrets.yml # CI:fmt + clippy + musl 构建 + Release 上传 + 飞书通知
|
||||
.vscode/tasks.json # 本地测试任务(build / config / search / add+delete / update / audit 等)
|
||||
release-check.sh
|
||||
setup-gitea-actions.sh
|
||||
.gitea/workflows/secrets.yml
|
||||
.vscode/tasks.json
|
||||
```
|
||||
|
||||
## 数据库
|
||||
|
||||
- **Host**: `<host>:<port>`
|
||||
- **Database**: `secrets`
|
||||
- **连接串**: `postgres://postgres:<password>@<host>:<port>/secrets`
|
||||
- **表**: `entries`(主表)+ `secrets`(加密字段子表)+ `entries_history` + `secrets_history` + `audit_log` + `kv_config`,首次连接自动建表(auto-migrate)
|
||||
- **建议库名**:`secrets-mcp`(专用实例,与历史库名区分)。
|
||||
- **连接**:环境变量 **`SECRETS_DATABASE_URL`**(本分支无本地配置文件路径)。
|
||||
- **表**:`entries`(含 `user_id`)、`secrets`、`entries_history`、`secrets_history`、`audit_log`、`users`、`oauth_accounts`、`api_keys`,首次连接 **auto-migrate**。
|
||||
|
||||
### 表结构
|
||||
### 表结构(摘录)
|
||||
|
||||
```sql
|
||||
entries (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(), -- PG18 时间有序 UUID
|
||||
namespace VARCHAR(64) NOT NULL, -- 一级隔离: "refining" | "ricnsmart"
|
||||
kind VARCHAR(64) NOT NULL, -- 类型: "server" | "service" | "key"(可扩展)
|
||||
name VARCHAR(256) NOT NULL, -- 人类可读标识
|
||||
tags TEXT[] NOT NULL DEFAULT '{}', -- 灵活标签: ["aliyun","hongkong"]
|
||||
metadata JSONB NOT NULL DEFAULT '{}', -- 明文描述: ip, desc, domains, location...
|
||||
version BIGINT NOT NULL DEFAULT 1, -- 乐观锁版本号,每次写操作自增
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
user_id UUID, -- 多租户:NULL=遗留行;非空=归属用户
|
||||
namespace VARCHAR(64) NOT NULL,
|
||||
kind VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
version BIGINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(namespace, kind, name)
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
```
|
||||
|
||||
@@ -70,8 +51,8 @@ entries (
|
||||
secrets (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
entry_id UUID NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
||||
field_name VARCHAR(256) NOT NULL, -- 明文字段名: "username", "token", "ssh_key"
|
||||
encrypted BYTEA NOT NULL DEFAULT '\x', -- 仅加密值本身:nonce(12B)||ciphertext+tag
|
||||
field_name VARCHAR(256) NOT NULL,
|
||||
encrypted BYTEA NOT NULL DEFAULT '\x',
|
||||
version BIGINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
@@ -79,588 +60,109 @@ secrets (
|
||||
)
|
||||
```
|
||||
|
||||
### users / oauth_accounts / api_keys
|
||||
|
||||
```sql
|
||||
kv_config (
|
||||
key TEXT PRIMARY KEY, -- 如 'argon2_salt'
|
||||
value BYTEA NOT NULL -- Argon2id salt,首台设备 init 时生成
|
||||
users (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
email VARCHAR(256),
|
||||
name VARCHAR(256) NOT NULL DEFAULT '',
|
||||
avatar_url TEXT,
|
||||
key_salt BYTEA, -- PBKDF2 salt(32B),首次设置密码短语时写入
|
||||
key_check BYTEA, -- 派生密钥加密已知常量,用于验证密码短语
|
||||
key_params JSONB, -- 算法参数,如 {"alg":"pbkdf2-sha256","iterations":600000}
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
|
||||
oauth_accounts (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
provider_id VARCHAR(256) NOT NULL,
|
||||
...
|
||||
UNIQUE(provider, provider_id)
|
||||
)
|
||||
|
||||
api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
key_prefix VARCHAR(12) NOT NULL,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
```
|
||||
|
||||
### audit_log 表结构
|
||||
### audit_log / history
|
||||
|
||||
```sql
|
||||
audit_log (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
action VARCHAR(32) NOT NULL, -- 'add' | 'update' | 'delete' | 'rollback'
|
||||
namespace VARCHAR(64) NOT NULL,
|
||||
kind VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
detail JSONB NOT NULL DEFAULT '{}', -- 变更摘要(tags/meta keys/secret keys,不含 value)
|
||||
actor VARCHAR(128) NOT NULL DEFAULT '', -- 操作者($USER 环境变量)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
```
|
||||
与迁移脚本一致:`audit_log`、`entries_history`、`secrets_history` 用于审计与时间旅行恢复;字段定义见 `crates/secrets-core/src/db.rs` 内 `migrate` SQL。
|
||||
|
||||
### entries_history 表结构
|
||||
### 字段职责
|
||||
|
||||
```sql
|
||||
entries_history (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
entry_id UUID NOT NULL,
|
||||
namespace VARCHAR(64) NOT NULL,
|
||||
kind VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
version BIGINT NOT NULL, -- 被快照时的版本号
|
||||
action VARCHAR(16) NOT NULL, -- 'add' | 'update' | 'delete' | 'rollback'
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
actor VARCHAR(128) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
```
|
||||
|
||||
### secrets_history 表结构
|
||||
|
||||
```sql
|
||||
secrets_history (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
entry_id UUID NOT NULL,
|
||||
secret_id UUID NOT NULL, -- 对应 secrets.id
|
||||
entry_version BIGINT NOT NULL, -- 关联 entries_history 的版本号
|
||||
field_name VARCHAR(256) NOT NULL,
|
||||
encrypted BYTEA NOT NULL DEFAULT '\x',
|
||||
action VARCHAR(16) NOT NULL, -- 'add' | 'update' | 'delete' | 'rollback'
|
||||
actor VARCHAR(128) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
```
|
||||
|
||||
### 字段职责划分
|
||||
|
||||
| 字段 | 存什么 | 示例 |
|
||||
|------|--------|------|
|
||||
| `namespace` | 项目/团队隔离 | `refining`, `ricnsmart` |
|
||||
| 字段 | 含义 | 示例 |
|
||||
|------|------|------|
|
||||
| `namespace` | 隔离空间 | `refining` |
|
||||
| `kind` | 记录类型 | `server`, `service`, `key` |
|
||||
| `name` | 唯一标识名 | `i-example0abcd1234efgh`, `gitea` |
|
||||
| `tags` | 多维分类标签 | `["aliyun","hongkong","ricn"]` |
|
||||
| `metadata` | 明文非敏感信息 | `{"ip":"192.0.2.1","desc":"Grafana","key_ref":"my-shared-key"}` |
|
||||
| `secrets.field_name` | 加密字段名(明文) | `"username"`, `"token"`, `"ssh_key"` |
|
||||
| `secrets.encrypted` | 仅加密值本身 | AES-256-GCM 密文 |
|
||||
| `name` | 标识名 | `gitea`, `i-example0…` |
|
||||
| `tags` | 标签 | `["aliyun","prod"]` |
|
||||
| `metadata` | 明文描述 | `ip`、`url`、`key_ref` |
|
||||
| `secrets.field_name` | 加密字段名(明文) | `token`, `ssh_key` |
|
||||
| `secrets.encrypted` | 密文 | AES-GCM |
|
||||
|
||||
### PEM 共享机制(key_ref)
|
||||
### PEM 共享(`key_ref`)
|
||||
|
||||
同一 PEM 被多台服务器共享时,将 PEM 存为独立的 `kind=key` 记录,服务器通过 `metadata.key_ref` 引用:
|
||||
|
||||
```bash
|
||||
# 1. 存共享 PEM
|
||||
secrets add -n refining --kind key --name my-shared-key \
|
||||
--tag aliyun --tag hongkong \
|
||||
-s content=@./keys/my-shared-key.pem
|
||||
|
||||
# 2. 服务器通过 metadata.key_ref 引用(run 时自动合并 key 的 secrets)
|
||||
secrets add -n refining --kind server --name i-example0xyz789 \
|
||||
-m ip=192.0.2.1 -m key_ref=my-shared-key \
|
||||
-s username=ecs-user
|
||||
|
||||
# 3. 轮换只需更新 key 记录,所有引用服务器自动生效
|
||||
secrets update -n refining --kind key --name my-shared-key \
|
||||
-s content=@./keys/new-key.pem
|
||||
```
|
||||
|
||||
## 数据库配置
|
||||
|
||||
首次使用需显式配置数据库连接,设置一次后在该设备上持久生效:
|
||||
|
||||
```bash
|
||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||
secrets config show # 查看当前配置(密码脱敏)
|
||||
secrets config path # 打印配置文件路径
|
||||
```
|
||||
|
||||
`set-db` 会先验证连接可用,成功后才写入配置文件;连接失败时提示 "Database connection failed" 且不修改配置。
|
||||
|
||||
配置文件:`~/.config/secrets/config.toml`,权限 0600。`--db-url` 参数可一次性覆盖。
|
||||
|
||||
## 主密钥与加密
|
||||
|
||||
首次使用(每台设备各执行一次):
|
||||
|
||||
```bash
|
||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||
secrets init # 提示输入主密码,Argon2id 派生主密钥后存入 OS 钥匙串
|
||||
```
|
||||
|
||||
主密码不存储;salt 存于 `kv_config`,首台设备生成后共享,确保同一主密码在所有设备派生出相同主密钥。
|
||||
|
||||
主密钥存储后端:macOS Keychain、Windows Credential Manager、Linux keyutils(会话级,重启后需再次 `secrets init`)。
|
||||
|
||||
**从旧版(明文 JSONB)升级**:升级后执行 `secrets init` 即可(明文记录需手动重新 add 或通过 update 更新)。
|
||||
|
||||
## CLI 命令
|
||||
|
||||
### AI 使用主路径
|
||||
|
||||
**读取一律用 `search`,写入用 `add` / `update`,避免反复查帮助。**
|
||||
|
||||
输出格式规则:
|
||||
- 默认始终输出 `json`(pretty-printed),无论 TTY 还是管道
|
||||
- 显式 `-o json-compact` → 单行 JSON(管道处理时更紧凑)
|
||||
- 显式 `-o text` → 人类可读文本格式
|
||||
|
||||
---
|
||||
|
||||
### init — 主密钥初始化(每台设备一次)
|
||||
|
||||
```bash
|
||||
# 首次设备:生成 Argon2id salt 并存库,派生主密钥后存 OS 钥匙串
|
||||
secrets init
|
||||
|
||||
# 后续设备:复用已有 salt,派生主密钥后存钥匙串(主密码需与首台相同)
|
||||
secrets init
|
||||
```
|
||||
|
||||
### search — 发现与读取
|
||||
|
||||
```bash
|
||||
# 参数说明(带典型值)
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name gitea | i-example0abcd1234efgh | mqtt
|
||||
# --tag aliyun | hongkong | production
|
||||
# -q / --query mqtt | grafana | gitea (模糊匹配 name/namespace/kind/tags/metadata)
|
||||
# secrets schema search 默认展示 secrets 字段名、类型与长度(无需 master_key)
|
||||
# -f / --field metadata.ip | metadata.url | metadata.default_org
|
||||
# --summary 不带值的 flag,仅返回摘要(name/tags/desc/updated_at)
|
||||
# --limit 20 | 50(默认 50)
|
||||
# --offset 0 | 10 | 20(分页偏移)
|
||||
# --sort name(默认)| updated | created
|
||||
# -o / --output text | json | json-compact
|
||||
|
||||
# 发现概览(起步推荐)
|
||||
secrets search --summary --limit 20
|
||||
secrets search -n refining --summary --limit 20
|
||||
secrets search --sort updated --limit 10 --summary
|
||||
|
||||
# 精确定位单条记录
|
||||
secrets search -n refining --kind service --name gitea
|
||||
secrets search -n refining --kind server --name i-example0abcd1234efgh
|
||||
|
||||
# 精确定位并获取完整内容(secrets 保持加密占位)
|
||||
secrets search -n refining --kind service --name gitea -o json
|
||||
|
||||
# 直接提取 metadata 字段值(最短路径)
|
||||
secrets search -n refining --kind service --name gitea -f metadata.url
|
||||
secrets search -n refining --kind service --name gitea \
|
||||
-f metadata.url -f metadata.default_org
|
||||
|
||||
# 需要 secrets 时,改用 run
|
||||
secrets run -n refining --kind service --name gitea -- printenv
|
||||
|
||||
# 模糊关键词搜索
|
||||
secrets search -q mqtt
|
||||
secrets search -q grafana
|
||||
secrets search -q 192.0.2
|
||||
|
||||
# 按条件过滤
|
||||
secrets search -n refining --kind service
|
||||
secrets search -n ricnsmart --kind server
|
||||
secrets search --tag hongkong
|
||||
secrets search --tag aliyun --summary
|
||||
|
||||
# 分页
|
||||
secrets search -n refining --summary --limit 10 --offset 0
|
||||
secrets search -n refining --summary --limit 10 --offset 10
|
||||
|
||||
# 管道 / AI 调用(默认 json,直接可解析)
|
||||
secrets search -n refining --kind service | jq '.[].name'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### add — 新增或全量覆盖(upsert)
|
||||
|
||||
```bash
|
||||
# 参数说明(带典型值)
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name gitea | i-example0abcd1234efgh
|
||||
# --tag aliyun | hongkong(可重复)
|
||||
# -m / --meta ip=10.0.0.1 | desc="ECS" | url=https://... | tls:cert@./cert.pem(可重复)
|
||||
# -s / --secret token=<value> | ssh_key=@./key.pem | password=secret123 | credentials:content@./key.pem(可重复)
|
||||
|
||||
# 添加服务器
|
||||
secrets add -n refining --kind server --name i-example0abcd1234efgh \
|
||||
--tag aliyun --tag shanghai \
|
||||
-m ip=10.0.0.1 -m desc="Aliyun Shanghai ECS" \
|
||||
-s username=root -s ssh_key=@./keys/deploy-key.pem
|
||||
|
||||
# 添加服务凭据
|
||||
secrets add -n refining --kind service --name gitea \
|
||||
--tag gitea \
|
||||
-m url=https://code.example.com -m default_org=refining -m username=voson \
|
||||
-s token=<token> -s runner_token=<runner_token>
|
||||
|
||||
# 从文件读取 token
|
||||
secrets add -n ricnsmart --kind service --name mqtt \
|
||||
-m host=mqtt.example.com -m port=1883 \
|
||||
-s password=@./mqtt_password.txt
|
||||
|
||||
# 多行文件直接写入嵌套 secret 字段
|
||||
secrets add -n refining --kind server --name i-example0abcd1234efgh \
|
||||
-s credentials:content@./keys/deploy-key.pem
|
||||
|
||||
# 使用类型化值(key:=<json>)存储非字符串类型
|
||||
secrets add -n refining --kind service --name prometheus \
|
||||
-m scrape_interval:=15 \
|
||||
-m enabled:=true \
|
||||
-m labels:='["prod","metrics"]' \
|
||||
-s api_key=abc123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### update — 增量更新(记录必须已存在)
|
||||
|
||||
只有传入的字段才会变动,其余全部保留。
|
||||
|
||||
```bash
|
||||
# 参数说明(带典型值)
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name gitea | i-example0abcd1234efgh
|
||||
# --add-tag production | backup(不影响已有 tag,可重复)
|
||||
# --remove-tag staging | deprecated(可重复)
|
||||
# -m / --meta ip=10.0.0.1 | desc="新描述" | credentials:username=root(新增或覆盖,可重复)
|
||||
# --remove-meta old_port | legacy_key | credentials:content(删除 metadata 字段,可重复)
|
||||
# -s / --secret token=<new> | ssh_key=@./new.pem | credentials:content@./new.pem(新增或覆盖,可重复)
|
||||
# --remove-secret old_password | deprecated_key | credentials:content(删除 secret 字段,可重复)
|
||||
|
||||
# 更新单个 metadata 字段
|
||||
secrets update -n refining --kind server --name i-example0abcd1234efgh \
|
||||
-m ip=10.0.0.1
|
||||
|
||||
# 轮换 token
|
||||
secrets update -n refining --kind service --name gitea \
|
||||
-s token=<new-token>
|
||||
|
||||
# 新增 tag 并轮换 token
|
||||
secrets update -n refining --kind service --name gitea \
|
||||
--add-tag production \
|
||||
-s token=<new-token>
|
||||
|
||||
# 移除废弃字段
|
||||
secrets update -n refining --kind service --name mqtt \
|
||||
--remove-meta old_port --remove-secret old_password
|
||||
|
||||
# 从文件更新嵌套 secret 字段
|
||||
secrets update -n refining --kind server --name i-example0abcd1234efgh \
|
||||
-s credentials:content@./keys/deploy-key.pem
|
||||
|
||||
# 删除嵌套字段
|
||||
secrets update -n refining --kind server --name i-example0abcd1234efgh \
|
||||
--remove-secret credentials:content
|
||||
|
||||
# 移除 tag
|
||||
secrets update -n refining --kind service --name gitea --remove-tag staging
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### delete — 删除记录(支持单条精确删除与批量删除)
|
||||
|
||||
删除时会自动将 entry 与所有关联 secret 字段快照到历史表,并写入审计日志,可通过 `rollback` 命令恢复。
|
||||
|
||||
```bash
|
||||
# 参数说明(带典型值)
|
||||
# -n / --namespace refining | ricnsmart(必填)
|
||||
# --kind server | service(指定 --name 时必填;批量时可选)
|
||||
# --name gitea | i-example0abcd1234efgh(精确匹配;省略则批量删除)
|
||||
# --dry-run 预览将删除的记录,不实际写入(仅批量模式有效)
|
||||
# -o / --output text | json | json-compact
|
||||
|
||||
# 精确删除单条记录(--kind 必填)
|
||||
secrets delete -n refining --kind service --name legacy-mqtt
|
||||
secrets delete -n ricnsmart --kind server --name i-old-server-id
|
||||
|
||||
# 预览批量删除(不写入数据库)
|
||||
secrets delete -n refining --dry-run
|
||||
secrets delete -n ricnsmart --kind server --dry-run
|
||||
|
||||
# 批量删除整个 namespace 的所有记录
|
||||
secrets delete -n ricnsmart
|
||||
|
||||
# 批量删除 namespace 下指定 kind 的所有记录
|
||||
secrets delete -n ricnsmart --kind server
|
||||
|
||||
# JSON 输出
|
||||
secrets delete -n refining --kind service -o json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### history — 查看变更历史
|
||||
|
||||
```bash
|
||||
# 参数说明
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name 记录名
|
||||
# --limit 返回条数(默认 20)
|
||||
|
||||
# 查看某条记录的历史版本列表
|
||||
secrets history -n refining --kind service --name gitea
|
||||
|
||||
# 查最近 5 条
|
||||
secrets history -n refining --kind service --name gitea --limit 5
|
||||
|
||||
# JSON 输出
|
||||
secrets history -n refining --kind service --name gitea -o json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### rollback — 回滚到指定版本
|
||||
|
||||
```bash
|
||||
# 参数说明
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name 记录名
|
||||
# --to-version <N> 目标版本号(省略则恢复最近一次快照)
|
||||
|
||||
# 撤销上次修改(回滚到最近一次快照)
|
||||
secrets rollback -n refining --kind service --name gitea
|
||||
|
||||
# 回滚到版本 3
|
||||
secrets rollback -n refining --kind service --name gitea --to-version 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### run — 向子进程注入 secrets 并执行命令
|
||||
|
||||
仅注入 secrets 表中的加密字段(解密后),不含 metadata。secrets 仅作用于子进程环境,不修改当前 shell,进程退出码透传。
|
||||
|
||||
使用 `-s/--secret` 指定只注入哪些字段(最小权限原则);使用 `--dry-run` 预览将注入哪些变量名及来源,不执行命令。
|
||||
|
||||
```bash
|
||||
# 参数说明
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name 记录名
|
||||
# --tag 按 tag 过滤(可重复)
|
||||
# -s / --secret 只注入指定字段名(可重复;省略则注入全部)
|
||||
# --prefix 变量名前缀
|
||||
# --dry-run 预览变量映射,不执行命令
|
||||
# -o / --output json(默认)| json-compact | text
|
||||
# -- <command> 执行的命令及参数(--dry-run 时可省略)
|
||||
|
||||
# 注入全部 secrets 到脚本
|
||||
secrets run -n refining --kind service --name gitea -- ./deploy.sh
|
||||
|
||||
# 只注入特定字段(最小化注入范围)
|
||||
secrets run -n refining --kind service --name aliyun \
|
||||
-s access_key_id -s access_key_secret -- aliyun ecs DescribeInstances
|
||||
|
||||
# 按 tag 批量注入(多条记录合并)
|
||||
secrets run --tag production -- env | grep -i token
|
||||
|
||||
# 预览将注入哪些变量(不执行命令,默认 JSON 输出)
|
||||
secrets run -n refining --kind service --name gitea --dry-run
|
||||
|
||||
# 配合字段过滤预览
|
||||
secrets run -n refining --kind service --name gitea -s token --dry-run
|
||||
|
||||
# text 模式预览(人类阅读)
|
||||
secrets run -n refining --kind service --name gitea --dry-run -o text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### upgrade — 自动更新 CLI 二进制
|
||||
|
||||
从 Release 服务器下载最新版本,校验对应 `.sha256` 摘要后替换当前二进制,无需数据库连接或主密钥。
|
||||
|
||||
**配置方式**:`SECRETS_UPGRADE_URL` 必填。优先用**构建时**:`SECRETS_UPGRADE_URL=https://... cargo build`,CI 已自动注入。或**运行时**:写在 `.env` 或 `export` 后执行。
|
||||
|
||||
```bash
|
||||
# 检查是否有新版本(不下载)
|
||||
secrets upgrade --check
|
||||
|
||||
# 下载、校验 SHA-256 并安装最新版本
|
||||
secrets upgrade
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### export — 批量导出记录
|
||||
|
||||
将匹配的记录(含解密后的明文 secrets)导出到文件或 stdout。支持 JSON、TOML、YAML 三种格式,文件格式由扩展名自动推断。使用 `--no-secrets` 时无需主密钥。
|
||||
|
||||
```bash
|
||||
# 参数说明
|
||||
# -n / --namespace refining | ricnsmart
|
||||
# --kind server | service
|
||||
# --name gitea | i-example0abcd1234efgh
|
||||
# --tag aliyun | production(可重复)
|
||||
# -q / --query 模糊关键词
|
||||
# --file <path> 输出文件路径,格式由扩展名推断(.json / .toml / .yaml / .yml)
|
||||
# --format json | toml | yaml 显式指定格式(输出到 stdout 时必须指定)
|
||||
# --no-secrets 不导出 secrets,无需主密钥
|
||||
|
||||
# 全量导出到 JSON 文件
|
||||
secrets export --file backup.json
|
||||
|
||||
# 按 namespace 导出为 TOML
|
||||
secrets export -n refining --file refining.toml
|
||||
|
||||
# 按 kind 导出为 YAML
|
||||
secrets export -n refining --kind service --file services.yaml
|
||||
|
||||
# 按 tag 过滤导出
|
||||
secrets export --tag production --file prod.json
|
||||
|
||||
# 模糊关键词导出
|
||||
secrets export -q mqtt --file mqtt.json
|
||||
|
||||
# 仅导出 schema(不含 secrets,无需主密钥)
|
||||
secrets export --no-secrets --file schema.json
|
||||
|
||||
# 输出到 stdout(必须指定 --format)
|
||||
secrets export -n refining --format yaml
|
||||
secrets export --format json | jq '.'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### import — 批量导入记录
|
||||
|
||||
从导出文件读取记录并写入数据库,自动重新加密 secrets。支持 JSON、TOML、YAML 三种格式,文件格式由扩展名自动推断。
|
||||
|
||||
```bash
|
||||
# 参数说明
|
||||
# <file> 必选,输入文件路径(格式由扩展名推断)
|
||||
# --force 冲突时覆盖已有记录(默认:报错并停止)
|
||||
# --dry-run 预览将执行的操作,不写入数据库
|
||||
# -o / --output text | json | json-compact
|
||||
|
||||
# 导入 JSON 文件(遇到已存在记录报错)
|
||||
secrets import backup.json
|
||||
|
||||
# 导入 TOML 文件,冲突时覆盖
|
||||
secrets import --force refining.toml
|
||||
|
||||
# 导入 YAML 文件,冲突时覆盖
|
||||
secrets import --force services.yaml
|
||||
|
||||
# 预览将执行的操作(不写入)
|
||||
secrets import --dry-run backup.json
|
||||
|
||||
# JSON 格式输出导入摘要
|
||||
secrets import backup.json -o json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### config — 配置管理(无需主密钥)
|
||||
|
||||
```bash
|
||||
# 设置数据库连接(每台设备执行一次,之后永久生效;先验证连接可用再写入)
|
||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||
|
||||
# 查看当前配置(密码脱敏)
|
||||
secrets config show
|
||||
|
||||
# 打印配置文件路径
|
||||
secrets config path
|
||||
# 输出: /Users/<user>/.config/secrets/config.toml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 全局参数
|
||||
|
||||
```bash
|
||||
# debug 日志(位于子命令之前)
|
||||
secrets --verbose search -q mqtt
|
||||
secrets -v add -n refining --kind service --name gitea -m url=xxx -s token=yyy
|
||||
|
||||
# 或通过环境变量精细控制
|
||||
RUST_LOG=secrets=trace secrets search
|
||||
|
||||
# 一次性覆盖数据库连接
|
||||
secrets --db-url "postgres://..." search -n refining
|
||||
```
|
||||
将共享 PEM 存为 `kind=key` 的 entry;其它记录在 `metadata.key_ref` 指向该 key 的 `name`。更新 key 记录后,引用方通过服务层解析合并逻辑即可使用新密钥(实现见 `secrets_core::service`)。
|
||||
|
||||
## 代码规范
|
||||
|
||||
- 错误处理:统一使用 `anyhow::Result`,不用 `unwrap()`
|
||||
- 异步:全程 `tokio`,数据库操作 `sqlx` async
|
||||
- SQL:使用 `sqlx::query` / `sqlx::query_as` 绑定参数,禁止字符串拼接(搜索的动态 WHERE 子句除外,需使用参数绑定 `$1/$2`)
|
||||
- 新增 `kind` 类型时:只需在 `add` 调用时传入,无需改代码
|
||||
- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query,`-v`=verbose,`-f`=field,`-o`=output
|
||||
- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!`
|
||||
- 审计:`add`/`update`/`delete` 成功后调用 `audit::log_tx`,写入 `audit_log` 表;失败只 warn 不中断
|
||||
- 加密:`encrypted` 列存储 AES-256-GCM 密文;`add`/`update`/`search`/`delete` 需主密钥(`secrets init` 后从 OS 钥匙串加载)
|
||||
- 输出:读命令通过 `OutputMode` 支持 text/json/json-compact;默认始终 `json`(pretty),`-o text` 供人类阅读;写命令 `add` 同样支持 `-o json`
|
||||
- 错误:业务层 `anyhow::Result`,避免生产路径 `unwrap()`。
|
||||
- 异步:`tokio` + `sqlx` async。
|
||||
- SQL:`sqlx::query` / `query_as` 参数绑定;动态 WHERE 仍须用占位符绑定。
|
||||
- 日志:运维用 `tracing`;面向用户的 Web 响应走 axum handler。
|
||||
- 审计:写操作成功后尽量 `audit::log_tx`;失败可 `warn`,不掩盖主错误。
|
||||
- 加密:密钥由用户密码短语通过 **PBKDF2-SHA256(600k 次)** 在客户端派生,服务端只存 `key_salt`/`key_check`/`key_params`,不持有原始密钥。Web 客户端在浏览器本地完成加解密;MCP 客户端通过 `X-Encryption-Key` 请求头传递密钥,服务端临时解密后返回明文。
|
||||
- MCP:tools 参数与 JSON Schema(`schemars`)保持同步,鉴权以请求扩展中的用户上下文为准。
|
||||
|
||||
## 提交前检查(必须全部通过)
|
||||
|
||||
每次提交代码前,请在本地依次执行以下检查,**全部通过后再 push**:
|
||||
|
||||
优先使用:
|
||||
## 提交前检查
|
||||
|
||||
```bash
|
||||
./scripts/release-check.sh
|
||||
```
|
||||
|
||||
它等价于先检查版本号 / tag,再执行下面的格式、Lint、测试。
|
||||
|
||||
### 1. 版本号(按需)
|
||||
|
||||
若本次改动需要发版,请先确认 `Cargo.toml` 中的 `version` 已提升,避免 CI 打出的 Tag 与已有版本重复。**升级版本后需同时更新 `Cargo.lock`**(运行 `cargo build` 即可自动同步),否则 CI 中 `cargo clippy --locked` 会因 lock 与 manifest 不一致而失败。可通过 git tag 判断:
|
||||
或手动:
|
||||
|
||||
```bash
|
||||
# 查看当前 Cargo.toml 版本
|
||||
grep '^version' Cargo.toml
|
||||
|
||||
# 查看是否已存在该版本对应的 tag(CI 使用格式 secrets-<version>)
|
||||
git tag -l 'secrets-*'
|
||||
cargo fmt -- --check
|
||||
cargo clippy --locked -- -D warnings
|
||||
cargo test --locked
|
||||
```
|
||||
|
||||
若当前版本已被 tag(例如已有 `secrets-0.3.0` 且 `Cargo.toml` 仍为 `0.3.0`),则应在 `Cargo.toml` 中 bump 版本号,再执行 `cargo build` 同步 `Cargo.lock`,最后一并提交,以便 CI 自动打新 Tag 并发布 Release。
|
||||
|
||||
### 2. 格式、Lint、测试
|
||||
发版前确认未重复 tag:
|
||||
|
||||
```bash
|
||||
cargo fmt -- --check # 格式检查(不通过则运行 cargo fmt 修复)
|
||||
cargo clippy -- -D warnings # Lint 检查(消除所有 warning)
|
||||
cargo test # 单元/集成测试
|
||||
```
|
||||
|
||||
或一次性执行:
|
||||
|
||||
```bash
|
||||
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test
|
||||
grep '^version' crates/secrets-mcp/Cargo.toml
|
||||
git tag -l 'secrets-mcp-*'
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
- Gitea Actions(runners: debian / darwin-arm64 / windows)
|
||||
- 触发:`src/**`、`Cargo.toml`、`Cargo.lock` 变更推送到 main
|
||||
- 构建目标:`x86_64-unknown-linux-musl`、`aarch64-apple-darwin`、`x86_64-apple-darwin`(由 ARM mac runner 交叉编译)、`x86_64-pc-windows-msvc`
|
||||
- 新版本自动打 Tag(格式 `secrets-<version>`)并上传二进制与对应 `.sha256` 摘要到 Gitea Release
|
||||
- Release 仅在 Linux/macOS/Windows 构建全部成功后才会从 draft 发布
|
||||
- 通知:飞书 Webhook(`vars.WEBHOOK_URL`)
|
||||
- 所需 secrets/vars:`RELEASE_TOKEN`(Release 上传,Gitea PAT)、`vars.WEBHOOK_URL`(通知,可选)
|
||||
- **注意**:Gitea Actions 的 Secret/Variable 创建时,`data`/`value` 字段需传入**原始值**,不要使用 base64 编码
|
||||
- **触发**:`main` / `feat/mcp`(以仓库 workflow 为准);路径含 `crates/**`、`deploy/**`、`Cargo.toml`、`Cargo.lock`。
|
||||
- **构建**:`x86_64-unknown-linux-musl` → `secrets-mcp`。
|
||||
- **Release**:tag `secrets-mcp-<version>`,上传 tar.gz + `.sha256`。
|
||||
- **部署**:可选在仓库 Actions 中配置 `vars.DEPLOY_HOST`、`vars.DEPLOY_USER` 与 `secrets.DEPLOY_SSH_KEY`(勿写进 workflow);可用 `scripts/setup-gitea-actions.sh` 调 Gitea API 写入。Actions **secrets 须为原始值**(如 PEM 全文、PAT 明文),**不要**先 base64 再写入,否则工作流内无法识别(例如 SSH 私钥无效)。**勿**在 CI 中保存 `GOOGLE_CLIENT_SECRET`、DB 密码。
|
||||
- **通知**:`vars.WEBHOOK_URL`(可选)。
|
||||
|
||||
## 环境变量
|
||||
## 环境变量(secrets-mcp)
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `RUST_LOG` | 日志级别,如 `secrets=debug`、`secrets=trace`(默认 warn) |
|
||||
| `USER` | 审计日志 actor 字段来源,Shell 自动设置,通常无需手动配置 |
|
||||
| `SECRETS_UPGRADE_URL` | upgrade 的 Release API 地址。构建时(cargo build)或运行时(.env/export) |
|
||||
| `SECRETS_DATABASE_URL` | **必填**。PostgreSQL URL。 |
|
||||
| `BASE_URL` | 对外基址;OAuth 回调 `${BASE_URL}/auth/google/callback`。 |
|
||||
| `SECRETS_MCP_BIND` | 监听地址,默认 `0.0.0.0:9315`。 |
|
||||
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | 可选;仅运行时配置。 |
|
||||
| `RUST_LOG` | 如 `secrets_mcp=debug`。 |
|
||||
| `USER` | 若写入审计 `actor`,由运行环境提供。 |
|
||||
|
||||
数据库连接通过 `secrets config set-db` 持久化到 `~/.config/secrets/config.toml`,不支持环境变量。
|
||||
> `SERVER_MASTER_KEY` 已不再需要。新架构下密钥由用户密码短语在客户端派生,服务端不持有。
|
||||
|
||||
Reference in New Issue
Block a user