Pod Security AdmissionでPodの特権を制限する

この記事は[NTTコムウェア Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nttcomware)の21日目の記事です。

Kubernetes(k8s)には、Podによる他リソースへのアクセス権限をポリシーとして定義し制御できる機能として、Pod Security Policy(PSP)という機能がありました。

が、しかし、このPSPはk8s v1.21から非推奨となりました[^1]。

そして非推奨となったPSPの後継機能として新たに**[Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/)**という機能がk8s v1.22から提供されるようになりました。

本稿では、この新機能Pod Security Admission(PSA)について概要や具体例等を紹介します。

[^1]: [こちらのk8s公式ブログ](https://kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/)に非推奨に至る経緯等が詳しく記載されています。

なお、本稿は、k8sの概要およびある程度の操作を習得している方を対象とします。

## 前提知識：Podセキュリティ標準とは

PSAを理解するための前提知識として「Podセキュリティ標準(Pod Security Standards)」というものを知っておく必要があるため、ここで紹介します。

k8sの公式サイトに「[Podセキュリティ標準](https://kubernetes.io/ja/docs/concepts/security/pod-security-standards/)」というドキュメントがあります。このドキュメントでは、Podが他リソースへアクセスする際の権限のセットを以下の3つのレベルに大別し、Podに動作要件や目的に合致したレベルの権限のみを与えることで、k8sクラスタのセキュリティを確保しましょう、という内容が記載されています。
これは例えるなら、Linuxにおいて、rootユーザ、一般ユーザ、ゲストユーザ[^2]を定義し、利用者ごとに適切なユーザ権限を与えましょう（全員にroot権限を与えないで）、という話に似ています。

[^2]: Linuxに明確にゲストユーザという概念はありませんが、例えばユーザは存在し一部サービスへのアクセスは出来るが、ssh等でのログインは禁止されている、など、権限が一般ユーザよりさらに制限されているという状況をイメージしてください。

**「Podセキュリティ標準」で定義の3つの権限レベル**

* **privileged(特権)**
   * 全く制限がかかっていないポリシー。あらゆる特権昇格を認める。
   * Linuxに例えるとrootユーザ。
* **baseline**
   * privileged(特権)とrestricted(制限)の中間にあたり、最小限の制限を行うポリシー。
   * 端的に言えば、[Dockerのデフォルト設定でコンテナを動かす際の権限](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)が許可されている状態。
   * Linuxに例えると一般ユーザ。
* **restricted(制限)**
   * ほとんどの権限が厳しく制限されたポリシー。
   * 理想的にはなるべくこのポリシーでPodを動作させるとよりセキュアになる。
   * Linuxに例えるとゲストユーザ[^2]。

PSAにはこの3つのレベルのポリシーが内部的に実装されており、上記のレベル名を指定するだけで、ポリシーを適用し権限を制限する（または制限しない）ことができます。

なお、上記3つのレベルで具体的にどんな権限が制限または許可されているか、については[こちらの公式ドキュメント](https://kubernetes.io/ja/docs/concepts/security/pod-security-standards/#%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC)をご覧ください。

## Pod Security Admissionとは

Pod Security Admission(PSA)は、PSPの後継として開発された、Podによる他リソースへのアクセス権限を制限するための機能です。
NamespaceごとにPodに与える/与えない権限のセット（＝前章で紹介の3種類のレベル）を適用し、それに違反するPodは起動できないようにします。

k8s v1.22で初めてアルファ機能として提供が開始されました。

:::note warn
PSAはk8s v1.22ではまだアルファ機能であるためデフォルトでは無効化されています。有効化するには、<a rel="noreferrer noopener external" href="https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/">feature gates</a>でPodSecurityをtrueとする必要があります。
k8s v1.23以上であればデフォルトで有効化されています。
:::

PSAには、前章で紹介の3つのレベルのポリシーがあらかじめ内包されており、これをNamespaceに適用することで、そのNamespaceで起動できるPodの権限を制御します。

また、PSAではポリシーに違反した際の実際の挙動について、以下3つのモードにそれぞれ前述のレベルが選択できます。

* **enforce**
   * ポリシーに違反する場合、Podの起動は拒否されます。
* **warn**
   * ポリシーに違反する場合、コンソールへその旨の警告が出力されます。Podの起動は拒否されません。
* **audit**
   * ポリシーに違反する場合、監査ログへその旨の警告が出力されます。Podの起動は拒否されません。

enforceは、ポリシー違反の場合Podの起動が拒否されます。
warnとauditは違反してもPod起動の拒否はされませんがコンソールや監査ログに警告メッセージが出力されます。warnやauditは、警告を出力するだけなので、試験環境でPodの特権をチェックしたり、今後PSAを導入予定の環境で事前にユーザにポリシー準拠を促す目的などでも使用できます。

この3つのモードと、前述の3つのレベルを組み合わせ、Namespaceのラベルに記述することで、当該Namespace内の全Podに対しポリシーを適用します。モードとレベルの関係を図にするとこんな感じです。各モードが独立しそれぞれレベルを1つ選択できます。

<div align="center">
<img width="50%" alt="PSA-Mode-Level.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2345308/3456a6c3-8e9d-1f18-c750-d6546f1e29b3.png">
</div>


以下は「このNamespaceにはbaselineレベルを満たさないPodは起動できない」、というポリシーを持ったNamespaceを定義するマニフェストの例です。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-ns
  labels:
    pod-security.kubernetes.io/enforce: baseline
```

モードとレベルの組み合わせは複数指定することができます。
以下は、3つのポリシー/モードの組み合わせをNamespaceに定義しており、（1）～（3）の全ての挙動を実行します。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-ns
  labels:
    pod-security.kubernetes.io/enforce: baseline # （1）
    pod-security.kubernetes.io/warn: baseline    # （2）
    pod-security.kubernetes.io/audit: baseline   # （3）
```

* （1）：baselineレベルを満たさないPodは起動できない。
* （2）：baselineレベルを満たさないPodが作成されようとした場合、コンソールに警告を出力する。
* （3）：baselineレベルを満たさないPodが作成されようとした場合、監査ログに警告を出力する。

なお、以下コマンドですでに作成済みのNamespaceに後からPSA用のラベルを設定することもできます。
この場合、すでに起動済みのPodは仮に新たなポリシーに違反していてもそのまま動き続けます。が、Pod再起動時などは新たなポリシーが適用され、起動できなくなるので注意が必要です。

```shell-session
# kubectl label --overwrite namespace my-ns \
    pod-security.kubernetes.io/enforce=restricted \
    pod-security.kubernetes.io/warn=restricted \
    pod-security.kubernetes.io/audit=restricted
```

PSAの概要紹介は以上です。

## PSAの具体例

PSAの挙動については、習うより慣れろ、ということでここからは具体的な例を挙げながら実際の挙動を紹介していきます。

### 筆者環境

筆者は動作確認に以下の環境を使用しました。
重要なのは**Kubernetesのバージョンがv1.22以上**であることであり、ホストOSやディストリビューションは参考までにとどめていただければOKです。

* ホストOS：[Rocky Linux](https://rockylinux.org/) 8.4
* Kubernetes： v1.22
   * [k3s](https://k3s.io/) v1.22.2+k3s2 を利用し構築
   * コンテナランタイム：containerd v1.5.5-k3s1 (k3sに同梱)
   * 1ノード構成
   * feature gate「PodSecurity」を有効化[^3]

[^3]: k3sの場合、通常のk3s起動コマンドに以下4つの引数を追加します。
  --kube-apiserver-arg=feature-gates=PodSecurity=true \
  --kube-scheduler-arg=feature-gates=PodSecurity=true \
  --kube-controller-manager-arg=feature-gates=PodSecurity=true \
  --kubelet-arg=feature-gates=PodSecurity=true

### 実行例

以下のマニフェストでNamespaceを作成します。
baselineレベルを満たさないPodは起動を拒否するポリシーを持ちます。

```yaml:my-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-ns
  labels:
    pod-security.kubernetes.io/enforce: baseline
```

```shell-session
# kubectl apply -f my-ns.yaml
# kubectl get ns my-ns --show-labels
NAME    STATUS   AGE   LABELS
my-ns   Active   18s   kubernetes.io/metadata.name=my-ns,pod-security.kubernetes.io/enforce=baseline
```

このNamespaceに、以下2つのDeploymentを定義しPodを作成してみます。
1つ目のDeploymentは、[nginx公式イメージ](https://hub.docker.com/_/nginx)をごく普通に起動するものです。
2つ目もnginx公式イメージを使用していますが、（本来不要ですがあえて）privileged(特権)を使用する設定を加えています。

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        imagePullPolicy: IfNotPresent
```

```yaml:deployment-priv.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-priv
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-priv
  template:
    metadata:
      labels:
        app: nginx-priv
    spec:
      containers:
      - name: nginx-priv
        image: nginx:latest
        ports:
        - containerPort: 80
        imagePullPolicy: IfNotPresent
        securityContext:
          privileged: true #privileged(特権)を使用
```

以下コマンドで、上記2つのDeploymentを作成します。
コマンドは特にエラー等無く"created"と表示されます。

```shell-session
# kubectl -n my-ns apply -f deployment.yaml -f deployment-priv.yaml
deployment.apps/nginx created
deployment.apps/nginx-priv created
```

各リソースがちゃんと作成されたか確認すると、Podが1つしかありません。DeploymentおよびReplicaSetのREADY欄を見ると、2つ目のDeployment「nginx-priv」(privileged(特権)への昇格を要求した方)が0になっており、このPodが作成されていないようです。

```shell-session
# kubectl -n my-ns get all
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-75b69bd684-j4crg   1/1     Running   0          24s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx        1/1     1            1           25s
deployment.apps/nginx-priv   0/1     0            0           21s

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-75b69bd684        1         1         1       25s
replicaset.apps/nginx-priv-6db9787dc9   1         0         0       21s
```

「nginx-priv」の方のReplicaSetの詳細をkubectl describeコマンドで確認してみます。
Events:欄にprivilegedがtrueであってはならない旨のエラーが表示されています。

```shell-session
# kubectl -n my-ns describe replicaset -l app=nginx-priv
・・・
Events:
  Type     Reason        Age                 From                   Message
  ----     ------        ----                ----                   -------
  Warning  FailedCreate  30s (x14 over 71s)  replicaset-controller  Error creating: privileged (container "nginx-priv" must not set securityContext.privileged=true)
```

PSAにより、privileged(特権)への昇格を要求した方のPodの起動が制限されることを確認できました。
これで、不用意にPodに特権を与えてしまい、Podの中からノードや他のリソースを好き勝手にいじられる、といった被害を防ぐことができます。

皆様も、是非PSAを活用し、よりセキュアにk8sを運用し、心安らかにクリスマス＆年末をお過ごしください。

※本稿に記載されている製品名、サービス名は、各団体の商標または登録商標です。