Docker 可以将应用程序及其依赖项打包到一个虚拟容器中,该容器可以在任何 Linux、Windows 或 macOS 计算机上运行。这使应用程序可以在各种位置运行,例如本地、公共(请参阅分散计算、分布式计算和云计算)或私有云。在 Linux 上运行时,Docker 使用Linux 内核的资源隔离特性(例如cgroups和内核命名空间)和支持联合的文件系统(例如OverlayFS)允许容器在单个 Linux 实例中运行,避免启动和维护虚拟机的开销。macOS上的 Docker使用 Linux虚拟机来运行容器。由于 Docker 容器是轻量级的,单个服务器或虚拟机可以同时运行多个容器。 2018 年的一项分析发现,典型的 Docker 用例涉及每台主机运行 8 个容器,四分之一的分析组织每台主机运行 18 个或更多容器。它也可以安装在像Raspberry Pi这样的单板计算机上。Linux 内核对名称空间的支持主要隔离了应用程序对操作环境的看法,包括进程树、网络、用户 ID 和已安装的文件系统,而内核的 cgroups 为内存和 CPU 提供资源限制。从 0.9 版开始,Docker 除了通过libvirt、LXC和systemd-nspawn使用抽象的虚拟化接口外,还包括自己的组件(称为“ libcontainer ”)以使用 Linux 内核直接提供的虚拟化设施。Docker 实现了一个高级API来提供独立运行进程的轻量级容器。
定期检查pipeline中的漏洞非常重要。要执行的步骤之一是对 Docker 映像执行漏洞扫描。在本文中,您将了解如何执行漏洞扫描、如何修复漏洞以及如何将其添加到您的 Jenkins pipeline中。
一、简介
首先,为什么要检查漏洞?您必须及时了解最新的安全修复程序。许多安全漏洞是公开的所以很容易被利用。因此,必须尽快修复安全漏洞,以最大程度地减少攻击面,但是我们平时主要专注于业务,不想从事修复安全漏洞的全职工作。这就是为什么自动扫描应用程序和 Docker 镜像很重要。Grype 可以帮助扫描您的 Docker 镜像,Grype 将检查操作系统漏洞以及特定语言包(例如 Java JAR 文件)中的漏洞并报告它们。这样,您就有了一个很棒的工具,可以为您自动执行安全检查。请注意,Grype 不仅限于扫描 Docker 镜像。它还可以扫描文件和目录,因此可用于扫描您的源代码。
在本文中,我们将创建一个包含 Spring Boot 应用程序的易受攻击的 Docker 映像,安装并使用 Grype 以扫描Docker镜像并修复漏洞。最后,我将介绍如何将扫描添加到您的 Jenkins pipeline中。
二、先决条件
三、易受攻击的应用程序
找到Spring Initializr并选择 Maven 构建、Java 17、Spring Boot 2.7.6 和 Spring Web 依赖项。这不会是一个非常脆弱的应用程序,因为 Spring 已经确保您使用最新的 Spring Boot 版本。因此,将 Spring Boot 版本更改为 2.7.0。可以使用以下命令构建 Spring Boot 应用程序,该命令将为您创建 jar 文件:
$ mvn clean verify
我们接着扫描一个 Docker 镜像,因此需要创建一个 Dockerfile。您将使用一个非常基本的 Dockerfile,它只包含创建镜像所需的最少指令。
FROM eclipse-temurin:17.0.1_12-jre-alpine
WORKDIR /opt/app
ARG JAR_FILE
COPY target/${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
为了构建 Docker 镜像,dockerfile-maven-plugin将使用 Spotify 的一个分支。因此,将以下片段添加到pom文件中。
<plugin>
<groupId>com.xenoamess.docker</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.25</version>
<configuration>
<repository>mydeveloperplanet/mygrypeplanet</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
使用此插件的好处是您可以轻松地重用配置。可以通过单个 Maven 命令创建 Docker 映像,如下所示:
$ mvn dockerfile:build
我们现在已经准备好开始使用 grype。
四、安装
可以通过执行以下脚本来安装 grype:
$ curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
通过执行以下命令验证安装:
$ grype version
Application: grype
Version: 0.54.0
Syft Version: v0.63.0
BuildDate: 2022-12-13T15:02:51Z
GitCommit: 93499eec7e3ce2704755e9f51457181b06b519c5
GitDescription: v0.54.0
Platform: linux/amd64
GoVersion: go1.18.8
Compiler: gc
Supported DB Schema: 5
五、扫描镜像
扫描 Docker 镜像是通过调用grype后跟上docker:来完成的,表示您要扫描来自 Docker 守护程序的镜像、镜像和标签:
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT
Application: grype
Version: 0.54.0
Syft Version: v0.63.0
Vulnerability DB [updated]
Loaded image
Parsed image
Cataloged packages [50 packages]
Scanned image [42 vulnerabilities]
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
busybox 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High
jackson-databind 2.13.3 java-archive CVE-2022-42003 High
jackson-databind 2.13.3 java-archive CVE-2022-42004 High
jackson-databind 2.13.3 2.13.4 java-archive GHSA-rgv9-q543-rqg4 High
jackson-databind 2.13.3 2.13.4.1 java-archive GHSA-jjjh-jjxp-wpff High
java 17.0.1+12 binary CVE-2022-21248 Low
java 17.0.1+12 binary CVE-2022-21277 Medium
java 17.0.1+12 binary CVE-2022-21282 Medium
java 17.0.1+12 binary CVE-2022-21283 Medium
java 17.0.1+12 binary CVE-2022-21291 Medium
java 17.0.1+12 binary CVE-2022-21293 Medium
java 17.0.1+12 binary CVE-2022-21294 Medium
java 17.0.1+12 binary CVE-2022-21296 Medium
java 17.0.1+12 binary CVE-2022-21299 Medium
java 17.0.1+12 binary CVE-2022-21305 Medium
java 17.0.1+12 binary CVE-2022-21340 Medium
java 17.0.1+12 binary CVE-2022-21341 Medium
java 17.0.1+12 binary CVE-2022-21360 Medium
java 17.0.1+12 binary CVE-2022-21365 Medium
java 17.0.1+12 binary CVE-2022-21366 Medium
libcrypto1.1 1.1.1l-r7 apk CVE-2021-4160 Medium
libcrypto1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High
libcrypto1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium
libretls 3.3.4-r2 3.3.4-r3 apk CVE-2022-0778 High
libssl1.1 1.1.1l-r7 apk CVE-2021-4160 Medium
libssl1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High
libssl1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium
snakeyaml 1.30 java-archive GHSA-mjmj-j48q-9wg2 High
snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High
snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium
snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium
snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium
snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium
snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium
spring-core 5.3.20 java-archive CVE-2016-1000027 Critical
ssl_client 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High
zlib 1.2.11-r3 1.2.12-r0 apk CVE-2018-25032 High
zlib 1.2.11-r3 1.2.12-r2 apk CVE-2022-37434 Critical
这个输出告诉我们如下信息:
当您仔细查看输出时,您会注意到并非每个漏洞都有已确认的修复。那么在这种情况下你会怎么做?Grype 提供了一个选项,以便仅显示已确认修复的漏洞。添加--only-fixed标志就可以了。
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
Vulnerability DB [no update available]
Loaded image
Parsed image
Cataloged packages [50 packages]
Scanned image [42 vulnerabilities]
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
busybox 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High
jackson-databind 2.13.3 2.13.4 java-archive GHSA-rgv9-q543-rqg4 High
jackson-databind 2.13.3 2.13.4.1 java-archive GHSA-jjjh-jjxp-wpff High
libcrypto1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High
libcrypto1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium
libretls 3.3.4-r2 3.3.4-r3 apk CVE-2022-0778 High
libssl1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High
libssl1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium
snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High
snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium
snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium
snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium
snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium
snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium
ssl_client 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High
zlib 1.2.11-r3 1.2.12-r0 apk CVE-2018-25032 High
zlib 1.2.11-r3 1.2.12-r2 apk CVE-2022-37434 Critical
六、修复漏洞
在这种情况下,修复漏洞非常容易。首先,您需要更新 Docker 基础映像。更改 Docker 镜像中的第一行:
FROM eclipse-temurin:17.0.1_12-jre-alpine
进入:
FROM eclipse-temurin:17.0.5_8-jre-alpine
构建镜像并再次运行扫描:
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
Vulnerability DB [no update available]
Loaded image
Parsed image
Cataloged packages [62 packages]
Scanned image [14 vulnerabilities]
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
jackson-databind 2.13.3 2.13.4 java-archive GHSA-rgv9-q543-rqg4 High
jackson-databind 2.13.3 2.13.4.1 java-archive GHSA-jjjh-jjxp-wpff High
snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High
snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium
snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium
snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium
snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium
snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium
正如您在输出中看到的,只有java-archive漏洞仍然存在。其他漏洞已经解决。
接下来修复Spring Boot的依赖漏洞。在 POM 中将 Spring Boot 的版本从 2.7.0 更改为 2.7.6。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
构建 JAR 文件,构建 Docker 镜像,然后再次运行扫描:
$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
Vulnerability DB [no update available]
Loaded image
Parsed image
Cataloged packages [62 packages]
Scanned image [10 vulnerabilities]
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High
snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium
snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium
snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium
snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium
snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium
因此,您修复了jackson-databind漏洞,但没有修复snakeyaml漏洞。那么,在哪个依赖项中snakeyaml 1.30使用了?你可以通过dependency:treeMaven命令找到。为简洁起见,此处仅显示部分输出:
$ mvnd dependency:tree
...
com.mydeveloperplanet:mygrypeplanet:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.6:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.7.6:compile
[INFO] || +- org.springframework.boot:spring-boot:jar:2.7.6:compile
[INFO] || +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.6:compile
[INFO] || +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.6:compile
[INFO] || | +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] || || \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] || | \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.30:compile
...
输出信息告诉我们依赖是spring-boot-starter-web依赖的一部分。那么,你如何解决这个问题?严格来说是Spring需要解决的。但如果你不想等待解决方案,你可以自己解决。
解决方案 1:您不需要依赖项。这是最简单的修复方法,风险很低。spring-boot-starter-web只需从pom.xml 中的依赖项中排除依赖项即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
构建 JAR 文件,构建 Docker 镜像,然后再次运行扫描:
$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
Vulnerability DB [no update available]
Loaded image
Parsed image
Cataloged packages [61 packages]
Scanned image [3 vulnerabilities]
No vulnerabilities found
解决方案 2:您确实需要依赖项。您可以通过dependencyManagement在 pom.xml 中替换此传递依赖项。这有点棘手,因为更新的传递依赖没有使用spring-boot-starter-web依赖进行测试。是否要这样做是一种权衡。将以下部分添加到 pom:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.32</version>
</dependency>
</dependencies>
</dependencyManagement>
构建 jar 文件,构建 Docker 镜像,然后再次运行扫描:
$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
Vulnerability DB [no update available]
Loaded image
Parsed image
Cataloged packages [62 packages]
Scanned image [3 vulnerabilities]
No vulnerabilities found
同样,不再存在漏洞。
解决方案 3:当您不想做任何事情或不管是否是误报通知时,可以使用本解决方案。创建一个.grype.yaml文件,您可以在其中排除高严重性的漏洞,并使用标志执行扫描,--config后跟.grype.yaml包含排除项的文件。
该.grype.yaml文件如下所示:
ignore:
- vulnerability: GHSA-3mc7-4q67-w48m
再次运行扫描:
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
Vulnerability DB [no update available]
Loaded image
Parsed image
Cataloged packages [62 packages]
Scanned image [10 vulnerabilities]
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium
snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium
snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium
snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium
snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium
该High漏洞不再显示。
七、持续集成
现在您知道如何手动扫描 Docker 镜像了。但是,您可能希望将扫描镜像作为持续集成pipeline的一部分。本节给出使用Jenkins作为CI平台时的解决方案。
要回答的第一个问题是当发现漏洞时如何进行通知。到目前为止,您只能通过查看标准输出来注意到漏洞。这不是 CI pipeline的解决方案。你想得到通知,这可以通过构建失败来完成。Grype 有用于此目的的标志--fail-on <severity>。当发现具有严重性的negligible漏洞时,您可能不希望pipeline失败了。
让我们看看当手动执行此操作时会发生什么。首先再次介绍一下Spring Boot应用和Docker镜像中的漏洞。
构建 JAR 文件,构建 Docker 镜像并使用--fail-on标志运行扫描:
$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed --fail-on high
...
1 error occurred:
* discovered vulnerabilities at or above the severity threshold
此处并未显示所有输出,仅显示重要部分。而且,如您所见,在输出的末尾,会显示一条消息,表明扫描产生了错误。这将导致 Jenkins pipeline失败,并通知开发人员出现问题。
为了将其添加到 Jenkins pipeline中,存在多种选择。这里选择创建 Docker 镜像并从 Maven 中执行 grype Docker 扫描。grype 没有单独的 Maven 插件,但可以使用exec-maven-plugin 间接实现此目的。将以下内容添加到POM 的构建插件部分。
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<executable>grype</executable>
<arguments>
<argument>docker:mydeveloperplanet/mygrypeplanet:${project.version}</argument>
<argument>--scope</argument>
<argument>all-layers</argument>
<argument>--fail-on</argument>
<argument>high</argument>
<argument>--only-fixed</argument>
<argument>-q</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
这里添加了两个额外的标志:
可以使用以下命令进行调用:
$ mvnd exec:exec
您可以将其添加到withMaven wrapper内的 Jenkinsfile 中:
withMaven() {
sh 'mvn dockerfile:build dockerfile:push exec:exec'
}
八、结论
在本文中,介绍了如何使用 grype 扫描 Docker 镜像。Grype 有一些有趣的、用户友好的特性,可以让你有效地将它们添加到你的 Jenkins pipeline中。此外,安装 grype 非常简单。Grype 绝对是对 Anchor Engine 的巨大改进。