🛡️

今更解説する Pod Security Admission with GKE

2022/12/28に公開約32,300字

こんにちは、クラウドエースの阿部です。
こちらの記事では、Pod Security Policy の後継機能となる Pod Security Admission Controller について解説します。
なお、Pod Security Policy廃止とPod Security Admissionについては以前より様々なブログで解説記事があります。
それらの記事と重複する部分は多々ありますが、それらは踏まえつつ自分なりに今更解説していこうと思います。[1]

TL;DR: Pod SecurityPolicy の廃止と Pod Security Admission について

Kubernetes では、Pod のセキュリティ向上の機能として Security Context があり、 Security Context の設定を強制する機能として Pod Security Policy が提供されていました。
しかし、この Pod Security Policyは Kubernetes バージョン1.21で非推奨となり、バージョン1.25で削除予定になっています。そのため、Pod Security Policyの機能を利用しているGKEユーザーは、コントロールプレーンがバージョン1.25に強制アップグレードされる前に代替手段へ移行する必要があります。
Pod Security Policyの代替機能として、GKEでは以下の機能が提案されています。なお、本記事では GatekeeperとGKE Autopilotに関する説明は行いません。

Pod Security Policyのサポート終了については、下記のドキュメントを参照してください。

https://cloud.google.com/kubernetes-engine/docs/deprecations/podsecuritypolicy

Pod の Security Context について

Pod の Security Context は、Podで動作するコンテナアプリケーションに対して一定の制約を設定する事ができ、万が一Pod内のコンテナが攻撃を受けてもその後の影響を最小限に留める事が可能になります。
Pod の Security Context は Deployment の podTemplate等で設定することができ、コンテナの動作に一定の制限をかけます。

https://kubernetes.io/docs/tasks/configure-pod-container/security-context/

Pod Security Policy について

Pod の Security Context は、基本的には(アプリケーションの本質的な動作に影響を与えない限り)できるだけ設定するべきものです。
しかし、殆どの場合、Pod の定義はアプリケーション開発者が行うものであり、普段からセキュリティに注意を払っていなければ、設定漏れは発生します。
そこで、 Security Context の設定を強制し、設定が漏れている場合はPodの起動を行えないようにする機能が存在します。それがPod Security Policyになります。
Pod Security Policy Admission Controllerをクラスタで有効化することで、適切なPod Security Policy設定と、ポリシーに沿ったPod(DeploymentのpodTemplate)の Security Context 設定を組み合わせないとPodが起動できない状態になります。
この機能によって、Pod の Security Context 設定を強制することが可能になります。
しかし、Pod Security Policyはv1.21で非推奨になり、v1.25で廃止する事が決まりました。

https://kubernetes.io/docs/concepts/security/pod-security-policy/

Pod Security Policyの廃止の経緯

廃止の経緯については筆者も詳しくないため、詳細な経緯について興味のある方は下記のQiita記事やKEP-2579をご覧頂ければ思います。

https://qiita.com/kiyoshim55/items/0262d42b57320bea2d00

https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2579-psp-replacement#motivation

ざっくり経緯ついて箇条書きにすると、Pod Security Policyには以下のような問題点があり、普及しなかったため廃止になったと考えられます。

  • Pod Security PolicyはRBACを使った認可によりポリシー適用を行うため、PodをデプロイするユーザーだけでなくDeploymentやReplicaSetといったPodコントローラのサービスアカウントにも認可を行う必要がある。そのためRBAC設定やPodのサービスアカウント設定等が複雑になる。
  • Pod Security Policy Admission Controllerは有効化すると全てのNamespaceのPodに対して有効になるため、有効化するためにはクラスタ全てのPodの設定を変更する必要がある。また、Auditログだけ出力するモードもないため、本番環境は一発勝負で設定することになる。
  • 複数のポリシーが有効な場合、優先順位が分かりにくい。

Pod Security Admission について

複雑で分かりにくいPod Security Policyに代わり、Pod Security Admissionが提案されました。
Pod Security Admissionは、Pod Security Standards に基づいて Pod の Security Context 設定をチェックし、必要に応じてWarningメッセージ、Auditログ、強制化を設定することができます。
Pod Security Standards は、Pod に必要なセキュリティ設定を規定したガイドラインです。このガイドラインに従ったPod設定にすることで、一定の安全性をもったアプリケーションを運用することが可能になります。

