2. CI/CD 管道安全威胁:常见攻击面、密钥泄露风险、供应链攻击
聊到 CI/CD 安全,我见过不少团队把精力全花在写代码上,管线那边基本是裸奔状态。一旦管线被攻破,你的代码仓库、生产环境、客户数据,全都有可能暴露。所以,我们得先搞清楚敌人长什么样。这一讲,我就把 CI/CD 管道里最常见的几类威胁拆开,和大家讲讲我踩过的坑,以及那些容易忽视的细节。
2.1 常见攻击面
攻击面,说白了就是别人能从哪里摸进你的管线。我自己做安全审计时,最喜欢先看这三个入口:代码仓库(SCM)、执行节点(Runner)、以及 Webhook。为什么?因为这仨最容易出问题。
- SCM 入侵:攻击者拿到了仓库的 Write 权限,就能直接修改你的
.gitlab-ci.yml或Jenkinsfile。我遇到过一家客户,他们的 GitHub 组织压根没开分支保护,一个实习生不小心把密钥推到了 feature 分支,结果攻击者通过 PR 的workflow_run事件把恶意脚本注入了生产流水线。 - Runner 接管:GitHub Actions 也好,GitLab Runner 也罢,执行节点如果允许拉取未签名的容器镜像,攻击者就能通过一个恶意镜像拿到 Runner 的 Shell。我在项目中见过一种攻击手法叫“DIND 逃逸”——攻击者通过 privileged 容器内运行 docker-in-docker,直接控制了宿主机。
- Webhook 欺骗:很多团队在自己搭的 Jenkins 上暴露了不带签名的 Webhook 接口。攻击者可以伪造 push 事件,反复触发管线,进行信息探测甚至 DoS。
2.2 密钥泄露风险
密钥泄露这事儿,说多了都是泪。为什么管线里老是出现密钥?本质上是因为开发和运维之间缺乏一个“密钥保险箱”。常见的泄露途径包括:
| 泄露途径 | 典型场景 | 严重程度 |
|---|---|---|
| 硬编码在代码中 | 开发在配置文件中写死 AWS Access Key,提交到 Git 历史后再也没删干净 | 极高 |
| 日志/产物中暴露 | 构建脚本 echo $DB_PASSWORD,日志直接传到了 ELK |
高 |
| CI/CD 变量泄露 | GitHub Actions 里 env. 被 pr: 事件触发,PR 提交者通过 debug 模式读取 |
高 |
| 构建产物包含密钥 | Docker Image 里把 .env 文件打进去了,任何人都能 docker inspect |
极高 |
这里我想重点聊聊第一条:硬编码。很多人觉得用 .gitignore 就万事大吉了。其实不然,一旦密钥提交进了 Git 历史,哪怕后来删除或添加 .gitignore,攻击者依然可以直接从 commit 历史里翻出来。我记得有一次帮一个创业公司清理问题,他们在一年前的 commit 里泄露了生产数据库密码。那密码一直没改过。嗯,你想想看,这就等于给攻击者留了后门。
git filter-branch 清理密钥,但忘记在所有副本上执行 git push --force。结果旧 commit 还在 CI/CD 的缓存镜像里躺着。所以,清理密钥不能用“删文件”糊弄,必须走完整的凭据轮换流程。
2.3 供应链攻击
供应链攻击,这是我个人觉得最头疼的一类。为什么?因为攻击者不是直接打你的代码,而是通过你信任的依赖、基础镜像、甚至是上游的 CI/CD 管道来渗透你。这就好比在送水的车里下毒,你只检查自己的水杯,却忽略了整个水源。
常见的供应链攻击形式:
- 依赖投毒:攻击者往 npm、PyPI、Maven 等包管理器上传同名或相似名字的恶意包。只要你的
package.json或requirements.txt里不小心引用了,管线里一跑,恶意代码就执行了。我在项目中遇到过一个小公司,他们用了某个几乎没人维护的 npm 包,结果包作者被社工后发布了恶意更新——版本号只升了 0.0.1。 - 镜像篡改:很多人直接从 Docker Hub 拉
latest标签,这太危险了。去年有个叫“Docker Hub 劫持”的事件,攻击者通过泄露的 Docker Hub 更新了多个流行镜像的latest标签,塞进了挖矿软件。你每次构建都拉最新镜像,就等于把大门钥匙交给了攻击者。 - 上游后门:如果你引用的底层库(比如 openssl、libcurl)被植入了后门,那么你的整个软件供应链都会受牵连。这不是危言耸听——2024 年初的 xz 后门事件(CVE-2024-3094)就是最典型的例子:一个维护者潜伏了两年,逐渐获得信任,然后在压缩包中植入了 SSH 后门。你的 CI/CD 里跑的构建脚本,可能在不知不觉中就把后门编译进了你的生产服务。
- 锁死依赖版本:不用
^或~,直接用package-lock.json或yarn.lock。 - 镜像要用 digest(
sha256:xxxx)拉取,别用latest。 - 在管线里集成 SBOM(软件物料清单)扫描,每次构建都自动对比已知漏洞库。
2.4 避坑指南与防御思路
说了这么多威胁,你可能会问:那怎么办?我习惯用一个“三不”原则来指导团队:
- 不信任:对所有外部输入做校验,包括 Webhook 负载、PR 提交的 yaml 文件、环境变量。永远不要假设用户不会发恶意数据。
- 不硬编码:所有密钥、凭证、 都走专用的密钥管理服务。GitHub Actions 里用
secrets,GitLab CI 里用CI/CD Variables并勾选 Masked 和 Protected。 - 不默认:不要默认使用
latest镜像,不要默认开放所有权限,不要默认相信上游依赖。所有默认配置都是留给攻击者的入口。
最后,我想分享一个我曾在团队中推行过的“应急检查清单”:
| 检查项 | 操作 | 频率 |
|---|---|---|
| Git 历史扫描 | 使用 truffleHog 或 GitLeaks 全量扫描仓库历史 |
每次合并请求前 |
| 依赖漏洞扫描 | 集成 npm audit、pip audit、Trivy 等工具 |
每次构建 |
| 密钥轮换 | 对 CI/CD 中用到的所有生产密钥执行强制轮换 | 每 90 天 |
| 权限最小化 | 检查 Runner 的 IAM 角色、GitHub 的作用域 | 每季度 |
| Webhook 签名校验 | 所有 Webhook 必须配置 secret 并校验 HMAC | 一次性配置 + 持续审计 |
讲到这里,你会发现其实大部分攻击都不是多高深的手法——它们往往就是利用了“我以为这样没事”的侥幸心理。作为一个工程师,我觉得最该做的不是追求银弹工具,而是先建立起对管线全链路的敬畏心。把你的管线当作一个公开暴露的服务来对待,安全策略自然就有了方向。