레이블이 OCI인 게시물을 표시합니다. 모든 게시물 표시
레이블이 OCI인 게시물을 표시합니다. 모든 게시물 표시

runc 사용하기

# runc 는 docker, kubernetes(k8s)등에서 사용하는 OCI(Open Container Initiative) 표준을 준수하는 container 기동(spawning and running) CLI 툴이다.

# golang 설치
wget https://go.dev/dl/go1.17.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local/ -zxf go1.17.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

# runc 빌드 및 설치(최신 golang 버전 필요)
cd runc
make && sudo make install

# 일반유저(vagrant)도 docker 명령을 실행할 수 있게 vagrant 계정을 docker 그룹에 추가
# 한번 exit 하고 다시 로그인해야 한다.
sudo usermod -aG docker vagrant

# busybox 이미지의 파일시스템(파일들)을 tar 로 export
docker export $(docker create busybox) -o busybox_exported.tar

# root 계정으로 전환
sudo -Es

# container 에 압축 풀기
# runc 기동시 rootfs 이름의 디렉토리를 찾는다.
cd ~
mkdir -p ./ysoftman_container/rootfs
tar xvf busybox_exported.tar -C ./ysoftman_container/rootfs

# runc 에서 사용할 명세서 생성
# 기본 명세의 config.json 파일이 생성된다.
# spec -> create a new specification file
cd ysoftman_container
tree -L 2
runc spec
ls config.json

# runc 로 ysoftman1 이름의 컨테이너 기동
runc run ysoftman1

# ysoftman1 컨테이너에서 namespace 확인
ls -ahl /proc/$$/ns

# 호스트 터미널 하나더 열고 root 계정으로 전환
sudo -Es

# 호스트에서 runc 같와 runc 가 실행한 sh 프로세스가 확인
ps -ef | grep -E "runc| sh$"

# 호스트에서 
# runc 네임스페이스 확인
lsns -p $(ps -ef | grep -v grep | grep "runc run ysoftman1" | awk '{print $2}')

# ysoftman1컨테이너(runc)가 실행한 sh pid 로 namespace 상태 확인
lsns -p $(ps --ppid=$(ps -ef | grep -v grep | grep "runc run ysoftman1" | awk '{print $2}') -o pid | grep -v PID)

# 호스트에서 runc 로 기동된 컨테이너 리스트 보기
runc list

# 테스트 결과

# runc help
checkpoint  checkpoint a running container
create      create a container
delete      delete any resources held by the container often used with detached container
events      display container events such as OOM notifications, cpu, memory, and IO usage statistics
exec        execute new process inside the container
kill        kill sends the specified signal (default: SIGTERM) to the container's init process
list        lists containers started by runc with the given root
pause       pause suspends all processes inside the container
ps          ps displays the processes running inside a container
restore     restore a container from a previous checkpoint
resume      resumes all processes that have been previously paused
run         create and run a container
spec        create a new specification file
start       executes the user defined process in a created container
state       output the state of a container
update      update container resource constraints
features    show the enabled features

#####

# 위 golang, runc, busybox 준비되어 있는 상태에서
# rootless container 생성해 보기
# root 계정이라면 일반 계정으로 나오기
exit

# ysoftman_rootless_container 에 압축풀기
cd ~
mkdir -p ./ysoftman_rootless_container/rootfs
tar xvf busybox_exported.tar -C ./ysoftman_rootless_container/rootfs

# rootless 옵션으로 runc 에서 사용할 명세서 생성
cd ysoftman_rootless_container
runc spec --rootless

# /tmp/runc 를 루트로 하는 ysoftman_rootless_container 실행
runc --root /tmp/runc run ysoftman_rootless_container

# 컨터이너에서 user id 와 네임스페이스 확인
id
ls -l /proc/$$/ns

# 호스트에서
# 루트 경로 및 state.json 확인해보기
tree /tmp/runc
cat /tmp/runc/ysoftman_rootless_container/state.json | jq . | head -10

# 호스트에서 runc 같와 runc 가 실행한 sh 프로세스가 확인
ps -ef | grep -E "runc| sh$"

# runc 네임스페이스 확인
lsns -p $(ps -ef | grep -v grep | grep "runc run ysoftman_rootless_container" | awk '{print $2}')

# ysoftman1컨테이너(runc)가 실행한 sh pid 로 namespace 상태 확인
lsns -p $(ps --ppid=$(ps -ef | grep -v grep | grep "runc run ysoftman_rootless_container" | awk '{print $2}') -o pid | grep -v PID)

# user 네이스페이스 id 가 ysoftman_rootless_container 가 호스트와 다르다.
# ysoftman_rootless_container 의 root 로 보이지만 호스트의 root는 아니다.

# 테스트 결과

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 없이 뭔가를 한다는것 아주 위험하다.

k8s sandbox OCI runtime create failed 에러