https://kubernetes.io/docs/concepts/security/pod-security-admission/

GKEにおける Pod Security Admission について

Pod Security Admission は Kubernetes v1.23からbetaとして提供されており、v1.25からstableとして提供予定です。
GKEではコントロールプレーンのバージョンがv1.23以降であれば使用可能です。

https://cloud.google.com/kubernetes-engine/docs/how-to/podsecurityadmission

Pod Security Admission の概要

Pod Security Admission はKubernetesにデフォルトで組み込まれており、Feature Gatesで有効化するだけで使う事ができます。
GKEでは、Pod Security Policy のように構築オプションで有効化する必要はなく、デフォルトで有効になっています。

Podに対して Pod Security Admission を有効化するには、Namespace のラベルを設定します。

設定するラベル

Pod Security Admissionを有効にする際にNamespaceに設定するラベルは以下の通りです。

ラベル名 設定値 動作
pod-security.kubernetes.io/<MODE> ProfileのLEVEL(Profile名) 設定値のガイドラインに適合しているかをチェックし、適合していない場合は<MODE>に従った動作を実行します。
pod-security.kubernetes.io/<MODE>-version Profileのバージョン 適用するPod Security Standardsのバージョンを指定します。省略した場合はコントロールプレーンの動作バージョンと同じもの(つまりlatest)が適用されます。
コントロールプレーンのマイナーバージョンが上がった場合、適用されるPod Security Standardsも変更される場合があるため、バージョン間の非互換影響を抑える場合に設定します。

Pod Security AdmissionのMODE

Pod Security Admissionの動作を指定するMODEは以下の通りです。

MODE 動作
warn kubectlコマンド等で Pod Security Standards に違反した Pod を作成したときに警告メッセージを応答します。Pod の作成には影響を与えません。
audit Pod Security Standards に違反した Pod を作成したときに監査ログを出力します。Pod の作成には影響を与えません。
enforce Pod Security Standards に違反した Pod を作成するとき、Pod の作成を停止してエラーメッセージを出力します。また、作成しなかったことを示すログも出力します。

Pod Security AdmissionのLEVEL

Podに対して適用する Pod Security Standards のLEVEL値(Profile名)は以下の通りです。

Profile名 LEVEL 説明
Privileged privileged Pod には制限をかけないポリシー。権限昇格等が可能です。未設定の場合はこの設定が適用されます。
Baseline baseline 権限昇格を防止する最小限のポリシー。Pod Security Standardsで必要とされる最低限のセキュリティ設定を要求します。
Restricted restricted Pod Security Standardsにおけるベストプラクティスに基づいて厳しく制限するポリシー。

設定例

例えば、default Namespaceに、enforceモードでRestrictedレベルのPod Security Admissionを設定する場合は以下の様なコマンドラインを実行します。

kubectl label --overwrite ns default pod-security.kubernetes.io/enforce=restricted

動作確認

では、実際の動作を確認していきます。

環境準備

まずは、適当な請求先アカウントが有効な Google Cloud プロジェクトを用意します。この手順の詳細は割愛します。

gcloud CLI のデフォルトプロジェクト設定

gcloud コマンドにデフォルトプロジェクト設定を行います。下記コマンドの {プロジェクトID} 部分を、前述で用意されている Google Cloud プロジェクト名に置き換えてください。
厳密に言うとこの設定をしなくても gcloud コマンドは使えるのですが、以降の gcloud コマンドサンプルに --project {プロジェクトID} オプションを追加する必要があります。

gcloud config set project {プロジェクトID}

VPC ネットワーク作成

psa-testネットワーク、および、psa-testサブネットワークを作成します。

# VPC ネットワーク作成
gcloud compute networks create psa-test --subnet-mode=custom

# VPC サブネットワーク作成
gcloud compute networks subnets create psa-test \
  --range=10.10.0.0/16 --network=psa-test --region=asia-northeast1 \
  --secondary-range=pods-cidr=10.11.0.0/16,services-cidr=10.12.0.0/20 \
  --enable-private-ip-google-access

