コンテナイメージのセキュリティチェック＆脆弱性診断を簡易的に実施する

どうも、若松です。

最近、コンテナイメージをビルドしまくっており、オレオレなコンテナイメージが増殖しています。
そんなおり、以下の記事が目に止まりました。

[DockerHubで公開されているコンテナが安全か確かめてみた結果【人気のコンテナ上位800個】](https://qiita.com/tomoyamachi/items/e0e69da521505e73237b)

そりゃあ脆弱性あるわなぁと思いつつ、じゃあオレのコンテナイメージは大丈夫か？と思いました。
というわけで上記の記事で使用されていた`dockle`と`Trivy`を使って、オレオレコンテナイメージをチェックしていきたいと思います。

# 使用ツール
## dockle
CISベンチマークやDockerベストプラクティスに準拠したコンテナイメージかをチェックできます。
WARNやFATALの項目には、どのように改善すればよいかのヒントが添えられていて小憎いです

それぞれの基準は以下の通り
[CISベンチマーク](https://www.cisecurity.org/cis-benchmarks/)
[Dockerベストプラクティス](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)

## Trivy
OSやアプリケーションに潜む脆弱性をチェックできます。
CVE番号と共に脆弱性の詳細を表示してくれるので、どのように改善するかの計画が立てやすいです。

対象OSとアプリケーションは以下の通り
https://github.com/knqyf263/trivy#vulnerability-detection

# インストール
今回はMacのローカルにコンテナイメージがあるため、`brew`でインストールしました。
手順はそれぞれのREADMEに記載があるため、省略します。

- dockle
https://github.com/goodwithtech/dockle

- Trivy
https://github.com/knqyf263/trivy

# 実行結果
前回の記事で作成したLaravelコンテナを対象にしました。
https://qiita.com/t_wkm2/items/910b4d7079b8413895a6
## dockle
```bash
$ dockle laravel:latest
WARN	- CIS-DI-0001: Create a user for the container
	* Last user should not be root
PASS	- CIS-DI-0005: Enable Content trust for Docker
WARN	- CIS-DI-0006: Add HEALTHCHECK instruction to the container image
	* not found HEALTHCHECK statement
PASS	- CIS-DI-0007: Do not use update instructions alone in the Dockerfile
PASS	- CIS-DI-0008: Remove setuid and setgid permissions in the images
PASS	- CIS-DI-0009: Use COPY instead of ADD in Dockerfile
PASS	- CIS-DI-0010: Do not store secrets in ENVIRONMENT variables
PASS	- CIS-DI-0010: Do not store secret files
PASS	- DKL-DI-0001: Avoid sudo command
PASS	- DKL-DI-0002: Avoid sensitive directory mounting
PASS	- DKL-DI-0003: Avoid apt-get/apk/dist-upgrade
PASS	- DKL-DI-0004: Use apk add with --no-cache
PASS	- DKL-DI-0005: Clear apt-get caches
WARN	- DKL-DI-0006: Avoid latest tag
	* Avoid 'latest' tag
PASS	- DKL-LI-0001: Avoid empty password
PASS	- DKL-LI-0002: Be unique UID
PASS	- DKL-LI-0002: Be unique GROUP
```
大きな問題はありませんでしたが、WARNが3件出て、改善に向けたアドバイスが表示されていることがわかります。

## Trivy
```bash
$ trivy laravel:latest
2019-07-16T00:26:44.376+0900	INFO	Updating vulnerability database...
2019-07-16T00:26:46.203+0900	WARN	You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed
2019-07-16T00:26:48.159+0900	INFO	Detecting Alpine vulnerabilities...
2019-07-16T00:26:48.171+0900	INFO	Updating composer Security DB...
2019-07-16T00:26:51.928+0900	INFO	Detecting composer vulnerabilities...
2019-07-16T00:26:51.928+0900	INFO	Updating composer Security DB...
2019-07-16T00:26:53.211+0900	INFO	Detecting composer vulnerabilities...
2019-07-16T00:26:53.214+0900	INFO	Updating composer Security DB...
2019-07-16T00:26:54.422+0900	INFO	Detecting composer vulnerabilities...
2019-07-16T00:26:54.422+0900	INFO	Updating composer Security DB...
2019-07-16T00:26:56.052+0900	INFO	Detecting composer vulnerabilities...

var/www/laravel/vendor/psy/psysh/vendor-bin/box/composer.lock
=============================================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)


var/www/laravel/vendor/hamcrest/hamcrest-php/composer.lock
==========================================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)


laravel:latest (alpine 3.10.1)
==============================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)


var/www/laravel/vendor/phar-io/manifest/composer.lock
=====================================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)


var/www/laravel/composer.lock
=============================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
```
脆弱性は検出されませんでしたが、AlpineLinuxに加えてComposerも対象に診断を行ってくれることがわかります。

# 引っかかる場合の表示
上記でチェックしたコンテナイメージは最新のベースイメージであったため、引っかかる項目が少ない結果となりました。
しかしながら、これではチェックに引っかかった場合の表示がわからないため、ずいぶん前(`docker images`で見ると9ヶ月前)にpullして手元にあった`httpd:2.4-alpine`をチェックしてみます。
## dockle
```bash
$ dockle httpd:2.4-alpine
WARN	- CIS-DI-0001: Create a user for the container
	* Last user should not be root
PASS	- CIS-DI-0005: Enable Content trust for Docker
WARN	- CIS-DI-0006: Add HEALTHCHECK instruction to the container image
	* not found HEALTHCHECK statement
PASS	- CIS-DI-0007: Do not use update instructions alone in the Dockerfile
PASS	- CIS-DI-0008: Remove setuid and setgid permissions in the images
PASS	- CIS-DI-0009: Use COPY instead of ADD in Dockerfile
PASS	- CIS-DI-0010: Do not store secrets in ENVIRONMENT variables
PASS	- CIS-DI-0010: Do not store secret files
PASS	- DKL-DI-0001: Avoid sudo command
PASS	- DKL-DI-0002: Avoid sensitive directory mounting
PASS	- DKL-DI-0003: Avoid apt-get/apk/dist-upgrade
FATAL	- DKL-DI-0004: Use apk add with --no-cache
	* Use --no-cache option if use 'apk add': /bin/sh -c set -eux; 	runDeps=' 		apr-dev 		apr-util-dev 		apr-util-ldap 		perl 	'; 	apk add --no-cache --virtual .build-deps 		$runDeps 		ca-certificates 	coreutils 		dpkg-dev dpkg 		gcc 		gnupg 	libc-dev 		libressl 		libressl-dev 		libxml2-dev 		lua-dev 		make 		nghttp2-dev 		pcre-dev 		tar 		zlib-dev 	; 		ddist() { 		local f="$1"; shift; 		local distFile="$1"; shift; 		local success=; 		local distUrl=; 		for distUrl in $APACHE_DIST_URLS; do 		if wget -O "$f" "$distUrl$distFile" && [ -s "$f" ]; then 		success=1; 				break; 			fi; 	done; 		[ -n "$success" ]; 	}; 		ddist 'httpd.tar.bz2' "httpd/httpd-$HTTPD_VERSION.tar.bz2"; 	echo "$HTTPD_SHA256 *httpd.tar.bz2" | sha256sum -c -; 		ddist 'httpd.tar.bz2.asc' "httpd/httpd-$HTTPD_VERSION.tar.bz2.asc"; 	export GNUPGHOME="$(mktemp -d)"; for key in 		A93D62ECC3C8EA12DB220EC934EA76E6791485A8 	B9E8213AEFB861AF35A41F2C995E35221AD84DFF 	; do 		gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; 	done; 	gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2; 	command -v gpgconf && gpgconf --kill all || :; 	rm -rf "$GNUPGHOME" httpd.tar.bz2.asc; 		mkdir -p src; 	tar -xf httpd.tar.bz2 -C src --strip-components=1; 	rm httpd.tar.bz2; 	cd src; 		patches() { 		while [ "$#" -gt 0 ]; do 			local patchFile="$1"; shift; 			local patchSha256="$1"; shift; 			ddist "$patchFile" "httpd/patches/apply_to_$HTTPD_VERSION/$patchFile"; 	echo "$patchSha256 *$patchFile" | sha256sum -c -; 			patch -p0 < "$patchFile"; 			rm -f "$patchFile"; 	done; 	}; 	patches $HTTPD_PATCHES; 		gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; 	./configure 		--build="$gnuArch" 		--prefix="$HTTPD_PREFIX" 		--enable-mods-shared=reallyall 		--enable-mpms-shared=all 	; 	make -j "$(nproc)"; 	make install; 		cd ..; 	rm -r src man manual; 		sed -ri 		-e 's!^(\s*CustomLog)\s+\S+!\1 /proc/self/fd/1!g' 		-e 's!^(\s*ErrorLog)\s+\S+!\1 /proc/self/fd/2!g' 		"$HTTPD_PREFIX/conf/httpd.conf"; 	runDeps="$runDeps $( 		scanelf --needed --nobanner --format '%n#p' --recursive /usr/local 			| tr ',' '\n' 		| sort -u 			| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' 	)"; 	apk add --virtual .httpd-rundeps $runDeps; 	apk del .build-deps
PASS	- DKL-DI-0005: Clear apt-get caches
PASS	- DKL-DI-0006: Avoid latest tag
FATAL	- DKL-LI-0001: Avoid empty password
	* No password user found! username : root
PASS	- DKL-LI-0002: Be unique UID
PASS	- DKL-LI-0002: Be unique GROUP
```
FATALが出ているのが確認できます。
最新のAlpineLinuxでは出ないものなので、定期的にチェックは必要だなと思います。

## Trivy
```bash
$ trivy httpd:2.4-alpine
2019-07-16T00:48:09.514+0900	INFO	Updating vulnerability database...
2019-07-16T00:48:13.096+0900	INFO	Detecting Alpine vulnerabilities...

httpd:2.4-alpine (alpine 3.7.1)
===============================
Total: 13 (UNKNOWN: 0, LOW: 2, MEDIUM: 5, HIGH: 5, CRITICAL: 1)

+------------+------------------+----------+-------------------+---------------+--------------------------------+
|  LIBRARY   | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION |             TITLE              |
+------------+------------------+----------+-------------------+---------------+--------------------------------+
| bzip2      | CVE-2019-12900   | HIGH     | 1.0.6-r6          | 1.0.6-r7      | bzip2: out-of-bounds write in  |
|            |                  |          |                   |               | function BZ2_decompress        |
+------------+------------------+          +-------------------+---------------+--------------------------------+
| expat      | CVE-2018-20843   |          | 2.2.5-r0          | 2.2.7-r0      | expat: large number of colons  |
|            |                  |          |                   |               | in input makes parser consume  |
|            |                  |          |                   |               | high amount...                 |
+------------+------------------+----------+-------------------+---------------+--------------------------------+
| libxml2    | CVE-2018-14404   | MEDIUM   | 2.9.7-r0          | 2.9.8-r1      | libxml2: NULL pointer          |
|            |                  |          |                   |               | dereference in                 |
|            |                  |          |                   |               | xpath.c:xmlXPathCompOpEval()   |
|            |                  |          |                   |               | can allow attackers to cause   |
|            |                  |          |                   |               | a...                           |
+            +------------------+          +                   +               +--------------------------------+
|            | CVE-2018-14567   |          |                   |               | libxml2: Infinite loop when    |
|            |                  |          |                   |               | --with-lzma is used allows for |
|            |                  |          |                   |               | denial of service...           |
+            +------------------+----------+                   +               +--------------------------------+
|            | CVE-2018-9251    | LOW      |                   |               | libxml2: infinite loop in      |
|            |                  |          |                   |               | xz_decomp function in xzlib.c  |
+------------+------------------+----------+-------------------+---------------+--------------------------------+
| perl       | CVE-2018-18311   | HIGH     | 5.26.2-r1         | 5.26.3-r0     | perl: Integer overflow         |
|            |                  |          |                   |               | leading to buffer overflow in  |
|            |                  |          |                   |               | Perl_my_setenv()               |
+            +------------------+          +                   +               +--------------------------------+
|            | CVE-2018-18314   |          |                   |               | perl: Heap-based buffer        |
|            |                  |          |                   |               | overflow in S_regatom()        |
+            +------------------+          +                   +               +--------------------------------+
|            | CVE-2018-18312   |          |                   |               | perl: Heap-based               |
|            |                  |          |                   |               | buffer overflow in             |
|            |                  |          |                   |               | S_handle_regex_sets()          |
+            +------------------+----------+                   +               +--------------------------------+
|            | CVE-2018-18313   | MEDIUM   |                   |               | perl: Heap-based buffer read   |
|            |                  |          |                   |               | overflow in S_grok_bslash_N()  |
+------------+------------------+----------+-------------------+---------------+--------------------------------+
| postgresql | CVE-2019-10164   | CRITICAL | 10.5-r0           | 10.9-r0       | PostgreSQL: stack-based        |
|            |                  |          |                   |               | buffer overflow via setting a  |
|            |                  |          |                   |               | password                       |
+            +------------------+----------+                   +---------------+--------------------------------+
|            | CVE-2019-10129   | MEDIUM   |                   | 10.8-r0       | postgresql: Memory disclosure  |
|            |                  |          |                   |               | in partition routing           |
+            +------------------+----------+                   +               +--------------------------------+
|            | CVE-2019-10130   | LOW      |                   |               | postgresql: Selectivity        |
|            |                  |          |                   |               | estimators bypass row security |
|            |                  |          |                   |               | policies                       |
+------------+------------------+----------+-------------------+---------------+--------------------------------+
| sqlite     | CVE-2018-20346   | MEDIUM   | 3.21.0-r1         | 3.25.3-r0     | CVE-2018-20505 CVE-2018-20506  |
|            |                  |          |                   |               | sqlite: Multiple flaws in      |
|            |                  |          |                   |               | sqlite which can be triggered  |
|            |                  |          |                   |               | via...                         |
+------------+------------------+----------+-------------------+---------------+--------------------------------+
```
ライブラリ毎に表示されるので見やすいですね。
CRITICALが出ているので、ここは最低でも塞いで起きたいところです。

# まとめ
簡単にイメージスキャンができることがわかっていただけたと思います。
今回は素のままのコマンド実行でしたが、表示形式の変更やファイルへの出力などのオプションもあるので、うまく使って様々な箇所に応用していきましょう。