M004:管理环境:默认值、必填项、redaction 与 secret 边界
目标:为普通配置与敏感输入建立不同通道,并验证 required、redaction 和 secret store 的职责边界。
0–7 分钟:先按所有权给变量分类
不要从 TOML 语法开始,先问“谁拥有这个值、能否公开、缺失时该怎样”。
| 类型 | 例子 | 推荐来源 | 仓库中保存什么 |
|---|---|---|---|
| 安全默认值 | APP_ENV=development | 提交的 [env] | 值本身 |
| 必填但非仓库所有 | API_TOKEN | 开发者环境或 CI secret store | 变量名、用途、缺失提示 |
| 个人覆盖 | 本机服务端口、SDK 路径 | 被忽略的 local 文件 | 示例和忽略规则 |
| 生产 secret | token、私钥、数据库密码 | 受控 secret manager | 只保存引用或变量契约 |
required 只验证调用者或后续配置已经提供值,不会替你生成或保存值。redact 在支持的 task 输出路径中遮盖敏感值,但不是加密、权限控制、分发或轮换系统。[S04]
7–20 分钟:实现一个不会提交 secret 的契约
步骤 1:建立文件边界
cd /tmp/mise-config-lab/project
cat > .gitignore <<'EOF'
mise.local.toml
.env
EOF
cat > .env.example <<'EOF'
# Copy variable names only; use a disposable value locally.
API_TOKEN=replace-me-locally
EOF
确认示例里没有真实 token。.env.example 的价值是说明接口,不是提供凭据。
步骤 2:写安全配置
将实验项目的 mise.toml 调整为:
[tools]
python = "3.12"
[env]
APP_ENV = { default = "development" }
API_TOKEN = { required = "Set API_TOKEN via developer or CI secret storage", redact = true }
[tasks.show-env]
run = 'printf "APP_ENV=%s token_present=%s\n" "$APP_ENV" "${API_TOKEN:+yes}"'
这里故意只打印 token_present=yes/no,不打印 token。安全验证不需要泄露被验证对象。
步骤 3:先验证缺失路径
确保当前 shell 没有这个实验变量,然后运行:
unset API_TOKEN
mise env
printf 'exit=%s\n' "$?"
预期观察:mise env 因缺少 API_TOKEN 失败,并显示你写的帮助文字;退出码非零。若命令成功,检查是否有 parent、global、local 或当前 shell 已提供同名变量。
步骤 4:注入一次性假值
API_TOKEN='course-only-not-a-secret' mise run show-env
预期观察:输出 APP_ENV=development token_present=yes。这证明 required 输入由调用者提供,同时普通默认值进入 task 环境。
步骤 5:验证仓库没有携带敏感值
git status --short
grep -R "course-only-not-a-secret" . --exclude-dir=.git || true
预期 grep 没有命中。若命中,删除该文件并检查 Git 历史/暂存区,而不只是清空终端。
步骤 6:验证 local 覆盖但不提交
cat > mise.local.toml <<'EOF'
[env]
APP_ENV = "local-development"
EOF
API_TOKEN='course-only-not-a-secret' mise run show-env
git status --short
预期 task 显示 APP_ENV=local-development,而 git status 不列出 mise.local.toml。这说明“值能生效”和“值应提交”是两个独立判断。
20–26 分钟:理解 redaction 的真实边界
redaction 通过拦截 task 输出并替换匹配值工作。若 task 使用 raw = true,stdout/stderr 直接交给终端,行级 redaction 无法应用。CI 平台还需要自己的 masking;官方 mise GitHub Action 可以处理被标记的 redacted 值,但这仍不把 mise 变成 secret store。[S04]
按下面顺序做威胁检查,不要真的打印 token:
- 存储:真实值是否进入 Git、artifact、cache 或镜像层?
- 注入:开发者和 CI 分别从哪里提供?谁能读取?
- 输出:普通 task、raw task、子程序日志和 CI 注释是否可能打印?
- 生命周期:谁负责轮换和撤销?
- 最小权限:不可信 PR job 是否根本不应该获得这个值?
Worked example
错误方案:
[env]
API_TOKEN = { value = "real-token", redact = true }
逐步判断:
redact = true只影响某些输出。value已经把真实 token 写进配置,Git、代码审阅和备份都可能得到它。- 因此即使日志里显示
***,存储边界仍然失败。 - 正确方向是
required + redact,值由外部 secret 系统注入。
常见失败与下一步:
- 缺失测试意外成功:运行
env | grep '^API_TOKEN=',再检查mise config和 local 文件。 - task 打印了真实值:立刻停止复制日志,轮换真实凭据,并检查 CI/artifact;“之后加 redact”不能撤回已泄露值。
.gitignore后仍显示 tracked:检查git ls-files; 不要在确认 secret 清理前提交。- raw task 需要敏感值:避免输出该值,并使用 CI 平台 masking;不要把 redaction 当唯一防线。
26–30 分钟:检索问题
问题:redaction 为什么不是 secret store?
作答后查看参考答案
redaction 只在支持的输出路径中遮盖值;它不负责加密、访问控制、安全分发、轮换或撤销。raw 输出、子程序日志和其他系统仍可能泄露值。
问题:为什么 required 变量缺失时应该失败,而不是默认为空字符串?
作答后查看参考答案
空字符串会让错误延后到更危险、更难诊断的位置。required 把“调用者必须提供”变成可执行契约,在运行任务前暴露配置缺口。
通过标准
- 普通默认值、个人覆盖和真实 secret 使用不同来源。
- 缺失 required 输入时能得到清晰、非零失败。
- 成功路径只证明值存在,不打印值本身。
- 能解释 raw output、CI masking 与 secret store 各自的边界。