Secrets 管理
one secrets 是 One CLI 的工作区级密钥方案,底层基于 Infisical。从 v0.3.0 起替换了原先的 SOPS+age 实现。
它解决的问题:
- 密钥不再放在 git 里(对比 SOPS:密文虽加密但仍提交;现在密钥只存在于 Infisical 后端)
- monorepo 友好:每个子项目自动映射到 Infisical 的独立 folder,互相隔离
- 多环境:dev / staging / prod 走同一套 folder 树,环境横切
- 共享密钥:根 folder 的 key 可被子项目继承,跨服务共用的密钥写一次
第一次初始化
Section titled “第一次初始化”Step 1:拿到 Infisical 凭据
Section titled “Step 1:拿到 Infisical 凭据”去 Infisical Web → Organization → Access Control → Identities 创建一个 Universal Auth machine identity,记下 client ID 和 client secret。
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<...>export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<...>我们只支持环境变量。不要把凭据存到磁盘——避免 .env / 配置文件意外提交。CI 直接走 secrets,本地用 direnv / 1Password CLI 之类。
Step 2:在工作区里 init
Section titled “Step 2:在工作区里 init”cd my-workspaceone secrets init --project-id <infisical-project-id>会做三件事:
- 联网验证凭据 + projectId 可达(
--skip-verify可绕过) - 把配置写入
one.manifest.json#env:
{ "version": 2, "env": { "provider": "infisical", "projectId": "<id>", "siteUrl": "https://app.infisical.com", "environments": ["dev", "staging", "prod"], "defaultEnv": "dev", "rootPath": "/" }}- 在 stdout 输出
one-cli/secrets-init/v1的 JSON envelope(auth_status: verified / skipped)
自托管 Infisical 实例:加 --site-url https://your-instance.example.com。
写入单个变量
Section titled “写入单个变量”# 显式 pathone secrets set DATABASE_URL "postgres://localhost/db" --env dev --path /services/user-api
# 在子项目目录里执行时 path 自动从 cwd 推导cd services/user-apione secrets set DATABASE_URL "postgres://localhost/db" --env dev
# 共享密钥写到根 folderone secrets set OBSERVABILITY_TOKEN "tk_..." --env dev --path /
# 已有不同值时需要 --yes 确认覆盖one secrets set DATABASE_URL "new-value" --env dev --yesaction 字段:
created— 新增的 keyupdated— 覆盖了已有不同值unchanged— 值跟现有相同,没改动
读取 / 列出
Section titled “读取 / 列出”# 单个值(JSON 模式可被脚本消费)one secrets get DATABASE_URL --env dev --path /services/user-api --json | jq -r .value
# 列名(不显示值,安全展示)one secrets list --env dev --path /services/user-apione secrets list --env dev --recursive # 递归包括所有子 folder拉取到本地 .env(核心命令)
Section titled “拉取到本地 .env(核心命令)”# 拉所有子项目(每个子项目独立的 .env.example 过滤)one secrets pull --env dev
# 只拉单个子项目one secrets pull --env dev --path /services/user-api
# 看会写哪些 key 但不实际落盘one secrets pull --env dev --dry-run --json双层安全契约
Section titled “双层安全契约”pull 不会把整个 Infisical project 的密钥灌到所有子项目。两层过滤:
Layer 1:Infisical folder 隔离
每个子项目的 path 只允许它看到自己 folder + 父 folder 的 key。apps/dashboard(path = /apps/dashboard)拿不到 /services/user-api/DATABASE_URL,因为不在它的继承链上。
Layer 2:.env.example 过滤
即使父 folder 包含子项目用不到的 key(继承机制下会一并进入候选集),最终写入 .env 的只有该子项目 .env.example 里声明过的 key。
举个例子:
Infisical project, env=dev:├── / # OBSERVABILITY_TOKEN, STRIPE_SECRET├── /services/user-api/ # DATABASE_URL, JWT_SECRET└── /apps/dashboard/ # NEXT_PUBLIC_API_URL
services/user-api/.env.example # 只声明 DATABASE_URL, JWT_SECRET, OBSERVABILITY_TOKENapps/dashboard/.env.example # 只声明 NEXT_PUBLIC_API_URL, OBSERVABILITY_TOKENdocs/ # 没有 .env.exampleone secrets pull --env dev 的结果:
services/user-api/.env:DATABASE_URL + JWT_SECRET + OBSERVABILITY_TOKEN(不含 STRIPE_SECRET,因为没声明)apps/dashboard/.env:NEXT_PUBLIC_API_URL + OBSERVABILITY_TOKEN(不含任何后端密钥)docs/:跳过(没.env.example,安全开门约定)
这是核心的安全保障:
- 前端子项目无法拿到数据库密码
- DB credentials 不可能被打进 client bundle(即使前端代码引用
import.meta.env) - 想接收某个 key 的子项目必须显式在
.env.example里声明
默认不会覆盖已有 .env
Section titled “默认不会覆盖已有 .env”one secrets pull --env dev# 已有 .env 内容跟 Infisical 不一致 → SECRETS_PULL_CONFLICT,拒绝写入
one secrets pull --env dev --force# 显式覆盖(destructive)判断”内容是否一致”是语义级的:parse + 比较 dotenv 记录,不是字节比较,所以行尾 / 引号差异不会触发 false conflict。
子项目级配置覆盖(可选)
Section titled “子项目级配置覆盖(可选)”默认 path 推导是 / + relativeDir(例如 services/user-api → /services/user-api)。子项目可以在 one.manifest.json 对应 subprojects[] 条目的 env 字段里显式覆盖:
{ "subprojects": [ { "name": "charge", "relativeDir": "services/charge", "templateId": "api-nest", "toolchain": "node", "env": { "path": "/teams/payments/services/charge", "inherits": true } } ]}path:覆盖默认推导inherits:默认true,从根 folder 一路继承到该 path;设false则只看 path 自己的 key"disabled": true:完全跳过此子项目(连.env都不写)
CI / 部署场景
Section titled “CI / 部署场景”# CI runner 的 secret 注入export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=$INFISICAL_CLIENT_IDexport INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=$INFISICAL_CLIENT_SECRET
# 拉 staging 环境的所有密钥one secrets pull --env staging --json
# 然后是常规构建步骤docker compose upCI 用同一个 machine identity;每个 CI 跑一次 pull 就够,构建过程中不需要再调 Infisical。
| 错误码 | 原因 | 怎么办 |
|---|---|---|
INFISICAL_NOT_CONFIGURED | one.manifest.json#env 不在 | one secrets init --project-id <id> |
INFISICAL_AUTH_MISSING | 没设凭据环境变量 | export 两个 INFISICAL_UNIVERSAL_AUTH_CLIENT_* 变量 |
INFISICAL_AUTH_FAILED | client id/secret 错误或过期 | Infisical Web 重新生成 secret |
INFISICAL_PROJECT_NOT_FOUND | projectId 错或机器身份没权限 | 核对 projectId;确认 machine identity 有该 project 的访问权 |
INFISICAL_NETWORK_ERROR | 连不上 Infisical API | 检查网络 + siteUrl |
SECRETS_PULL_CONFLICT | 已有 .env 跟拉取内容不一致 | --force 覆盖 |
SECRETS_KEY_NOT_FOUND | secrets get 找不到 key | 核对 KEY 拼写 + --path + --env |
SECRETS_INVALID_KEY | KEY 非法 | 必须匹配 POSIX env-var 模式 ^[A-Za-z_][A-Za-z0-9_]*$ |
SECRETS_INVALID_ENV_NAME | env 名称非法 | 必须匹配 ^[a-zA-Z0-9][a-zA-Z0-9-_]*$,例如 dev / staging / prod |
完整错误码表见 error codes。
应该提交:
one.manifest.json(包含env配置块;无敏感信息)- 每个目录的
.env.example(声明该目录消费哪些 key;不含值)
不应该提交:
- 任何
.env(明文,pull的产物) - 任何包含
INFISICAL_UNIVERSAL_AUTH_CLIENT_*的本地 dotfile
工作区根的 .gitignore 默认忽略 .env。如果你之前误提交过 .env,需要 git rm --cached 清理。
从 SOPS+age 迁移
Section titled “从 SOPS+age 迁移”老仓库(v0.2.x 时代)的迁移步骤:
- 在 Infisical Web 创建一个新 project
one secrets init --project-id <new-id>写入新配置- 手动把原
.env.<env>.enc里的值在 Infisical Web 里重新创建(按原本子项目对应的 folder 组织) - 删除
.sops.yaml、.secrets/、所有.env.*.enc - 把
INFISICAL_UNIVERSAL_AUTH_CLIENT_*加到 CI secrets one secrets pull --env dev验证一切到位
我们没有自动迁移工具——SDK 没有 “convert SOPS dump to Infisical” 的合理路径,且手工核对值是好事(迁移正是审计密钥的好时机)。