[Kubernetes 1.25] Alpha: Pod内でのUser Namespaceを試してみた

# はじめに
Kubernetes 1.25でAlpha機能ではありますが、PodでのUser Namespaceがサポートされました！
- User Namespaceの概要について書いた記事は以下の通りです。
[[Kubernetes 1.25] Added alpha support for user namespaces in pods phase 1](https://qiita.com/junya19862/items/c59c2bb17075b5b515e8)

今までは、Podは.spec.securityContext.runAsUserで非rootで動作させることが可能でしたが、その場合だと、起動にroot権限が必要なnginxなどのコンテナは起動できず、PodがCrashLoopBackOffとなっていました。そこで、Kubernetes 1.25で実装されたPodでのUser Namespaceを利用し、ノードのある範囲のUIDを、PodのUID　0-65535にマッピングすることで、見せかけのroot(UID 0)で起動することが可能となります。

またPodはrootで動作していないので、プロセスが奪取されてもUser Namespace外の操作はできず、リスクも軽減されます。例えば、コンテナ内にノードのディレクトリがマウントされているとして、マッピングされているノードUID範囲外のUIDで所有されているファイルがそのディレクトリに配置されていても、コンテナのプロセスは権限不足で読むことができません。

なお、現在Alphaでは以下のvolumeのみがサポートされており、[今後Alpha→Beta→GAと昇格する際に他のvolumeもサポートされる予定です](https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/README.md#phases)。
・configmap
・secret
・downwardAPI
・emptyDir
・projected

# 検証環境
今回の環境は以下の通りです。
- Kubernetes v1.25.0
- CRI: CRI-O v.1.25.0

# 前提条件
- FeatureGate:UserNamespacesStatelessPodsSupportが有効であること
- CRIがCRI-O v1.25以上であること
  ※containerdはv1.7からuser namespaceをサポート予定です。

## FeatureGate:UserNamespacesStatelessPodsSupport有効化対象
Kubernetesのドキュメントを確認すると、以下の通りkube-scheduler、kube-proxy、kube-apiserver、kube-controller-managerがヒットします。
![UserNamespacesStatelessPodsSupport検索スクショ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117488/b89d888f-c1fd-b9b8-6db6-96cbdde76560.png)
しかし、実際には全部にFeatureGateを有効にしても、User Namespaceは使えません。

そこで、User NamespaceのPR時のコードを確認すると、kube-scheduler、kube-proxy関連のコードに変更はないことがわかります。

https://github.com/kubernetes/kubernetes/pull/111090/files

また、kubeletのコードに変更が加えられており、User NamespaceのManager作成時やUIDのマッピングの際に、FeatureGate:UserNamespacesStatelessPodsSupportが有効か、確認しているようでした。

https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/userns_manager.go#L187-L204
```golang
func MakeUserNsManager(kl userNsPodsManager) (*usernsManager, error) {
	m := usernsManager{
		// Create a bitArray for all the UID space (2^32).
		// As a by product of that, no index param to bitArray can be out of bounds (index is uint32).
		used:   makeBitArray((math.MaxUint32 + 1) / userNsLength),
		usedBy: make(map[types.UID]uint32),
		kl:     kl,
	}
	// First block is reserved for the host.
	m.used.set(0)


	// Second block will be used for phase II. Don't assign that range for now.
	m.used.set(1)


	// do not bother reading the list of pods if user namespaces are not enabled.
	if !utilfeature.DefaultFeatureGate.Enabled(features.UserNamespacesStatelessPodsSupport) {
		return &m, nil
	}
:
```
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/userns_manager.go#L422-L426
```golang
// GetOrCreateUserNamespaceMappings returns the configuration for the sandbox user namespace
func (m *usernsManager) GetOrCreateUserNamespaceMappings(pod *v1.Pod) (*runtimeapi.UserNamespace, error) {
	if !utilfeature.DefaultFeatureGate.Enabled(features.UserNamespacesStatelessPodsSupport) {
		return nil, nil
	}
```
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/userns_manager.go#L532-L541
```golang
// getHostIDsForPod if the pod uses user namespaces, takes the uid and gid
// inside the container and returns the host UID and GID those are mapped to on
// the host. If containerUID/containerGID is nil, then it returns the host
// UID/GID for ID 0 inside the container.
// If the pod is not using user namespaces, as there is no mapping needed, the
// same containerUID and containerGID params are returned.
func (m *usernsManager) getHostIDsForPod(pod *v1.Pod, containerUID, containerGID *int64) (hostUID, hostGID *int64, err error) {
	if !utilfeature.DefaultFeatureGate.Enabled(features.UserNamespacesStatelessPodsSupport) {
		return containerUID, containerGID, nil
	}
:
```

ということで、最終的に以下のコンポーネントにFeatureGateを有効化することでUser Namespaceが利用できるようになりました。
- kube-apiserver
- kube-controller-manager
- WokerNodeのkubelet

# 試してみる
1.CRI-Oのバージョン確認
前提条件を満たすため、CRI-Oはv1.25.0を利用します。
```
$ sudo crio version
INFO[2022-08-31 01:09:11.421365971Z] Starting CRI-O, version: 1.25.0, git: unknown(clean) 
Version:        1.25.0
GitCommit:      unknown
GitCommitDate:  unknown
GitTreeState:   clean
BuildDate:      2022-08-29T19:48:32Z
GoVersion:      go1.19
Compiler:       gc
Platform:       linux/amd64
Linkmode:       dynamic
BuildTags:      
  apparmor
  seccomp
  containers_image_ostree_stub
  exclude_graphdriver_btrfs
  exclude_graphdriver_devicemapper
LDFlags:          -s -w -X github.com/cri-o/cri-o/internal/pkg/criocli.DefaultsPath="" -X github.com/cri-o/cri-o/internal/version.buildDate=2022-08-29T19:48:32Z 
SeccompEnabled:   true
AppArmorEnabled:  true
Dependencies:     
```
2.Kubernetes Cluster確認
```
$ k get node -owide
NAME          STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION    CONTAINER-RUNTIME
v125-master   Ready    control-plane   21h   v1.25.0   10.146.0.8    <none>        Ubuntu 20.04.4 LTS   5.15.0-1016-gcp   cri-o://1.25.0
v125-worker   Ready    <none>          21h   v1.25.0   10.146.0.9    <none>        Ubuntu 20.04.4 LTS   5.15.0-1016-gcp   cri-o://1.25.0
```

3.FeatureGate有効化
以下のファイルにfeature-gatesパラメータを追記し、有効化します。
- kube-apiserver
/etc/kubernetes/manifests/kube-apiserver.yaml 
```yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.146.0.8:6443
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    :
    - --feature-gates=UserNamespacesStatelessPodsSupport=true
```
- kube-controller-manager
/etc/kubernetes/manifests/kube-controller-manager.yaml 
```yaml
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    component: kube-controller-manager
    tier: control-plane
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    :
    - --feature-gates=UserNamespacesStatelessPodsSupport=true
```
- WokerNodeのkubelet
/var/lib/kubelet/kubeadm-flags.env
```
KUBELET_KUBEADM_ARGS="--container-runtime=remote --container-runtime-endpoint=unix:///var/run/crio/crio.sock --pod-infra-container-image=registry.k8s.io/pause:3.8 --feature-gates=UserNamespacesStatelessPodsSupport=true"
```
→ systemctl restart kubelet

以上で全ての前提条件が満たせました。
いよいよUser Namespaceを利用してみます。

4.User Namespaceを利用するPod作成
.spec.hostUsersフィールドをfalseにすることでUser Namespaceが利用可能です。
```
$ cat << EOT | kubectl apply -f - 
> apiVersion: v1
> kind: Pod
> metadata:
>   name: userns
> spec:
>   hostUsers: false
>   containers:
>   - name: shell
>     command: ["sleep", "infinity"]
>     image: debian
> EOT
pod/userns created
```

5.WorkerNode上でUID確認
PodにマッピングされているUIDをWorkerNode上のファイルから確認可能です。
`/var/lib/kubelet/pods/\<Podの.metadata.uid\>/userns`ファイルを確認することで、マッピング状況が確認できます。
以下の場合、UIDの範囲が65536なので、WorkerNode上のUID 131072〜196607が、PodのUID　０〜65535にマッピングされていることが分かります。
```
$ sudo cat /var/lib/kubelet/pods/$(k get po userns -o jsonpath='{.metadata.uid}')/userns
{"uidMappings":[{"hostId":131072,"containerId":0,"length":65536}],"gidMappings":[{"hostId":131072,"containerId":0,"length":65536}]}
```
6.Pod内でのUID確認
`/proc/self/uid_map`ファイルを確認することで、マッピング状況がPodからも確認できます。
WorkerNode上で確認したマッピング状況と同じになってます。
これで、WorkerNodeとPodのUIDが分離できていることが分かりました。
```
$ k exec userns -it -- bash
root@userns:/# 
root@userns:/# id
uid=0(root) gid=0(root) groups=0(root)
root@userns:/# 
root@userns:/# cat /proc/self/uid_map
         0     131072      65536
```

7.root権限が必要なコンテナを持つPodを作成
以下の通り、User Namespaceを利用し、見せかけのrootとして起動しているので、RUNNINGとなります。
```
$ cat << EOT | kubectl apply -f -
> apiVersion: v1
> kind: Pod
> metadata:
>   labels:
>     run: nginx
>   name: nginx
> spec:
>   hostUsers: false
>   containers:
>   - image: nginx
>     name: nginx
> EOT
pod/nginx unchanged
$ 
$ k get po nginx 
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   2          27h
$ 
$ sudo cat /var/lib/kubelet/pods/$(k get po nginx -o jsonpath='{.metadata.uid}')/userns
{"uidMappings":[{"hostId":196608,"containerId":0,"length":65536}],"gidMappings":[{"hostId":196608,"containerId":0,"length":65536}]}
```

# さいごに
いかがでしたでしょうか?
まだAlphaの機能ですが、我慢できずに試してみました。
本番運用レベルGAになるにはまだ先ですが、今後の開発状況を監視しつつ楽しみに待ちましょう。