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

KCSA — Kubernetesセキュリティアソシエイト

昨日と同じ nginx:latest は、今日も同じイメージか

TT Labで続きを見る

目標

同じアプリケーションを指す3通りのイメージ表記(ダイジェスト固定 / latestタグ / 社内レジストリのバージョンタグ)を 並べてデプロイして比較し、imagePullPolicyとpull secretを出所に合わせて設定したうえで、ValidatingAdmissionPolicyで 「許可レジストリのイメージか、ダイジェストで固定されたイメージだけ」を受け入れるルールを実行し、拒否を観察します。

なぜ重要なのか

クラウドネイティブセキュリティの4C(Cloud・Cluster・Container・Code)のうち、内側の2つの層は何を動かすかの問題です。 クラスターをどれだけ堅く守っても、Podが取得してくるイメージが誰かにすり替えられたものなら、防御は内側から崩れます。

タグはラベルにすぎず、レジストリ側でいつでも別のイメージに付け替えられます。nginx:latestは今日と明日で 別のイメージかもしれませんし、app:1.2.3のようなバージョンタグも、上書きを防ぐ設定をしていなければ同じです。一方、 @sha256:...のダイジェストはイメージの内容のハッシュなので、1文字違うだけでも別の値になります。そのため、サプライチェーンセキュリティの第一歩は、 「どのイメージがタグでしか指されていないか」を見分けることです。

imagePullPolicyは、ノードがキャッシュを信頼するかを決めます。タグ付きのイメージにIfNotPresentを使うと、ノードごとに異なる時点の イメージを動かすことになりかねません。社内レジストリの認証情報はpull secretで渡し、ServiceAccountに付けておけば、 Podごとに繰り返さなくてすみます。

最後に、アドミッションポリシーは作成リクエストの時点でしか評価されません。ステップ2でポリシーなしで作成したDeploymentは、ステップ6の ポリシーが有効になったあともそのまま残っています。ただし、mutable(nginx:latest)のPodが削除されてReplicaSetが新しいPodを 作ろうとすると、その作成リクエストは拒否されます。ポリシーを有効にする前に、既存のワークロードを先に点検すべき理由です。

ステップ

  1. ネームスペースkcsa-supplyを作成します。
  2. pinned(ダイジェスト)・mutable(nginx:latest)・internal(registry.example.com/app:1.2.3)のDeploymentを作成します。
  3. 各イメージをdigest/tagに分類して、/root/kcsa-supply/classify.txtに書きます。
  4. タグ付きのイメージはimagePullPolicy: Always、ダイジェスト付きのイメージはIfNotPresentに変更します。
  5. docker-registryのSecretregcredを作成し、ServiceAccountdeployerのimagePullSecretsに設定します。
  6. 許可レジストリかダイジェスト固定だけを通すポリシーrequire-allowed-imageとバインディングを作成します。
  7. docker.io/library/evil:1のPodが拒否されるメッセージを/root/kcsa-supply/denied-image.txtに保存します。
  8. 点検結果を/root/kcsa-supply/report.txtに残します。

参考

出所を問う作業区画を作る

ネームスペースkcsa-supplyを作成してください。後のステップのポリシーバインディングは、自動で付くkubernetes.io/metadata.name=kcsa-supplyラベルで、このネームスペースを選びます。

すべてのネームスペースには、自分の名前を値とするkubernetes.io/metadata.nameラベルが自動で付きます。createを--dry-run=clientで生成してapplyすれば、何度実行しても安全です。

同じnginxなのに、出所の表記が3つとも違う

ネームスペースkcsa-supplyに、Deploymentを3つ作成してください(各replicasは1、selectorとPodのラベルを一致させてください)。pinnedはイメージnginx@sha256:0000000000000000000000000000000000000000000000000000000000000000、mutableはnginx:latest、internalはregistry.example.com/app:1.2.3です。各Deploymentの最初のコンテナのイメージが、正確にこの文字列である必要があります。

イメージ参照は、[레지스트리/]저장소[:태그][@sha256:다이제스트]の形です(プレースホルダーはレジストリ、リポジトリ、タグ、ダイジェストです)。kubectl create deploymentは、イメージの文字列からコンテナ名を作り出しますが、@sha256:を含むイメージでは、名前がルールに合わず失敗することがあります。コンテナ名を直接書くYAMLマニフェストで作成するほうが安全です。ダイジェストは、16進数の64桁でなければ文法上有効ではありません。

どのイメージが明日変わりうるかを見分ける

3つのDeploymentのイメージをdigest(ダイジェストで固定)またはtag(タグで指している)に分類して、/root/kcsa-supply/classify.txtに이름=분류の形式(プレースホルダーは名前と分類です)で1行ずつ(pinned、mutable、internal)書いてください。

タグは、レジストリ側でいつでも別のイメージを指し直せるラベルで、ダイジェストはイメージの内容のハッシュなので変わりません。latestだけでなく1.2.3のようなバージョンタグも、結局はタグです。kubectl get deploy -o jsonpathで実際のイメージの文字列を見て、@sha256:があるかどうかで分けてください。

タグを信じず、毎回確認し直させる

DeploymentのコンテナのimagePullPolicyを変更してください。タグを使うmutableとinternalはAlways、ダイジェストで固定したpinnedはIfNotPresentにします。

タグは指す対象が変わりうるので、ノードのキャッシュに同じ名前の古いイメージがあっても、レジストリに改めて問い合わせる必要があります。ダイジェストは内容のハッシュなので、キャッシュにあればそのまま使っても安全です。デフォルト値は、タグがlatestであるか、ない場合はAlways、それ以外はIfNotPresentなので、internalは自分で変更する必要があります。kubectl patchで、spec.template.spec.containersの該当するコンテナ(名前で照合)を修正してください。

社内レジストリの認証情報をServiceAccountに付ける

ネームスペースkcsa-supplyに、docker-registryタイプのSecretregcred(サーバーはregistry.example.com、ユーザーはu、パスワードはp)を作成し、ServiceAccountdeployerを作成して、そのimagePullSecretsにregcredを入れてください。

kubectl create secretにはdocker-registryというサブコマンドがあり、kubernetes.io/dockerconfigjsonタイプのSecretを作成してくれます(サーバー・ユーザー・パスワードのフラグ)。ServiceAccountにimagePullSecretsを付けておけば、そのSAで起動するPodごとにSecretを別に書かなくてすみます。SAを作成したあとで、patchでフィールドを追加してください。

許可レジストリの外のイメージは受け入れない

ValidatingAdmissionPolicyrequire-allowed-imageとバインディングrequire-allowed-image-bindingを作成してください。ポリシーは、PodのCREATEで、すべてのコンテナイメージがregistry.example.com/で始まるか、@sha256:を含む場合に通過させ(メッセージはimage must come from registry.example.com or be digest-pinned、failurePolicy: Fail)、バインディングはvalidationActionsを["Deny"]にして、kubernetes.io/metadata.name: kcsa-supplyのネームスペースにだけ適用します。

CELの文字列には、startsWith()とcontains()があります。プレフィックスの末尾に/まで入れないと、registry.example.com.evil.io/...のような名前がすり抜けてしまいます。ポリシーは検査の内容を、バインディングは適用範囲と動作(Deny)を決めるという点を覚えておいてください。

Docker Hubから来た見慣れないイメージが入口で止められる

ネームスペースkcsa-supplyに、イメージがdocker.io/library/evil:1のPodを作成してみて、APIサーバーが返した拒否メッセージ全体を/root/kcsa-supply/denied-image.txtに保存してください。

拒否されると、kubectlは0以外の終了コードで終了し、エラーを標準エラー出力に書きます。標準エラー出力までファイルに受け取ってください。バインディングの直後は、反映に1–2秒かかることがあるので、Podが作成されてしまったら削除して、もう一度試してください。ステップ2のDeploymentのPodがそのまま生きていることも確認してみてください。アドミッションは、作成リクエストにしか作用しません。

イメージの出所の点検結果を帳簿として残す

点検結果を/root/kcsa-supply/report.txtに4行で書いてください。pinned=digest、mutable=tag、latest-pullpolicy=Always、untrusted-registry=deniedです。採点ツールは、各行を、実際のDeploymentのspecと、許可リストの外のイメージでPodを作成してみた結果に照合します。

前のステップで自分で確認した値を書き写せばかまいません。latest-pullpolicyはmutableコンテナの実際のimagePullPolicyの値(大文字小文字はそのまま)、untrusted-registryはステップ7で見た結果(deniedかallowed)です。