何が入ってくるのかを知ることが先だ
一言でいうと
サプライチェーンセキュリティの核心の問いは、「このイメージは安全か」ではなく、「いまクラスターで動いているそのバイト列が正確には何で、どこから来て、すり替えられていないか」です。
なぜ必要なのか
myapp:v1.2のようなタグは、変更可能なポインターです。同じタグに別のイメージをプッシュでき、latestはなおさらです。Podスペックにタグだけが書かれていると、いまノードで動いているバイト列が何なのかを、誰も証明できません。ロールバックしても、同じタグが別のものを指していることがあります。ダイジェスト(@sha256:...)は内容のハッシュなので、この問題を根本からなくします。
同じ論理が、言語のパッケージにも当てはまります。あるプロジェクトで直接書いた依存関係は37個なのに、実際にインストールされたパッケージは1,123個で、そのうち486個が本番ランタイムにまとめてデプロイされました。レビューして選んだのは37個で、信頼することにしたのは1,123個です。ロックファイルの整合性ハッシュが保証するのは、「このパッケージは安全だ」ではなく、「このパッケージは私が最後に見たものと同じだ」だけです。この2つの文の違いが、サプライチェーンセキュリティのほとんどすべてです。
どう動くのか
統制は4つの層で積み上がります。
| 層 | 何に答えるか | ツール・フィールド |
|---|---|---|
| 固定 | いま動いているバイト列は何か | イメージダイジェスト、imagePullPolicy |
| 出所 | どこから来たか | 許可レジストリの一覧、imagePullSecrets |
| 証明 | 自分たちのパイプラインが作ったものか | 署名(cosign)、SBOM、来歴証明(provenance attestation) |
| 強制 | そうでないものをどう止めるか | アドミッションポリシー(VAP、Kyverno、Gatekeeper) |
imagePullPolicyはよく誤解されます。Alwaysは毎回レジストリでマニフェストを確認し、IfNotPresentはノードにないときだけ取得します。タグを使うならAlwaysが安全に見えますが、それはタグが変わりうるという問題に毎回遭遇するということでもあります。ダイジェストで固定すれば、内容は定義上変わらないので、IfNotPresentにしておくほうが合理的で、レジストリの障害がデプロイの障害に波及しません。
スキャナーへの誤解も正しておく必要があります。イメージスキャナーがしていることは、2段階だけです。イメージレイヤーを展開して、インストールされているパッケージの一覧を作り、その一覧を脆弱性データベースと突き合わせます。そのため、curlでダウンロードしてコピーしたバイナリや、ソースから直接ビルドしたライブラリは、まったく見えません。スキャン結果がクリーンだからといって、安全とは限らない1つ目の理由です。
最後の層がアドミッションです。スキャン結果や署名には、それ自体に強制力がありません。パイプラインを迂回して手でプッシュされたイメージが、クラスターに入ってこられないようにできるのは、アドミッションポリシーだけです。それまでは、スキャンゲートは迂回可能な勧告に近いものです。
現場での姿
スキャンレポートを初めて接続すると、たいていこのような数字を目にします。payments:1.4.2イメージ1つに1,247件(LOW 812、MEDIUM 289、HIGH 137、CRITICAL 9)。この状態でチームのチャンネルに投げても、何も起きません。--ignore-unfixedを1つ付けると、4件(HIGH 3、CRITICAL 1)に減ります。何も安全にはなっていませんが、いま対処できる項目だけが残りました。ここにCISA KEVのリストを突き合わせると、実際に悪用が確認されたのは1件でした。今日やることは、その1つです。
そして、残りの1,200件あまりをなくしたのは、個別のパッチではなくベースイメージの置き換えでした。同じアプリケーションをnode:22(パッケージ432個)からdistrolessベース(19個)に移したところ、修正可能なCRITICALが9件から0件になりました。シェルとパッケージマネージャーがなくなったことは、おまけではなく本質です。侵入者が使う道具がイメージにないという意味だからです。その代わり、kubectl execで入れなくなるので、デバッグはエフェメラルコンテナを付ける方式に変わります。
エアギャップ環境では、レジストリの統制がそのまま可用性の問題にもなります。containerd 1.xはpauseイメージのアドレスをsandbox_imageキーで読んでいましたが、2.xではpinned_imagesの下のsandboxキーに移りました。社内レジストリに切り替えてあるクラスターが2.xにアップグレードされると、古いキーが黙って無視され、デフォルト値である公開レジストリに戻ります。結果は、そのノードでPodが1つも起動しないことです。特定のワークロードではなくノード全体が止まるため、ネットワーク障害と誤認しやすいのです。
イメージを統制する3つの層
サプライチェーンの分野は、「このイメージを使えないようにしなさい」という形で出題されます。止める場所は3つあり、問題ごとにどの層を問うているかが違います。
1つ目は、コンテナランタイムです。ノードの設定で特定のレジストリだけを許可したり、署名を検証させたりします。クラスター全体に効きますが、ノードごとに設定する必要があります。
2つ目は、アドミッション制御(admission)です。クラスターの中でポリシーによって止めます。試験で最もよく出る場所です。ImagePolicyWebhookはイメージを許可するかどうかを外部に問い合わせる専用の仕組みで、ポリシーエンジン(Kyverno・Gatekeeper)はより一般的な条件を設定します。
# ImagePolicyWebhook 을 켤 때 함께 필요한 것
# --admission-control-config-file 로 설정 파일을 주고,
# 그 파일이 kubeconfig 를 가리키고, 둘 다 정적 파드에 마운트되어야 한다.
このコードブロックの韓国語コメントは、ImagePolicyWebhookを有効にするときは、フラグで設定ファイルを渡し、そのファイルがkubeconfigを指し、両方を静的Podにマウントする必要がある、という意味です。
defaultAllow: falseにすると、Webhookに到達できないときはすべて拒否します。安全ですが、Webhookが落ちるとクラスターに何もデプロイできなくなります。試験では、たいていこの値が問われます。
3つ目は、イメージそのものです。署名とSBOMです。署名検証は「誰が作ったのか」に、スキャンは「何が入っているのか」に答えます。この2つは互いの代わりになりません。署名付きの脆弱なイメージも、クリーンな無名のイメージもありえます。
タグではなくダイジェストで固定します。ポリシーで:latestを禁止する問題がよく出題されます。タグは動くため、検証したものと動いているものが食い違うことがあります。
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"
"}{end}' | sort -u | grep -vE '@sha256:'
静的解析もサプライチェーンの一部です。マニフェストをデプロイ前に検査するツール(kubesec、kube-linter、trivy config)が出題されます。クラスターに入れる前に止めるのが最も安い層だという点が要点です。
次のラボですること
まずイメージをダイジェストで固定し、許可レジストリの一覧を作り、プライベートレジストリ認証用のSecretをServiceAccountに付けます。署名・SBOMの検証ポリシーは、マニフェストファイルとして書きます。次のラボでは、クラスターに実際に適用できるValidatingAdmissionPolicyで、「タグだけを使ったイメージ」をCEL式で拒否するポリシーを自分で作ります。