Rootless Docker라는 이름만 들으면 “이제 root 권한 없이 안전하게 컨테이너를 돌린다”처럼 들려. 방향은 맞아. Docker 데몬이 host root로 떠 있고, /var/run/docker.sock에 접근한 사람이 사실상 host를 장악할 수 있는 구조는 분명히 위험하니까.
그런데 여기서 한 발만 더 들어가면 이야기가 바뀐다. rootless는 권한을 없애는 기술이 아니라, 권한의 위치와 실패 반경을 바꾸는 기술에 가까워.1
좋은 질문은 "rootless인가 아닌가"가 아니라, 어떤 권한을 어디에서 빌려오고 어떤 보호 장치를 대신 내려놓는가야.
무슨 일
기존 Docker의 큰 위험은 데몬이 root로 돈다는 점이야. 이미지 빌드, 네임스페이스 생성, 마운트, 네트워크 설정 같은 일에는 강한 권한이 필요해. 그래서 Docker 데몬이 root로 떠 있고, Docker 소켓에 접근할 수 있는 사용자는 그 데몬을 통해 host에 강한 영향을 줄 수 있어.
Rootless Docker는 이 구조를 바꿔. 각 사용자가 자기 계정 아래에서 개인 Docker 데몬을 띄우고, 소켓도 보통 /run/user/1000/docker.sock 같은 사용자별 위치에 둔다. 데몬이 뚫려도 공격자는 host root가 아니라 그 사용자 권한에 머물 가능성이 커져.
핵심 장치는 user namespace야. user namespace 안에서는 프로세스가 root처럼 보일 수 있지만, 바깥 host에서는 일반 사용자 UID로 매핑돼. 컨테이너 안의 UID 0이 host의 UID 0과 같은 사람이 아니라는 뜻이야.
flowchart TD A["기존 Docker"] --> B["root 데몬"] B --> C["소켓 접근자가 host에 강한 영향"] D["Rootless Docker"] --> E["사용자 계정 아래 데몬"] E --> F["user namespace 안의 root"] F --> G["host에서는 일반 사용자 UID로 매핑"]
이건 의미 있는 개선이야. 컨테이너 탈출이나 데몬 취약점이 생겼을 때 곧장 host root로 떨어지는 길을 좁히니까. Ken Muse의 글도 이 장점을 부정하지 않아. 문제는 그다음이야. Rootless Docker가 돌아가려면 user namespace가 꽤 많은 kernel 인터페이스를 만지게 된다.1
권한은 사라지지 않는다
컨테이너를 만들려면 PID namespace, mount namespace, network namespace 같은 격리 장치를 만들어야 해. 일반 사용자는 원래 이런 일을 마음대로 할 수 없어. Rootless Docker는 RootlessKit과 newuidmap, newgidmap 같은 보조 프로그램으로 user namespace를 먼저 만들고, 그 안에서 필요한 네임스페이스를 다시 만든다.
이 구조에서는 컨테이너 안에서 root처럼 보이는 권한이 host의 진짜 root 권한과 분리돼. 파일도 컨테이너 안에서는 root 소유처럼 보여도 host에서는 일반 사용자나 /etc/subuid에 정의된 범위로 매핑돼. 이게 rootless의 실익이야.
하지만 user namespace 안의 프로세스는 그 네임스페이스 안에서 CAP_SYS_ADMIN, CAP_NET_ADMIN 같은 강한 capability를 얻어. capability는 Linux에서 root 권한을 잘게 쪼갠 권한 묶음이라고 보면 돼. 문제는 kernel의 오래된 코드 중 상당수가 “이 인터페이스는 믿을 수 있는 root만 부른다”는 가정 아래 만들어졌다는 점이야.
user namespace는 그 문을 일반 사용자에게도 열어준다. 범위는 네임스페이스 안으로 제한되지만, 호출 경로 자체는 넓어진다. 네트워크 설정, 마운트 처리, iptables 같은 영역에 숨어 있던 kernel 취약점이 일반 사용자에게 닿을 수 있는 공격면이 되는 거야.
그래서 rootless는 단순히 더 안전한 모드가 아니다. root 데몬 위험을 줄이는 대신, user namespace가 여는 kernel 공격면을 받아들이는 선택이야.
unconfined의 비용
실무에서 더 까다로운 부분은 빌드 환경이야. Rootless BuildKit을 컨테이너 안에서 돌리려면 seccomp=unconfined나 apparmor=unconfined 같은 설정이 필요해지는 경우가 있다.1
이 단어들은 가벼운 튜닝 옵션처럼 보이지만, 실제로는 보호 장치를 끄는 말이야.
seccomp는 프로세스가 kernel에 요청할 수 있는 system call을 걸러. Docker의 기본 seccomp profile은 위험한 system call 여럿을 막아. 그런데 seccomp=unconfined는 이 필터를 꺼버린다. rootless build가 필요로 하는 unshare, mount 같은 호출을 허용하려고 문을 넓히는 셈이야.
AppArmor는 프로세스가 어떤 파일, 네트워크, capability에 접근할 수 있는지 profile로 제한해. Ubuntu 24.04 이후처럼 AppArmor가 unprivileged user namespace 생성을 제한하는 환경에서 apparmor=unconfined를 쓰면, 바로 그 제한을 풀어버리는 효과가 난다.
그러면 교환이 선명해져. root로 떠 있는 Docker 데몬을 피하는 대신, 제한이 풀린 일반 사용자 프로세스가 kernel의 깊은 인터페이스에 접근할 수 있게 된다. host root는 아니지만, 아무 위험도 없는 상태도 아니야.
CI에서 특히 헷갈린다
이 얘기는 GitHub Actions Runner Controller 같은 CI/CD 환경에서 특히 중요해. 팀들은 Docker-in-Docker를 피하고 싶어 해. privileged container를 띄우는 게 무섭기 때문이지.
그런데 Dockerfile 방식으로 이미지를 만들려면 결국 어디선가 강한 권한이 필요해. Docker 데몬에 권한을 주든, rootless BuildKit에 user namespace와 unconfined 설정을 주든, Kubernetes API로 새 pod를 만들 권한을 주든, 선택지가 바뀔 뿐이야.
즉 “privileged container를 안 썼다”가 곧 “권한 문제를 해결했다”는 뜻은 아니야. 권한이 Docker 데몬에 있는지, kernel namespace 인터페이스에 있는지, Kubernetes API에 있는지를 봐야 해.
다음에 볼 것
첫째, rootless를 켜기 전에 그 시스템에서 unprivileged user namespace를 허용해야 하는지부터 봐야 해. 브라우저나 컨테이너 런타임 때문에 이미 켜진 배포판도 많지만, hardened 환경에서는 일부러 막아두기도 해.
둘째, rootless build를 위해 seccomp=unconfined나 apparmor=unconfined를 요구하는지 확인해야 해. 이 둘을 끄는 순간 “rootless라서 안전하다”는 문장은 너무 거칠어진다.
셋째, untrusted workload를 어디까지 믿을지 정해야 해. rootless는 피해 반경을 줄일 수 있지만, 모르는 코드를 안전하게 돌리는 만능 격리벽은 아니야. kernel patch, 최소 권한, 짧은 수명, 격리된 runner, 감사 로그가 같이 붙어야 한다.
Rootless Docker는 좋은 도구야. 다만 이름이 약속하는 것보다 실제 계약은 더 좁아. root를 없애는 게 아니라 root에 가까운 일을 더 작은 방 안으로 옮기는 것. 그 방의 문과 창문이 어디로 열리는지까지 봐야 제대로 쓴 거야.
댓글