TT Lab
はじめる
学ぶ 学習パス コース

コンテナセキュリティ

スキャン結果が綺麗でも安全という意味ではない

TT Labで続きを見る

一言でいうと

イメージスキャナーが行うのは、2つの段階だけです。レイヤーを展開してインストールされたパッケージの一覧を 作り、その名前とバージョンを脆弱性DBと照合します。イメージを実行することも、コードを 分析することもしません。

なぜ必要なのか

スキャナーを魔法の箱とみなすと、2つの誤った結論にたどり着きます。「スキャンがきれいだから 安全だ」と、「1,247件出たから全部直すべきだ」です。どちらも、スキャナーが何をしているのかを 知らないところから生まれます。

動作原理を知れば、限界も自然にわかります。

(a)パッケージのメタデータがなければ見えません。Debian系なら /var/lib/dpkg/status、RPM系ならRPM DB、言語ランタイムならロックファイルを読みます。 そのため、Dockerfileでcurl | tar xzによって入れたバイナリは、SBOMにもスキャン 結果にも現れません。これが、スキャン結果がきれいだからといって安全だという 意味にはならない、1つ目の理由です。

(b)一覧にあれば、実行されるかどうかに関係なく報告されます。アプリケーションが一度も 呼び出さないtarの脆弱性も、そのまま上がってきます。

どう動くのか

実際の数値で見ると、感覚がつかめます。payments:1.4.2 (debian 12.6)をスキャンすると、 Total: 1247 (LOW 812, MEDIUM 289, HIGH 137, CRITICAL 9)と出ます。 ここに--ignore-unfixed --severity CRITICAL,HIGHを適用すると、Total: 4になり、 既知の悪用リスト(KEV)との共通部分は1件です。

整理するとこうです。1,247件が4件になりました。何も安全にはなっていませんが、 いま対処できる項目だけが残りました。

そして本当の解決策は、パッチではなくベースの交換です。

ベース サイズ 検出パッケージ 修正可能なCRITICAL HIGH
node:22 1.12GB 432 9 137
node:22-slim 231MB 118 2 24
node:22-alpine 148MB 41 0 3
distroless/nodejs22 187MB 19 0 1
静的バイナリ + scratch 24MB 0 0 0

1,247個を消すのはパッチではなく、ベースイメージの交換です。そして判断 基準は、この一文です。アプリケーションが実際にリンクする共有ライブラリが8個なのに、 イメージにパッケージが432個入っているなら、問題は脆弱性ではなくイメージの構成です。

現場での姿

ゲートのポリシーは、このジレンマから出発します。Criticalのすべてで失敗させると、開発が止まります。失敗させなければ、誰も見ません。実務で定着する妥協は、 修正版が存在するCritical/Highでだけ失敗させることです。直せない ものでビルドを止めると、人々はゲートを無効にする方法を先に覚えます。そして、例外には 必ず有効期限(expired_at)を付ける必要があります。例外が静かに永続化することが、 ゲートが無力化する最もよくある経路です。

優先順位は、CVSSのスコアだけでは決まりません。

CVE-2021-44228  epss 0.9444
CVE-2023-45853  epss 0.00412
CVE-2024-5535   epss 0.00196

3つともCVSSではCriticalの等級です。実際の優先順位は、 悪用可能性 × 露出の有無 × 到達可能性で決まります。

SBOMのフォーマットは2系統あります。SPDXはLinux Foundationが作り、ISO/IEC 5962:2021 の標準になり、ライセンスコンプライアンスの側に根が深くあります。CycloneDXはOWASPで 始まってECMA-424になり、セキュリティの脆弱性管理に焦点が合っています。ただし、SBOMの 本当の用途は、文書の保管ではなくインシデント当日の5分です。新しい脆弱性が出たとき、 40個のサービスのうち、どこにそのパッケージが何バージョンで入っているかを即答できるか、 その1点で、SBOMの価値が決まります。

スキャナーの数字を実際のリスクに変える

最初にスキャンを実行すると、CRITICALが200件ずつ出ます。その一覧をそのままチケットにすると、 誰も処理しません。減らす順序が必要です。

1つ目の問い: その脆弱性は、このイメージで実際に実行されるのか。ベースイメージに 入っていても、自分たちのプログラムが呼び出さないライブラリがほとんどです。最近のスキャナーは、 バイナリが実際にその関数を参照しているかを見る機能を持っており、これを有効にするだけで、一覧が 大きく減ります。

2つ目の問い: 攻撃者がその経路に到達できるか。インターネットに公開されたサービスの HTTPパーサーにある脆弱性と、内部のバッチ処理が使う圧縮ライブラリの、同じスコアの 脆弱性では、リスクが異なります。CVSSのスコアは環境を知らずに付けられた値なので、そのまま 順位付けしてはいけません。

3つ目の問い: 直せるか。--ignore-unfixedを有効にすると、まだパッチのない 項目が除かれます。これは無視することではなく、いまできることと、見守ることを 分けることです。パッチのない項目は、別に一覧を作っておき、パッチが出た日に処理します。

trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.2.3
trivy image --format cyclonedx -o sbom.json myapp:1.2.3

ほとんどは、ベースイメージを変えれば消えます。脆弱性の8割は、自分たちが使いもしない OSパッケージから出てきます。スリムなイメージやdistrolessに変えれば、その8割が丸ごと なくなります。個別のCVEを1つずつ処理するより、土台を入れ替えるほうが、常に 安くすみます。

定期的に作り直すことが、スキャンより重要です。イメージは、作った日のパッケージを そのまま持っているので、デプロイしなければ、脆弱性は積み重なり続けます。コードが変わらなくても、 週1回再ビルドするパイプラインがあれば、ほとんどが自然に解決します。

例外は期限とともに記録します。「これは直せない」として、期限なしの除外リストに入れると、 その項目は永遠に残ります。.trivyignoreに、有効期限と理由をあわせて書き、期限切れの 項目があれば、ビルドが警告するようにします。

次のラボですること

パッケージのインベントリを自分で作成して、ベースとランタイムのイメージを比較し、増えた攻撃 対象領域を数字で数えます。SBOMを手で作ってみて、ポリシーゲートのスクリプトを書いたあと、 ENVで入れたトークンがイメージに焼き付けられることを確認し、実行時の注入で 修正します。