2. CI/CD 管道安全威胁:常见攻击面、密钥泄露风险、供应链攻击

聊到 CI/CD 安全,我见过不少团队把精力全花在写代码上,管线那边基本是裸奔状态。一旦管线被攻破,你的代码仓库、生产环境、客户数据,全都有可能暴露。所以,我们得先搞清楚敌人长什么样。这一讲,我就把 CI/CD 管道里最常见的几类威胁拆开,和大家讲讲我踩过的坑,以及那些容易忽视的细节。

CI/CD 管道安全威胁体系 CI/CD Pipeline 常见攻击面 SCM 入侵 Runner 接管 Webhook 欺骗 密钥泄露风险 硬编码 日志泄露 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。
一个小技巧:SCM 的 Webhook 密钥一定要校验。GitLab 和 GitHub 都支持 secret 验证。我曾见过某团队直接用空字符串验证……那跟没设差不多。

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 里跑的构建脚本,可能在不知不觉中就把后门编译进了你的生产服务。
给个实在的建议:
  1. 锁死依赖版本:不用 ^ 或 ~,直接用 package-lock.json 或 yarn.lock。
  2. 镜像要用 digest(sha256:xxxx)拉取,别用 latest。
  3. 在管线里集成 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 一次性配置 + 持续审计

讲到这里,你会发现其实大部分攻击都不是多高深的手法——它们往往就是利用了“我以为这样没事”的侥幸心理。作为一个工程师,我觉得最该做的不是追求银弹工具,而是先建立起对管线全链路的敬畏心。把你的管线当作一个公开暴露的服务来对待,安全策略自然就有了方向。