Cloud NAT作成

psa-testネットワークに、psa-test-nat というCloud NATリソースを作成します。

# Cloud Router作成
gcloud compute routers create psa-test-nat --region=asia-northeast1 --network=psa-test

# Cloud NAT作成
gcloud compute routers nats create psa-test-nat --region=asia-northeast1 \
  --router=psa-test-nat --auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges

GKEクラスタ作成

GKEクラスタを作成します。ゾーンは asia-northeast1-bで、ノードには外部IPアドレスを付与せず、SPOTインスタンスを使う設定が入っています。
また、前述までに作成したVPCネットワーク・サブネットワークを指定するオプション等も付与しています。なお、--cluster-version オプションで指定するバージョンは、 gcloud container get-server-config コマンドで確認してください。

# クラスタ作成
gcloud container clusters create "psa-test" --zone "asia-northeast1-b" \
  --cluster-version "1.24.7-gke.900" --release-channel "regular" \
  --machine-type "e2-medium" --disk-type "pd-standard" --disk-size "50" --spot --num-nodes "1" \
  --enable-private-nodes --master-ipv4-cidr "172.16.0.0/28" --enable-ip-alias \
  --network psa-test --subnetwork psa-test \
  --cluster-secondary-range-name "pods-cidr" --services-secondary-range-name "services-cidr" \
  --no-enable-master-authorized-networks --node-locations "asia-northeast1-b"

クラスタ作成後、kubectlの認証情報を更新してください。

gcloud container clusters get-credentials psa-test --zone asia-northeast1-b

各モードの動作確認

default Namespaceに、各モードでRestrictedプロファイルを設定した場合に、モード毎にのような違いがあるかを見ていきます。
サンプルで使用するアプリケーションはみんな大好きNginxコンテナを使用します。

Warning モードの動作確認

まずは、 default Namespaceに Warning モードでPod Security Admissionを設定します。

kubectl label --overwrite ns default pod-security.kubernetes.io/warn=restricted

設定を確認するコマンドは、 kubectl get ns --show-labels です。

$ kubectl get ns --show-labels 
NAME              STATUS   AGE   LABELS
default           Active   12m   kubernetes.io/metadata.name=default,pod-security.kubernetes.io/warn=restricted
kube-node-lease   Active   12m   kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   12m   kubernetes.io/metadata.name=kube-public
kube-system       Active   12m   kubernetes.io/metadata.name=kube-system

default NamespaceのLABELS列を見てもらうと、pod-security.kubernetes.io/warn=restricted というラベルが追加されていることを確認できます。

この状態で、 Pod を作成します。マニフェストファイルを使ってkubectl applyコマンドで作成してもよいのですが、簡便にするため以下のコマンドを実行します。

kubectl run nginx --image=nginx

すると、以下の様なメッセージが表示されます。

Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
pod/nginx created

上記のように、WarningモードでPod Security Admissionを設定した場合は、実行したコマンドラインに対してWarningメッセージを表示する動作になります。
あくまでWarningメッセージであるため、Pod自体は起動状態です。

$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          2m3s

Deployment作成時も同様にWarningメッセージが表示されます。

$ kubectl create deployment nginx --image nginx
Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
deployment.apps/nginx created

Deploymentにおいても同様にPodは起動状態になります。

$ kubectl get pod
NAME                    READY   STATUS    RESTARTS   AGE
nginx                   1/1     Running   0          5m4s
nginx-8f458dc5b-gjnlb   1/1     Running   0          77s

Warning モード動作確認のクリーンアップ

以下のコマンドを実行し、作成したPodの削除とdefault Namespaceラベルのクリアを行います。

# Deployment削除
kubectl delete deployment nginx
# Pod削除
kubectl delete pod nginx
# Labels削除
kubectl label ns default pod-security.kubernetes.io/warn-

Audit モードの動作確認

default Namespaceに Audit モードでPod Security Admissionを設定します。

kubectl label --overwrite ns default pod-security.kubernetes.io/audit=restricted

