KubernetesのSeccompDefaultを試した

## Kubernetesとseccomp

Kubernetesではコンテナが必要以上の権限を持たないようSandbox機能としてLinuxのCapabilityやseccompが利用できます。LinuxのCapabilityはルートユーザが持つ特権をいくつかのグループに分けたもので、そのグループ単位でコンテナに権限を与えられます。seccompはsyscallを制限するためのフィルターで、それを利用しコンテナとして実行されるアプリケーションが呼び出せるsyscallを制限できます。これら両方で許可された操作のみコンテナで許可された操作となります。どちらもコンテナのセキュリティを高めるために利用できる機能ですが、今回は特にseccompに関係する機能について検証した内容となっています。コンテナランタイムの実装にもよると思いますが、PodのSpecでseccompのprofileを指定しないとデフォルトでは`unconfined`として扱われるものがあるようで、例えばcontainerdだったりcri-oはそのような動作をするようです。そしてこの`unconfined`ではコンテナランタイムで[seccompを無効化する特別なプロファイル名](https://github.com/kubernetes/cri-api/blob/v0.23.0/pkg/apis/runtime/v1/api.proto#L308-L309)として扱われます。

安全を考慮するとアプリケーション毎に利用する最低限のsyscallのみを許容すべきだとは思いますが、アプリケーション毎に設定・メンテナンスし続けるのは大変だと思います。メンテナンスを楽にしながらも可能な限りセキュアにコンテナを動かしたい！と考えるのは私だけではないはず。そのためRuntimeDefaultと呼ばれる特殊なProfileがあり、これを利用するとある程度コンテナアプリケーションで利用しないようなsyscallを制限したProfileが利用できます。しかも例えばcontainerdではLinuxのCapabilitiyに合わせた[Profileを動的に生成してくれる](https://github.com/containerd/containerd/blob/main/contrib/seccomp/seccomp_default.go)ようで、seccomp的には利用できるのにCapabilityで制限を外し忘れるということもないのもメリットです。

## PodSecurityPolicyとseccomp

`SeccompDefault`は名前からも想像できるようにseccomp profileをdefaultingしてくれる機能です。どんな値をdefaultingしてくれるかというと、先ほど紹介したRuntimeDefaultです。SeccompDefaultを利用しなくても、Kubernetesの機能だけでdefaultingは可能で、PodSecurityPolicyを利用することで実現できます。
以下のようにPSPリソースのannotationでdefaultのseccomp profileを指定できます。

```yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: sample-psp
  annotations:
    seccomp.security.alpha.kubernetes.io/defaultProfileName: 'runtime/default'
  (省略)
```

このPSPが適用されたPod内のコンテナはseccompが指定されていないと`RuntimeDefault`がPodSpecがdefaultingされます。実際に試してみましょう。

```sh
$ kubectl create deploy --image=nginx nginx
```

作成されたPodを確認すると以下のようにsecurityContextが指定されていることがわかります。

```yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/psp: sample-psp
    seccomp.security.alpha.kubernetes.io/pod: runtime/default
  labels:
    app: nginx
    pod-template-hash: 6799fc88d8
  name: nginx-6799fc88d8-jvcbl
  namespace: default
  :
spec:
  :
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  :
```

このようにseccompProfileフィールドへRuntimeDefaultを指定せずにPodを作成しても、PSPがその値を埋めてくれます。しかしv1.25でPSPは廃止予定のため、この方法は利用できなくなってしまいます。新たなseccomp警察が必要です。そこでSeccompDefaultの出番です :tada: 

## SeccompDefaultとは？

SeccompDefaultはKubernetesのv1.22で新たにalpha機能として追加されました。これはPodのSpecでseccomp profileが指定されなかった時に、RuntimeDefaultでコンテナを動かすものです。

## SeccompDefaultを動かしてみる

SeccompDefaultを実際に試してみましょう。今回はkindで動かします。SeccompDefaultはkubeletのfeature gatesで有効化し、かつ`--seccomp-default`をセットする必要があります。例えば1ノードのクラスタを起動するときのコンフィグは以下のようになります。

```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  image: kindest/node:v1.22.0
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        feature-gates: "SeccompDefault=true"
        seccomp-default: "true"
```

kindのコンフィグで[feature gates専用のフィールド](https://kind.sigs.k8s.io/docs/user/configuration/#feature-gates)がありますが、どうやらそこではkubeletに対してはfeature gatesがセットされないようです(今回の検証でそのことに気づきました...)。そのため上記のようにkubeletのextra argsで直接追加する必要があります。

上記で作成されたクラスタに対してSeccompDefaultが有効になっているか確認してみましょう。[amicontained](https://github.com/genuinetools/amicontained)を利用するとブロックされるsyscallをチェックできるようなので、確認にはこのアプリケーションを利用しました。以下のようにseccompを明示的にRuntimeDefaultを指定したもの(seccomp-runtime-default)、seccompを明示的にUnconfinedを指定したもの(seccomp-unconfined)、そして何も指定していないもの(seccomp-nil)を動かして確認します。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-runtime-default
  namespace: default
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  restartPolicy: Never
  containers:
  - image: alpine
    imagePullPolicy: Always
    name: alpine
    command:
    - /bin/sh
    - -c
    - |
      apk add curl >/dev/null 2>&1
      curl -sSfL -o amicontained https://github.com/genuinetools/amicontained/releases/download/v0.4.9/amicontained-linux-amd64
      chmod +x amicontained
      ./amicontained
---
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-unconfined
  namespace: default
spec:
  securityContext:
    seccompProfile:
      type: Unconfined
  restartPolicy: Never
  containers:
  - image: alpine
    imagePullPolicy: Always
    name: alpine
    command:
    - /bin/sh
    - -c
    - |
      apk add curl >/dev/null 2>&1
      curl -sSfL -o amicontained https://github.com/genuinetools/amicontained/releases/download/v0.4.9/amicontained-linux-amd64
      chmod +x amicontained
      ./amicontained
---
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-nil
  namespace: default
spec:
  restartPolicy: Never
  containers:
  - image: alpine
    imagePullPolicy: Always
    name: alpine
    command:
    - /bin/sh
    - -c
    - |
      apk add curl >/dev/null 2>&1
      curl -sSfL -o amicontained https://github.com/genuinetools/amicontained/releases/download/v0.4.9/amicontained-linux-amd64
      chmod +x amicontained
      ./amicontained
```

結果は以下です。seccompに関わる部分以外を省略しています。
結果からわかるようにRuntimeDefaultを指定したものと、nilのものはSeccompがfilteringとなり、Unconfinedのものよりブロックされている数がたくさんあります。
そしてRuntimeDefaultを指定したものとnilのもののブロックされているものが同じことから、nilのものにRuntimeDefaultが適用されていることがわかるかと思います。

```sh
$ kubectl logs seccomp-runtime-default
 :
Seccomp: filtering
Blocked Syscalls (61):
        MSGRCV PTRACE SYSLOG SETSID USELIB USTAT SYSFS VHANGUP PIVOT_ROOT _SYSCTL ACCT SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL IOPERM CREATE_MODULE INIT_MODULE DELETE_MODULE GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFSSERVCTL GETPMSG PUTPMSG AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MEMPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANOTIFY_INIT NAME_TO_HANDLE_AT OPEN_BY_HANDLE_AT SETNS PROCESS_VM_READV PROCESS_VM_WRITEV KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD PKEY_MPROTECT PKEY_ALLOC PKEY_FREE

$ kubectl logs seccomp-unconfined
 : 
Seccomp: disabled
Blocked Syscalls (19):
        MSGRCV SYSLOG SETSID VHANGUP PIVOT_ROOT ACCT SETTIMEOFDAY SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME INIT_MODULE DELETE_MODULE LOOKUP_DCOOKIE FANOTIFY_INIT OPEN_BY_HANDLE_AT FINIT_MODULE BPF

$ kubectl logs seccomp-nil
 :
Seccomp: filtering
Blocked Syscalls (61):
        MSGRCV PTRACE SYSLOG SETSID USELIB USTAT SYSFS VHANGUP PIVOT_ROOT _SYSCTL ACCT SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL IOPERM CREATE_MODULE INIT_MODULE DELETE_MODULE GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFSSERVCTL GETPMSG PUTPMSG AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MEMPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANOTIFY_INIT NAME_TO_HANDLE_AT OPEN_BY_HANDLE_AT SETNS PROCESS_VM_READV PROCESS_VM_WRITEV KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD PKEY_MPROTECT PKEY_ALLOC PKEY_FREE
```

しかしこれはPodSecurityPolicyのようにPodのSpecをdefaultingしてくれません。
どうやら[CRIへリクエストを投げるときに行うdefaulting](https://github.com/kubernetes/kubernetes/blob/aff056d8a197f6a404ad5e02210ca662d16c3dbe/pkg/kubelet/kuberuntime/security_context.go#L39-L41)のようで、PodのSpecのdefaultingではありません。
これは機能を有効化するためのfeature gateやオプションをkube-apiserverやkube-controller-managerなどではなく、kubeletに対して設定することからも想像できるかと思います。

## PodSecurity AdmissionとSeccompDefault

PodSecurityは作成・更新しようとするPodに対して[Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/)で定義されているポリシーを適用するものです。作成しようとしているPodがそれを満たさない場合、作成を拒否したり、警告に表示したり、audit logへ記録するといったことを制御するAdmission Controllerとして実装されています。詳細は拙作の[PodSecurityPolicyの廃止に備えて、一足先にPodSecurity Admissionを試してみよう!](https://qiita.com/uesyn/items/cf47e12fba5e5c5ea25f)をご参照ください。PodSecurity Standardではもちろんseccompのprofileに関しても定義されています。PodSecurity AdmissionのBaselineと呼ばれるプロファイルでは、`nil`,`RuntimeDefault`,`Localhost`のみが許可されており、`Unconfined`は許容されません。(PodSeucirty AdmissionのBaselineの実装は[こちら](https://github.com/kubernetes/pod-security-admission/blob/v0.23.0/policy/check_seccompProfile_baseline.go)で確認できます)。nilが許容されているため、冒頭の説明通りnilを指定することで`unconfined`として扱われてしまい、実質制限していないことと同じです。しかしこのRuntimeDefaultと組み合わせることで、PodSecurity AdmissionのBaselineが適用された場合seccompを確実に適用できます。

## 最後に

SeccompDefaultの動作を確認できました。Kubernetes v1.23時点でこの機能はまだalpha機能となっています。PodSecurityPolicyの廃止までに安心して利用できるようv1.25ではGAになってくれると嬉しいですね。