Pod Security Policyの疑問点をソースから確認してみる。

この記事はKubernetes3 Advent Calendar 2020 20日目の記事です。

マルチテナントなKubernetesクラスターを作成する際に活きてくるKubernetesのセキュリティ機能の一つに`Pod Security Policy(PSP)`という機能があります。
https://kubernetes.io/docs/concepts/policy/pod-security-policy

具体的な設定に関するプラクティスについては過去記事をみていただけると幸いです。

>マルチテナントクラスターにおけるPod Security Policyの個人的ベストプラクティス
>https://qiita.com/jlandowner/items/b17562f7f1c7045d99fc

今回はPodSecurityPolicyについて、個人的にドキュメントだけではよく理解できなかった以下の点をソースから確認してみたいと思います。
1. **Admission Controllerなのに、なぜ実行時にrootユーザーでの実行拒否ができるのか？**
2. **PodのServiceAccountとPodを作成したユーザーのPSP、どう選択されるのか？**

# 検証環境の準備

検証環境はEKS v1.18を使用します。

まずは特権PSPと制限付きPSPを用意します。PSPでは許可ポリシーが優先されるため、特権PSPは特に何にも付与せず、クラスター全体に制限付きPSPを適用します。

特権PSPは[EKSにデフォルトで存在する](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/pod-security-policy.html)`eks.privileged`、制限付きPSPは[EKS Best PracticeのRecommendation](https://aws.github.io/aws-eks-best-practices/security/docs/pods/#recommendations)の`restricted`を使用します。

事前準備として、krew（krewのインストール手順は[こちら](https://krew.sigs.k8s.io/docs/user-guide/setup/install/)）からkubectlプラグインであるkubectxとpsp-utilをインストールします。

```sh
kubectl krew install ctx
kubectl krew install psp-util
```

### 一般ユーザーの作成
EKSではクラスターを作成した際のIAMロールが`system:master`グループに所属されますので、一般ユーザーを改めて作成します。

<details><summary>具体的な設定は長いので折り畳んでおきます。</summary><div>

新たにIAMロールを作成し、EKSのグループに所属させます。詳細は[公式の手順](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/add-user-role.html)に譲ります。
設定結果は以下の通りです。

```sh
# aws-auth configMap
$ kubectl describe cm aws-auth -n kube-system
Name:         aws-auth
Namespace:    kube-system
Labels:       <none>
Annotations:  ...

Data
====
mapRoles:
----
- rolearn: arn:aws:iam::XXXXXXXXXXXXX:role/eksctl-test-eks-1-nodegroup-NodeG-NodeInstanceRole-XXXXXXXXXXXXX
  username: system:node:{{EC2PrivateDNSName}}
  groups:
    - system:bootstrappers
    - system:nodes
- rolearn: arn:aws:iam::XXXXXXXXXXXXX:role/eks-user-role
  username: user-role
  groups:
    - user-role


# AWS CLIのConfig
$ cat ~/.aws/config
[default]
region = ap-northeast-1
output = json

[profile user-role]
region = ap-northeast-1
output = json
role_arn = arn:aws:iam::XXXXXXXXXXXXX:role/eks-user-role
source_profile = default

# Kubeconfig
$ cat ~/.kube/config
・・・省略
- name: arn:aws:eks:ap-northeast-1:XXXXXXXXXXXXX:cluster/test-eks-1-user-role
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - ap-northeast-1
      - eks
      - get-token
      - --cluster-name
      - land-EKS-1
      - --profile # user-roleのプロファイルを使用する
      - user-role # user-roleのプロファイルを使用する
      command: aws
```

</div></details>

特権ユーザーと作成した一般ユーザーを切り替えられるようにcontextを2つ用意しておきます。

```sh
$ kubectl ctx
arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/test-eks-1           # 特権
arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/test-eks-1-user-role # 一般
```

### PSPの設定
<details><summary>具体的な設定は長いので折り畳んでおきます。</summary><div>

```sh
# クラスター全体に特権PSPが適用されているため外す
kubectl delete ClusterRoleBinding eks:podsecuritypolicy:authenticated

# 制限付きPSPの適用
cat <<EOF | kubectl apply -f -
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'
        seccomp.security.alpha.kubernetes.io/defaultProfileName:  'runtime/default'
        apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
    privileged: false
    # Required to prevent escalations to root.
    allowPrivilegeEscalation: false
    # This is redundant with non-root + disallow privilege escalation,
    # but we can provide it for defense in depth.
    requiredDropCapabilities:
    - ALL
    # Allow core volume types.
    volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    # Assume that persistentVolumes set up by the cluster admin are safe to use.
    - 'persistentVolumeClaim'
    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
EOF

# クラスター全体に制限付きPSPを適用
kubectl psp-util attach restricted --group system:authenticated
kubectl psp-util attach restricted --group system:unauthenticated
```

</div></details>

設定結果は以下の通りです。

```sh:実行結果
$ kubectl psp-util tree
📙 PSP eks.privileged
└── 📕 ClusterRole eks:podsecuritypolicy:privileged

📙 PSP restricted
└── 📕 ClusterRole psp-util.restricted
    └── 📘 ClusterRoleBinding psp-util.restricted
        └── 📗 Subject{Kind: Group, Name: system:authenticated, Namespace:}
        └── 📗 Subject{Kind: Group, Name: system:unauthenticated, Namespace: }
```


# 1. Admission Controllerなのに、なぜ実行時にrootユーザーでの実行拒否ができるのか？

Pod Security PolicyはControl Planeに[Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecuritypolicy)として実装されており、Podリソースの作成時にPodリソースの設定内容がPodSecurityPolicyリソースで定義したポリシーを満たしているかを確認し、バリデーションエラーの場合は作成を拒否します。

ただしこれだけではOpenPolicyAgentや自前で実装するValidation Webhookでも実現できるものです。PodSecurityPolicyの強力な機能の一つに、コンテナ実行時にポリシーを満たしていなければ実行をブロックする機能があります。これはどのように実装されているのでしょうか？

## 試してみる
試しに一般ユーザーで`privileged: true`なPodを作成してみます。

```shell:実行結果
$ kubectl ctx arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/test-eks-1-user-role
Switched to context "arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/land-EKS-1-user-role".

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: test-nginx
spec:
  containers:
    - name:  nginx
      image: nginx:alpine
      securityContext:
        privileged: true
EOF
Error from server (Forbidden): error when creating "STDIN": pods "test-nginx" is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
```

エラーとなりPodの作成ができませんでした。次に`privileged: true`を外して起動してみます。

```sh:実行結果
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: test-nginx
spec:
  containers:
    - name:  nginx
      image: nginx:alpine
EOF                   
pod/test-nginx created
```

作成ができたので、ステータスを確認します。

```sh:実行結果
$ kubectl get po test-nginx
NAME                        READY   STATUS                       RESTARTS   AGE
test-nginx                  0/1     CreateContainerConfigError   0          18s

$ kubectl describe po test-nginx
・・・省略
Events:
  Type     Reason     Age                From                                                        Message
  ----     ------     ----               ----                                                        -------
  Normal   Scheduled  81s                default-scheduler                                           Successfully assigned default/test-nginx to ip-10-110-100-199.ap-northeast-1.compute.internal
  Normal   Pulled     14s (x8 over 80s)  kubelet, ip-10-110-100-199.ap-northeast-1.compute.internal  Container image "nginx:alpine" already present on machine
  Warning  Failed     14s (x8 over 80s)  kubelet, ip-10-110-100-199.ap-northeast-1.compute.internal  Error: container has runAsNonRoot and image will run as root
```

Podの作成は成功しているものの、ステータスは`CreateContainerConfigError`となり、`Error: container has runAsNonRoot and image will run as root`として実行できません。

## ソースをみてみる

Pod Security PolicyはAdmission controllerとして実装されており、APIサーバーの起動オプションで有効にするか無効にするか制御できます。
https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecuritypolicy

Admission controllerの実装は`plugin`ディレクトリにあります。
https://github.com/kubernetes/kubernetes/blob/master/plugin/pkg/admission/security/podsecuritypolicy

実際のバリデーションロジックは`pkg/srcurity/podsecuritypolicy`にありますが、先の例のように`privileged: true`のような設定内容は、Admission Controller内でPod適用時にバリデートされます。

```go:github.com/kubernetes/kubernetes/blob/master/pkg/security/podsecuritypolicy/provider.go#L353-L355
if !s.psp.Spec.Privileged && privileged != nil && *privileged {
	allErrs = append(allErrs, field.Invalid(scPath.Child("privileged"), *privileged, "Privileged containers are not allowed"))
}
```

ではrootユーザーでの実行拒否はどのように実装されているかというと、Admission ControllerでチェックされたPSPの情報をkubeletに連携し、kubeletが実行時にチェックしています。

root実行拒否を例にとって確認します。
Admission controllerでは、先に示したバリデーションだけでなく、他のコンポーネントにシグナルを送るためにPodのSecurityContextをミューテートし、情報を付与します。Root実行拒否については、PSPの設定を元にsecurityContextに`RunAsNonRoot`フラグを設定します。

```go:github.com/kubernetes/kubernetes/blob/master/pkg/security/podsecuritypolicy/provider.go#L167-L173
// if we're using the non-root strategy set the marker that this container should not be
// run as root which will signal to the kubelet to do a final check either on the runAsUser
// or, if runAsUser is not set, the image UID will be checked.
if sc.RunAsNonRoot() == nil && sc.RunAsUser() == nil && s.psp.Spec.RunAsUser.Rule == policy.RunAsUserStrategyMustRunAsNonRoot {
	nonRoot := true
	sc.SetRunAsNonRoot(&nonRoot)
}
```

先ほど実行時エラーになったPodをyamlで確認するとsecurityContextに以下のような情報が付与されています。

```sh:実行結果
$ kubectl get pod test-nginx -o yaml
・・・省略
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
        - ALL
      runAsNonRoot: true
・・・
```

kubelet側の実装では、`startContainer`メソッド内で呼ばれる`generateContainerConfig`メソッドにてこのフラグをチェックし、実行するコンテナのUIDを判定しています。

```go:github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kuberuntime/kuberuntime_container.go#L244-L247
// Verify RunAsNonRoot. Non-root verification only supports numeric user.
if err := verifyRunAsNonRoot(pod, container, uid, username); err != nil {
	return nil, cleanupAction, err
}
```

そのため、実行時に`CreateContainerConfigError`となるのですね。

# 2. PodのServiceAccountとPodを作成したユーザーのPSP、どう選択されるのか？

Pod Security Policyを適用する際は、PodSecurityPolicyを使用(use)するClusterRoleを作成し、ClusterRoleBindingまたはRoleBindingで紐付けします。
紐づけられる対象は通常のClusterRoleBindingと同様にUser、GroupまたはServiceAccountです。

ここで注意が必要なのは、通常Deployment等でPodを作成しようとすると、実際にPodを作成するのはDeploymentを作成したユーザーではなく、Control Plane内の`Replicaset Controller`がPodを作成します。

公式ドキュメントにはDeploymentやDaemonSetが作成したPodにPSPを適用するために、Podが使用するServiceAccountにPSP付与することで適用する必要がある、とあります。

>Most Kubernetes pods are not created directly by users. Instead, they are typically created indirectly as part of a Deployment, ReplicaSet, or other templated controller via the controller manager. Granting the controller access to the policy would grant access for all pods created by that controller, so the preferred method for authorizing policies is to grant access to the pod's service account (see example).
>https://kubernetes.io/docs/concepts/policy/pod-security-policy/#authorizing-policies

## 試してみる

先ほどrootユーザーで実行時エラーとなったPodをDeploymentで作成してみます。

```sh:実行結果
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-nginx
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-nginx
  template:
    metadata:
      labels:
        app: test-nginx
    spec:
      containers:
      - image: nginx:alpine
        name: nginx
EOF
deployment.apps/test-nginx created

$ kubectl get po
NAME                              READY   STATUS                       RESTARTS   AGE
pod/test-nginx-65c4cf9ff5-pf2tk   0/1     CreateContainerConfigError   0          4s
```

実行時エラーになりました。SerivceAccountは指定していないので、`default`になっています。

```sh:実行結果
$ kubectl get pod/test-nginx-65c4cf9ff5-pf2tk -o jsonpath='{.spec.serviceAccount}'
default
```

次に`default` ServiceAccountに特権PSPを付与してみます。

```sh:実行結果
$ kubectl psp-util attach eks.privileged --sa default -n default
Managed ClusterRole is not found...Created
Managed ClusterRoleBinding is not found...Created

$ kubectl psp-util tree
📙 PSP eks.privileged
└── 📕 ClusterRole eks:podsecuritypolicy:privileged
└── 📕 ClusterRole psp-util.eks.privileged
    └── 📘 ClusterRoleBinding psp-util.eks.privileged
        └── 📗 Subject{Kind: ServiceAccount, Name: default, Namespace: default}

📙 PSP restricted
└── 📕 ClusterRole psp-util.restricted
    └── 📘 ClusterRoleBinding psp-util.restricted
        └── 📗 Subject{Kind: Group, Name: system:authenticated, Namespace: }
        └── 📗 Subject{Kind: Group, Name: system:unauthenticated, Namespace: }
```

先ほどDeploymentから作成されたPodを削除して、新しいPodが作られるか確認してみます。

```sh:実行結果
$ kubectl delete pod/test-nginx-65c4cf9ff5-pf2tk
pod "test-nginx-65c4cf9ff5-pf2tk" deleted

$ kubectl get po
NAME                          READY   STATUS                       RESTARTS   AGE
test-nginx-65c4cf9ff5-nfs2h   1/1     Running                      0          14s
```

新しくできたPodは無事作成されていました。

次に`default` ServiceAccountに付与したPSPの適用解除し、今度はkube-system内の`replicaset-controller`というServiceAccountにPSPを付与してみます。

```sh:実行結果
$ kubectl psp-util detach eks.privileged --sa default -n default
$ kubectl psp-util attach eks.privileged --sa replicaset-controller -n kube-system
$ kubectl psp-util tree
📙 PSP eks.privileged
└── 📕 ClusterRole eks:podsecuritypolicy:privileged
└── 📕 ClusterRole psp-util.eks.privileged
    └── 📘 ClusterRoleBinding psp-util.eks.privileged
        └── 📗 Subject{Kind: ServiceAccount, Name: replicaset-controller, Namespace: kube-system}

📙 PSP restricted
└── 📕 ClusterRole psp-util.restricted
    └── 📘 ClusterRoleBinding psp-util.restricted
        └── 📗 Subject{Kind: Group, Name: system:authenticated, Namespace: }
        └── 📗 Subject{Kind: Group, Name: system:unauthenticated, Namespace: }
```

それではこの状態でもう一度Podを削除してみます。

```sh:実行結果
$ kubectl delete pod test-nginx-65c4cf9ff5-nfs2h
pod "test-nginx-65c4cf9ff5-nfs2h" deleted

$ kubectl get po
NAME                          READY   STATUS                       RESTARTS   AGE
test-nginx-65c4cf9ff5-6s5vb   1/1     Running                      0          28s
```

起動しました。つまりServiceAccountかPodの作成ユーザーのどちらかにでもrootコンテナを作れる権限があれば実行できるようです。

反対にマルチテナントクラスターで各サービスチームにrootコンテナで動かせないようにしたい場合は、各サービスチームが使用できるServiceAccountに権限を付与しないのはもちろんのことですが、`Replicaset controller`はクラスター内の全Podを作成するため、これらが使用するkube-system内のServiceAccountに付与してしまうと、DeploymentでどんなPodでも作成できてしまいます。

## ソースをみてみる

Admission Controllerの実装は非常にシンプルです。使用するPSPを選ぶ条件として、単純にServiceAccountと作成Userの両方をOR条件で確認しているだけです。

```go:github.com/kubernetes/kubernetes/plugin/pkg/security/podsecuritypolicy/admission.go
func isAuthorizedForPolicy(ctx context.Context, user, sa user.Info, namespace, policyName string, authz authorizer.Authorizer) bool {
	// Check the service account first, as that is the more common use case.
	return authorizedForPolicy(ctx, sa, namespace, policyName, authz) ||
		authorizedForPolicy(ctx, user, namespace, policyName, authz)
}
```

## 注意点
マルチテナントクラスターでは、サービスチームにNamespaceを割り当てることが多いかと思いますので、`system:serviceaccounts:<Namespace名>`グループにPSPを割り当てるとNamespace内の全ServiceAccountに同じPod Security Policyを適用することができます。

しかし、kube-systemについては`Replicaset controller`や`DaemonSet controller`が使用するServiceAccountがあるので、`system:serviceaccounts:kube-system`グループを使用すると、DeploymentやDaemonSetでどんなPodでも作成できてしまいます。

そのためkube-systemではCNIコンテナやkube-proxyが使用するServiceAccountに個別でPSPを適用する必要があります。ただこれを忘れてCNIコンテナに特権PSPが付与されていないとCNIコンテナが起動せず、NodeステータスがReadyにならないのでご注意を。