ラベル付与状態を確認すると以下の様になります。

$ kubectl get ns --show-labels 
NAME              STATUS   AGE   LABELS
default           Active   28m   kubernetes.io/metadata.name=default,pod-security.kubernetes.io/audit=restricted
kube-node-lease   Active   28m   kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   28m   kubernetes.io/metadata.name=kube-public
kube-system       Active   28m   kubernetes.io/metadata.name=kube-system

この状態で、Warningモードのときと同様にPodを作成します。

kubectl run nginx --image=nginx

Auditモードの場合、コマンドラインにはメッセージが表示されません。

$ kubectl run nginx --image=nginx
pod/nginx created

Podは起動状態になります。

$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          17m

しかし、以下のクエリで Cloud Logging を検索するとAuditモードで検出したログエントリを確認することができます。

Cloud Logging クエリ
resource.type="k8s_cluster"
labels."pod-security.kubernetes.io/audit-violations"!=""

コマンドラインで確認したい場合は、gcloud logging readコマンドでクエリを実行すればよいです。

gcloud logging read 'resource.type="k8s_cluster" AND labels."pod-security.kubernetes.io/audit-violations"!=""'

※上記のコマンドで実行時点から過去1日分を検索します。必要に応じて、--freshnessオプションか、クエリに timestamp属性を追加する等してください。

ログエントリのlabels.pod-security.kubernetes.io/audit-violations属性を参照すると、以下の様な内容が出力されています。
この内容は、WarningモードでPod Security Admissionを設定したときのコマンドライン実行後に表示されたメッセージと同じ内容です。

would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

実行・結果表示は割愛しますが、Deploymentにおいても作成することができ、Podは起動状態になります。

Audit モード動作確認のクリーンアップ

# Pod削除
kubectl delete pod nginx
# Labels削除
kubectl label ns default pod-security.kubernetes.io/audit-

Enforce モードの動作確認

さて、それでは Enforce モードを設定します。

kubectl label --overwrite ns default pod-security.kubernetes.io/enforce=restricted

念のためNamespaceラベルを確認します。

$ kubectl get ns --show-labels 
NAME              STATUS   AGE   LABELS
default           Active   58m   kubernetes.io/metadata.name=default,pod-security.kubernetes.io/enforce=restricted
kube-node-lease   Active   58m   kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   58m   kubernetes.io/metadata.name=kube-public
kube-system       Active   58m   kubernetes.io/metadata.name=kube-system

Podを実行してみます。

$ kubectl run nginx --image=nginx
Error from server (Forbidden): pods "nginx" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

Warningモードと同様に、コマンドラインにメッセージを応答します。ただ、Warningモードのときはメッセージのプレフィックスが Warning:でしたが、Enforceモードでは Error from server (Forbidden): になっています。
Podが起動しているかを確認します。

$ kubectl get pod
No resources found in default namespace.

Podは起動しませんでした。Enforce=強制という名前通り、Podの起動を阻害しています。

また、EnforceモードではAuditモードと同様に特有のログエントリを生成します。
以下のクエリで Cloud Logging を検索するとEnforceモードで検出したログエントリを確認することができます。

resource.type="k8s_cluster"
protoPayload.response.reason="Forbidden"

Auditモードのとき同様、コマンドラインで確認したい場合は、gcloud logging readコマンドでクエリを実行すればよいです。

gcloud logging read 'resource.type="k8s_cluster" AND protoPayload.response.reason="Forbidden"'

上記のログエントリで検索したとき、Enforceモードのメッセージは protoPayload.response.message 属性で表示されます。

同様に、Deploymentも作成してみます。

$ kubectl create deployment nginx --image nginx 
deployment.apps/nginx created

Deployment自体は作成されますが、DeploymentでコントロールしているPodは作成されません。

$ kubectl get deployment
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   0/1     0            0           50s
$ kubectl get pod
No resources found in default namespace.

Deploymentで作成した場合、エラーメッセージはコマンドライン実行で表示されないため、若干分かりにくいです。
kubectl describe deploymentコマンドでEventsを確認しても表示されていませんでした。
kubectl get deployment -o yamlコマンドでstatusを確認すると、メッセージが出力されていました。

