GitLab CI/CD 安全加固实战:流水线供应链防护指南

适用场景

GitLab CI/CD 安全加固适用于以下场景:团队使用 GitLab 承载代码托管与流水线构建,需要防止密钥泄露与恶意代码注入;企业内部正在建设 DevSecOps 流程,要求流水线内置安全扫描;以及发生过依赖投毒、Runner 滥用或构建产物被篡改等供应链风险的复盘整改。流水线一旦被攻破,攻击者可窃取生产凭据、篡改构建产物,危害远超单个应用漏洞,因此必须将安全能力内建到流水线全生命周期。

前置条件

  • GitLab CE/EE 15.0 及以上版本(安全功能在 EE 或 CE + 安全模板中可用)
  • 已注册可用的 GitLab Runner,具备管理项目 Settings 的权限
  • 梳理清楚流水线使用的全部敏感变量与镜像来源

原理说明

GitLab CI/CD 流水线的攻击面主要包括四类:凭据泄露(CI 变量、SSH Key 被非授权作业读取)、作业权限失控(任意 MR 分支触发高权限作业)、依赖投毒(恶意 npm/PyPI 包进入构建)、镜像篡改(基础镜像被替换为带后门版本)。GitLab 提供了对应的原生能力:受保护变量仅对受保护分支/标签的作业可见,受保护 Runner 只处理受保护分支任务,rules 与作业上下文可限制触发条件,内置的 Secret Detection、Dependency Scanning 可在流水线中自动扫描,配合镜像固定 digest 与 cosign 签名校验形成完整闭环。

操作步骤

1. 配置受保护变量

# Settings → CI/CD → Variables 中新增变量时勾选:
#  - Protected:仅受保护分支/标签的作业可读取
#  - Masked:日志中自动脱敏(变量值需满足掩码规则)
#  - Environment scope:限定到生产等指定环境

# .gitlab-ci.yml 中使用变量(示例)
deploy_prod:
  stage: deploy
  script:
    - echo "$PROD_DEPLOY_KEY" | base64 -d > /tmp/key   # 变量在作业中引用
  rules:
    - if: $CI_COMMIT_TAG =~ /^v/                       # 仅打标签触发

2. Runner 隔离与标签限制

# 专用 Runner:为生产构建注册独立 Runner,配置 tag 与最大并发
gitlab-runner register   --url https://gitlab.example.com   --token <PROJECT_RUNNER_TOKEN>   --executor docker   --docker-image alpine:latest   --tag-list prod-builder   --run-untagged=false

# 项目中关闭共享 Runner 的随意使用(仅允许打标作业使用)
# 流水线作业显式指定 tag:
build:
  tags: [prod-builder]
  script: make build

3. 作业最小权限与触发限制

# 用 rules 限制高危作业仅受保护分支/标签触发
security_review:
  stage: test
  script: ./run-security-checks.sh
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"   # 仅 MR 触发
    - when: never

# 禁用浅克隆放大权限(GIT_STRATEGY)并限制并发
variables:
  GIT_STRATEGY: clone
  GIT_DEPTH: 50

4. 内置安全扫描:Secret Detection 与 Dependency Scanning

# .gitlab-ci.yml 引入官方安全模板(GitLab 15+)
include:
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/SAST.gitlab-ci.yml

# 扫描结果在 MR 的 Security 标签页展示,阻断高危漏洞合并
# 可在 Settings → Merge requests 中开启“安全告警阻断合并”

5. 镜像固定与签名校验

# 固定镜像版本与 digest,禁止使用 latest 可变标签
build:
  image: node:20.15.0@sha256:9b0c9c8b2f0a1c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d
  script: npm ci
# 用 cosign 校验基础镜像签名(流水线步骤示例)
cosign verify --key /keys/cosign.pub   registry.example.com/base/nginx:1.27@sha256:...   && echo "镜像签名校验通过"

配置验证

# 1. 校验 .gitlab-ci.yml 语法
curl --header "PRIVATE-TOKEN: <token>" "https://gitlab.example.com/api/v4/projects/<id>/ci/lint" \
     --data-urlencode "content=$(cat .gitlab-ci.yml)"

# 2. 触发一条流水线,检查 Variables 中受保护变量是否按预期注入
# 3. 查看 Security 标签页,确认 Secret Detection / Dependency Scanning 报告生成
# 4. 故意在脚本中 echo 变量值,验证 Masked 变量日志中显示 [MASKED]

验证要点:受保护变量在非受保护分支作业中不可见;未打标作业无法调度到专用 Runner;MR 中高危漏洞/密钥能阻断合并;镜像使用固定 digest 且签名校验通过。

常见问题

Q1:受保护变量在 MR 流水线中取不到值,构建失败怎么办?

这是预期行为:受保护变量只在受保护分支(如 main、发布标签)触发的流水线中注入。MR 流水线属于分支合并前验证,不应访问生产密钥。正确做法是:将生产部署作业的触发条件限定为 $CI_COMMIT_BRANCH == "main" 或打标签事件,MR 阶段仅做测试与扫描,凭据只在最终部署阶段使用。

Q2:Secret Detection 扫描范围有限,漏掉历史提交中的密钥怎么办?

默认 Secret Detection 只扫描默认分支的新增提交。对存量仓库,应手动对全历史执行一次全量扫描:在流水线中设置 SECRET_DETECTION_HISTORIC_SCAN: "true" 并切到历史分支跑一次,或使用 gitleaks 全仓库扫描。发现历史密钥后立即轮换该密钥,并将扫描纳入日常 MR 流程防止新增泄露。

总结

GitLab CI/CD 安全加固的核心是把安全左移到流水线:凭据最小可见(受保护变量+掩码)、执行环境隔离(受保护 Runner+标签)、权限按需授予(rules 限制触发)、风险自动发现(Secret Detection/SAST/依赖扫描)、供应链可信(镜像 digest+签名)。建议将安全模板纳入项目组统一模板库,新项目默认继承,并在合并请求环节开启安全阻断,形成“不安全不合并”的流水线纪律。