前阵子安全合规部门突然甩过来一份安全审计报告,指着我们好几个线上核心业务的 Docker 镜像说里面堆满了高危 CVE 漏洞。我点进去一看瞬间心肺停止:很多开发为了图省事,基础镜像直接用 node:latest 或者 ubuntu:latest。里面的软件包动辄两三年没更新,甚至还有人用 root 权限去跑 Node 应用。更有甚者,直接把数据库的配置密码用 ENV 形式写死在 Dockerfile 里,这要是镜像泄漏出去,简直就是把内网大门敞开给人进。

过去我们对 Docker 镜像的管控基本全靠人工 Review,效率低不说,漏网之鱼更是一抓一大把。把安全责任全押在开发个人的“心智负担”上是最不靠谱的。我们必须干脆利落地把镜像审计和元数据安全扫描全部塞进 CI/CD 流程里,不合规的构建直接在 Pipeline 里拉闸打回。

要达成这个目标,我们需要两件武器:一个是针对 Dockerfile 语法与规范进行静态分析的 Hadolint,另一个是专门扫描镜像层 CVE 漏洞的 Trivy

我们在 GitLab CI/CD 中实现了全自动的双重审计卡点。首先,在代码提交阶段,Hadolint 会作为第一道门槛,拒绝任何写得不规范的 Dockerfile。比如基础镜像没有指定具体版本(用了 latest)、没有清理 apt-get 缓存或者漏写了 USER 指令。第二道关卡则是等镜像构建完成后,由 Trivy 对镜像进行全量 CVE 漏洞扫描,一旦发现 CRITICAL 级别的系统漏洞,直接阻断部署。

这是我们调优出来的完整 .gitlab-ci.yml 流水线配置:

stages:
  - lint
  - build
  - scan

variables:
  IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

lint-dockerfile:
  stage: lint
  image: hadolint/hadolint:latest-debian
  script:
    - mkdir -p reports
    - hadolint --format codeclimate Dockerfile > reports/hadolint-json.json
    - hadolint Dockerfile
  artifacts:
    name: "hadolint-report"
    when: always
    paths:
      - reports/
    reports:
      codequality: reports/hadolint-json.json

build-image:
  stage: build
  image: docker:24.0.7
  services:
    - docker:24.0.7-dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker build --pull -t "$IMAGE_NAME" .
    - docker push "$IMAGE_NAME"

scan-image:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  variables:
    TRIVY_NO_PROGRESS: "true"
    TRIVY_CACHE_DIR: ".trivycache/"
  script:
    - trivy image --clear-cache
    - trivy image --download-db-only
    # 第一次扫描:生成报告供 GitLab UI 展示,但不让 Pipeline 崩溃
    - trivy image --exit-code 0 --severity HIGH,CRITICAL --format template --template "@/contrib/gitlab.tpl" --output reports/gl-container-scanning-report.json "$IMAGE_NAME"
    # 第二次扫描:如果发现 Critical(致命)级漏洞,直接返回 exit code 1 挂掉流水线
    - trivy image --exit-code 1 --severity CRITICAL "$IMAGE_NAME"
  cache:
    key: trivy-cache
    paths:
      - .trivycache/
  artifacts:
    reports:
      container_scanning: reports/gl-container-scanning-report.json

这里面有几个踩坑后总结出来的细节需要特别注意:

第一,Hadolint 我们启用了 codeclimate 格式输出,并挂载到了 GitLab 的 reports: codequality 里。这样一旦有违规写法,开发在 Merge Request 页面能直接看到高亮的具体报错行,而不需要去翻看冗长的 CI 日志。

第二,Trivy 扫描时,我们做了两次。第一次是输出 JSON 报告并挂载到 GitLab 的 container_scanning 安全仪表盘上;第二次才是核心防御,设置 --exit-code 1。只要扫出 CRITICAL 漏洞,流水线当场中断,彻底拦截了带病上线的镜像。

第三,缓存策略。Trivy 每次运行都要拉取庞大的漏洞库元数据,如果不做 TRIVY_CACHE_DIR 缓存,每次执行都要耗费数分钟。把缓存文件夹加入到 CI 缓存中后,速度能提升 5 倍以上。

自从这套卡点上线以后,我们后台构建 Dockerfile 的质量直线上升。开发在本地开发阶段,也开始自发安装 Hadolint 插件在 IDE 里做实时检查,免得代码提交后又被 CI 打回来重新折腾。

你们团队目前是如何保证容器镜像安全的?是完全依赖云厂商提供的后端扫描,还是也把安全卡点提前放到了 CI/CD 的最前端?