LabHub

Rootless Podman 운영 · rootless 가 가능한 이유 · 이론

user namespace 와 subuid/subgid

LabHub 에서 이어서 보기

한 줄 요약

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

왜 이게 필요했나

컨테이너를 만들려면 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/subuidpodster: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 이다.

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

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

확인하는 법

grep '^podster:' /etc/subuid /etc/subgidpodman unshare cat /proc/self/uid_mappodman 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를 작성한다.