CI/CD 流水线拥有通往生产的所有钥匙,也成了攻击者的高价值目标。一次 PR 里的恶意脚本、一个泄露的部署密钥、一个失控的 Runner,都能让"代码即信任"崩塌。本文按防御纵深展开:认识攻击面(RCE/提权/投毒)→ 凭证治理(短时令牌/动态 Secret)→ Runner 隔离与最小权限 → 签名与 SBOM 门禁 → Gitleaks 硬编码凭证扫描 → 审计检测 → 事件响应。
目录
- 1. 流水线攻击面:RCE 提权与投毒
- 2. 凭证管理:短时令牌与动态 Secret
- 3. 隔离与最小权限:Runner 隔离
- 4. 签名与 SBOM 门禁
- 5. 硬编码凭证治理:Gitleaks
- 6. 审计与检测
- 7. 事件响应
- 8. 供应链防御纵深
- 9. 案例与最佳实践
1. 流水线攻击面:RCE 提权与投毒
1.1 流水线为什么是目标
流水线有生产凭证、能改镜像、能发版本 = 通往一切的钥匙
攻击者只要控制一个环节,就能"合法地"把恶意产物送进生产
1.2 三类核心攻击向量
| 攻击向量 | 场景 | 典型后果 |
|---|---|---|
| RCE | 恶意 PR 触发构建脚本执行命令 | 在 Runner 上执行任意代码 |
| 提权 | 窃取流水线里挂的宽权限凭证 | 横向到云/制品库/生产 |
| 投毒 | 篡改依赖/镜像/构建产物 | 把恶意组件送进生产 |
1.3 威胁模型
入口:未审 PR、恶意依赖、失陷的第三方 Action/工具镜像;载体:Runner 执行/凭证/产物
信任链原则:任何"未经验证就信任"的环节都是弱点
2. 凭证管理:短时令牌与动态 Secret
2.1 静态 Secret 的致命伤
把云密钥/数据库密码直接写进流水线变量 = 泄露一次全完蛋:
泄露无感知、无法单独吊销、任何人可复用
目标:生产凭证永不静态存在于 CI 配置里
2.2 OIDC 联邦:CI 短时令牌
# GitHub Actions OIDC:工作负载直接从 IdP 换短时云凭证
permissions:
id-token: write
contents: read
# 换取 AWS 短时凭证(无需存储任何长期密钥)
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::xxx:role/ci-deploy \
--web-identity-token "$GITHUB_OIDC_TOKEN" \
--duration-seconds 900
关键收益:凭证有效期只有 15 分钟、绑定本次运行、按角色最小授权
2.3 动态 Secret:Vault
# 流水线运行期从 Vault 动态拉取,用完即失效
vault read database/creds/app # 动态生成一次性数据库账号
vault kv get secret/ci/app # 动态拉取应用密钥
# 动态 secret:短生命周期、自动轮转、按角色权限分发
分级:能 OIDC 用 OIDC;需"任意时刻可用"的走 Vault 动态 Secret;静态 secret(第三方密钥)加密托管 + 定期轮转
3. 隔离与最小权限:Runner 隔离
3.1 Runner 隔离
隔离等级:SaaS Runner 天然隔离适合普通构建;自托管 Runner 当生产资源保护
不可信代码跑容器/沙箱;不同信任级别用不同 Runner 池
3.2 最小权限
# GitHub Actions:声明最小权限,别用默认全量 token
permissions:
contents: read
pull-requests: write # 只给需要的
issues: none
原则:凭证按"本次任务最小需要"授予:构建只需读代码就不给写制品库;
不同环境用不同角色(staging 角色碰不到生产资源)
3.3 网络与行为隔离
Runner 出网最小化:按需放行(拉依赖、推制品、调 API),其余拦截
禁止 Runner 有"通用网络通路":出网要审计、入网要拒绝
高风险构建(跑第三方代码)与生产部署构建分开池
4. 签名与 SBOM 门禁
4.1 产物签名门禁
流水线产出必须签名(cosign):构建后签名 → 部署前验签 → 准入控制强制
没有有效签名的产物 = 来源不明,禁止进入下一阶段
cosign sign --key cosign.key registry:5000/shop/api:v1.2.3
cosign verify --key cosign.pub registry:5000/shop/api:v1.2.3
4.2 SBOM 门禁
每个产物必须生成 SBOM 并扫描通过才能进发布:
syft 生成 → grype 扫描 → 新引入高危漏洞拦截
SBOM 缺失或扫描失败 → 流水线红灯
4.3 依赖来源校验
# 只允许从可信源拉依赖/镜像(示例策略)
allowed_sources:
registries: [registry:5000, registry.npmjs.org]
forbid_scripts: true # 禁止第三方安装脚本自动执行
lockfile_required: true
对第三方 Action/工具:固定到 commit SHA 而非 tag(防 tag 被改写)
5. 硬编码凭证治理:Gitleaks
5.1 为什么用 Gitleaks
# 快速、可配置、能当 CI 门禁的 secret 扫描器
gitleaks detect --source . --exit-code 1 --report-format sarif --report-path gitleaks.sarif
# 扫描历史(不留存量尾巴)
gitleaks detect --source . --log-opts="--all"
能识别:云密钥、API key、私钥、token 等 100+ 种模式;
自定义正则覆盖内部系统专用密钥格式
5.2 扫进 CI 的三种位置
PR 时扫:新提交引入 secret → 阻止合并(最快反馈)
合并前扫:全量 diff 检查,防止漏网
定期全仓扫:扫历史与分支,清除存量泄露
# GitHub Actions PR 门禁
steps:
- uses: actions/checkout@v4
- run: gitleaks detect --exit-code 1
5.3 发现后的处置流程
发现即"假设已泄露":立刻轮转/吊销,不只"删掉代码"
流程:定位 → 轮转 → 清理历史 → 分析泄露面(是否进入制品/镜像/日志)→ 通知相关方
6. 审计与检测
6.1 审计日志
流水线全量审计:谁触发/哪个 Runner/什么凭证/产物 hash;三处日志对齐(平台+系统+凭证访问)
审计日志不可篡改,接入 SIEM 留存 >180 天
6.2 行为检测
对"异常行为"告警而非只对"已知攻击":陌生 Runner/触发者/仓库、凭证被异常 IP 使用、
产物 hash 与声明不符、深夜非计划触发;用过去 N 周基线做异常检测
6.3 合规基线
基线落地检查:无静态凭证 / OIDC+动态 secret / Runner 分级隔离 / 权限最小化 / 签名+SBOM 门禁 / secret 扫描全覆盖
定期自查 + 外部审计,把流水线当"生产系统"对待
7. 事件响应
7.1 疑似攻破的处理
发现异常(可疑 PR/凭证外泄/产物被改):立即隔离(暂停流水线/Runner、冻结凭证)
→ 取证(保留日志/产物/镜像勿清理)→ 评估攻击者触达范围
7.2 应急止血清单
□ 轮转所有受影响凭证(不确定就全轮转);下线可疑 Runner 重建环境
□ 回滚/下线被投毒产物,验签线上镜像;按 SBOM 定位受影响版本
□ 通知利益相关方,按预案沟通
7.3 复盘与加固
复盘三问:怎么进来的/为什么没拦住/下次怎么拦住 → 落成新门禁
供应链是持续对抗,避免只修一个洞就不管了
8. 供应链防御纵深
8.1 纵深分层
入口层:依赖白名单 + 锁文件 + 固定 SHA;构建层:Runner 隔离 + 最小权限 + 短时凭证
产物层:签名 + SBOM + 扫描门禁;部署层:准入验签 + 部署后监控
每一层独立失效也要能兜住——攻击者要穿透全部才有机会
8.2 工具链组合
| 能力 | 工具 |
|---|---|
| secret 扫描 | Gitleaks |
| 依赖/SBOM | Syft + Grype + osv-scanner |
| 签名 | cosign |
| 凭证动态化 | Vault / OIDC 联邦 |
| 准入控制 | Kyverno/Gatekeeper |
8.3 持续改进节奏
季度重扫全量 secret + 权限评审 + 基线自查;事件后必复盘沉淀门禁
发布前门禁清单过一遍(签名/SBOM/扫描/最小权限)
9. 案例与最佳实践
9.1 真实案例
某 SaaS 公司(概念案例):PR 中第三方依赖被投毒,构建时窃取挂着的云密钥,攻击者拿到生产部署权限并篡改镜像
修复:凭证改 OIDC 短时令牌 + Vault 动态 Secret;Runner 分级隔离;Gitleaks 扫 PR + 全仓历史;
cosign 签名 + 部署验签;SBOM 门禁拦截新引入高危依赖
结果:泄露影响面从"全部生产"缩小到"单个 15 分钟令牌",半年零复发
9.2 最佳实践 Checklist
□ 生产凭证无静态:OIDC 短时令牌 + Vault 动态 Secret
□ Runner 分级隔离,不可信代码跑沙箱,出网最小化
□ 流水线权限最小化声明,按环境分角色
□ 产物全部 cosign 签名,部署准入验签
□ SBOM 生成 + 扫描进发布门禁,新漏洞拦截
□ 第三方工具固定 commit SHA,依赖来源白名单;Gitleaks 扫 PR + 全仓历史,发现即轮转
□ 审计日志不可篡改,异常行为告警
□ 事件响应有预案,攻破后按止血清单处置并复盘
9.3 常见坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 静态密钥挂流水线 | 一次泄露全盘皆输 | OIDC + Vault 动态化 |
| Runner 全权共用 | 一个任务波及全局 | 分级隔离 + 最小权限 |
| 只扫新代码 | 存量 secret 漏网 | 全仓历史扫描 + 轮转 |
| tag 引用第三方工具 | tag 被改写即投毒 | 固定 commit SHA |
| 签名只签不验 | 部署环节被绕过 | 准入控制部署时验签 |
| 发现 secret 只删代码 | 泄露持续可利用 | 立即轮转,假设已泄露 |
| 攻破后不复盘 | 同一条船翻两次 | 复盘沉淀新门禁 |
小结
CI/CD 流水线安全 = 攻击面认知(RCE/提权/投毒)→ 凭证治理(OIDC 短时令牌 + Vault 动态 Secret)→ Runner 隔离与最小权限 → 产物签名 + SBOM 门禁 → Gitleaks 硬编码凭证扫描 → 审计与异常检测 → 事件响应与复盘。核心原则:“流水线里的信任必须被验证,凭证必须短命且最小,任何一次泄露都按’已泄露’处置”。先把静态凭证换掉、把 secret 扫描进 PR 门禁、给产物加上签名与验签,这三件事做扎实,就能挡住绝大多数供应链攻击;再逐步补全 Runner 隔离、审计与事件响应,把流水线当成生产系统持续加固。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。