今更解説する Pod Security Admission with GKE
こんにちは、クラウドエースの阿部です。
こちらの記事では、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 Admission Controller
- OPA Gatekeeper (Anthos Config Management の Policy Controller)
- GKE Autopilot
Pod Security Policyのサポート終了については、下記のドキュメントを参照してください。
Pod の Security Context について
Pod の Security Context は、Podで動作するコンテナアプリケーションに対して一定の制約を設定する事ができ、万が一Pod内のコンテナが攻撃を受けてもその後の影響を最小限に留める事が可能になります。
Pod の Security Context は Deployment の podTemplate等で設定することができ、コンテナの動作に一定の制限をかけます。
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で廃止する事が決まりました。
Pod Security Policyの廃止の経緯
廃止の経緯については筆者も詳しくないため、詳細な経緯について興味のある方は下記のQiita記事やKEP-2579をご覧頂ければ思います。
ざっくり経緯ついて箇条書きにすると、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設定にすることで、一定の安全性をもったアプリケーションを運用することが可能になります。
GKEにおける Pod Security Admission について
Pod Security Admission は Kubernetes v1.23からbetaとして提供されており、v1.25からstableとして提供予定です。
GKEではコントロールプレーンのバージョンがv1.23以降であれば使用可能です。
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モードで検出したログエントリを確認することができます。
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
が怪しげな設定です。
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
の行を削除しました。
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ファイルを用意します。
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"
がないとサービスがネットワークポートを開く事ができなくなるため、ネットワークサービスのコンテナに必要です。
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ユーザーで起動すればいいのではないかという雑な推測です。
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で読み書きできるように変更する必要があります。
結構大変ですね…… ただ、記事の最後に記載されているように、 nginx-unprivileged
というコンテナを使うことで、nginxユーザーで起動する状態のイメージを使う事が可能です。
そこで、再度YAMLファイルを修正します。今度は、nginxのイメージを、 公式のnginxイメージからnginx-unprivileged
イメージに変更します。
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 を使っている方は、早めに確認していただくのがよいと思います。
その他の参考情報
-
この記事のタイトルは私がよく視聴するゲーム実況者ふぅさんの「今更解説するダークソウル」シリーズにあやかってつけました。この場を借りて謝辞申し上げます。 ↩︎
Discussion