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

ポリシーをコードで

警告だけ付けたまま enforce に上げたら、デプロイが全部止まった

TT Labで続きを見る

目標

Kubernetesに内蔵されたPodセキュリティアドミッション(PSA)を、ネームスペースのラベルだけで運用します。観測(warn・audit)からブロック(enforce)へ上げる順序を自分で踏み、上げる前に何がブロックされるかを先に取り出して、レポートにします。

なぜ重要なのか

PSAは、インストールするものがありません。APIサーバーにすでにオンになっていて、ネームスペースにラベルを3つかけるだけです。そのため簡単に見えますが、事故は常に、順序を飛ばしたときに起きます。enforceをいきなりかけたチームは、その日のデプロイがすべてブロックされ、warnだけをオンにしておいたチームは、警告を誰も読まないため、半年後に同じ場所に立ちます。この2つの失敗の間にあるのが、このラボで扱う順序です。観測で現況を集め、レベルのバージョンを固定してクラスターのアップグレードとポリシーの変更を切り離し、切り替えの直前に、既存のPodの違反を先に取り出して直す一覧を作ってから、上げます。そして、上げた後も、すでに起動しているPodはブロックできないという限界を知っていてはじめて、ラベルを変えた日を完了と勘違いしません。

ステップ

  1. /root/psa/plain.yamlにPodを1つ書いてください。名前web、ラベルapp: web、コンテナ名web、イメージnginx:1.27で、securityContextは1行も入れません。ネームスペースpsa-legacyを、PSAのラベルなしで作成し、このPodを適用してください。そして、/root/psa/default.txtに、ラベルが1つもないネームスペースに適用されるenforceのレベルの名前を、1語で書いてください。
  2. ネームスペースpsa-observeを作成して、pod-security.kubernetes.io/warn=restrictedとpod-security.kubernetes.io/audit=restrictedの2つのラベルだけをかけてください(enforceはかけません)。同じplain.yamlをkubectl create -f plain.yaml -n psa-observeで入れて、標準出力と標準エラー出力を一緒に/root/psa/warn.txtに保存してください。Podが作られたという行と、4つの違反の理由が、すべてそのファイルにある必要があります。
  3. ネームスペースpsa-strictを作成して、pod-security.kubernetes.io/enforce=restrictedをかけてください。同じplain.yamlをkubectl create -f plain.yaml -n psa-strict --dry-run=serverで入れて、拒否メッセージを標準エラー出力まで/root/psa/denied.txtに保存してください。そのメッセージに書かれた4つのすべてを直したPodを/root/psa/hardened.yamlに書き(名前web-hardened、イメージはそのままnginx:1.27)、psa-strictに実際に適用してください。
  4. ネームスペースpsa-baselineを作成して、pod-security.kubernetes.io/enforce=baselineをかけてください。/root/psa/privileged.yamlに、baselineにも違反するPodを書いてください(名前breaker、コンテナ名breaker、イメージbusybox:1.36)。そして、/root/psa/gap.txtに、3つのマニフェスト(plain.yaml・hardened.yaml・privileged.yaml)を2つのネームスペースにそれぞれ入れてみた結果を、<파일이름> baseline=<allow|deny> restricted=<allow|deny>(プレースホルダーはファイル名と、allowまたはdenyです)の形で、1行ずつ書いてください。判定は--dry-run=serverで行い、Podは実際には作りません。
  5. 3つのネームスペースのレベルのラベルごとに、ペアになるバージョンのラベルをv1.30でかけてください。psa-strictとpsa-baselineはpod-security.kubernetes.io/enforce-version、psa-observeはpod-security.kubernetes.io/warn-versionとpod-security.kubernetes.io/audit-versionです。そして、/root/psa/pinned.shを作成してください。引数なしで実行すると、enforce・audit・warnのラベルはかかっているのに、そのペアである-versionラベルがないネームスペースの名前だけを、1行ずつ(ソートして)出力します。
  6. まず、psa-legacyにPodをさらに2つ入れてください。plain.yamlの名前だけをapiに変えたものと、hardened.yamlの名前だけをbatchに変えたものです。そのあと、判定専用のネームスペースpsa-canaryを作成して、pod-security.kubernetes.io/enforce=restrictedとpod-security.kubernetes.io/enforce-version=v1.30をかけてください。kubectl label ns psa-legacy pod-security.kubernetes.io/enforce=restricted --overwrite --dry-run=serverの出力を、標準エラー出力まで/root/psa/preview.txtに保存してください。ラベルが実際に付いてはいけません。最後に、/root/psa/violators.sh <네임스페이스>(プレースホルダーはネームスペースです)を作成してください。そのネームスペースのPodを1つずつpsa-canaryに再提出して(--dry-run=server)、restrictedに違反するものの名前だけを、1行ずつソートして出力します。./violators.sh psa-legacyの結果を、/root/psa/violators.txtに保存してください。
  7. psa-legacyに、pod-security.kubernetes.io/enforce=restrictedとpod-security.kubernetes.io/enforce-version=v1.30を、今度は本当にかけてください。そのあと、hardened.yamlをpsa-legacyに適用し(通過する必要があります)、plain.yamlをkubectl create -n psa-legacy --dry-run=serverで入れて、拒否メッセージを標準エラー出力まで/root/psa/enforced.txtに保存した後、kubectl get pod -n psa-legacyの出力を、そのファイルの後ろに追記してください。すでに起動していたwebとapiは、削除しません。
  8. /root/psa/targets.txtに、検査するネームスペースの名前を1行ずつ書いてください。psa-legacy・psa-observe・psa-strict・psa-baselineの4つです。/root/psa/readiness.sh [목록파일](プレースホルダーは一覧ファイルです)を作成してください(引数を省略すると/root/psa/targets.txtを使います)。一覧のネームスペースごとに、判定を1行ずつ出力します。すでにenforce=restrictedなら<이름> ENFORCED、違反するPodが1つもなければ<이름> READY、あれば<이름> BLOCKED <개수>、そのようなネームスペースがなければ<이름> MISSINGです(プレースホルダーは名前と個数です)。ラベルを実際に変えてはいけません。実行結果を/root/psa/report.txtに保存してください。