$ kubectl get deployment nginx -o yaml
...(snip)...
status:
  conditions:
  - lastTransitionTime: "2022-12-26T11:13:18Z"
    lastUpdateTime: "2022-12-26T11:13:18Z"
    message: Created new replica set "nginx-8f458dc5b"
    reason: NewReplicaSetCreated
    status: "True"
    type: Progressing
  - lastTransitionTime: "2022-12-26T11:13:18Z"
    lastUpdateTime: "2022-12-26T11:13:18Z"
    message: Deployment does not have minimum availability.
    reason: MinimumReplicasUnavailable
    status: "False"
    type: Available
  - lastTransitionTime: "2022-12-26T11:13:18Z"
    lastUpdateTime: "2022-12-26T11:13:18Z"
    message: 'pods "nginx-8f458dc5b-vt7zj" is forbidden: violates PodSecurity "restricted:latest":
      allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false),
      unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
      runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
      seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type
      to "RuntimeDefault" or "Localhost")'
    reason: FailedCreate
    status: "True"
    type: ReplicaFailure
  observedGeneration: 1
  unavailableReplicas: 1

Pod Security AdmissionをEnforceモードで設定していて、Deployment作成後にPodが起動しないケースでは、describe ではなく get で情報を参照した方がよさそうです。

Enforceモード動作確認のクリーンアップ

# Labels削除
kubectl label ns default pod-security.kubernetes.io/enforce-

注意事項

モード毎の動作でドキュメントにある説明以外で注意した方がよい事は、AuditモードとEnforceモードではログメッセージの検索クエリやメッセージ出力箇所が微妙に異なるということです。
アラート設定を共通化したいが、Enforceモードを使う環境とAuditモードを使う環境でクエリを変えると面倒くさいという場合は、AuditモードとEnforceモードを併用することも可能です。
その場合は以下の様にラベルを設定します。

# Auditモードを有効化
kubectl label --overwrite ns default pod-security.kubernetes.io/audit=restricted
# Enforceモードを有効化
kubectl label --overwrite ns default pod-security.kubernetes.io/enforce=restricted

以下の様に設定されます。

$ kubectl get ns --show-labels 
NAME              STATUS   AGE   LABELS
default           Active   85m   kubernetes.io/metadata.name=default,pod-security.kubernetes.io/audit=restricted,pod-security.kubernetes.io/enforce=restricted
kube-node-lease   Active   85m   kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   85m   kubernetes.io/metadata.name=kube-public
kube-system       Active   85m   kubernetes.io/metadata.name=kube-system

上記のように2つのモードを設定すると両方の動作が有効になるため、Pod Security Standardsに違反しているかをMonitoring Alertでも検知したい場合は、上記のように設定してAuditモードのログエントリで検出するのがよいと思います。

プロファイル毎の動作の違い

さて、モード毎の違いは確認できましたが、プロファイルではどのように動作が変わるでしょうか?

Baseline プロファイルでの動作

NginxコンテナをEnforceモードでBaselineプロファイルを強制した場合にどのような動作になるかを確認します。
まず、サンプルとなるマニフェストを用意します。簡便な検証にするため、Podリソースを作成するものとして、以下のようなYAMLファイルを用意します。
hostNetwork: trueが怪しげな設定です。

psa-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx
  hostNetwork: true

次に、BaselineプロファイルでPod Security Admissionを有効にします。

kubectl label --overwrite ns default pod-security.kubernetes.io/enforce=baseline

psa-pod.yaml を実行します。

kubectl create -f psa-pod.yaml

以下の様になりました。やはり hostNetwork: true は許されなかったようです。

$ kubectl create -f psa-pod.yaml 
Error from server (Forbidden): error when creating "psa-pod.yaml": pods "nginx" is forbidden: violates PodSecurity "baseline:latest": host namespaces (hostNetwork=true)

YAMLファイルを以下の様に修正します。hostNetworkの行を削除しました。

psa-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx

以下の通り実行できました。

$ kubectl create -f psa-pod.yaml 
pod/nginx created
$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          50s

