Podman의 루트리스 구현은 사용자 네임스페이스, UID 분리, Linux capabilities를 조합해 컨테이너 프로세스의 호스트 권한을 제한함
Copy Fail은 루트리스 컨테이너도 취약점에 면역이 아니지만, Podman 설정으로 침해 후 공격 범위를 줄일 수 있음을 보여줌
루트리스 컨테이너의 동작 방식
기본 예제: 비권한 사용자 bar가 HTTP 서버 실행
예제 환경은 UID 1001인 비권한 사용자 bar가 Podman으로 ubuntu:latest 기반 이미지를 빌드하고, python3 -m http.server를 실행하는 구성임
호스트에서 ps로 보면 python3 프로세스는 사용자 bar 소유로 실행됨
Podman은 fork/exec 모델을 사용하므로 컨테이너 프로세스는 podman run 프로세스의 후손이 되며, 일반적인 UID 분리로 컨테이너 프로세스를 호스트 root나 다른 사용자와 분리할 수 있음
일반적인 Docker 구성에서는 비권한 사용자가 docker run을 실행해도 Docker 클라이언트가 루트 권한 데몬과 통신하고, 데몬이 최종적으로 컨테이너 프로세스를 생성하므로 호스트에서 컨테이너 프로세스가 root로 보일 수 있음
루트리스 rootful
컨테이너 이미지는 명시적인 USER 지시어나 --user 플래그가 없으면 보통 컨테이너 명령을 내부 root로 실행함
podman top 출력에서 HTTP 서버 프로세스는 호스트 사용자 1001로 매핑되지만, 컨테이너 내부 사용자로는 root로 실행됨
이 구성은 호스트에서는 비권한 사용자로 실행되지만 컨테이너 내부에서는 root인 rootless rootful 상태임
사용자 네임스페이스
Podman 루트리스 컨테이너는 사용자 네임스페이스를 사용해 컨테이너 안팎의 UID/GID를 다르게 매핑함
예제에서 컨테이너 내부 UID 0인 root는 호스트의 UID 1001인 bar로 매핑됨
/etc/subuid의 bar:165536:65536 설정은 bar의 네임스페이스 프로세스에 할당 가능한 UID 범위를 정함
예제에서는 bar의 UID 1001 외에 165536부터 231072까지의 UID가 bar 프로세스에 할당될 수 있음
컨테이너 내부 사용자 www-data로 sleep을 실행하면 내부에서는 www-data지만 호스트에서는 165568로 표시됨
podman unshare로 사용자 네임스페이스에 들어가면, 호스트에서 bar:bar 소유인 홈 디렉터리가 네임스페이스 내부에서는 root:root로 보임
Docker도 사용자 네임스페이스를 지원하지만 별도 설정이 필요하고 하나의 사용자 네임스페이스만 허용되는 반면, Podman은 각 UNIX 사용자의 루트리스 컨테이너를 해당 사용자 네임스페이스에서 실행함
권한 작업과 Linux capabilities
Podman은 Linux capabilities를 사용해 컨테이너 프로세스에 세분화된 루트 권한을 부여함
이미지 빌드 중 apt install 같은 작업은 chown, dac_override, fowner, setgid, setuid, net_bind_service, sys_chroot 같은 capability 조합으로 가능해짐
podman build --cap-drop=all로 모든 capability를 제거하면 apt가 setgroups, setegid, seteuid, chown 등에 실패해 이미지 빌드가 실패함
필요한 capability만 추가하는 방식도 가능하며, 예제에서는 CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER를 추가해 패키지 설치를 수행함
기본 실행 상태의 HTTP 서버는 컨테이너 내부 root로 실행되며 CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT 같은 많은 effective capabilities를 가짐
HTTP 서버에는 이런 권한이 필요 없으므로 podman run --cap-drop=all로 모든 capability를 제거할 수 있으며, 이때 podman top에서는 effective capabilities가 none으로 표시됨
루트리스 non-root
컨테이너 내부에서도 비권한 사용자로 HTTP 서버를 실행하려면 기존 /etc/passwd의 사용자, 예를 들어 www-data를 쓰거나 이미지 빌드 중 전용 사용자를 만들 수 있음
예제에서는 UID 1002인 foo 사용자와 그룹을 만들고 /var/www/html에 읽기 권한을 부여한 뒤 USER foo:foo를 설정함
이 이미지를 --cap-drop=all로 실행하면 프로세스는 컨테이너 내부 foo, 호스트 UID 166537, effective capabilities none 상태가 됨
컨테이너 프로세스는 필요한 최소 권한으로 실행해야 하며, 예를 들어 foo가 권한 포트 80에 바인딩해야 한다면 --cap-add=CAP_NET_BIND_SERVICE를 추가해야 함
컨테이너 실행 방식은 네 가지로 구분됨
root 호스트 사용자 + 컨테이너 root: root rootful
root 호스트 사용자 + 컨테이너 비권한 사용자: root non-root
비권한 호스트 사용자 + 컨테이너 root: rootless rootful
비권한 호스트 사용자 + 컨테이너 비권한 사용자: rootless non-root
Podman은 rootless rootful 컨테이너 실행을 쉽게 만들며, 컨테이너 프로세스를 비권한 사용자로 실행할 수 있다면 rootless non-root 구성도 비교적 쉽게 만들 수 있음
바인드 마운트와 UID 격리
호스트 디렉터리를 컨테이너에 마운트하면 호스트 root, 호스트 bar, 네임스페이스 foo가 소유한 파일의 접근 가능 여부가 UID 매핑에 따라 달라짐
예제에서는 /var/lib/bar/test 디렉터리에 호스트 root 소유 root.txt, 호스트 bar 소유 bar.txt를 만들고, 컨테이너에서 /test로 읽기/쓰기 마운트함
컨테이너를 foo로 실행하면 호스트 bar 소유 파일은 컨테이너 내부에서 root:root로 보이고, 호스트 root 소유 파일은 네임스페이스에 매핑되지 않아 nobody:nogroup으로 보임
컨테이너 내부 foo는 bar.txt와 root.txt를 읽지 못하며, rootless non-root가 rootless rootful보다 추가 격리를 제공함
foo가 마운트 디렉터리에 만든 foo.txt는 호스트에서 UID 166537 소유로 표시되고, 호스트 사용자 bar는 해당 파일 내용을 읽지 못함
컨테이너를 내부 root로 실행하면 네임스페이스 root는 호스트 bar 소유 파일과 foo 소유 파일을 읽을 수 있지만, 호스트 root 소유 파일은 읽지 못함
내부 root로 실행하면서 --cap-drop=all을 적용하면 foo 파일도 읽지 못하고, 호스트 bar 소유 파일만 읽을 수 있음