参考

ラベルが1つもないネームスペースは、何もブロックしない

/root/psa/plain.yamlにPodを1つ書いてください。名前web、ラベルapp: web、コンテナ名web、イメージnginx:1.27で、securityContextは1行も入れません。ネームスペースpsa-legacyを、PSAのラベルなしで作成し、このPodを適用してください。そして、/root/psa/default.txtに、ラベルが1つもないネームスペースに適用されるenforceのレベルの名前を、1語で書いてください。

PodSecurityは、APIサーバーにすでにオンになっているアドミッションコントローラーで、どのレベルで見るかは、ネームスペースのラベルだけが決めます。ラベルがなければクラスターのデフォルトが使われますが、そのデフォルトは、何もブロックしない側です。Pod Security Standardsにはレベルが3つあります。そのうち、最もゆるやかなものの名前です。

警告だけをオンにしたら、同じPodが警告とともに作られた

ネームスペースpsa-observeを作成して、pod-security.kubernetes.io/warn=restrictedとpod-security.kubernetes.io/audit=restrictedの2つのラベルだけをかけてください(enforceはかけません)。同じplain.yamlをkubectl create -f plain.yaml -n psa-observeで入れて、標準出力と標準エラー出力を一緒に/root/psa/warn.txtに保存してください。Podが作られたという行と、4つの違反の理由が、すべてそのファイルにある必要があります。

PSAの3つのモードは、互いに独立しています。enforceだけがリクエストをブロックし、warnはリクエストした人に警告を返し、auditは監査ログに注釈を残します。警告は標準エラー出力に出るので、> 파일 2>&1(プレースホルダーはファイルです)のように、両方を受け取る必要があります。このステップの価値は、「何もブロックできないのに、なぜオンにするのか」にあります。

4つの理由を読んで、Podをrestrictedに合わせて直す

ネームスペースpsa-strictを作成して、pod-security.kubernetes.io/enforce=restrictedをかけてください。同じplain.yamlをkubectl create -f plain.yaml -n psa-strict --dry-run=serverで入れて、拒否メッセージを標準エラー出力まで/root/psa/denied.txtに保存してください。そのメッセージに書かれた4つのすべてを直したPodを/root/psa/hardened.yamlに書き(名前web-hardened、イメージはそのままnginx:1.27)、psa-strictに実際に適用してください。

拒否メッセージが、直す場所をそのまま教えてくれます。どれがPodレベルのsecurityContextで、どれがコンテナレベルかだけを見分けて入れればよいです。runAsNonRootをtrueにするということは、イメージがrootで動かないという意味なので、実行するUIDも一緒に決めておくほうが安全です。capabilitiesは、すべて捨てた後で、必要なものだけを再び追加する順序です。

baselineは通過させ、restrictedだけがブロックするPod

ネームスペースpsa-baselineを作成して、pod-security.kubernetes.io/enforce=baselineをかけてください。/root/psa/privileged.yamlに、baselineにも違反するPodを書いてください(名前breaker、コンテナ名breaker、イメージbusybox:1.36)。そして、/root/psa/gap.txtに、3つのマニフェスト(plain.yaml・hardened.yaml・privileged.yaml)を2つのネームスペースにそれぞれ入れてみた結果を、<파일이름> baseline=<allow|deny> restricted=<allow|deny>(プレースホルダーはファイル名と、allowまたはdenyです)の形で、1行ずつ書いてください。判定は--dry-run=serverで行い、Podは実際には作りません。

baselineは、広く知られた権限昇格だけを防ぎ、restrictedは、そこにハードニングのルールを追加します。どちらでも使えないのは、ホストのネームスペースと特権コンテナです。同じ名前のPodがすでにあると、アドミッションまで行く前にAlreadyExistsで終わるので、提出するときに、名前とネームスペースを変えて送るのが安全です(kubectl create -f 파일 --dry-run=client -o jsonで取り出してjqで修正した後、もう一度渡せばよいです。プレースホルダーはファイルです)。