Nginxコンテナであれば、特別な工夫をしなくても問題無く起動するようです。
Pod Security StandardsのBaselineセクションを見ると、ノードへ直接影響するような特権的な設定(hostNetworkの有効化等)を行わなければ、問題なさそうでした。

次の検証のために、Podは削除しておきましょう。

kubectl delete pod nginx

Restrictedプロファイルでの動作

次に、Restrictedプロファイルで動作を確認します。
Pod Security Admission のレベルを、 Baseline から Restricted に変更します。

kubectl label --overwrite ns default pod-security.kubernetes.io/enforce=restricted

先ほどBaselineでは問題無かったPodのYAMLファイルを用意します。

psa-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx

Podを起動してみます。

kubectl create -f psa-pod.yaml

以下の様なエラーメッセージが表示され、Podは起動できませんでした。

メッセージ
Error from server (Forbidden): error when creating "psa-pod.yaml": pods "nginx" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

メッセージを読み解くと、以下のような点がPod Security Standardsに違反しているようです。

  • allowPrivilegeEscalation が False ではない
  • Linux capabilities が制限されていない (システムコール呼び出しの制限がない)
  • runAsNonRoot が有効になっていない
  • seccompProfile が未設定である (RuntimeDefault または Localhost に設定する必要がある)

ということで、違反にならないように設定してみます。
以下は、前述のPod Security Standards違反の設定を、securityContextに追加したYAMLファイルになります。
違反に対する設定以外のものとして、securityContext.capabilities.add に、"NET_BIND_SERVICE"を追加しています。 "NET_BIND_SERVICE" がないとサービスがネットワークポートを開く事ができなくなるため、ネットワークサービスのコンテナに必要です。

psa-pod-2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx
      securityContext:
        allowPrivilegeEscalation: false
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
        capabilities:
          add: [ "NET_BIND_SERVICE" ]
          drop: [ "ALL" ]

再びPodを起動すると、今度はPodの起動に成功します。

$ kubectl create -f psa-pod-2.yaml 
pod/nginx created

しかし、Pod内のコンテナはエラーで起動していません。

$ kubectl get pod
NAME    READY   STATUS                       RESTARTS   AGE
nginx   0/1     CreateContainerConfigError   0          4m5s
$ kubectl describe pod nginx
...(snip)...
Events:
  Type     Reason     Age                    From               Message
  ----     ------     ----                   ----               -------
  Normal   Scheduled  4m19s                  default-scheduler  Successfully assigned default/nginx to gke-psa-test-default-pool-d9db09e5-mr66
  Normal   Pulled     4m12s                  kubelet            Successfully pulled image "nginx" in 5.261239314s
  Normal   Pulled     4m11s                  kubelet            Successfully pulled image "nginx" in 373.631503ms
  Normal   Pulled     3m55s                  kubelet            Successfully pulled image "nginx" in 396.083814ms
  Normal   Pulled     3m39s                  kubelet            Successfully pulled image "nginx" in 359.522063ms
  Normal   Pulled     3m26s                  kubelet            Successfully pulled image "nginx" in 447.688578ms
  Normal   Pulled     3m13s                  kubelet            Successfully pulled image "nginx" in 329.080794ms
  Normal   Pulled     2m58s                  kubelet            Successfully pulled image "nginx" in 474.825275ms
  Warning  Failed     2m42s (x8 over 4m12s)  kubelet            Error: container has runAsNonRoot and image will run as root (pod: "nginx_default(77102f5d-9429-43c8-90fe-fdd1dbd3673d)", container: nginx)
  Normal   Pulled     2m42s                  kubelet            Successfully pulled image "nginx" in 379.975173ms
  Normal   Pulling    2m27s (x9 over 4m18s)  kubelet            Pulling image "nginx"

どうやら、securityContext でrunAsNonRootを有効にしたにも関わらず、nginxコンテナはrootで起動しているため、起動失敗しているようです。
とりあえずPodは削除しておきます。

kubectl delete pod nginx

