user namespace 로 kubernetes 와 container 보안 향상

user namespace 로 kubernetes 와 container 보안 향상


!!! Caution 잘못된 번역이나 이해로 틀릴 수 있음!!! ^^;;

[user namespace 란?]
user namespace 는 user IDs, group IDs 를 격리(isolate)한다. 리눅스에서 모든 프로세스는 특정 userid(/etc/passwd에 정의)와 groupid(/etc/group에 정의)로 소유된다. user namespace 는 container 안에서는 host의 userid,groupid 의 일부만 볼 수 있도록 한다.
예를 들어 container 안에서의 root(user id 0)는 사실 호스트의 userid=100000 이다.

[user namespace and capabilities]
container 에서 root 로 보이지만 실제는 user namespace 로 구분되는 사용자다. 이와 관련된 linux kernel 히스토리를 살펴보자.

- linux 2.2 이전에는 실제 root(user id 0)는 1개 였다.
- linux 2.2 이후부터 root 권한이 여러개의 기능 구분되는 capabilities(https://man7.org/linux/man-pages/man7/capabilities.7.html)로 나뉘어졌다. ex) CAP_NET_ADMIN : network 설정권한, CAP_SYS_ADMIN : mount 설정권한
- linux 3.8 (2013년) 에 user namespace 가 소개되고 capabilities 는 더이상 글로벌하게 사용하지 않고 user namespace 의 context 에 의해서 해석된다.

예로 sshfs(fuse 로 ssh filesystem 에 마운트) 프로그램을 실행할때 CAP_SYS_ADMIN capability 가 요구된다. 이건 host 와 container 사이를 격리시키는 목적에 맞지 않게 container 에거 너무 막강한 권한을 부여하게 된다.
(참고로 FUSE:Filesystem in Userspace 로 user 레벨에서 파일시스템을 만들 수 있도록 해준다.)

container 가 새로운 user namespace 없이 기동된 상태에서 sshfs 성공하기 위핸선 CAP_SYS_ADMIN 을 허용할 수 밖에 없어 보안 취약점을 내포하고 있다.(호슽와 같은 name space 를 쓰면 sshfs 프로세스가 CAP_SYS_ADMIN 권한을 가지고 있어 container 안에서 host 로 나쁜짓을 할 수 있음?)

하지만 container 가 새로운 user namespace 로 격리돼 실행된다면 호스트 user namespace 의 CAP_SYS_ADMIN 없이도 container 내에서의 CAP_SYS_ADMIN 를 허용할 수 있다.(host user namespace 가 아닌 container user namespace 의 CAP_SYS_ADMIN 로 container 의 mount namespace 의 filesystem 에 마운트하는것이라 호스트에는 영향이 없다.)
container 내에서의 root 는 호스트의 root 가 아니기 때문에 host 에 영향을 줘서는 안된다.

[user namespace and filesystems]
linux 는 user namespace 에서의 mounting 안전을 고려해 FS_USERNS_MOUNT flag 를 마크해 filesystems 리스트를 유지한다. 초기화되지 않은 user namespace 에 새로운 마운트를 생성하려면 FS_USERNS_MOUNT 타입이어야 한다. linux 저장소에서 다음과 검색해보면 리스트 찾아 볼 수 있다. 
git grep -nw FS_USERNS_MOUNT

초기화 되지 않은 uesr namespace 는 초기에는 profs 와 sysfs 타입의 파일 시스템으로 제한되었는데 지금은 아래와 같은 파일 시스템이 허용된다. 이런 파일 시스템은 권한이 없는 사용자도 안전하게 사용할 수 있기 때문에 허용됐다.

파일 시스템         user namespace 에서 허용됨
procfs,sysfs --> linux 3.8, 2012
tmpfs        --> linux 3.9, 2013
cgroupfs     --> linux 4.6, 2016
FUSE         --> linux 4.18, 2018
overlay      --> linux 5.11, 2020
NFS,ext4,btrfs,etc  --> 허용 안됨

[impact on container security]
user namespace 는 호스트와 container 를 격리하는 보안 레이어다.
호스트의 runc(OCI(Open Container Initiative)를 따른 컨테이너 런타임)가 container 에 의해 overwritten 될 수 있는 취약점이 있었다. 이 취약점은  runc 바이너리는 root 소유이고 container 에 매핑되지 않았는데도, 컨테이너의 프로세스가 /proc/self/exe 를 통해 호스트의 runc 를 참조를 허용한다. 
flatcar container linux 는 호스트의 runc 바이너리를 읽기전용 파일시스템을 두어 완화했다.

[enabling user namespace for FUSE filesystem]
linux 커널은 파일이 변경되었다면 읽거나 쓰기전에 IMA(integrity Measurement Architecture) 서브시스템을 사용해 감지하고 감사(audit)한다.
FUSE 와 같은 파일시스템은 커널이 remeasure, reppraise, reaudit 없이 파일을 서빙할 수 있다. 
memfs FUSE driver 의 패치를 테스트 하기 위해 다음과 같은 시나오리가 있다고 하자.
1. 첫번째 요청에 FUSE 드라이버는 파일의 초기 내용으로 서빙된다.
2. IMA 파일에 대한 측정(measurement)을 제공한다.
3. 두번째 요청에 FUSE 드라이버는 같은 파일의 변경된 내용으로 서빙된다.
4. IMA 내용을 다시 측정하지 않아 내용 변경이 측정되지 않는다.

IMA 에 'force' 옵션을 주어 항상 remeasure, reppraise, reaudit 하도록 하는 패치를 생각해보자.
커널이 아무 변경도 감지하지 못했는데 모든 요청마다 강제로 측정이 수행된다. FUSE 에서 다르게 동작하는 부분이 다른 파일스템들에서 모두 알수는 없어, 이로 인해 'force' 옵션이 잘못된 레이어에 추가될 수 있다. 이 문제를 해결하기 위해 IMA 캐싱 관련 옵션 수행하는 FS_NO_IMA_CACHE(v1,v2,v3,v4) flag 를 제공한다. IMA 는 FS_NO_IMA_CACHE flat 를 체크한 후 'force' 이 있으면 사용한다. 이런 방법으로 IMA 은 다른 파일 시스템들 모두를 알 필요가 없다.
하지만 IMA 'force' 옵션은 모든 문제를 해결하지는 못하고 아직 중요사항들은 검증되지 않았다. IMA 관리자들에 의해 linux 4.17 에는 다음과 같은 flag가 구현되어졌다.
- SB_I_IMA_UNVERIFIABLE_SIGNATURE flag : 중요 검증되지 않았음을 표시하는
- SB_I_UNTRUSTED_MOUNTER flag : 신뢰하지 않는 user namespace 에 마운트 되었을 때

[bring user namespace to kubernetes]
kubernetes(k8s)에서는 user namespace 기능을 사용할 수 없다. 2016년 관련 개선을 논의하기 시작했다.
첫번째 시도로 k8s 는 default 가 아닌 Container Runtime Interface(CRI)를 사용해 kubelet 는 CRI 의 gRPC interface 로 컨테이너와 통신하고 주요 목적으로 user namespace 를 제공한다.

kubelet 이 container runtime 와 통신하는 방법

- kubelet docker api 로 통신 (deprecated)
Kubelet --> docker --> containers

- kubelet CRI의 gRPC 로 통신, CRI shim(CRI protocol 처리)
Kubelet --(CRI)--> CRI shim --> container runtime --> containers

- CRI protocol처리하는 containerd/cri 로 컴파일된 containerd 를 사용
Kubelet --(CRI)--> containerd/cri|containerd --(OCI spec)--> runc --> containers

[CRI changes for user namespaces]
user namespace 지원을 위한 CRI 변경 아이디어

1. kubelet 이 sandbox(infrastructure container) pod 시작을 위해 RunPodSandBox()로 container runtime 에 요청하면 pod 안에 container 들의 namespace 를 공유
2. kubelet 이 sandbox 에 CreateContainer(), pod 의 각 container들은 sandbox 를 참조하고 있음.

이렇게 해서 pod 의 컨테이너들은 use namespace 를 공유한다.

IPC 와 network namespace 도 pod 레벨에서 공유된다. IPC, network 는 pod 의 user namespacce 의 의해 소유되고 이 네임스페이스에서의 capabilities 가 허용된다.
pod 의 각 container 의 mount namespace 도 user namespace 의 의해 소유된다. 
(pod 에 network, ipc, user namespace 를 두면 호스트와는 격리된체 pod 내 container 들은 자유롭게 통신할 수 있다.)

[kubernetes volumes]
container 들이 같은 volume에 접근할때 다음 시나리오를 고려해 보자.

1. container1 에서 NFS 에 파일을 쓴다. 이 파일은 uid=10000(container1 mapping 되있음)
2. container2 에서 NFS 의 파일을 읽는다. container2에 uid=10000이 mapping 되어 있지 않아 uid=65534(nobody)로 보여진다. 

이 문제를 해결하기 위해 다음과 같은 방법을 사용한다.

1. 모든 pod 에 매핑되어 있는 uid 를 사용한다. 컨테이너들간의 격리는 줄어들지만 user namespace 사용하지 않는것 보단 안전하다.
2. 각 pod 에 매핑되어 다른 uid 를 사용하고 실제 파일을 사용할때 마다 uid를 convert 할 수 있는 메커니즘(mechanism)기능을 추가한다. linux 커널에는 shiftfs, fsid mapping, new mout api 와 같은 메커니즘을 제공한다. 하지만 아직까지는 linux upstream 제품에는 없다.

오늘날 volume 사용하지 않는 작업이 많이 있고 이는 user namespace 에 있어서 이익이다. 완벽한 해결책을 찾으려고 k8s 에 구현중인 user namespace 를 막지는 말자.

[conclusion]
user namespace 는 linux 에서 container 들을 더 안전하게 사용할 수 있는 유용한 레이어를 제공한다. 이는 지난 몇몇 취약점들을 완화하는 것으로 입증됐다. volume 제공시 단점으로 어려움을 겪고 있지만 linux kernel 에서 개발중이니 앞으로 많이 개선될것으로 기대한다.
언젠가 kubernetes 가 user namespace 를 지원을 완료하면 kubernetes 는 호스트와 컨테이너들 간에 더욱 안전하게 격리 효과를 볼 것이다. 이는 좀더 많은 권한으로 운영되는 container 들에서의 새로운 상황에도 적용된다. user namespace 없이 뭔가를 한다는것 아주 위험하다.

comments: