一、为什么要做Docker镜像漏洞扫描?
你有没有过这种经历:辛辛苦苦把代码打包成Docker镜像,推到仓库准备上线,结果突然收到安全告警——镜像里藏着高危漏洞,可能被黑客入侵。更麻烦的是,要是等到上线后才发现,修复起来不仅要重新打包、重新测试,还可能影响业务运行,甚至造成数据损失。
简单说,Docker镜像漏洞就是镜像里的某个软件有安全问题,比如某个旧版本的Linux系统组件、某个开源依赖包存在未修复的漏洞。要是带着这个镜像跑起来,黑客就能利用这个漏洞拿到服务器权限,干各种坏事。所以,提前在镜像上线前就把漏洞找出来、修好,是每个做容器化的开发者都必须做的事。
那怎么找漏洞?总不能人工一个个查吧?这时候就需要专门的工具了,比如今天要讲的Trivy,它是一款专门用来扫描容器镜像漏洞的工具,不用复杂配置,新手也能快速上手。
二、Trivy的基础使用方法
在把Trivy集成到CI管道之前,我们得先会用它单独扫描镜像,不然连工具都不会用,更别说集成了。
2.1 安装Trivy
Trivy的安装特别简单,不用搞复杂的编译,直接用包管理工具就行。比如用Ubuntu系统的话,执行下面的命令就能装:
# 添加Trivy的GPG密钥,用来验证安装包的合法性
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
# 添加Trivy的软件源
echo deb https://aquasecurity.github.io/trivy-repo/deb/ $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
# 更新软件源并安装Trivy
sudo apt-get update && sudo apt-get install trivy
要是你用的是Mac或者Windows,也有对应的安装方法,不过这里我们统一用Ubuntu的命令,方便后面的CI管道配置。
2.2 用Trivy扫描本地镜像
装完之后,我们先拿一个本地的Docker镜像试试手。比如你之前打包了一个叫my-app:v1的镜像,扫描命令就是:
# 扫描本地的my-app:v1镜像,参数--format table是用表格形式展示结果,清晰好读
trivy image --format table my-app:v1
执行完之后,Trivy会把镜像里所有的漏洞列出来,包括漏洞的级别(低、中、高、严重)、漏洞编号、影响的软件包名。比如要是有一个严重漏洞,Trivy会标红,你一眼就能看到。
这里要注意,Trivy第一次运行的时候会自动下载漏洞数据库,这个数据库是实时更新的,所以每次扫描前最好更新一下,不然可能扫不到新漏洞。更新命令很简单:
# 更新Trivy的漏洞数据库
trivy image --update-db
三、把Trivy集成到CI管道
单独用Trivy扫描只是第一步,我们要做的是让它自动跑起来,不用每次打包都手动扫。这就要用到CI管道了——CI就是持续集成,简单说就是你每次提交代码,CI会自动帮你做编译、测试、打包、扫描这些事,要是有问题就直接告诉你,不让有问题的镜像上线。
我们选一个常用的CI工具:GitHub Actions,因为很多人都用GitHub托管代码,而且GitHub Actions是免费的,配置也简单。
3.1 先搞懂GitHub Actions的基本逻辑
GitHub Actions的配置文件是一个YAML文件,放在代码仓库的.github/workflows/目录下。每次你提交代码到指定分支(比如main或者dev),或者有人给你提PR,这个配置文件里的任务就会自动跑。
一个GitHub Actions的配置文件主要分三部分:触发条件(什么时候跑)、运行环境(用什么系统跑)、具体步骤(跑什么命令)。
3.2 完整的CI配置示例
我们来写一个完整的配置,这个配置会做三件事:拉代码、打包Docker镜像、用Trivy扫描镜像,要是有严重漏洞就不让镜像推到仓库。
首先,在你的代码仓库里新建一个文件:.github/workflows/docker-scan.yml,内容如下:
# 这个CI的名字,随便取,方便你在GitHub的Actions页面识别
name: Docker Build and Scan
# 触发条件:每次有人推代码到main分支,或者给main分支提PR的时候,自动跑这个CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# 运行环境:用Ubuntu最新版本,因为我们之前装Trivy用的是Ubuntu命令
jobs:
build-and-scan:
runs-on: ubuntu-latest
# 具体步骤
steps:
# 第一步:拉代码到CI的运行环境里
- name: Checkout code
uses: actions/checkout@v4
# 第二步:打包Docker镜像,镜像名用github的仓库名加分支名,方便区分
- name: Build Docker image
run: |
# 这里的${{ github.repository }}是GitHub的内置变量,比如你的仓库叫xxx/my-app,这个变量就是xxx/my-app
docker build -t ${{ github.repository }}:${{ github.ref_name }} .
# 第三步:安装Trivy,和我们之前手动装的命令一样
- name: Install Trivy
run: |
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb/ $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy
# 第四步:用Trivy扫描镜像,这里加了个参数--severity CRITICAL,HIGH,意思是只扫严重和高危漏洞
# 要是扫到这些漏洞,Trivy会返回非0的退出码,CI就会失败,不让镜像推
- name: Scan Docker image with Trivy
run: |
trivy image --severity CRITICAL,HIGH ${{ github.repository }}:${{ github.ref_name }}
# 第五步:只有扫描通过(没有严重和高危漏洞),才把镜像推到GitHub的容器仓库
- name: Push Docker image
# 这个条件的意思是:只有当CI是推代码(不是PR)的时候才推镜像,因为PR只是测试,不用推
if: github.event_name == 'push'
# 先登录GitHub的容器仓库,用的是GitHub自动生成的令牌,不用你自己弄
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker tag ${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:${{ github.ref_name }}
docker push ghcr.io/${{ github.repository }}:${{ github.ref_name }}
这个配置看起来长,其实每一步都很简单,我们拆开来解释:
- 触发条件:只有推main分支或者给main提PR的时候才跑,避免每次改代码都浪费资源。
- 打包镜像:用当前的代码打包,镜像名带分支名,方便后续排查。
- 安装Trivy:和手动装的命令完全一样,只是放在CI里自动跑。
- 扫描镜像:只扫严重和高危漏洞,因为低危漏洞暂时不影响业务,要是扫到,CI直接失败,后面的步骤就不跑了,镜像也不会推。
- 推镜像:只有扫描通过,而且是推代码(不是PR)的时候才推,保证推上去的镜像都是安全的。
3.3 测试这个CI配置
写完这个配置之后,你把它提交到GitHub仓库,然后推到main分支。这时候你去仓库的Actions页面,就能看到这个CI任务正在跑。要是你的镜像里没有严重和高危漏洞,CI会显示成功,镜像也会推到仓库;要是有漏洞,CI会显示失败,你就能在日志里看到漏洞的详细信息。
比如要是你用的是一个旧版本的Ubuntu作为基础镜像,里面有个CVE编号是CVE-2023-23934的严重漏洞,Trivy扫描的时候就会把这个漏洞列出来,CI失败,你就能知道要换基础镜像了。
四、漏洞修复的流程
扫出漏洞之后,怎么修?总不能直接把漏洞忽略吧?我们来梳理一下完整的修复流程,分几种情况:
4.1 基础镜像漏洞的修复
很多时候,漏洞都是来自你用的基础镜像。比如你用的是ubuntu:20.04,这个基础镜像里的某个组件有漏洞,这时候最简单的修复方法就是换一个更新的基础镜像。
比如把ubuntu:20.04换成ubuntu:22.04,然后重新打包镜像,再扫一遍,大部分漏洞就没了。要是你不想换大版本,也可以在Dockerfile里加一步更新基础镜像的组件:
# 原来的Dockerfile可能是这样的
FROM ubuntu:20.04
WORKDIR /app
COPY . .
CMD ["node", "app.js"]
# 修复之后的Dockerfile,加了更新组件的步骤
FROM ubuntu:20.04
WORKDIR /app
# 更新基础镜像的所有组件,修复漏洞
RUN apt-get update && apt-get upgrade -y && apt-get clean
COPY . .
CMD ["node", "app.js"]
加了RUN apt-get update && apt-get upgrade -y && apt-get clean之后,基础镜像里的旧组件会被更新到最新的安全版本,漏洞就修复了。
4.2 应用依赖漏洞的修复
要是漏洞来自你自己的应用依赖,比如Node.js项目里的某个npm包有漏洞,这时候就要更新这个依赖包。比如你用的是lodash@4.17.19,这个版本有漏洞,你就可以把它更新到最新的安全版本:
# 更新lodash到最新版本
npm update lodash
更新之后,重新打包镜像,再扫一遍,漏洞就没了。
4.3 临时处理:忽略特定漏洞
要是你扫到一个漏洞,但是暂时没法修(比如这个漏洞的修复版本还没出来,或者更新依赖会导致业务崩溃),这时候可以临时忽略这个漏洞,但是一定要做好记录,后续一定要跟进修复。
比如你要忽略CVE-2023-23934这个漏洞,Trivy扫描的时候加个参数就行:
# 扫描的时候忽略CVE-2023-23934这个漏洞
trivy image --severity CRITICAL,HIGH --ignore-unfixed --ignore-vuln CVE-2023-23934 my-app:v1
这里的--ignore-vuln参数就是指定要忽略的漏洞编号,--ignore-unfixed是忽略还没修复的漏洞(有些漏洞虽然存在,但是厂商还没出修复版本,这时候也可以临时忽略)。
不过要注意,忽略漏洞只是临时的,一定要定期复查,等漏洞有修复版本了,及时更新,不然会有安全风险。
五、应用场景、优缺点和注意事项
5.1 应用场景
这个流程适合所有用Docker做容器化的项目,不管是个人项目还是企业项目。特别是以下几种场景:
- 上线前的安全检查:保证上线的镜像没有严重漏洞,避免被黑客入侵。
- 多人协作的项目:每次有人提交代码,自动扫描镜像,避免有人不小心用了有漏洞的基础镜像或者依赖。
- 持续部署的项目:要是你的项目是持续部署(每次代码更新就自动上线),这个流程能保证上线的镜像都是安全的。
5.2 技术优缺点
优点
- 自动化:不用每次打包都手动扫描,节省时间,也避免忘记扫描。
- 实时性:漏洞数据库实时更新,能扫到最新的漏洞。
- 轻量:Trivy本身很小,扫描速度快,不会给CI管道增加太多时间。
- 易集成:能集成到几乎所有的CI工具,比如GitHub Actions、GitLab CI、Jenkins等,不用改太多配置。
缺点
- 误报:有时候Trivy会扫到一些误报的漏洞,比如某个漏洞的影响范围和你的项目不相关,但是Trivy还是会列出来,这时候你要自己判断是不是真的有风险。
- 依赖漏洞数据库:要是漏洞数据库没更新,可能扫不到新漏洞,所以要定期更新数据库。
- 低危漏洞太多:有时候扫出来的低危漏洞很多,要是你扫所有级别的漏洞,日志会很乱,所以一般只扫严重和高危漏洞。
5.3 注意事项
- 定期更新基础镜像:不要一直用同一个基础镜像,要定期更新,避免基础镜像有漏洞。
- 不要随便忽略漏洞:只有在真的没法修的时候才忽略漏洞,而且要做好记录,定期复查。
- 扫描的时候要指定漏洞级别:不要扫所有级别的漏洞,不然日志会很乱,一般只扫严重和高危漏洞。
- 镜像推到仓库之后还要再扫一遍:有时候镜像推到仓库之后,可能会被篡改,所以推上去之后最好再扫一遍,保证安全。
六、文章总结
Docker镜像漏洞扫描和集成CI管道的流程,核心就是把安全检查自动化,不用人工干预,保证上线的镜像都是安全的。我们从Trivy的基础使用开始,到把Trivy集成到GitHub Actions的CI管道,再到漏洞修复的流程,一步步讲了怎么实现这个功能。
总结下来,整个流程的关键就是三点:选对工具(Trivy)、集成到CI(自动扫描)、及时修复漏洞(保证镜像安全)。只要把这个流程跑通,你的Docker镜像安全就能得到很大的提升,避免很多不必要的安全风险。
Comments