PodSecurityPolicyの廃止に備えて、一足先にPodSecurity Admissionを試してみよう!

# PodSecurityPolicyの廃止とPodSecurity

Kubernetesのv1.21でPodSecurityPolicy(以下PSP)が非推奨となり、v1.25では削除される予定となっています。
PSPが廃止されてしまいますが、代わりにPodSecurityという新しい組み込みのAdmission Controllerが追加されるようです。
順調に開発が進めばv1.22からalpha機能として利用可能となるようですが、待ちきれずコア機能がmergeされた段階で今できることを検証してみました。
以下はドキュメントもない状態なので、コードを軽く確認しながら動作てみたため記事にしてみました。

**2020/09/02 追記**
Kubernetes Meetup Tokyo #44でこの内容を若干アップデートした内容を発表させていただきました。スライドは[こちら](https://speakerdeck.com/uesyn/from-psp-to-podsecurity)にあります。

## 検証バージョンについて

**PodSecurityは現在(2021/7/1)時点でこちらはalphaとしてすら公開されているものではないため、急速に変更が加わっています。そのためalpha機能提供時にはこちらの内容と大きな差がある可能性があります。**

[こちらのコミット時点](https://github.com/kubernetes/kubernetes/tree/5bd3334ad69a074fafa7f1153e48ae08ca196b07)のKubernetesで検証しています。PodSecurityのコア機能が追加され、多少のポリシーのチェックが追加されたような状態です。

# PSPを軽くおさらいする

PodSecurityの前に、PSPの利用について振り返ってみます。PSPについては[こちらの記事](https://qiita.com/tkusumi/items/6692af743ae03dc0fdcc)が参考になります。
KubernetesのRBACを利用するとPodを作成できるかどうかの制御ができますが、Podの特性を確認した上で作成してよいかどうかという制御ができません。
それに対してPSPを利用することで、PodのsecurityContextやマウント可能なvolume、ネットワークの機能を制限することができ、制限された機能を利用しようとしたPodは作成できなくなります。これにより必要以上の権限を持ったPodを作成せずに済んだり、意図しない権限昇格を防ぐことができるようになります。

PSPは以下のような専用のリソースを作成し、そのリソースに対するRBACをユーザ・サービスアカウントに付与することで利用可能となります。

```yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default'
    apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
    apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
  privileged: false
  # Required to prevent escalations to root.
  allowPrivilegeEscalation: false
  requiredDropCapabilities:
    - ALL
  # Allow core volume types.
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    # Assume that ephemeral CSI drivers & persistentVolumes set up by the cluster admin are safe to use.
    - 'csi'
    - 'persistentVolumeClaim'
    - 'ephemeral'
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    # Require the container to run without root privileges.
    rule: 'MustRunAsNonRoot'
  seLinux:
    # This policy assumes the nodes are using AppArmor rather than SELinux.
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      # Forbid adding the root group.
      - min: 1
        max: 65535
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      # Forbid adding the root group.
      - min: 1
        max: 65535
  readOnlyRootFilesystem: false
```

上記リソースの権限付与は以下のようなCLusterRole/ClusterRoleBindingで実現します。

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: restricted-psp
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - restricted
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: restricted-psp
roleRef:
  kind: ClusterRole
  name: restricted-psp
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated
```

利用可能なPSPがない場合、Podが利用できないため、既存のクラスタを有効にするときは注意が必要です。
利用可能なPSPがない場合のデフォルトのPSPを実現するには、上記のマニフェストのように認証済みのユーザ(サービスアカウントも含む)の属性である`system:authenticated`グループに対して利用可能なPSPを定義しておくことで実現することができます。

## PSPの問題点

PSPが廃止される理由は以下の記事やPodSecurityのKEPのMotivationを参照ください。

- [PodSecurityPolicy Deprecation: Past, Present, and Future](https://kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/#why-is-podsecuritypolicy-going-away)
- [KEP-2579: Pod Security Admission Control - Motivation](https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2579-psp-replacement#motivation)

公式の意見としては上記のリンク先の通りなのですが、個人的にPSPを触り始めて大変だなと感じていたのは`PSPはPod作成時のリクエストユーザ、またはPodに指定されたServiceAccountの権限でどのPSPが適用されるか決定される`ということです。
単純にPodを作成するだけであれば理解しやすいと思います。問題となるのはDeploymentやStatefulSetのように`PodTemplate`を持つものを動かした場合です。これらは**実際にPod作成のリクエストを投げるのはユーザではなくそれぞれのコントローラ**となります。
そのためDeploymentを作成しようとした時に、利用可能なPSPが存在せずPodが起動しないということとなり得ます。「自分はPrivilegedなPodを作成できるはずなのに、なぜPodが作成できないのか？」という経験をした事がある方もいらっしゃるのではないでしょうか。
これを回避するためにはコントローラに強めのPSPを利用できるようにするか、以下のようにPodのspecにPod作成に必要なPSPが利用できるServiceAccountを指定することになります。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  serviceAccountName: my-sa
```

コントローラに強めのPSPを利用できるようにするのは、実質制限がなくなると同義になってしまうため、ServiceAccountを利用することとなるはずです。

さて上記を踏まえた上で、あるNamespace Aにとても強いPSP(例えばPrivilegedなPodが作成できるなど)が利用可能なServiceAccount Aが存在している場合を考えてみましょう。
あるユーザAはNamespace AでPodを作成することができるとします。しかしこのユーザAは制限されたPSPしか利用できないように設定されていたとしましょう。このユーザがPod作成時にServiceAccount Aを指定してPrivilegedなPodを作成したらどうなるでしょうか？
この場合ユーザはPrivilegedなPodが作成できてしまいます。**ユーザが許可されていなくとも、Namespace内のサービスアカウントで許可されているPSPをユーザは利用できるということです。**
個人的にこれは非常にわかりづらい挙動でした。また、あるユーザがあるNamespaceで許されているPSPはどうなっているのか？という確認が、この挙動により非常に面倒となっていました。PSPの柔軟さを残したまま、このような部分がPodSecurityで解決していると嬉しいなと思いながら調査をしました。

# PodSecurity

やっと本題のPodSecurityです。
現時点でこれについて一番詳細なドキュメントは、おそらく[KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2579-psp-replacement)かと思われます。公式のドキュメントには執筆時点でPodSecurityについて使い方等の説明はありません。そのためコードやKEPを適宜確認しながら挙動を確認していきました。

## PodSecurityを有効化する

まずPodSecurityを有効化する方法ですが、FeatureGatesにて`PodSecurity`を有効化することで利用可能となります。PodSecurityはAdmission Controllerとして実装されるのですが、[PSPと異なりデフォルトで有効](https://github.com/kubernetes/kubernetes/blob/5bd3334ad69a074fafa7f1153e48ae08ca196b07/pkg/kubeapiserver/options/plugins.go#L164)となっています。そのためFeatureGatesを有効化するだけで利用可能です。

## PodSecurityについて

PodSecurityは[Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/)で定義されているポリシーを適用し、作成しようとしているPodがそれを満たさない場合、作成を拒否したり、警告に表示したり、audit logへ記録するといったことを制御します。(**検証時点ではPodSecurityStandardの全ては実装されていません**)
PodSecurityはValidationは実行しますが、ポリシーを満たさないPodをポリシーを満たすようにリソースを編集したりはしません。

Pod Security Standardsには`enforce`,`audit`,`warn`の3つのモードがあります。それらに対してそれぞれPod Security Standardsで定義されている`restricted`, `baseline`, `privileged`をポリシーとして設定します。`privileged`は制限がなく`baseline`, `restricted`の順に制限が厳しいものとなっています。それぞれのモードでポリシーに違反するPodの作成が検知された場合モード毎に挙動が異なります。またそれぞれのモードは別々のポリシーを設定可能です。

- enforce: Podの作成を拒否します。
- audit: audit logのアノテーションとして記録します。これを満たさない場合でもPodの作成の可否には影響しません。
- warn: warningとして表示します。これを満たさない場合でもPodの作成の可否には影響しません。

上記の実装部分は[こちら](https://github.com/kubernetes/kubernetes/blob/5bd3334ad69a074fafa7f1153e48ae08ca196b07/staging/src/k8s.io/pod-security-admission/admission/admission.go#L338-L379)です。

## PodSecurityを適用する方法

PodSecurityによるポリシーの強制はNamespaceのラベルにより制御します。そのためPSPのような専用のリソースはありません。
それぞれのモード毎に設定が可能であり、`pod-security.kubernetes.io/{enforce,audit,warn}` のようなラベルに対して、値として`restricted`, `baseline`, `privileged`を指定します。
例えば`baseline`を違反した場合には警告を表示するが、Podの作成を拒まない場合には以下のように設定します。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  labels:
    pod-security.kubernetes.io/enforce: privileged
    pod-security.kubernetes.io/audit: privileged
    pod-security.kubernetes.io/warn: baseline
  name: test
```

このようにNamespaceのラベルで制御するため、**Namespaceの作成・編集権限があるということは、ユーザへPSPを利用できるようにClusterRole/ClusterRolebindingを作成するのと同じような権限がある**ということになります。
PSPを利用しているクラスタで運用者と利用者が分かれている場合は、PSPからPodSecurityへ移行する上で気をつけなければいけない点になるかと思います。

そしてPSPと異なり、PodSecuirtyにはデフォルト値が存在します。[設定がないモードは`privileged`つまり制限がないものとして扱われます](https://github.com/kubernetes/kubernetes/blob/5bd3334ad69a074fafa7f1153e48ae08ca196b07/staging/src/k8s.io/pod-security-admission/admission/api/v1alpha1/defaults.go#L29-L37)。
KEPによると今後PodSecurityはデフォルト有効化されるようなので、デフォルト有効化されたバージョンになってもこれらの設定がなければ「PodSecurityは機能しているが、制限がない状態」ということになり、既存のクラスタへの影響はなさそうです。

## PodSecurityの設定を変更する方法

他の組み込みのAdmission Controllerと同様にPodSecurityも設定が変更できコードを確認した限り、以下のような設定が可能のようです(動作未検証)。

- `pod-security.kubernetes.io/{enforce,audit,warn}`ラベルが付与されていないときのデフォルト値
- 設定がないモードのデフォルト値やポリシーのバージョン
- PodSecurityの制約を受けないユーザ
- PodSecurityの制約を受けないNamespace
- PodSecurityの制約を受けないRuntimeClass

組み込みのAdmission Controllerはkube-apiserverの`--admission-control-config-file`により設定できそうです。
KEPの段階ではPodSecurityのためのコンフィグは`apiserver.config.k8s.io`だったようですが[議論](https://github.com/kubernetes/kubernetes/pull/103099#discussion_r660100675)の末、[`pod-security.admission.config.k8s.io`となった](https://github.com/kubernetes/kubernetes/blob/5bd3334ad69a074fafa7f1153e48ae08ca196b07/staging/src/k8s.io/pod-security-admission/admission/api/v1alpha1/register.go#L25)ようです。確認時点で設定ファイルのVersionは`v1alpha1`のみが存在する状態でした。

```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  path: podsecurity.yaml
```

```yaml:podsecurity.yaml
apiVersion: pod-security.admission.config.k8s.io/v1alpha1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  configuration:
    defaults:  # pod-security.kubernetes.io/{enforce,audit,warn} ラベルがセットされていないとき
      enforce:         <default enforce policy level>
      enforce-version: <default enforce policy version>
      audit:         <default audit policy level>
      audit-version: <default audit policy version>
      warn:          <default warn policy level>
      warn-version:  <default warn policy version>
    exemptions: # PodSecurityの対象外の設定
      usernames:         [ <array of authenticated usernames to exempt> ]
      runtimeClassNames: [ <array of runtime class names to exempt> ]
      namespaces:        [ <array of namespaces to exempt> ]
...
```

上記設定を見てお気づきかもしれませんが、ポリシーにはバージョンの概念があります。
まだポリシーの実装が完全には終わっていなかったため、どのようにバージョンが利用されていくか確認できておりませんが、PodのSpecやPod Security Standardsが更新がありPodSecurity内のポリシーの実装も更新の必要があった時にバージョンが上がっていくのでしょうか？ 指定しない場合は最新のバージョンである`latest`が指定されるようです。


## PodSecurityを利用する

### dry-runで実行する

今後PodSecurityがデフォルト有効となっても、上記の通りnamespaceにPodSecurityのラベルがなければ制限はされません。
既にクラスタを利用していて、Podが存在するnamespaceにPodSecurityのenforceを設定したくなることもあるでしょう。
特にPodSecurityで`enforce`を適用する場合、満たさないPodがいた場合、次回Pod作成時に起動しなくなってしまいます(admission controllerなので起動中のPodには影響がないため)。
PodSecurityでは実際に`enforce`の設定を適用する前にdry-runが実行できるようになっています。

ここでは`default`ネームスペースに`pod-security.kubernetes.io/enforce: baseline`を適用したいが、それを満たさないPodが存在していたという状況を考えてみます。
以下のPrivilegedなPodを`default`ネームスペースへ作成しておきます。このPodは`baseline`では違反となるPodです。

```yaml:privileged-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    securityContext:
      privileged: true
```

PodSecurityのdry-runはNamespaceのラベルの更新リクエストをserver-sideなdry-runで実行することで実行できます。

```sh
$ kubectl label --dry-run=server --overwrite ns default pod-security.kubernetes.io/enforce=baseline
Warning: nginx: privileged == true
namespace/default labeled
```

上記のようにdry-runでコマンドを実行すると、nginxがprivilegedなコンテナを含むという警告が表示されていることが確認できます。
enforceのようにPodの起動に影響してしまう設定を、事前にdry-runで確認できるのは素敵ですね。


### PodSecurityを適用する

さて今度は別のnamespaceでテストします。ここでは`test-ps`というnamespaceを作成しました。
このnamespaceは`enforce`と`warn`が`baseline`に設定してあります。そのため先ほどの`privileged: true`なコンテナを持つPodは起動できないようになっているはずです(そして作成しようとすると警告も表示されるはず)。
先ほどと同じspecのPodを、そのnamespaceへ作成を試みます。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: test-ps
  labels:
    kubernetes.io/metadata.name: test-ps
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/audit: privileged
    pod-security.kubernetes.io/warn: baseline
```

```sh
$ kubectl apply -f privileged-pod.yaml
Warning: Pod violates PodSecurity profile baseline:latest: privileged == true (spec.containers[0].securityContext.privileged)
Error from server (Forbidden): error when creating "privileged-pod.yaml": pods "nginx" is forbidden: privileged == true (spec.containers[0].securityContext.privileged)
```

正常にPodの作成がrejectされました :tada:

そしてKEPによると、DeploymentやStatefulSetのようなPodTemplateの領域もwarnやauditは機能してくれそうです。
enforceは実際のPod作成時のみなので、それらのリソースが作成できなくなるわけではありませんが、Deployment作成直後やdry-runによって事前にPodが作成できるかどうかが確認できます。PSPの時のようにリソース作成後にReplicaSetやStatefulSetのイベントを確認しなくても、事前にチェックできるのは嬉しいですね！PodTemplateは[built-inなリソース](https://github.com/kubernetes/kubernetes/blob/5bd3334ad69a074fafa7f1153e48ae08ca196b07/staging/src/k8s.io/pod-security-admission/admission/admission.go#L88-L98)であれば[Validationしてくれそうです](https://github.com/kubernetes/kubernetes/blob/5bd3334ad69a074fafa7f1153e48ae08ca196b07/staging/src/k8s.io/pod-security-admission/admission/admission.go#L316-L336)。[KEPを見る限り](https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2579-psp-replacement#podtemplate-resources)カスタムリソースについても今後何かしら対応策がでてきそうです。

早速試してみましょう。上記のnamespaceに以下のようなPrivilegedなコンテナを含むDeploymentの作成をdry-runで試みます。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx
  name: nginx
  namespace: test-ps
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        securityContext:
          privileged: true
```

```sh
$ kubectl apply --dry-run=server -f privileged-deployment.yaml
Warning: Pod violates PodSecurity profile baseline:latest: privileged == true (spec.containers[0].securityContext.privileged)
deployment.apps/nginx created (server dry run)
```

Deploymentの作成リクエストで警告が確認できました :tada: 

## PodSecurityでカスタムポリシーを利用する

PodSecurityはPod Security Standardsで定義されている`privileged`, `baseline`, `restricted`を適用します。それ以外のポリシーを適用したい場合、PSPのように簡単には用意できなさそうです。そのため[Flexible Extension Support](https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2579-psp-replacement#flexible-extension-support)と[Custom Profiles](https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2579-psp-replacement#custom-profiles)を読む限り、APIServerのフラグでカスタムポリシーを設定し、そのポリシーを処理するPodSecurityのようなWebhookを自前で実装する必要があるようにみえます。`baseline`でhostPathの一部だけ許可したようなポリシーを適用したい場合でも、別途実装が必要となると...多少大変そうですね。

# 最後に

PSPより柔軟性はなくなっているものの、デフォルトで有効になっているセキュリティの機能としては非常に楽しみですね。
v1.22でPodSecurityがalphaとしてリリースされたらこの記事からの差分をまとめたいと考えています。
またカスタムポリシーのための実装がまとまってきたら、それも調査していこうと思います。