Secrets 与环境
这一课你将能做到
- 在仓库 Settings → Secrets and variables → Actions 创建 secret,并在 workflow 里用
${{ secrets.NAME }} 引用
- 解释为什么密码不能写进 YAML,以及日志里 secret 为什么显示为
***
- 给部署类 job 挂上
environment: production,并说清「环境保护规则(审批/等待)」与普通 repository secret 的差别
为什么要学
CI 迟早要碰真实凭据:推 GHCR、SSH 到 VPS、读云厂商 API。把密钥写进 ci.yml 再 push,等于把钥匙贴在大门上——Git 历史还会长期留下,删文件也删不掉历史。
GitHub Secrets 把值存在平台侧,YAML 里只出现名字;运行时注入环境变量或输入,日志自动打码。再配合 Environments,生产部署可以要求人工审批、加等待计时,还能用环境专属 secrets。
本课建立正确的存放与引用习惯;具体 Docker 登录、SSH 部署在后几课用到这些名字。
先建立图像
仓库 Settings
└─ Secrets and variables → Actions
DB_PASSWORD = ••••••(只存平台,不进 Git)
SSH_PRIVATE_KEY = ••••••
workflow YAML(进 Git,可公开读)
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }} ← 只有名字
runner 运行时
进程环境里有真实值;日志中自动打码为 ***
人话:Secrets 像保险箱;YAML 只写「去保险箱取哪一把钥匙」的标签,不写钥匙齿形。
| 存放处 | 适合 | 特点 |
|---|
| Repository secret | 单仓库 CI/CD | 有 admin 权限可管理;运行时可读 |
| Environment secret | 某环境(如 production) | 叠加审批、等待计时等保护规则 |
| Variable(非 secret) | 非敏感配置如区域名、Node 版本 | 明文可见、不打码 |
写死在 YAML / 提交 .env | 无 | 泄漏,禁止 |
跟着做
步骤 1:创建练习用 secret
仓库 → Settings → Secrets and variables → Actions → New repository secret:
- Name:
CI_TEST
- Value:随意字符串,如
hello-secret
保存后列表里只能看到名字,看不到值(明文不可再查看,只能覆盖更新)。
步骤 2:在 workflow 里引用
name: Secret smoke
on:
workflow_dispatch:
permissions:
contents: read
jobs:
show:
runs-on: ubuntu-latest
steps:
- name: 引用 secret(观察打码)
env:
# 值来自 Settings,不进 Git
CI_TEST: ${{ secrets.CI_TEST }}
run: |
# 日志中通常显示为 ***,不要用技巧拼回明文
echo "CI_TEST length=${#CI_TEST}"
echo "CI_TEST value=$CI_TEST"
手动跑一次:value 应被打成 ***,但 length 能对上。
若显示空:检查名字是否完全一致(大小写敏感)、是否处在 fork PR 场景(见后文)。
步骤 3:对照「绝对不要」的写法
# 错误示范:密钥进仓库
env:
DB_PASSWORD: "SuperSecret123"
即使后来删掉,历史 commit 仍可能残留。正确做法:轮换已泄漏的密钥,并改用 secrets。与「不提交 .env」是同一纪律。
步骤 4:挂 Environment(了解生产门闩)
Settings → Environments → 新建 production → 勾选 Required reviewers(填你自己练习)。
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # 使用该环境的保护规则与 secrets
steps:
- name: 假装部署
run: echo "deploying with protected env"
再跑:run 会停在 waiting 状态,直到有人在 run 页面点批准。笔记写:「Environment = 保护规则 + 可选专属 secrets。」
步骤 5:为本站列一张 secrets 清单(先名字后赋值)
| Name | 用途 | 何时需要 |
|---|
GITHUB_TOKEN | 内置,推包/评论 PR 等 | 自动注入;注意 permissions |
SSH_PRIVATE_KEY | SSH 到 VPS 部署 | 采用 Push 部署时 |
DEPLOY_HOST | 服务器地址 | 同上 |
DEPLOY_USER | 服务器账号 | 同上 |
| (云 OIDC) | 短期云凭证 | 进阶,代替长期 AK/SK |
把表写进 README 或内部文档,值只进 Settings。
步骤 6:了解 OIDC(概念即可)
长期 Access Key 放 secret 能用,但泄漏窗口大。云厂商推荐 OIDC:Actions 向云身份 provider 换取短时凭证,云侧按角色授权。本课不配云;知道「生产方向是短时联邦身份,而不是永久拷贝 AK 进 Actions」即可。
讲清楚要点
- 因为 Git 会永久记录,所以密钥只能进 Secrets,不能进 YAML 或提交的
.env——泄漏后的正确动作是轮换密钥,不是删文件。
- 因为 YAML 常被公开,所以文件里只出现
${{ secrets.NAME }} 引用——审流水线的人看见的是名字,不是钥匙。
- 因为日志可能被转发或截图,所以不要
echo 明文、不要 base64 拼解——打码不是绝对防护,主动规避才是。
- 因为生产部署风险更高,所以用 Environment 加审批与独立 secret——与「随便 push 就发生产」隔离。
- 因为 fork 来的 PR 默认拿不到 secrets,所以开源协作更安全——这也解释了「为什么 fork PR 里部署 job 没有密钥」。
- 因为
GITHUB_TOKEN 权限由 permissions 控制,所以推 GHCR 要显式开 packages: write——不是「有 token 就能为所欲为」。
你可能会懵的地方
-
「日志都是 ***,怎么知道注入成功了?」
看长度、看下游步骤是否真的登录成功(如 docker login 返回 0)。不要为调试打印明文——打码基于精确子串匹配,base64 等变换可能绕过,属严重事故。
-
「Organization secret 和 repo secret?」
组织级可下发到多仓库,可按环境限定可见仓库;本课主路径用 repo secret 足够。重名时的优先级规则见官方文档。
-
「pull_request_target 能读 secret,是不是更好用?」
能读,但也更容易被恶意 PR 组合滥用(在外部 PR 内容上跑你的高权限流程)。默认优先 pull_request;确需使用先完整读完官方安全说明,且严禁 checkout 并执行 PR 分支里的不可信脚本。
-
「Variables 和 Secrets 有啥区别?」
Variables 明文不打码,适合非敏感配置(如部署区域、功能开关);密码、密钥、token 一律 Secrets。
-
「secret 能用在工作流所有地方吗?」
大多数地方可以,但注意 schedule/fork PR 等场景可用性不同;另外 ${{ secrets.X }} 直接内联进 run: 脚本时有注入风险,推荐经 env: 传入再用。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 密码写在 workflow 明文 | 公开即泄漏且进 Git 历史 | ${{ secrets.XXX }} |
| 用 secret 存 Node 版本号这类公开配置 | 管理成本高、无意义打码 | 用 variable 或直接写 YAML |
| 为调试 base64 打印 secret 再解 | 打码失效,日志泄漏 | 用长度与下游成功与否验证 |
| 生产部署无 Environment 保护 | 任何人改 YAML 可能直推生产 | environment + reviewers |
| secret 名大小写与引用不一致 | 读到空值 | 命名统一大写下划线,逐字符核对 |
卡点急救
- secret 读到空值:三查——名字拼写与大小写是否完全一致;是否在 fork PR 中(默认拿不到 secrets);job 是否挂了不带该 secret 的 environment(environment secret 与 repo secret 作用域不同)。
- run 卡在 waiting for approval:这是 Environment 的 Required reviewers 在起作用,去 run 详情页点 Review deployments 批准即可;不想要审批就编辑环境的保护规则。
- 怀疑密钥已泄漏:立即在源平台轮换/吊销旧值,再在 Settings 更新 secret;若曾提交进 Git,还需清理历史(filter-repo)——但首要动作永远是轮换,历史清理不能替代。
面试常问
-
GitHub Actions 的 secrets 是怎么存储和保护的?
存储在 GitHub 平台侧,加密保存;workflow 里只出现名字引用,运行时注入到进程环境;日志输出基于存储值的精确子串自动打码为 ***。要点补充:打码可被子串变换绕过,所以最佳实践还包括不打印敏感值、最小 permissions、生产用 environment 审批隔离。
-
为什么 fork 仓库的 PR 拿不到 secrets?这在设计上解决了什么问题?
外部贡献者的 PR 可能包含恶意 workflow 改动,如果它能读 secrets,攻击者就能把密钥外传。所以 pull_request 事件来自 fork 时只有只读 token、无 secrets。这是「不可信输入与高权限隔离」的安全模型,也回答了为什么开源项目的 CI 对外部 PR 更受限。
-
如何设计生产部署的凭据管理?
分层答:① 尽量不把人类级凭据给 CI——GitOps 模式下 CI 只需推镜像权限与开 PR 权限;② 必须给时用 environment secrets + required reviewers + 最小作用域(如只写某个 namespace 的受限 kubeconfig);③ 云上优先 OIDC 短时凭证,避免长期 AK/SK;④ 定期轮换并审计 secret 使用记录。
自测三问
- 为什么不能把数据库密码直接写在
ci.yml?
${{ secrets.CI_TEST }} 出现在仓库哪个位置?真实值存在哪?
environment: production 主要解决什么问题?
小结一句话
Secrets 存 Settings;YAML 只引用名字;生产部署再用 Environment 加上门闩。
延伸