LabHub
배우기 러닝패스 코스

Running Rootless Podman

User Namespaces and subuid/subgid

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

rootless 컨테이너는 새 user namespace 를 만들면 그 안에서 UID 0 이 되기 때문에 가능하다. 밖에서 보면 여전히 평범한 사용자다.

Concept map: 새 user namespace 를 만들면 그 안에서 UID 0 이 되기 때문에 · 사실상 root 권한 그 자체 · 내 UID 가 0 으로 보이게 · 모든 capability 를 갖는다

왜 이게 필요했나

컨테이너를 만들려면 mount, pivot_root, 네트워크 네임스페이스 생성 같은 특권 작업이 필요하다. 그래서 오랫동안 컨테이너 데몬은 root 로 돌았다. 그 결과 /var/run/docker.sock사실상 root 권한 그 자체가 되었고, 이를 노리는 공격이 끊이지 않았다. 그 소켓에 접근할 수 있는 사용자는 호스트 루트 파일시스템을 마운트한 컨테이너를 띄우는 것만으로 호스트를 장악할 수 있다.

user namespace 는 이 문제를 뿌리부터 바꿨다.

어떻게 동작하나

매핑의 원리

unshare(CLONE_NEWUSER) 로 새 user namespace 를 만들면, 그 안에서 내 UID 가 0 으로 보이게 매핑할 수 있다. 그리고 새 namespace 안에서는 모든 capability 를 갖는다. 그래서 그 안에서 mount 나 네트워크 네임스페이스 생성 같은 작업이 가능해진다.

중요한 것은 그 권한이 namespace 안에서만 유효하다는 점이다. 밖의 파일에 대해서는 여전히 원래 UID 의 권한만 가진다. 컨테이너가 탈옥해도 손에 쥐는 것은 일반 사용자 권한뿐이다.

왜 UID 하나로는 부족한가

컨테이너 이미지 안에는 여러 UID 의 파일이 있다. root(0), nobody(65534), 애플리케이션 계정(1000) 등. UID 하나만 매핑하면 그 이미지를 제대로 풀 수 없다.

그래서 UID 대역을 빌려준다. /etc/subuid/etc/subgid 가 그 대장이다.

# /etc/subuid
podster:100000:65536

읽는 법: 사용자 podster 는 호스트 UID 100000 부터 65536 개를 자기 namespace 안에서 마음대로 쓸 수 있다.

매핑 결과는 이렇게 된다.

컨테이너 안 UID 호스트 UID
0 (root) podster 자신의 UID (예: 1000)
1 100000
2 100001
1000 100999
65535 165534

컨테이너 안 UID 1 이 호스트 100000 에 대응한다는 점을 주의해야 한다. 0 은 사용자 본인에게 매핑되고, 1부터가 subuid 대역의 시작이다. 그래서 컨테이너 안 UID N(N≥1)의 호스트 UID 는 100000 + N - 1 이다.

podster 에게 100000 부터 65536 개를 빌려줬을 때의 UID 매핑. 컨테이너 안 0 만 사용자 본인인 호스트 1000 에 가고, 1 부터가 빌린 대역의 시작이라 1 은 100000, 2 는 100001, 1000 은 100999, 65535 는 165534 에 대응한다

65536 개를 주는 이유는 컨테이너 이미지가 쓰는 UID 가 그 범위 안에 대부분 들어가기 때문이다. 사용자마다 겹치지 않는 대역을 줘야 하므로 보통 100000, 165536, 231072... 처럼 65536 씩 띄워 배정한다.

대역을 실제로 적용하려면 newuidmap / newgidmap 이라는 setuid 헬퍼가 필요하다(uidmap 패키지). 이 바이너리가 없거나 file capability 가 없으면 대역 매핑이 실패하고 단일 UID 모드로 폴백한다. 그 상태에서도 컨테이너는 뜨지만 여러 UID 를 쓰는 이미지에서 권한 오류가 난다.

확인하는 법

grep '^podster:' /etc/subuid /etc/subgid
podman unshare cat /proc/self/uid_map
podman info --format '{{.Host.Security.Rootless}}'
podman info --format '{{.Store.GraphRoot}}'

podman unsharepodman 이 쓰는 것과 같은 user namespace 안에서 명령을 실행한다. 파일 소유권 문제를 디버깅할 때 필수 도구다. 예를 들어 볼륨 디렉터리의 소유자가 이상하게 보일 때 podman unshare ls -l <경로> 로 보면 컨테이너 관점의 소유자가 나온다.

rootless 의 제약

제약 이유 우회
1024 미만 포트 바인딩 불가 특권 포트 net.ipv4.ip_unprivileged_port_start 조정, 또는 높은 포트 + 리버스 프록시
네트워크가 유저 공간 스택 pasta/slirp4netns 경유 초고성능이 필요하면 rootful
일부 스토리지 드라이버 제한 overlay 마운트 특권 fuse-overlayfs 또는 vfs
cgroup 제어 제한 cgroup v2 위임 필요 systemd 사용자 슬라이스 위임 설정
ping 이 안 될 수 있음 ICMP 소켓 권한 ping_group_range 조정

현장에서 만나는 모습

"docker system df 감각으로 디스크를 찾다가 당황한다." rootless podman 의 이미지는 /var/lib/docker 가 아니라 ~/.local/share/containers/storage 에 쌓인다. 홈 파티션이 작은 서버에서는 이것만으로 디스크가 찬다. 그래서 다음 모듈의 graph root 이전 시나리오가 실무에서 자주 나온다.

podman info 에서 rootless 가 false 로 나온다. root 로 실행했거나, 매핑 설정이 없어 rootful 로 폴백한 것이다. 어느 쪽인지 확인하지 않으면 "왜 파일 소유자가 이상하지" 를 며칠 헤맨다.

다음 확인에서 볼 것

이어지는 퀴즈에서는 user namespace의 권한 범위, subuid/subgid 매핑, rootless 네트워크·스토리지 제약을 구분한다. 그 기준을 확인한 뒤 다음 모듈에서 사용자 podster의 매핑과 storage.conf·registries.conf를 작성한다.