Ubuntu 24.04 rootless Docker — nsenter와 netns

기초·개념29분조회 –

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

하나는 보안 때문에 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)

컨테이너에 들어갈 수가 없으니 그 안에서 도는 작업이 전부 실패했습니다. 하나를 걷어내면 그 아래에서 다음 게 나오는 식이라, 한 번에 끝나지 않았습니다.

Ubuntu 24.04의 apparmor userns 제한

Ubuntu 24.04 는 kernel.apparmor_restrict_unprivileged_userns=1 이 기본으로 켜져 있어서 비특권 사용자가 user namespace 를 만드는 걸 막습니다. AppArmor 는 리눅스가 프로세스별로 할 수 있는 일을 제한하는 보안 모듈이고, 이 값은 그중 userns 생성을 막는 스위치로, 22.04 엔 없던 제한입니다.

직접 시험해 보니 이 제한은 userns 를 생성하는 것만 막을 뿐, nsenter -U 로 이미 있는 userns 에 합류(setns) 하는 것까지 막지는 않았습니다. nsenter 에 -U 를 붙여 rootless userns 에 먼저 들어간 다음 네트워크 네임스페이스로 가면 되고, -U 없이 바로 가면 Operation not permitted 입니다.

rootless docker의 detach-netns

nsenter 가 성공하고도 컨테이너에 못 닿은 이유는 이쪽이었습니다. rootless Docker 는 데몬을 RootlessKit 이라는 래퍼 위에 띄우고, 이게 사용자 네임스페이스와 네트워크 네임스페이스를 만들어 주는 물건입니다. detach-netns 는 그 RootlessKit v2.0.0 에서 들어온 모드고, 이걸 켜는 건 Docker 가 rootless 데몬을 띄울 때 쓰는 기동 스크립트입니다.

grep -n -B2 -A4 "DETACH_NETNS" /usr/bin/dockerd-rootless.sh
# * DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(true|false): whether to launch
#   rootlesskit with the "detach-netns" mode.
#   Defaults to "true". Set this to false only when facing a compatibility issue.
...
	case "$DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" in
		1 | true)
			DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="--detach-netns $DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS"

기본값이 true 라 아무 설정도 안 했으면 켜져 있다는 뜻이고, 그러면 네임스페이스 구조가 이렇게 갈립니다.

호스트 네임스페이스dockerd 만 있음컨테이너 브리지 없음빈 네임스페이스No route to hostchild 네임스페이스rootlesskit 이 만든 쪽컨테이너 브리지 있음여기가 실제 통신 경로컨테이너로 접속됨pgrep dockerd 로 찾은 PIDchild_pid 로 찾은 PID
들어갈 곳은 dockerd 가 아니라 rootlesskit child

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

내 환경에서 켜져 있는지는 $XDG_RUNTIME_DIR/docker/netns 가 있는지로 확인합니다. 이 경로가 있으면 네트워크 네임스페이스가 따로 떨어져 나온 구성입니다.

들어가야 할 곳은 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 로 맞추면 비특권으로 됩니다.

컨테이너 SSH 키가 아예 안 만들어짐

nsenter 를 고쳐 네트워크는 도달했지만 SSH 가 여전히 Permission denied 였습니다. 네임스페이스 쪽 여파가 남았나 싶었지만, 여기서 막힌 건 SSH 키 쪽이었습니다.

컨테이너의 /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

authorized_keys 퍼미션이 느슨하면 sshd 가 그 파일을 아예 안 읽으니 600 까지 같이 맞춥니다.

rootless 에서 nsenter — -U 와 child_pid

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

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

  1. 불러오는 중