latestにしたネームスペースが、クラスターを上げる日に止まる

3つのネームスペースのレベルのラベルごとに、ペアになるバージョンのラベルをv1.30でかけてください。psa-strictとpsa-baselineはpod-security.kubernetes.io/enforce-version、psa-observeはpod-security.kubernetes.io/warn-versionとpod-security.kubernetes.io/audit-versionです。そして、/root/psa/pinned.shを作成してください。引数なしで実行すると、enforce・audit・warnのラベルはかかっているのに、そのペアである-versionラベルがないネームスペースの名前だけを、1行ずつ(ソートして)出力します。

バージョンのラベルをかけないと、その場所はlatestで動きます。レベルの「現在のバージョン」という意味なので、クラスターを上げるとルールも一緒に上がり、昨日まで通過していたPodが、今日ブロックされます。ラベルは、kubectl get ns -o jsonの.items[].metadata.labelsにまるごと入っていて、ラベルが1つもないネームスペースでは、その場所がそもそもありません。3つのモードを語として回しながら、ペアのラベルの有無を見る問題です。

上げる前に、何がブロックされるかを先に取り出す

まず、psa-legacyにPodをさらに2つ入れてください。plain.yamlの名前だけをapiに変えたものと、hardened.yamlの名前だけをbatchに変えたものです。そのあと、判定専用のネームスペースpsa-canaryを作成して、pod-security.kubernetes.io/enforce=restrictedとpod-security.kubernetes.io/enforce-version=v1.30をかけてください。kubectl label ns psa-legacy pod-security.kubernetes.io/enforce=restricted --overwrite --dry-run=serverの出力を、標準エラー出力まで/root/psa/preview.txtに保存してください。ラベルが実際に付いてはいけません。最後に、/root/psa/violators.sh <네임스페이스>(プレースホルダーはネームスペースです)を作成してください。そのネームスペースのPodを1つずつpsa-canaryに再提出して(--dry-run=server)、restrictedに違反するものの名前だけを、1行ずつソートして出力します。./violators.sh psa-legacyの結果を、/root/psa/violators.txtに保存してください。

事前警告は、Podが多いと(and N other pods)に省略してしまいます。名前をすべて知るには、Podごとに別々に判定する必要があります。すでに起動しているPodはアドミッションを再び通らないため、同じspecを、そのレベルを強制するネームスペースに再提出してみるのが、方法です。kubectl get pod <이름> -n <ns> -o json(プレースホルダーは名前です)から、apiVersion・kind・specだけを抜き出し、metadataは新しく作ってあげればよいです。

上げた後も、すでに起動していたPodはそのまま動く

psa-legacyに、pod-security.kubernetes.io/enforce=restrictedとpod-security.kubernetes.io/enforce-version=v1.30を、今度は本当にかけてください。そのあと、hardened.yamlをpsa-legacyに適用し(通過する必要があります)、plain.yamlをkubectl create -n psa-legacy --dry-run=serverで入れて、拒否メッセージを標準エラー出力まで/root/psa/enforced.txtに保存した後、kubectl get pod -n psa-legacyの出力を、そのファイルの後ろに追記してください。すでに起動していたwebとapiは、削除しません。

PSAはアドミッションコントローラーなので、リクエストが入ってきたときにだけ判断します。すでに保存されたオブジェクトは再び見ないため、違反するPodが起動していても、追い出しません。そのため、enforceに上げた直後のクラスターは、「新しく入ってくるものだけがきれい」な状態で、既存のPodは、再デプロイされるときにはじめてブロックされます。この時間差を知らないと、上げた日に何も起きないのを見て、安全だと誤って判断することになります。

どのネームスペースから上げてよいかを、レポートにする

/root/psa/targets.txtに、検査するネームスペースの名前を1行ずつ書いてください。psa-legacy・psa-observe・psa-strict・psa-baselineの4つです。/root/psa/readiness.sh [목록파일](プレースホルダーは一覧ファイルです)を作成してください(引数を省略すると/root/psa/targets.txtを使います)。一覧のネームスペースごとに、判定を1行ずつ出力します。すでにenforce=restrictedなら<이름> ENFORCED、違反するPodが1つもなければ<이름> READY、あれば<이름> BLOCKED <개수>、そのようなネームスペースがなければ<이름> MISSINGです(プレースホルダーは名前と個数です)。ラベルを実際に変えてはいけません。実行結果を/root/psa/report.txtに保存してください。

前のステップのviolators.shをそのまま呼び出して使えば、個数は行数を数えるだけで済みます。すでにrestrictedの場所を先にふるい分ける必要がある理由は、そのようなネームスペースに同じ値でラベルをかけ直しても、値が変わらないため、事前警告そのものが出ないからです。一覧ファイルを引数で受け取るようにしておけば、同じスクリプトを別のまとまりにも使えます。