では、次のYAMLファイルではどうでしょうか。先ほどのYAMLに、 runAsUser: 101 を追加し、強制的に一般ユーザーで起動するように設定しました。
公式のDockerfileを参照すると、nginxコンテナのnginxユーザーはUID:101で作成されているため、rootの代わりにnginxユーザーで起動すればいいのではないかという雑な推測です。

psa-pod-3.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx
      image: nginx
      securityContext:
        allowPrivilegeEscalation: false
        runAsNonRoot: true
        runAsUser: 101
        seccompProfile:
          type: RuntimeDefault
        capabilities:
          add: [ "NET_BIND_SERVICE" ]
          drop: [ "ALL" ]

Podを起動して確認してみます。とりあえずPod自体は起動し、コンテナも起動したものの、プロセス起動エラーによりCrashLoopBackOffになっています。

$ kubectl create -f psa-pod-ng-3.yaml 
pod/nginx created
$ kubectl get pod
NAME    READY   STATUS             RESTARTS      AGE
nginx   0/1     CrashLoopBackOff   4 (18s ago)   108s
$ kubectl describe pod nginx
...(snip)...
Events:
  Type     Reason     Age                 From               Message
  ----     ------     ----                ----               -------
  Normal   Scheduled  2m6s                default-scheduler  Successfully assigned default/nginx to gke-psa-test-default-pool-d9db09e5-mr66
  Normal   Pulled     2m4s                kubelet            Successfully pulled image "nginx" in 422.888944ms
  Normal   Pulled     2m3s                kubelet            Successfully pulled image "nginx" in 433.888116ms
  Normal   Pulled     109s                kubelet            Successfully pulled image "nginx" in 413.270396ms
  Normal   Created    80s (x4 over 2m4s)  kubelet            Created container nginx
  Normal   Started    80s (x4 over 2m4s)  kubelet            Started container nginx
  Normal   Pulled     80s                 kubelet            Successfully pulled image "nginx" in 390.089508ms
  Warning  BackOff    51s (x7 over 2m3s)  kubelet            Back-off restarting failed container
  Normal   Pulling    37s (x5 over 2m5s)  kubelet            Pulling image "nginx"
  Normal   Pulled     36s                 kubelet            Successfully pulled image "nginx" in 425.511935ms
$ kubectl logs nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/12/27 06:12:02 [warn] 1#1: the "user" directive makes sense only if the master process runs with super-user privileges, ignored in /etc/nginx/nginx.conf:2
nginx: [warn] the "user" directive makes sense only if the master process runs with super-user privileges, ignored in /etc/nginx/nginx.conf:2
2022/12/27 06:12:02 [emerg] 1#1: mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)
nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)

どうやら、公式のnginxコンテナイメージは、root権限でないと一時ディレクトリにディレクトリ作成やファイル作成等が行えず、起動に失敗するようです。
下記のQiita記事にも記載されていますが、nginxユーザーで起動できるように、ファイル・ディレクトリの権限をnginxで読み書きできるように変更する必要があります。

https://qiita.com/nigamizawa/items/1cd6a1a73a43a51d2f1c#nginxユーザーでも起動できるようにファイルの権限を設定する

結構大変ですね…… ただ、記事の最後に記載されているように、 nginx-unprivileged というコンテナを使うことで、nginxユーザーで起動する状態のイメージを使う事が可能です。
そこで、再度YAMLファイルを修正します。今度は、nginxのイメージを、 公式のnginxイメージからnginx-unprivilegedイメージに変更します。

psa-pod-4.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: web
spec:
  containers:
    - name: nginx-unprivileged
      image: nginxinc/nginx-unprivileged
      securityContext:
        allowPrivilegeEscalation: false
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
        capabilities:
          add: [ "NET_BIND_SERVICE" ]
          drop: [ "ALL" ]

コンテナ起動に失敗するPodを削除した後、再度Podを起動します。
今度は、正しく起動できました。

