Skip to content

Secrets 管理

one secrets 是 One CLI 的工作区级密钥方案,底层基于 Infisical。从 v0.3.0 起替换了原先的 SOPS+age 实现。

它解决的问题:

  • 密钥不再放在 git 里(对比 SOPS:密文虽加密但仍提交;现在密钥只存在于 Infisical 后端)
  • monorepo 友好:每个子项目自动映射到 Infisical 的独立 folder,互相隔离
  • 多环境:dev / staging / prod 走同一套 folder 树,环境横切
  • 共享密钥:根 folder 的 key 可被子项目继承,跨服务共用的密钥写一次

去 Infisical Web → OrganizationAccess ControlIdentities 创建一个 Universal Auth machine identity,记下 client ID 和 client secret。

Terminal window
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<...>
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<...>

我们支持环境变量。不要把凭据存到磁盘——避免 .env / 配置文件意外提交。CI 直接走 secrets,本地用 direnv / 1Password CLI 之类。

Terminal window
cd my-workspace
one secrets init --project-id <infisical-project-id>

会做三件事:

  1. 联网验证凭据 + projectId 可达(--skip-verify 可绕过)
  2. 把配置写入 one.manifest.json#env
{
"version": 2,
"env": {
"provider": "infisical",
"projectId": "<id>",
"siteUrl": "https://app.infisical.com",
"environments": ["dev", "staging", "prod"],
"defaultEnv": "dev",
"rootPath": "/"
}
}
  1. 在 stdout 输出 one-cli/secrets-init/v1 的 JSON envelope(auth_status: verified / skipped)

自托管 Infisical 实例:加 --site-url https://your-instance.example.com

Terminal window
# 显式 path
one secrets set DATABASE_URL "postgres://localhost/db" --env dev --path /services/user-api
# 在子项目目录里执行时 path 自动从 cwd 推导
cd services/user-api
one secrets set DATABASE_URL "postgres://localhost/db" --env dev
# 共享密钥写到根 folder
one secrets set OBSERVABILITY_TOKEN "tk_..." --env dev --path /
# 已有不同值时需要 --yes 确认覆盖
one secrets set DATABASE_URL "new-value" --env dev --yes

action 字段:

  • created — 新增的 key
  • updated — 覆盖了已有不同值
  • unchanged — 值跟现有相同,没改动
Terminal window
# 单个值(JSON 模式可被脚本消费)
one secrets get DATABASE_URL --env dev --path /services/user-api --json | jq -r .value
# 列名(不显示值,安全展示)
one secrets list --env dev --path /services/user-api
one secrets list --env dev --recursive # 递归包括所有子 folder
Terminal window
# 拉所有子项目(每个子项目独立的 .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

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_TOKEN
apps/dashboard/.env.example # 只声明 NEXT_PUBLIC_API_URL, OBSERVABILITY_TOKEN
docs/ # 没有 .env.example

one 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 里声明
Terminal window
one secrets pull --env dev
# 已有 .env 内容跟 Infisical 不一致 → SECRETS_PULL_CONFLICT,拒绝写入
one secrets pull --env dev --force
# 显式覆盖(destructive)

判断”内容是否一致”是语义级的:parse + 比较 dotenv 记录,不是字节比较,所以行尾 / 引号差异不会触发 false conflict。

默认 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 都不写)
Terminal window
# CI runner 的 secret 注入
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=$INFISICAL_CLIENT_ID
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=$INFISICAL_CLIENT_SECRET
# 拉 staging 环境的所有密钥
one secrets pull --env staging --json
# 然后是常规构建步骤
docker compose up

CI 用同一个 machine identity;每个 CI 跑一次 pull 就够,构建过程中不需要再调 Infisical。

错误码原因怎么办
INFISICAL_NOT_CONFIGUREDone.manifest.json#env 不在one secrets init --project-id <id>
INFISICAL_AUTH_MISSING没设凭据环境变量export 两个 INFISICAL_UNIVERSAL_AUTH_CLIENT_* 变量
INFISICAL_AUTH_FAILEDclient id/secret 错误或过期Infisical Web 重新生成 secret
INFISICAL_PROJECT_NOT_FOUNDprojectId 错或机器身份没权限核对 projectId;确认 machine identity 有该 project 的访问权
INFISICAL_NETWORK_ERROR连不上 Infisical API检查网络 + siteUrl
SECRETS_PULL_CONFLICT已有 .env 跟拉取内容不一致--force 覆盖
SECRETS_KEY_NOT_FOUNDsecrets get 找不到 key核对 KEY 拼写 + --path + --env
SECRETS_INVALID_KEYKEY 非法必须匹配 POSIX env-var 模式 ^[A-Za-z_][A-Za-z0-9_]*$
SECRETS_INVALID_ENV_NAMEenv 名称非法必须匹配 ^[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 清理。

老仓库(v0.2.x 时代)的迁移步骤:

  1. 在 Infisical Web 创建一个新 project
  2. one secrets init --project-id <new-id> 写入新配置
  3. 手动把原 .env.<env>.enc 里的值在 Infisical Web 里重新创建(按原本子项目对应的 folder 组织)
  4. 删除 .sops.yaml.secrets/、所有 .env.*.enc
  5. INFISICAL_UNIVERSAL_AUTH_CLIENT_* 加到 CI secrets
  6. one secrets pull --env dev 验证一切到位

我们没有自动迁移工具——SDK 没有 “convert SOPS dump to Infisical” 的合理路径,且手工核对值是好事(迁移正是审计密钥的好时机)。