# k8s cronjob 수행으로 jobs -> pod 생성시 가끔 pod warning 이 발생한다.
# pod 내 container command(작업)는 정상적으로 수행되지만 아래와 같은 에러가 간혹 발생한다.
# 그런데 같은 클러스터에서 수행되는 다른 cronjob 은 에러가 발생하지 않는다.
# 참고로 kubernetes 버전은 1.15.5 이다.
# describe 로 pod Event 를 보면�
kubectl describe pod ysoftman-pod-xxx(가칭)

... 생략 ...

Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  3m54s  default-scheduler  Successfully assigned ysoftman-xxx/ysoftman-pod-xxx to ysoftman-worker-1
  Normal  Pulling    3m51s  kubelet,ysoftman-worker-1            Pulling image "ysoftman-test:test"
  Normal  Pulled     3m51s  kubelet,ysoftman-worker-1            Successfully pulled image "ysoftman-test:test"
  Normal  Created    3m51s  kubelet,ysoftman-worker-1            Created container ysoftman-pod
  Normal  Started    3m51s  kubelet,ysoftman-worker-1            Started container ysoftman-pod
  Normal  SandboxChanged 3m50s  kubelet,ysoftman-worker-1          Pod sandbox changed, it will be killed and re-created.
  Warning  FiledCreatePodSandBox 3m50s  kubelet,ysoftman-worker-1  Failed created pod sandbox... rpc error: code = Unknown desc = failed to start sandbox container for pod "ysoftman-pod-xxx": Error response from daemon: 
  OCI runtime create failed: container_linux.go:367: starting container process caused process_linux.go:495: container init caused: Running hook #0:: error running hook: exit status 1,stdout: , stderr: time="xxx" level=fatal msg="no such file or directory": unknown
  # 또는
  OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:319: getting the final child's pid from pipe caused \"EOF\"": unknown


# 위와 같은 이슈가 moby(docker 에서 만든 컨테이너 open source)에
# https://github.com/moby/moby/issues/40835 로 등록되어 있고 아직 리졸브되지 않았다.

# sandbox는 gke(goole kubernetes engin)가 pod내에서 실행되는 컨테이너가 보안의 위협이 되지 않도록 k8s 가 생성해 관리하는 컨테이너라고 한다.
# 에러 메시지(getting the final child's pid from pipe caused \"EOF\"": unknown. )의 뜻을 보면
# OCI(Open Container Initiative)를 준수하는 runC(OCI 를 따르는 컨테이너 런타임)이 pod->container 생성시
# 어떤 이유로 pid 를 할당받지 못하는건데, moby 이슈에 제기된 원인을 보면 크게 다음과 같다.

# 1. cpu, memory 리소스 부족 -> 노드별로 cpu, mem 상태는 넉넉한것으로 보인다.
# 2. 커널 파라메터 max pid, namespace 수 늘리기 -> 요것도 아직 그리 부족해 보이진 않고, 커널 파라메터까지 수정하는 경우는 거의 없어야 할것 같다.

# 테스트1
# 1번을 보면 현재 ysoftman-pod 의 cpu, mem 사용량은 크지 않은것 같고,
# 혹시 잦은 pod 생성 에 따른 문제는 아니진 해서 테스트로 1분 간격으로 실행하는 크론잡을 돌려 봤다.
# 하루동안 돌려봤지만, interval 1분이라 빨리 재현될줄 알았는데 pod 내 sandbox 생성 에러가 없다.

# 테스트2
# pod -> container 도 잘 수행되지만, sandbox 생성시 사용자의 컨테이너를 찾을 수 없어 발생할 수 있을것 같다.
# ysoftman-pod 의 컨테이너 커맨드가 빨리 끝나서 sandbox 컨테이너 생성시 참고하는 ysoftman-pod 컨테이너가 없어
# no such file or directory 또는 다른 에러(pid EOF 로 찾을 수 없다등)이 발생하는 것 같아
# 다음과 같이 1초내로 빨리 종료되는 크론잡 컨테이너를 설정했다.

... 생략 ...

containers:
- name: ysoftman-cronjob-test
  image: busybox
  imagePullPolicy: Always
  command:
  - /bin/sh
  - -c
  - date; echo "wait for 0s"; i=1; while [ $i -le 1 ]; do echo $i; sleep 1; i=$((i+1)); done; echo "job completed\!"
  resources:

# 위와 같이 설장하고 1분마다 크론잡이 실행되면, 100%는 아니지만 에러가 재현됐다.
# 실제 ysoftman-pod 는 사용자 업데이트를 가져오는 잡으로 대부분 업데이트 내용이 없어서 1~2초내로 바로 종료되는 컨테이너였다.

# 해결방법
# ysoftman-pod container 에 sleep 10초 주어 sandbox 생성시 사용자 컨테이너를 참조할 수 있는 시간을 주니 더이상 발생하지 않았다.