0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker Hardened Image(DHI)に切り替えたらイメージサイズと脆弱性はどれだけ減ったか実測してみた

0
Posted at

はじめに

先日のPHP2shellの対応で弊社のサービスでも緊急で対応をさせていただき、現在は特に被害もなくサービスを提供できています。

このような脆弱性の対応は新規開発のリソースを奪うことなるので、そもそも脆弱性を極力減らす仕組みを作るほうが良いと判断し、社内の複数サービスでコンテナのベースイメージを Docker Hardened Image(DHI) に切り替える対応を進めています。
「脆弱性が減る」「イメージが小さくなる」と言われますが、実際にどれくらいなのか、切り替え前後で実測しました。

この記事では、切り替え済みのサービス群と、これから切り替える予定のサービスについて、イメージサイズと trivy での脆弱性の件数を比べた結果をまとめます。リポジトリ名などの固有名詞は伏せています。

結論

  • runtime 版(-dev ではない DHI)にすると、CRITICAL / HIGH がほぼ 0 件になる。Node の runtime は全件でも 32 件だった。
  • サイズも小さくなる。元が Debian のフルイメージなら 3〜7 分の 1、slim でも約半分。
  • ただし、最終ステージを -dev のままにすると、元が alpine のサービスでは、サイズも脆弱性も増える。
  • -dev を外すには、最終ステージでシェルを使わない作りにする必要がある。

測り方

対象

  • 切り替え済みのサービス: Node / Python / Go の 10 件前後
  • 切り替え前のイメージと、DHI に切り替えた後のイメージを比較

サイズ

  • 最終ステージのベースイメージ同士を比較。アプリの層は前後で同じなので、土台の差がそのまま最終イメージの差になる。
  • docker images の DISK USAGE(展開後)と CONTENT SIZE(圧縮後、レジストリ転送量の目安)の両方を記録。
  • ローカル(arm64)で、実測日時点のタグを pull して測定。

脆弱性

  • trivy image --scanners vuln で、変更前後を同じ条件でスキャン。
  • 件数は「CVE ID とパッケージの組」で重複を除いた数。
  • 注意: trivy は DHI が公開している VEX(影響しない CVE の除外情報)を適用していない。DHI 側は実際より多めに出ている可能性がある。

結果: イメージサイズ

最終ステージのベースイメージを比べたものです(ディスク上のサイズ)。

切り替え内容 変更前 変更後 差
Node(slim)→ DHI runtime 349〜356MB 188MB −46〜47%
Node(フル)→ DHI -dev 1.64GB 433MB −74%
Python(フル)→ DHI -dev 1.62GB 220MB −86%
Go(フル)→ DHI -dev 1.25〜1.32GB 822MB −34〜38%
Node(alpine)→ DHI runtime 239MB 188MB −21%
Node(alpine)→ DHI -dev 235〜239MB 432MB +81〜84%
alpine(13.6MB)→ DHI debian-base -dev 13.6MB 117MB +760%

圧縮サイズ(レジストリ転送量)でも傾向は同じで、たとえば Node の slim → runtime は 85MB → 42MB(約 −51%)でした。

Go の API 8 本を実際にビルドして比べた結果

Go のアプリは静的バイナリなので、最終ステージを Debian の slim から DHI の static に替えられます。8 本の API を、変更前後で実際にビルドして測りました。

項目 変更前 変更後
ディスク上のサイズ 164〜278MB 24〜74MB
圧縮サイズ 36.6〜62.9MB 5.9〜14.2MB

ディスク上のサイズは 3〜7 分の 1 になりました。バイナリが同じなので、サイズの差は土台(Debian の slim と static)の差そのものです。

結果: 脆弱性の件数

Node / Python / Go

CRITICAL と HIGH の合計、全件の順に示します。

切り替え内容 変更前 変更後
Node(slim)→ DHI runtime 49〜62 / 183〜250 0 / 32
Node(slim)→ DHI runtime(別の slim 系列) 66〜102 / 253〜345 0 / 32
Node(フル)→ DHI -dev 485 / 4,376 36 / 242
Python(フル)→ DHI -dev 471 / 3,871 98 / 1,132
Go(フル)→ DHI -dev 153〜285 / 2,056〜3,147 100 / 1,250
Node(alpine)→ DHI -dev 6〜9 / 19〜22 36 / 242

Go の API 8 本(実ビルド、debian:trixie-slim → DHI static)

項目 変更前 変更後
CRITICAL / HIGH / MEDIUM / LOW 0 / 54 / 68 / 63(1 本だけ 0 / 58 / 91 / 131) 0 / 11 / 8 / 2
全件 185(1 本だけ 280) 21

変更後に残る 21 件は、OS ではなく Go のバイナリ自体(標準ライブラリや依存モジュール)が原因です。変更前の 185 件にも同じものが含まれていて、DHI の static 側は 0 件でした。これは DHI 化では減らず、Go のバージョンや依存の更新が必要です。

-dev を外した場合

-dev と runtime の違いは大きく、-dev はシェル、パッケージマネージャ、ビルド用のツールが入っています。最終ステージを runtime にした場合の件数は次のとおりです。

対象 変更前 -dev(現状) -dev を外した場合
Node(slim) 63 / 250 36 / 242 0 / 32
Node(alpine) 6 / 19 36 / 242 0 / 32
Node(フル) 485 / 4,376 36 / 242 0 / 32
Python 471 / 3,871 98 / 1,132 8 / 54
Go 153〜285 / 2,056〜3,147 100 / 1,250 3 / 45
alpine(13.6MB) 23 / 64 33 / 146 6 / 56
  • Node の runtime は、どのサービスでも 0 / 32 でした。-dev の 36 件の HIGH は、シェルやビルド用ツールが原因でした。
  • 元が alpine でも、-dev を外せば変更前より減ります。
  • Go の runtime(golang:*-debian13)は 297〜307MB あり、Go の実行環境が入ったイメージです。

ハマりどころ

1. 最終ステージを -dev のままにすると、効果が出ない

alpine などもともと小さいイメージから -dev に替えると、サイズも脆弱性も増えます。さらに、本番のコンテナにシェルが残るので、DHI の「シェルなし・非 root」という利点も出ません。

2. runtime にするとシェルがなくなる

runtime にすると、最終ステージで次のものが使えなくなります。

  • RUN(mkdir、chmod、ln -sf など)
  • シェル形式の CMD / ENTRYPOINT
  • シェル形式の HEALTHCHECK
  • npm や pnpm(実行時に pnpm start を呼ぶ構成)

置き換え方の例です。

  • npm ci --omit=dev や pnpm install --prod は、ビルド用のステージで実行して、node_modules を最終ステージに COPY する。
  • 起動は CMD ["pnpm", "start"] ではなく CMD ["node", "node_modules/.../bin/..."] のように、node を直接呼ぶ。
  • mkdir -p は WORKDIR で代用する。chmod +x は COPY --chmod=755 にする。
  • 非 root がデフォルトなので、useradd などのユーザー作成は不要。COPY --chown=node:node で所有者を合わせる。

3. ローカル開発用の Dockerfile は -dev のまま

apt-get、go install、ホットリロード用のツールを使うローカル開発用の Dockerfile は、runtime にできません。本番用と分けて考えるのが現実的です。

4. ビルドで外部ダウンロードが失敗する(TLS インターセプト)

社内ネットワークで TLS が傍受されていると、docker build の中の go mod download や npm install が、証明書エラーで失敗します。ビルド用ステージにだけ CA 証明書を足して回避しました。最終イメージには入りません。

5. サイズの見方に注意

新しい docker images は、DISK USAGE(展開後)と CONTENT SIZE(圧縮後)の 2 つを出します。同じ土台でも数字が 4 倍ほど違うので、どちらで比較したかを揃える必要があります。

注意点

  • 脆弱性の件数は、土台のイメージ(と Go バイナリ)だけを見ています。アプリの依存パッケージ(node_modules など)の脆弱性は含みません。
  • 件数は実測日時点の trivy の DB のものです。日が経つと変わります。
  • trivy は DHI の VEX を適用していないので、DHI 側の件数は実際より多い可能性があります。
  • 元のイメージは、切り替え当時のタグではなく、実測日時点のタグの内容です。
  • サイズはローカル(arm64)での値です。amd64 では少し変わります。

まとめ

  • DHI の runtime にすれば、土台の CRITICAL / HIGH はほぼ 0 件になり、サイズも小さくなる。
  • 元が Debian のフルイメージや slim のサービスは、-dev でも効果が大きい。
  • 元が alpine のような小さいイメージのサービスは、最終ステージで runtime を使わないと、むしろ増える。
  • runtime を使うには、最終ステージのシェル依存(RUN、pnpm start など)をなくす必要がある。まずは、最終ステージで何を使っているかを洗い出すのが近道。
  • Go は static にできれば、サイズも脆弱性も劇的に減る。残るのは Go のバイナリ側の脆弱性なので、Go のバージョンや依存の更新で対応する。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?