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

KCA — Kyverno認定アソシエイト

署名は安全だではなく、ここから出たことを証明する

TT Labで続きを見る

一言でいうと

イメージの署名は、そのイメージが安全だという意味ではありません。そのイメージが特定のパイプラインから出てきたという意味です。脆弱性だらけのイメージにも署名はできますが、それは署名の失敗ではなく定義です。verifyImagesルールは、この出どころの証明をアドミッションで強制する仕組みです。

なぜ必要なのか

レジストリにイメージがあるという事実は、何も保証しません。誰かがノートPCでビルドして手でプッシュしたのかもしれませんし、タグがあとから別の内容で上書きされたのかもしれません。CIパイプラインにどれだけ細かくスキャンゲートを設けても、そのパイプラインを迂回してプッシュされたイメージがクラスター上で起動できるなら、ゲートは勧告に近いものです。

署名は、この穴をふさぎます。パイプラインだけが知っている鍵(またはパイプラインのOIDC ID)でイメージに署名し、クラスターはその署名が付いたイメージだけを受け入れます。そうすれば、スキャンゲートが実際に強制されます。スキャン結果を強制力に変えるのが、この地点です。

どう動くのか

verifyImagesルールは、Podのスペックからイメージ参照をすべて取り出し、imageReferencesのパターンにかかるものだけを選んで、レジストリに署名を問い合わせます。見落としやすいのは、この問い合わせがクラスターの外へ出るネットワーク呼び出しだという点です。ポリシーを有効にした瞬間から、レジストリはイメージをダウンロードする経路であるだけでなく、Podを作成できるかどうかを決める経路にもなります。

検証主体はattestorsで表現します。静的な公開鍵(keys.publicKeys)、KMS、そしてkeylessの3つの方式があります。keylessはFulcioが発行する短命な証明書で署名し、Rekor透明性ログに記録を残す方式で、ここではsubjectとissuerが核心です。issuerはどのOIDCプロバイダーかを示し、subjectはその中の誰かを示します。GitHub Actionsなら、subjectがワークフローファイルのパスと参照タグまで含むので、リリースワークフローのファイル名を変えたりタグの規則を変えたりすると、その瞬間からすべてが止まります。この厳密さが目的ですが、運用上の負担でもあります。

attestors.countは、必ず知っておくべき落とし穴です。指定しなければ、entriesのすべての項目が通過しなければなりません。鍵を1つ追加した途端にすべてが失敗するなら、十中八九これが原因です。旧い鍵と新しい鍵の両方を入れてcountを1にすれば、どちらで署名されたイメージでも通過するので、入れ替え期間を無停止で乗り切れます。countを外すと、両方の鍵で署名されたものだけが通過し、正反対の結果になります。

attestationは、署名の上に載せる陳述です。SLSA provenanceは「このイメージがどのビルダーでどのソースから作られたか」を、CycloneDXやSPDXのSBOMは「この中にどんなコンポーネントが入っているか」を含みます。conditionsでその内容を検査できるので、たとえばSBOMの中の特定のライブラリのバージョンが脆弱なバージョンと一致しないことを求めるポリシーを書けます。署名だけを確認すると、誰がビルドしたかは検査していないことになるので、provenanceの条件も一緒に設定するほうが適切です。

フラグ4つも整理しておきましょう。requiredは、マッチしたイメージがすべて検証を経たことを強制し、verifyDigestはダイジェストの使用そのものを強制し、skipImageReferencesはマッチから除外するパターンのリストで、mutateDigestはタグをダイジェストに置き換えて書き直します。最後のものがパイプラインに与える影響は大きいです。有効にすると、Podのスペックに残るのはタグではなく@sha256:で始まる固定された参照になるので、同じタグをレジストリで上書きしても、スケールアウトで新しく起動するPodまで最初に固定されたイメージを使います。これが意図した不変性ですが、タグを上書きしてからPodを削除し、新しいイメージを取得させていたパイプラインは、この時点から静かに何もしなくなります。

パフォーマンスには、キャッシュが関わります。イメージ検証の結果はTTLキャッシュに保存され、これはポリシーのフィールドではなくインストールレベルの設定です。デフォルト値は、有効がtrue、最大キー数1,000個、TTL 60分です。レプリカ20個のロールアウトで、レジストリへの往復コストを実際に払うのは最初の1回だけなので、2回目の測定は1回目よりずっと速く出て、TTLが切れた次の最初のデプロイだけが特に遅くなります。キャッシュを切ってadmissionの遅延を測ってはじめて、レジストリが落ちたときに経験する最悪の姿に近い値が得られます。

現場での姿

著者のホームラボのレジストリはHarborで、MetalLBのプールから割り当てられた10.0.0.202で動いています。社内レジストリを使い始めた瞬間に必ず出会う問題が、認証情報の経路が2つありうるという点です。Podがイメージを問題なく取得できることと、Kyvernoが署名を問い合わせることは、別の経路です。Podは自分のimagePullSecretsを使い、Kyvernoは自分の認証情報を使います。Kyvernoの認証情報は、KyvernoのDeploymentの--imagePullSecrets引数か、ポリシーのimageRegistryCredentialsで渡します。著者が経験した時点では、この2つの経路は完全に分かれていました。Kyverno公式ドキュメントによると、1.18からは、評価中のPodのspec.imagePullSecretsをレジストリの認証情報として自動的に使います。したがって、使っているバージョンが1.18以降なら、Podにプルシークレットがある場合はポリシーに別途書かなくてもよく、それより前のバージョンなら、上の2つのどちらかを直接渡す必要があります。どちらの場合でも、Kyvernoがプライベートレジストリを読めなければ、署名が確かにあっても、ないように見えます。

スキャン側の経験も、同じ結論に行き着きます。あるイメージから合計1,247件の脆弱性が見つかり、Criticalが9件でしたが、修正版があるものだけを残すと4件、実際に悪用が確認されたリストと照合すると1件でした。そして、その1,247件をなくしたのは個別のパッチではなく、ベースイメージの入れ替えでした。アプリケーションが実際にリンクする共有ライブラリは8個なのに、イメージにはパッケージが432個入っていたからです。ディストロレスに移すと、OSパッケージの脆弱性が0になり、残ったのはアプリケーションのnpm依存関係の2件だけでした。署名とスキャンは、このようにつながります。スキャンでイメージを薄くし、通過したものに署名を付け、クラスターが署名されたものだけを受け入れるようにするのが一式です。

もう1つあります。ミラーレジストリにイメージだけをコピーして、署名のアーティファクトを一緒に移さなければ、このポリシーは例外なくすべて失敗します。エアギャップ環境にイメージを持ち込むときに最もよく起きることで、repositoryで署名の場所を別に指定するか、ミラーリングのパイプラインを直すまでは、Enforceを有効にしてはいけません。

次のラボですること

/root/kca-verify/に、静的な鍵とkeylessを併用するverifyImagesルール、SLSA provenanceとSBOMのattestationルール、許可レジストリのvalidateルールを書きます。そして、レジストリ認証情報のSecretと、それを付けたServiceAccount、ダイジェストで固定したDeploymentを実際のクラスターに作成します。