警告だけ付けたまま enforce に上げたら、デプロイが全部止まった
目標
Kubernetesに内蔵されたPodセキュリティアドミッション(PSA)を、ネームスペースのラベルだけで運用します。観測(warn・audit)からブロック(enforce)へ上げる順序を自分で踏み、上げる前に何がブロックされるかを先に取り出して、レポートにします。
なぜ重要なのか
PSAは、インストールするものがありません。APIサーバーにすでにオンになっていて、ネームスペースにラベルを3つかけるだけです。そのため簡単に見えますが、事故は常に、順序を飛ばしたときに起きます。enforceをいきなりかけたチームは、その日のデプロイがすべてブロックされ、warnだけをオンにしておいたチームは、警告を誰も読まないため、半年後に同じ場所に立ちます。この2つの失敗の間にあるのが、このラボで扱う順序です。観測で現況を集め、レベルのバージョンを固定してクラスターのアップグレードとポリシーの変更を切り離し、切り替えの直前に、既存のPodの違反を先に取り出して直す一覧を作ってから、上げます。そして、上げた後も、すでに起動しているPodはブロックできないという限界を知っていてはじめて、ラベルを変えた日を完了と勘違いしません。
ステップ
/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語で書いてください。- ネームスペース
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-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に実際に適用してください。 - ネームスペース
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は実際には作りません。 - 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行ずつ(ソートして)出力します。 - まず、
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に保存してください。 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は、削除しません。/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に保存してください。
参考
- ラボのPodの中には、kwokが起動した本物のkube-apiserver v1.30.4があります。まず
export KUBECONFIG=/root/.kube/configを実行してください。コンテナは実際には実行されませんが、アドミッションは本物として動作します。 - ラベルの形式:
pod-security.kubernetes.io/<enforce|audit|warn>=<privileged|baseline|restricted>と、ペアになるpod-security.kubernetes.io/<모드>-version=<latest|v1.x>(プレースホルダーはモードです)。 - アドミッションだけを通して、オブジェクトは残さないようにするには、
kubectl create -f - --dry-run=serverを使います。kubectl label ns ... --dry-run=serverは、そのネームスペースの既存のPodの違反を、警告として事前に知らせてくれます。 - よくあるミス: 警告が標準エラー出力に出ることを知らずに、
> 파일(プレースホルダーはファイルです)だけをかけて、空のファイルを残してしまうことです。 - よくあるミス: 同じ名前のPodがすでにあって、アドミッションまで行けずにAlreadyExistsで終わってしまうことです。判定用の提出は、名前を変えて送ります。
- よくあるミス: ラベルを上げた日に何も起きないのを見て、安全だと判断してしまうことです。既存のPodは、再デプロイされるときにブロックされます。
- Pod Security Admission · Pod Security Standards · Enforce Pod Security Standards with Namespace Labels · Admission Controllers Reference · Migrate from PodSecurityPolicy to PSA
ラベルが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の場所を先にふるい分ける必要がある理由は、そのようなネームスペースに同じ値でラベルをかけ直しても、値が変わらないため、事前警告そのものが出ないからです。一覧ファイルを引数で受け取るようにしておけば、同じスクリプトを別のまとまりにも使えます。