Docker Hardened Image (DHI) をCVEの観点で確認してみた #docker #security

はじめに
2025年5月、Docker社はデフォルトで安全なコンテナイメージであるDocker Hardened Imageを発表しました。従来からDocker HubではDocker社がメンテナンスしている「公式イメージ」がありましたが、さらにセキュリティを推し進めた形です。そしておよそ半年後の2025年12月、Docker Hardened Imageに無料プランが追加されました。
本稿では、このDocker Hardened Image (DHI)がどのようなものなのか、CVE (Common Vulnerabilities and Exposures; 共通脆弱性識別子)の観点で確かめてみます。
公式イメージの脆弱性
今回はベースイメージとして、公式イメージの python:3.14.6-slim-trixie をDHIに差し替えてみましょう。
まず、Trivyで公式イメージの脆弱性を確認しておきます。
% docker container run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \ image --severity HIGH,CRITICAL \ --scanners vuln \ python:3.14.6-slim-trixie@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 (略) Total: 20 (HIGH: 18, CRITICAL: 2) (略)
20件ありました。 python:3.14.6-slim-trixie イメージには、脆弱性を持ったパッケージが含まれていることがわかります。
CRITICALとなっている脆弱性は CVE-2026-42496 と CVE-2026-8376 でどちらも perl パッケージのものです。内容を確認したところ、前者は perl 標準ライブラリ、後者は32ビットビルド、ということで64ビットPythonアプリケーションを実行するなら関係ないはずです。それらを1つ1つ確認するのは大変です。
Docker Hardened Image (DHI)の脆弱性
DHIは「ほぼゼロのCVE」を謳っているので、もし脆弱性が検知されたとしても確認作業が大幅に減ることが期待できます。
差し替え先イメージは dhi.io/python:3.14.6-debian13 となります。これをTrivyでスキャンしてみましょう。なお、無料プランでも Docker Hub のアカウントでログインする必要があります。
% docker login dhi.io (略) % docker image pull dhi.io/python:3.14.6-debian13@sha256:c82da5a1a30a6214f45c42def5b6f5b85981c7dc7a1802015a6ebf264675436d (略) % docker container run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \ image --severity HIGH,CRITICAL \ --scanners vuln \ dhi.io/python:3.14.6-debian13@sha256:c82da5a1a30a6214f45c42def5b6f5b85981c7dc7a1802015a6ebf264675436d (略) Total: 53 (HIGH: 52, CRITICAL: 1) (略)
なんと、脆弱性の数が公式イメージより倍以上に増えてしまいました…。これは一体どういうことなのでしょうか?
SBOMの比較
Trivy には SBOM (Software Bill of Materials; ソフトウェア部品表) を出力する機能があります。これを利用して、両イメージが何を持っているのかを確認してみましょう。
% docker container run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \ image --format cyclonedx \ python:3.14.6-slim-trixie@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 > /tmp/python.json % docker container run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \ image --format cyclonedx \ dhi.io/python:3.14.6-debian13@sha256:c82da5a1a30a6214f45c42def5b6f5b85981c7dc7a1802015a6ebf264675436d > /tmp/dhi.json
両者の差分を取ります。
% diff -u <(cat /tmp/python.json | jq '.components[].purl' | sed -e 's/@.*//' -e 's!"pkg:deb/debian/!!') <(cat /tmp/dhi.json | jq '.components[].purl' | sed -e 's/@.*//' -e 's!"pkg:deb/debian/!!') --- /proc/self/fd/17 2026-07-14 18:34:39.823998223 +0900 +++ /proc/self/fd/20 2026-07-14 18:34:39.823998223 +0900 @@ -1,89 +1,51 @@ null -adduser -apt base-files -base-passwd -bash -bsdutils ca-certificates -coreutils -dash debconf -debian-archive-keyring -debianutils -diffutils -dpkg -findutils gcc-14-base -grep -gzip -hostname -init-system-helpers -libacl1 -libapt-pkg7.0 -libattr1 -libaudit-common -libaudit1 -libblkid1 -libbsd0 libbz2-1.0 -libc-bin +libc-dev-bin +libc6-dev libc6 -libcap-ng0 -libcap2 +libcrypt-dev libcrypt1 libdb5.3t64 -libdebconfclient0 +libexpat1-dev +libexpat1 libffi8 libgcc-s1 +libgdbm-compat4t64 libgdbm6t64 -libgmp10 -libhogweed6t64 -liblastlog2-2 -liblz4-1 liblzma5 -libmd0 -libmount1 libncursesw6 -libnettle8t64 -libpam-modules-bin -libpam-modules -libpam-runtime -libpam0g -libpcre2-8-0 +libpython-3.14-dev +libpython-3.14-minimal +libpython-3.14-stdlib +libpython-3.14 libreadline8t64 -libseccomp2 -libselinux1 -libsemanage-common -libsemanage2 -libsepol2 -libsmartcols1 libsqlite3-0 libssl3t64 libstdc%2B%2B6 -libsystemd0 libtinfo6 -libudev1 libuuid1 -libxxhash0 libzstd1 -login.defs -login -mawk -mount +linux-libc-dev ncurses-base ncurses-bin netbase openssl-provider-legacy openssl -passwd -perl-base +python-3.14-dev +python-3.14-minimal +python-3.14 +python3-pip-whl +python3-pip +python3-setuptools-whl +python3-setuptools +python3-wheel-whl +python3-wheel readline-common -sed -sqv -sysvinit-utils -tar +rpcsvc-proto tzdata -util-linux +zlib1g-dev zlib1g -"pkg:pypi/pip
これを見ると、DHIからは、シェルや基本的なコマンドが削除されていることがわかります。そのため、万が一コンテナに侵入されても自由な行動が困難であるとわかります。
一方で libc6-dev パッケージや linux-libc-dev パッケージといった開発用パッケージが多く追加されていることがわかります。おそらくこの dhi.io/python:3.14.6-debian13 イメージのみでPythonライブラリをビルドできるようにしているのでしょう。しかしイメージビルドのベストプラクティスは、開発用パッケージをビルドステージにとどめておき、成果物のみを含んだファイナルステージを構成するマルチステージビルドのはずです。ファイナルステージのベースイメージに使いたい「Hardened」イメージがこれというのはいかがなものかという気がします。
実はこの linux-libc-dev パッケージが1つで30以上のCVEを増やしているのですが、dhi.io/python:3.14.6-debian13では「Vulnerabilities 0」と表示されています。この50以上の数の食い違いは一体どういうことでしょうか?
VEX (Vulnerability Exploitability eXchange) とは
ここでVEXという仕組みを見ていきます。
VEX (Vulnerability Exploitability eXchange)とは、米国通信情報管理局が開発した、ソフトウェアが脆弱性の影響を受けるかどうかを示す、機械判読可能なフォーマットです。つまり、これを見ることでCVEが対策しなければいけないのか、無視できるものかを判断できるということです。
DHIのVEXは、Docker ScoutというDocker CLIのプラグインを通して取得できます。このプラグインは標準では含まれていないため、別途インストールの必要があります。
% docker scout vex get dhi.io/python:3.14.6-debian13 > /tmp/vex.json ✓ SBOM obtained from attestation, 82 packages found ✓ Provenance obtained from attestation ✓ Found 1 VEX attestation for image
取得したVEXを見てみると、問題の linux-libc-dev パッケージについて、CVEを無視する指定がなされていることがわかります。次は一部です。
{
"@id": "pkg:deb/debian/linux-libc-dev"
},
(略)
"status": "not_affected",
"status_notes": "Marked no-dsa by Debian Security Team; see https://security-tracker.debian.org/tracker/CVE-2004-0230",
"justification": "vulnerable_code_cannot_be_controlled_by_adversary",
"timestamp": "2026-04-28T21:44:53Z"
このCVE-2004-0230はカーネル本体のバグです。ヘッダファイルのみを含む linux-libc-dev パッケージには影響がないものですし、そもそもDockerコンテナはホストのカーネルで動作するものであるため、無視する判断がなされているのは妥当です。
このVEXを指定して再度Trivyでスキャンしてみましょう。
% docker container run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /tmp/vex.json:/tmp/vex.json:ro \ aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \ image --severity HIGH,CRITICAL \ --scanners vuln \ --vex /tmp/vex.json \ dhi.io/python:3.14.6-debian13@sha256:c82da5a1a30a6214f45c42def5b6f5b85981c7dc7a1802015a6ebf264675436d (略)
CVE未検出でした。
しかし…
これには2つの意見があると思います。
- Docker側で、VEXという共通フォーマットで影響がなく無視できるCVEを教えてくれているので、対象のCVEかどうか確認する手間が省ける。
- Docker側が自分自身で評価しているVEXを信頼できるのか。第三者の評価ではないので微妙に感じる。
疑い出したらキリがないですし、DHIのメリットがCVEの数だけではないのは確かです。
それでも「ほぼゼロのCVE」と言っているのに第三者の 素の Trivyでスキャンしたら差し替え前のイメージよりもCVEが多く出た、というのは少しガッカリしたというのが本音です。CIに組み込むのも面倒です。以前の記事「堅牢・安全・信頼の「Chainguard」を試してみた」で確認したChainguardのイメージは、VEXなどは使わず素のTrivyでCVEゼロでした。
そのChainguard社は「Going deep: Upstream distros and hidden CVEs」というブログで、競合関係にあるDHIの取り組みを不十分であると指摘しています。
まとめ
本稿では、Docker Hardened Image (DHI)をCVEの観点で見てみました。
DHIが特徴として掲げている「ほぼゼロのCVE」は、Docker社が影響がないと考えるCVEの検知をVEXという仕組みで抑制していることがわかりました。この方法は競合関係にあるChainguard社による批判を受けているということも見ました。
筆者は事前にChainguardを検証していたため、CVE検知数について多少バイアスがかかった物も見方をしてしまっています。ただ、今回確認した dhi.io/python:3.14.6-debian13は、「公式イメージ」のpython:3.14.6-slim-trixieと異なりシェルが入っていませんし、侵入に対する堅牢性が高められていることは事実です。本音を言うと開発パッケージすら含んでいない版もあるならより良かったのですが…。このようなパッケージの取捨選択はCVEのような指標で一律に判断できるものではないため、検討がより難しいです。
一方でDHIは最長5年イメージのメンテナンスを行うことを表明しており、無料プランでもバージョンタグを利用してイメージを利用可能です(Chainguardの無料プランでは :latest のみ)。
本稿が堅牢なコンテナイメージの検討の一助となれば幸いです。