$ kubectl delete pod nginx
pod "nginx" deleted
$ kubectl create -f psa-pod.yaml 
pod/nginx created
$ kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          18s
$ kubectl describe pod nginx
...(snip)...
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  33s   default-scheduler  Successfully assigned default/nginx to gke-psa-test-default-pool-d9db09e5-mr66
  Normal  Pulling    32s   kubelet            Pulling image "nginxinc/nginx-unprivileged"
  Normal  Pulled     25s   kubelet            Successfully pulled image "nginxinc/nginx-unprivileged" in 6.348946209s
  Normal  Created    25s   kubelet            Created container nginx-unprivileged
  Normal  Started    25s   kubelet            Started container nginx-unprivileged
$ kubectl logs nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/12/27 06:21:17 [notice] 1#1: using the "epoll" event method
2022/12/27 06:21:17 [notice] 1#1: nginx/1.23.3
2022/12/27 06:21:17 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2022/12/27 06:21:17 [notice] 1#1: OS: Linux 5.10.147+
2022/12/27 06:21:17 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2022/12/27 06:21:17 [notice] 1#1: start worker processes
2022/12/27 06:21:17 [notice] 1#1: start worker process 29
2022/12/27 06:21:17 [notice] 1#1: start worker process 30

試しにアクセスしてみます。下記のコマンドで、Podが提供している8080番ポートへ、端末から直接アクセスします。
画面コピーは割愛しますが、無事 Welcome to nginx! のページを参照できました。

# nginx PodのServiceを作成
kubectl port-forward pod/nginx 8080:8080

長くなりましたが、 nginx のように比較的広範に使われているコンテナでも、Restrictedレベルの制約に対応するには一工夫必要になります。

Google Cloud環境クリーンアップ

検証が完了したら、今回使用した環境は削除しておきましょう。

# GKEクラスタ削除
gcloud container clusters delete psa-test --zone asia-northeast1-b
# Cloud NAT削除
gcloud compute routers nats delete psa-test-nat --router psa-test-nat --region asia-northeast1
# Cloud Router削除
gcloud compute routers delete psa-test-nat --region asia-northeast1
# VPCサブネットワーク削除
gcloud compute networks subnets delete psa-test --region asia-northeast1
# VPCネットワーク削除
gcloud compute networks delete psa-test

Pod Security Admission のユースケース

Pod Security Admissionは、以下の様なケースで使うとよいと思います。

  • 新規でシステム開発する場合、運用したいアプリケーションのNamespaceに Baseline プロファイルか Restricted プロファイルをEnforceで設定する
    • セキュリティ要件が厳しい場合は Restrictedを、そうでない場合は Baseline で十分
  • 既存のシステムにPod Security Admissionを適用したい場合、本番環境はまずはAuditモードのみ適用し、開発環境やテスト環境等でEnforceモードを適用する
    • ログをそろえたい場合は、全環境Auditモードを設定し、Enforceモードは適用したい環境のみ併用
    • AuditモードやEnforceモードはあくまでPodが適切な制約で起動したことをチェックするため、実際にPod内部のコンテナが起動し運用に影響しないかはテストが必要
  • 現在 Pod Security Policy を使用していて、設定済みのポリシーの内容がPod Security StandardsのBaselineやRestrictedの内容で十分満たされる
    • 既に設定済みのポリシーがきめ細やかな場合はPod Security AdmissionではなくOPA Gatekeeper等別の仕組みの検討が必要
  • Namespace単位の大雑把な設定で十分である
    • より細やかなポリシーが必要な場合は別の仕組みが必要

まとめ

Pod Security Policy の後継機能、 Pod Security Admission について解説しました。
設定自体はNamespaceに特定のラベルを付与するだけで有効になり、設定の種類もシンプルであるため、非常に理解しやすい機能になっています。
何もせず代替できるものではありませんが、v1.25への強制アップグレードは1年切っているためこれくらいシンプルな方が移行しやすいのではないかと思います。
Pod Security Policy を使っている方は、早めに確認していただくのがよいと思います。

その他の参考情報

https://qiita.com/uesyn/items/cf47e12fba5e5c5ea25f

https://atmarkit.itmedia.co.jp/ait/articles/2208/30/news003.html

脚注
  1. この記事のタイトルは私がよく視聴するゲーム実況者ふぅさんの「今更解説するダークソウル」シリーズにあやかってつけました。この場を借りて謝辞申し上げます。 ↩︎

Discussion

ログインするとコメントできます