[Kubernetes]PodSecurityPolicyとAdmission Controllerの動作を確認する

#はじめに
PodSecurityPolicyはKubernetesクラスタに対するセキュリティ設定機能です。PodSecurityContextとSecurityContextと似ていますが、PodSecurityContextはPodに対して、SecurityContextはコンテナに対して設定しますので、制御する範囲が異なります。

[Pod Security Policies](https://kubernetes.io/docs/concepts/policy/pod-security-policy/)

また、PodSecurityPolicyを使用するにはAdmission Controllerで有効化する必要があります。

[Using Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/)
[kube-apiserver](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/)

#PodSecurityPolicyの設定
##設定項目
PodSecurityPolicyで設定できる項目（抜粋）は以下になります。PodSecurityContext/SecurityContextと同じ項目も設定できます。

|設定項目|概要
|:---|:---
|privileged|特権コンテナの実行可否
|hostPID|コンテナがホストプロセスID Namespaceを共有できるかどうか
|hostIPC|コンテナがホストIPC Namespaceを共有できるかどうか
|hostNetwork|PodがノードネットワークNamespaceを使用できるかどうか
|hostPorts|ホストネットワークNamespace使用できるポートの範囲
|volumes|許可するボリュームタイプ
|fsGroup|利用できるGIDの範囲
|runAsUser|コンテナを実行するUID
|supplementalGroups|コンテナに追加するGID
|allowPrivilegeEscalation|SecurityContextの設定可否
|allowedCapabilities|コンテナへの追加を許可するCapabilities
|seLinux|コンテナのSELinuxコンテキスト

詳細およびその他の設定項目は[こちら](https://kubernetes.io/docs/concepts/policy/pod-security-policy/#what-is-a-pod-security-policy)を参照

##設定
PodSecurityPolicyはAdmissionControllerで有効化することで適用されますが、AdmissionControllerで先に有効化すると、Podが作成できなくなることがあります。そのため、PodSecurityPolicyを先に設定します。

>Pod security policy control is implemented as an optional (but recommended) admission controller. PodSecurityPolicies are enforced by enabling the admission controller, but doing so without authorizing any policies will prevent any pods from being created in the cluster.

>Since the pod security policy API (policy/v1beta1/podsecuritypolicy) is enabled independently of the admission controller, for existing clusters it is recommended that policies are added and authorized before enabling the admission controller.

以下のマニフェストのPodSecurityPolicyを設定します。
ここでは、privilegedのみを「false」に設定して、特権コンテナの実行を拒否します。その他の設定は全て許可します。

```podSecurityPolicy.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: privileged
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
  privileged: false
  allowPrivilegeEscalation: true
  allowedCapabilities:
  - '*'
  volumes:
  - '*'
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  hostIPC: true
  hostPID: true
  runAsUser:
    rule: 'RunAsAny'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'
```
マニフェストをapplyして設定値を確認します。

```
$ kubectl apply -f podSecurityPolicy.yaml
podsecuritypolicy.policy/privileged created
$ kubectl get podsecuritypolicies.policy
NAME         PRIV    CAPS                          SELINUX    RUNASUSER   FSGROUP     SUPGROUP    READONLYROOTFS   VOLUMES
controller   false                                 RunAsAny   MustRunAs   MustRunAs   MustRunAs   true             configMap,secret,emptyDir
privileged   false   *                             RunAsAny   RunAsAny    RunAsAny    RunAsAny    false            *
speaker      true    NET_ADMIN,NET_RAW,SYS_ADMIN   RunAsAny   RunAsAny    RunAsAny    RunAsAny    true             configMap,secret,emptyDir
```

##ServiceAccountの作成
以下のServiceAccountを作成します。

```sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa
```

```
$ kubectl apply -f sa.yaml
serviceaccount/sa created
```

##ClusterRole/CulusterRoleBindingsの作成
ClusterRole/CulusterRoleBindingsを作成して、PodSecurityPolicyとServiceAccountを紐付けます。

```crb.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cr
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - privileged
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: crb
roleRef:
  kind: ClusterRole
  name: cr
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: sa
    namespace: default
```

```
$ kubectl apply -f crb.yaml
clusterrole.rbac.authorization.k8s.io/cr created
clusterrolebinding.rbac.authorization.k8s.io/crb created
```

#動作確認
PodSecurityPolicyの設定は以上ですが、AdmissionControllerはまだ設定していませんので、この時点ではPodSecurityPolicyは動作しません。ここでは「動作しない」ことを確認します。

##Podのデプロイ
特権コンテナがないPod（nginx-nomal-b）と、あるPod（nginx-privileged-b）をデプロイします。

```nginx-nomal-b.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-nomal-b
spec:
  containers:
  - name: nginx-nomal-b
    image: nginx:latest
  serviceAccountName: sa
  automountServiceAccountToken: true
```

```nginx-privileged-b.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-privileged-b
spec:
  containers:
  - name: nginx-privileged-b
    image: nginx:latest
    securityContext:
      privileged: true
  serviceAccountName: sa
  automountServiceAccountToken: true
```

```
$ kubectl apply -f nginx-nomal-b.yaml
pod/nginx-nomal-b created
$ kubectl apply -f nginx-privileged-b.yaml
pod/nginx-privileged-b created
$ kubectl get pod
NAME                 READY   STATUS    RESTARTS   AGE
nginx-nomal-b        1/1     Running   0          77s
nginx-privileged-b   1/1     Running   0          12s
```
どちらも正常にデプロイできます。

#AdmissionControllerの設定
今回の環境はkubeadmで構築したオンプレミスのクラスタ環境です。クラウドサービスのマネージドなKubernetes環境だと設定方法が異なるようです。

##デフォルトプラグインの確認
Kubernetes1.10以降の場合、AdmissionControllerはデフォルトでいくつかのプラグインが有効になっています。有効になっているプラグインは[こちら](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#which-plugins-are-enabled-by-default)に記載されていますが、kube-apiserverにログインして確認してみます。

kube-apiserver名を確認します。

```
$ kubectl get pod -n kube-system
NAME                                      READY   STATUS    RESTARTS   AGE
calico-kube-controllers-77c4b7448-6prr9   1/1     Running   114        123d
calico-node-2hc9b                         1/1     Running   115        123d
calico-node-cgdgk                         1/1     Running   116        123d
calico-node-tkcz5                         1/1     Running   109        123d
coredns-6955765f44-55wbn                  1/1     Running   114        123d
coredns-6955765f44-bhdvr                  1/1     Running   114        123d
etcd-k8s-master                           1/1     Running   120        123d
kube-apiserver-k8s-master                 1/1     Running   124        123d
kube-controller-manager-k8s-master        1/1     Running   122        123d
kube-proxy-8pngh                          1/1     Running   185        123d
kube-proxy-gqt42                          1/1     Running   184        123d
kube-proxy-pq2fb                          1/1     Running   195        123d
kube-scheduler-k8s-master                 1/1     Running   120        123d
metrics-server-fbc46dc5f-nlhvs            1/1     Running   114        79d
```

ログインして確認します。

```
$ kubectl -n kube-system exec -it kube-apiserver-k8s-master /bin/sh
# kube-apiserver -h | grep enable-admission-plugins
      --admission-control strings              Admission is divided into two phases. In the first phase, only mutating admission plugins run. In the second phase, only validating admission plugins run. The names in the below list may represent a validating plugin, a mutating plugin, or both. The order of plugins in which they are passed to this flag does not matter. Comma-delimited list of: AlwaysAdmit, AlwaysDeny, AlwaysPullImages, DefaultStorageClass, DefaultTolerationSeconds, DenyEscalatingExec, DenyExecOnPrivileged, EventRateLimit, ExtendedResourceToleration, ImagePolicyWebhook, LimitPodHardAntiAffinityTopology, LimitRanger, MutatingAdmissionWebhook, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, NodeRestriction, OwnerReferencesPermissionEnforcement, PersistentVolumeClaimResize, PersistentVolumeLabel, PodNodeSelector, PodPreset, PodSecurityPolicy, PodTolerationRestriction, Priority, ResourceQuota, RuntimeClass, SecurityContextDeny, ServiceAccount, StorageObjectInUseProtection, TaintNodesByCondition, ValidatingAdmissionWebhook. (DEPRECATED: Use --enable-admission-plugins or --disable-admission-plugins instead. Will be removed in a future version.)
      --enable-admission-plugins strings       admission plugins that should be enabled in addition to default enabled ones (NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, Priority, DefaultTolerationSeconds, DefaultStorageClass, StorageObjectInUseProtection, PersistentVolumeClaimResize, MutatingAdmissionWebhook, ValidatingAdmissionWebhook, RuntimeClass, ResourceQuota). Comma-delimited list of admission plugins: AlwaysAdmit, AlwaysDeny, AlwaysPullImages, DefaultStorageClass, DefaultTolerationSeconds, DenyEscalatingExec, DenyExecOnPrivileged, EventRateLimit, ExtendedResourceToleration, ImagePolicyWebhook, LimitPodHardAntiAffinityTopology, LimitRanger, MutatingAdmissionWebhook, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, NodeRestriction, OwnerReferencesPermissionEnforcement, PersistentVolumeClaimResize, PersistentVolumeLabel, PodNodeSelector, PodPreset, PodSecurityPolicy, PodTolerationRestriction, Priority, ResourceQuota, RuntimeClass, SecurityContextDeny, ServiceAccount, StorageObjectInUseProtection, TaintNodesByCondition, ValidatingAdmissionWebhook. The order of plugins in this flag does not matter.
```

ログが長くてわかりにくいので、デフォルトで有効になっているものを以下に抜き出しました。今回の環境は1.17です。最新の1.18とは異なるようです。また、PodSecurityPolicyは含まれていません。

* NamespaceLifecycle
* LimitRanger
* ServiceAccount
* TaintNodesByCondition
* Priority
* DefaultTolerationSeconds
* DefaultStorageClass
* StorageObjectInUseProtection
* PersistentVolumeClaimResize
* MutatingAdmissionWebhook
* ValidatingAdmissionWebhook
* RuntimeClass
* ResourceQuota

##PodSecurityPolicyプラグインの追加
/etc/kubernetes/manifests/kube-apiserver.yamlを編集して、--enable-admission-pluginsに「PodSecurityPolicy」を追記します。

```/etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=10.20.30.10
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction,PodSecurityPolicy #PodSecurityPolicyを追記
    - --enable-bootstrap-token-auth=true
    - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
    - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
    - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
    - --etcd-servers=https://127.0.0.1:2379
    - --insecure-port=0
    - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
    - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
    - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
    - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
    - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
    - --requestheader-allowed-names=front-proxy-client
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --requestheader-extra-headers-prefix=X-Remote-Extra-
    - --requestheader-group-headers=X-Remote-Group
    - --requestheader-username-headers=X-Remote-User
    - --secure-port=6443
    - --service-account-key-file=/etc/kubernetes/pki/sa.pub
    - --service-cluster-ip-range=10.96.0.0/12
    - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
    - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
    image: k8s.gcr.io/kube-apiserver:v1.17.3
    imagePullPolicy: IfNotPresent
    livenessProbe:
      failureThreshold: 8
      httpGet:
        host: 10.20.30.10
        path: /healthz
        port: 6443
        scheme: HTTPS
      initialDelaySeconds: 15
      timeoutSeconds: 15
    name: kube-apiserver
    resources:
      requests:
        cpu: 250m
    volumeMounts:
    - mountPath: /etc/ssl/certs
      name: ca-certs
      readOnly: true
    - mountPath: /etc/pki
      name: etc-pki
      readOnly: true
    - mountPath: /etc/kubernetes/pki
      name: k8s-certs
      readOnly: true
  hostNetwork: true
  priorityClassName: system-cluster-critical
  volumes:
  - hostPath:
      path: /etc/ssl/certs
      type: DirectoryOrCreate
    name: ca-certs
  - hostPath:
      path: /etc/pki
      type: DirectoryOrCreate
    name: etc-pki
  - hostPath:
      path: /etc/kubernetes/pki
      type: DirectoryOrCreate
    name: k8s-certs
status: {}
```

/etc/kubernetes/manifests/kube-apiserver.yamlファイルを編集すると、kubeletがコンテナを再起動させます。

少し待ってPodを確認すると、kube-apiserverが確認できません。

```
$ kubectl -n kube-system get pod
NAME                                      READY   STATUS    RESTARTS   AGE
calico-kube-controllers-77c4b7448-6prr9   1/1     Running   117        124d
calico-node-2hc9b                         1/1     Running   118        124d
calico-node-cgdgk                         1/1     Running   117        124d
calico-node-tkcz5                         1/1     Running   110        124d
coredns-6955765f44-55wbn                  1/1     Running   117        124d
coredns-6955765f44-bhdvr                  1/1     Running   117        124d
etcd-k8s-master                           1/1     Running   123        124d
kube-controller-manager-k8s-master        1/1     Running   128        124d
kube-proxy-8pngh                          1/1     Running   187        124d
kube-proxy-gqt42                          1/1     Running   186        124d
kube-proxy-pq2fb                          1/1     Running   201        124d
kube-scheduler-k8s-master                 1/1     Running   126        124d
metrics-server-fbc46dc5f-nlhvs            1/1     Running   116        80d
```

dockerコマンドで確認すると、コンテナが起動および再起動したのは確認できます。

```
$ docker ps | grep api
863b49a685f7        90d27391b780           "kube-apiserver --ad…"   9 minutes ago       Up 2 minutes                            k8s_kube-apiserver_kube-apiserver-k8s-master_kube-system_db52c836d7613d8535c638ac67356d5e_0
4321d74c37b7        k8s.gcr.io/pause:3.1   "/pause"                 9 minutes ago       Up 9 minutes                            
```

##動作確認

先ほどと同様にPodをデプロイして確認します。先ほどのPodと名前が異なるだけで設定が同じPodをデプロイします。

```
$ kubectl apply -f nginx-nomal-a.yaml
pod/nginx-nomal-a created
$ kubectl apply -f nginx-privileged-a.yaml
The Pod "nginx-privileged-a" is invalid: spec.containers[0].securityContext: Invalid value: core.SecurityContext{Capabilities:(*core.Capabilities)(0xc0072fe8a0), Privileged:(*bool)(0xc00617c63a), SELinuxOptions:(*core.SELinuxOptions)(nil), WindowsOptions:(*core.WindowsSecurityContextOptions)(nil), RunAsUser:(*int64)(nil), RunAsGroup:(*int64)(nil), RunAsNonRoot:(*bool)(nil), ReadOnlyRootFilesystem:(*bool)(0xc00617c64a), AllowPrivilegeEscalation:(*bool)(0xc0072ecce0), ProcMount:(*core.ProcMountType)(nil)}: cannot set `allowPrivilegeEscalation` to false and `privileged` to true
$ kubectl get pod
NAME                 READY   STATUS    RESTARTS   AGE
nginx-nomal-a        1/1     Running   0          33s
nginx-nomal-b        1/1     Running   3          2d
nginx-privileged-b   1/1     Running   3          2d
```
先ほど（AdmissionController設定前）とは異なり、特権がないPod（nginx-nomal-a）はデプロイできますが、特権を与えたPod（nginx-privileged-a）は失敗しています。

#まとめ
PodSecurityPolicyはクラスタ全体のポリシーを定義できますので、適切に設定することでセキュリティを高めることができますね。逆に間違えるとPodのデプロイができなくなったりもしますので、設定には注意する必要があります。
また、今回はkube-apiserver.yamlファイルを編集することで、AdmissionControllerを設定しましたが、kubectlコマンドで表示されなくなってしまいましたので、この方法をProduction環境で使うのはちょっと怖いですね。PodSecurityPolicyはまだベータ版ですので、GA版となったときに再度設定方法を確認する必要があります。


#参考資料
https://stackoverflow.com/questions/50352621/where-is-kube-apiserver-located
https://stackoverflow.com/questions/51489955/how-to-obtain-the-enable-admission-controller-list-in-kubernetes
https://tech.virtualtech.jp/entry/2020/03/23/152657

#補足（AdmissionControllerの設定方法）
AdmissionControllerを有効にする方法として、マニュアルには以下が記載されています。

>The Kubernetes API server flag enable-admission-plugins takes a comma-delimited list of admission control plugins to invoke prior to modifying objects in the cluster. For example, the following command line enables the NamespaceLifecycle and the LimitRanger admission control plugins:
>```
>kube-apiserver --enable-admission-plugins=NamespaceLifecycle,LimitRanger ...
>```

この通りにプラグインを並べて指定すると、全てのプラグインが「unknown」となり、設定できませんでした。
色々試しましたがこれ以上やるとクラスタを壊してしまいそうでしたので、一旦先に進んで壊してもいいときになったら、もう少し調査したいと思います。

```
# kube-apiserver --enable-admission-plugins="NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, Priority, DefaultTolerationSeconds, DefaultStorageClass, StorageObjectInUseProtection, PersistentVolumeClaimResize, MutatingAdmissionWebhook, ValidatingAdmissionWebhook, RuntimeClass, ResourceQuota, PodSecurityPolicy" --etcd-servers scheme://127.0.0.1:2379 --service-cluster-ip-range 192.168.0.0/16 --anonymous-auth=false
・・・enable-admission-plugins plugin " PodSecurityPolicy" is unknown]
```