Ubuntu 24.04 rootless Docker — nsenter와 netns

기초·개념4분조회

요청이 올 때마다 깨끗한 컨테이너를 새로 띄우고, 그 안에서 명령을 돌린 뒤 정리하는 서버가 있습니다. 여기에 특이한 점이 둘 있습니다.

하나는 보안 때문에 Docker 를 rootless 로, 그러니까 root 권한 없이 돌린다는 것이고, 다른 하나는 호스트에서 nsenter 로 각 컨테이너의 네트워크 네임스페이스에 들어가 명령을 실행한다는 것입니다. 네트워크 네임스페이스는 컨테이너가 호스트와 분리된 자기만의 네트워크를 갖게 하는 리눅스 기능이고, nsenter 는 그 격리된 공간 안으로 들어가는 도구입니다.

이 서버를 Ubuntu 22.04 에서 24.04 로 올렸더니 그 접속이 통째로 깨졌습니다.

증상 — nsenter 거부와 SSH 실패

nsenter: reassociate to namespace 'ns/net' failed: Operation not permitted
ssh root@172.17.0.x: Permission denied (publickey,password)

컨테이너에 들어갈 수가 없으니 그 안에서 도는 작업이 전부 실패했습니다. 하나를 걷어내면 다음 게 나오는 식으로, 원인이 세 겹이었습니다.

원인 1 — Ubuntu 24.04의 apparmor userns 제한

Ubuntu 24.04 는 kernel.apparmor_restrict_unprivileged_userns=1 이 기본으로 켜져 있습니다. 비특권 사용자가 user namespace 를 만드는 걸 막습니다. 22.04 엔 없던 제한입니다.

그런데 이 제한은 userns 를 생성하는 것만 막지, nsenter -U 로 이미 있는 userns 에 합류(setns) 하는 것까지 막지는 않습니다. 실측으로 확인한 부분입니다. 즉 nsenter-U 를 붙여 rootless userns 에 먼저 들어간 다음 네트워크 네임스페이스로 가면 됩니다. -U 없이는 Operation not permitted 입니다.

원인 2 — rootless docker의 detach-netns

진짜 원인은 이쪽이었습니다. 새 버전 rootless docker 는 detach-netns 가 켜지고, 그러면 네임스페이스 구조가 이렇게 갈립니다.

dockerd           → 호스트 네트워크 네임스페이스에 남음
컨테이너 브리지    → rootlesskit의 child 네임스페이스 안에 있음

기존 코드는 pgrep dockerd 로 찾은 PID 를 nsenter 대상으로 썼습니다. 그런데 detach-netns 가 켜지면 dockerd 는 호스트 네임스페이스에 있어서, 그리로 들어가면 컨테이너 브리지가 없는 빈 네임스페이스입니다. 컨테이너에 No route to host 가 됩니다.

들어가야 할 곳은 dockerd 가 아니라 rootlesskit 의 child 네임스페이스입니다. 그 PID 는 여기 있습니다.

cat "/run/user/$(id -u)/dockerd-rootless/child_pid"

child_pid 를 nsenter 대상으로 하면 컨테이너 네임스페이스에 도달합니다.

nsenter -U --preserve-credentials -n -t <child_pid> ip -o addr
# → docker0(172.17.0.1)가 보임

sudo 도 필요 없습니다. 처음엔 sudo nsenter 로 우회하려 했는데, 그건 대상 PID 가 틀렸던 걸 힘으로 덮으려던 임시조치였습니다. 대상만 child_pid 로 맞추면 비특권으로 됩니다.

원인 3 — 컨테이너 SSH 키 프로비저닝 갭

nsenter 를 고쳐 네트워크는 도달했는데 SSH 가 여전히 Permission denied 였습니다. 이건 별개 문제였습니다.

컨테이너의 /root/.ssh/authorized_keys 는 호스트의 특정 경로를 read-only bind 하는 구조였습니다. 그런데 새 노드에는 그 경로가 빈 디렉토리로 만들어져 있었습니다. 코드도 init 스크립트도 이 키를 안 만들었고, 첫 컨테이너를 생성할 때 Docker 가 bind 소스가 없으니 빈 디렉토리로 자동 생성해버린 겁니다.

그리고 여기서 흔한 실수 하나. authorized_keys(공개키)만 맞추면 안 됩니다. 접속하는 쪽의 id_rsa(개인키)와 같은 키쌍이어야 합니다. 노드가 자기 id_rsa 를 따로 만들어 쓰고 있으면, authorized_keys 만 채워도 키가 안 맞아 여전히 거부됩니다.

ssh-keygen -t rsa -b 2048 -N '' -f ~/.ssh/id_rsa
cp ~/.ssh/id_rsa.pub <bind-경>/authorized_keys
chmod 600 <bind-경>/authorized_keys

개인키와 공개키를 같은 키쌍으로 배치해야 합니다.

rootless에서 nsenter — -U와 child_pid

세 원인이 독립적이라 하나씩 걷어내야 했습니다. apparmor 제한, rootless docker 의 네임스페이스 구조 변화, 그리고 키 프로비저닝. 앞의 둘은 OS·docker 버전을 올리면서 기본 동작이 바뀐 것이고, 하나는 원래 있던 갭이 새 노드에서 드러난 것이었습니다.

rootless docker 에서 nsenter 로 컨테이너에 들어갈 때 핵심은 두 가지입니다. -U 로 rootless userns 에 먼저 들어갈 것, 그리고 대상 PID 를 dockerd 가 아니라 rootlesskit child_pid 로 할 것. 버전이 올라가며 detach-netns 가 켜지면 pgrep dockerd 방식은 조용히 엉뚱한 곳으로 들어갑니다.

  1. 불러오는 중