본문으로 건너뛰기

© 2026 Molayo

HN분석2026. 05. 09. 08:13

Podman rootless containers and the Copy Fail exploit

요약

본 기사는 CVE-2026-31431 (Copy Fail) 취약점을 분석하며, 이 취약점이 로컬 비권한 사용자가 루트 쉘을 획득하는 데 악용될 수 있음을 보여줍니다. 특히 Podman의 'rootless' 컨테이너 기능이 보안상 우수함에도 불구하고, Copy Fail은 rootless 환경에서도 컨테이너 내부의 루트 쉘을 획득할 가능성이 있음을 시사합니다. 하지만 필자는 Podman의 여러 기능을 활용하여 이 공격의 파급 효과를 제한하는 방어적 접근 방식(defence in depth)의 중요성을 강조하며, 사용자 네임스페이스 및 Linux capabilities 검토를 통해 보안 강화를 논하고 있습니다.

핵심 포인트

  • Copy Fail (CVE-2026-31431)은 로컬 비권한 사용자가 루트 쉘을 얻는 데 사용될 수 있는 취약점이다.
  • Podman은 'rootless' 컨테이너 실행을 지원하며, 이는 Docker의 rootful 방식보다 보안적으로 우수하다 (fork/exec 모델 사용).
  • Copy Fail은 Podman의 rootless 환경에서도 악용 가능성이 확인되었으나, 공격의 파급 범위는 제한적이다.
  • 컨테이너 탈출 및 권한 상승 방지를 위해 사용자 네임스페이스(User Namespace)와 Linux capabilities를 활용하는 '방어 심화(Defence in Depth)' 전략이 필수적이다.

4 월 29 일, https://copy.fail/ 에서 CVE-2026-31431 이 공개되었습니다. 이 취약점은 작성자가 공유한 Python 스크립트를 실행함으로써 로컬 비권한 사용자가 루트 쉘을 획득할 수 있게 합니다.

이 악용 사례는 Linux 컨테이너를 공격하는 데 사용될 수 있으며, 이는 모든 종류의 서비스를 실행하는 데 널리 사용됩니다: 공개 서비스, 개발 환경, 지속적 통합 작업 등. Copy Fail 을 악용한 컨테이너는 다양한 공격에 매우 효과적으로 사용할 수 있습니다.

이 CVE 는 제가 Docker 를 Podman 으로 컨테이너를 실행하기 위해 변경한 지 약 1 년이 지난 후에도 흥미롭습니다. 이 변경을 유도한 여러 가지 이유가 있지만, 가장 중요한 것은 Podman 의 보안 포지션 (security posture) 입니다.

Podman 은 비권한 사용자가 컨테이너를 실행하는 것을 매우 쉽게 만들며, 이를 "rootless" 로 실행한다고 합니다. Docker 와 달리 Podman 은 포크/실행 (fork/exec) 모델을 사용하여 컨테이너 프로세스는 podman run 프로세스의 후손이 됩니다. 결과적으로, 시스템의 root 나 다른 사용자로부터 컨테이너 프로세스를 격리하기 위해 표준 UID 분리를 신뢰할 수 있습니다.

Copy Fail 에 대해 읽었을 때, 특히 rootless 컨테이너에서의 사용에 대한 정보는 거의 없었습니다. 간단한 테스트를 수행한 후 Copy Fail 은 rootless 컨테이너에서 컨테이너 루트 쉘을 획득하는 데 실제로 악용 가능함을 확인했습니다. 그러나 Podman 의 여러 기능을 사용하여 이 폭파 반경은 상당히 제한적입니다.

발표 당시에는 컨테이너 탈출에 대한 정보는 많지 않았습니다:

  • 원인, scatterlist 다이어그램, 2011 → 2015 → 2017 역사, 그리고 악용 사례 walkthrough 는 Xint 블로그에 있습니다. Part 2 (Kubernetes 컨테이너 탈출) 은 곧 발표됩니다.

제 테스트에서, 컨테이너 root 는 호스트 레벨에서 컨테이너를 실행하는 비권한 사용자가 할 수 있는 것만 제한적입니다.

결국, Copy Fail 은 Podman 의 rootless 컨테이너 구현에 대해 글을 쓸 때 참조할 훌륭한 예가 되었습니다. 이 노트에서는 다양한 컨테이너 구성에 걸쳐 악용 사례를 재현하여 침해된 rootless 컨테이너의 노출을 이해하려 합니다.

이 글은 다소 길어졌으므로 필요한 경우 관련 부분으로 바로 이동해 주세요:

  • rootless 컨테이너, 사용자 네임스페이스 및 Linux capabilities 의 실용적 검토
  • rootless 컨테이너에서 Copy Fail 사용
  • 침해 발생 시 노출을 더 제한하기 위해 방어 심화 (defence in depth) 실천

Rootless containers 개요

HTTP 서버를 실행하여 일부 HTML 을 제공하는 것이 필요하다고 가정해 보겠습니다. 서버는 권한이 없는 사용자 bar 가 소유하는 컨테이너에서 실행됩니다.

UID 는 1001 입니다.

Podman 을 설치하고, 사용자 bar 를 생성한 뒤, 그 사용자로 전환합니다. 그런 다음 podman build 로 이미지를 빌드하고, podman run 으로 컨테이너를 실행합니다:

root@debian:~# apt install -y podman
root@debian:~# useradd -m -d /var/lib/bar -s /bin/bash -u 1001 bar
root@debian:~# su - bar
...

서버는 이제 요청에 응답해야 합니다:

bar@debian:~$ curl localhost:8000
<!DOCTYPE html><html lang="en"></html>

Rootless rootful

이 컨테이너 프로세스가 어떻게 보이는지 살펴보겠습니다. ps 를 사용하여 이 python3 프로세스가 사용자 bar 에 의해 소유되고 있음을 확인할 수 있습니다:

root@debian:~# ps -fC python3
UID PID PPID C STIME TTY TIME CMD
bar 4861 4859 0 19:26 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000

소개에서 언급했듯이, Podman 은 컨테이너를 실행하기 위해 fork/exec 모델을 사용합니다. 사용자 barpodman run 명령을 실행했고, 컨테이너 명령인 python3 는 그 프로세스에서 파생되었습니다. 이는 표준 Docker 설정과 대조적입니다. 권한이 없는 사용자로 docker run 을 실행하면 Docker 클라이언트가 실행되고, 이는 rootful 데몬과 상호작용하며 결국 컨테이너를 생성합니다:

bar@debian:~$ docker run --rm -it -d --name http-server-1 http-server
bar@debian:~$ ps -fC python3
UID PID PPID C STIME TTY TIME CMD
...

이제 컨테이너도 컨테이너 내부의 권한을 결정하기 위해 사용자 및 그룹을 가집니다. 대부분의 이미지는 Containerfile 또는 컨테이너 실행 시 --user 플래그에 명시적인 USER 지시가 없는 경우, 컨테이너 명령을 root 로 실행하도록 기본값으로 설정합니다.

podman top 를 사용하여 python3 컨테이너 프로세스가 root 로 실행되고 있음을 확인할 수 있습니다. (명시적으로 어떤 사용자가 프로세스를 실행하는지 지정하지 않았기 때문입니다):

bar@debian:~$ podman top http-server-1 huser,user,pid,args
HUSER USER PID COMMAND
1001 root 1 python3 -m http.server -b 0.0.0.0 8000

컨테이너는 호스트와 커널을 공유합니다. 컨테이너 내부에서 root 라다는 무엇을 의미할까요? 우리는 권한이 없는 사용자를 사용하고 있으므로, 이는 호스트 root 와 같지 않을 것 같습니다.

User namespaces

Podman 은 rootless 컨테이너를 위해 user namespaces 를 사용합니다. User namespaces 는 프로세스가 컨테이너 내부와 외부에서 다른 UID/GID 를 가도록 허용합니다. 이전 예제에서, python3 프로세스의 UID 는 0 입니다 (즉, 컨테이너 root)

namespace 내부에서 UID 1001 로 매핑됨 (즉, 호스트 bar

) namespace 외부.

사용자 bar 의 네임스페이스 프로세스에 할당할 수 있는 UID 범위는 /etc/subuid

에서 결정됩니다:

bar@debian:~$ grep bar /etc/subuid
bar:165536:65536

UID 1001 외에도, bar 의 프로세스에 할당할 수 있는 UID 는 65,537 개이며, 시작값은 165536, 종료값은 231072 (165536 + 65537) 입니다.

현재 이미지 는 ubuntu 를 기반으로 하며, 자체 사용자 집합을 가져옵니다:

bar@debian:~$ podman run --rm -it --name http-server-1 localhost/http-server:latest cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

이 사용자들의 프로세스 및 객체는 bar 사용자 namespace 내부에서 위와 같은 UIDs 를 가집니다. namespace 외부에서는 165537-231072 범위 내의 UID 로 매핑되며, 예외로 root 는 호스트 UID 1001 로 매핑됩니다.

예를 들어, bar 가 컨테이너 내에서 사용자 www-datasleep 를 실행한다고 가정해 봅시다:

bar@debian:~$ podman run --rm -it -d --name http-server-1 --user=www-data localhost/http-server:latest sleep 60
bar@debian:~$ podman top http-server-1 huser,user,args
HUSER USER COMMAND
...

sleep 프로세스 는 사용자 namespace 내부에서 www-data 로 실행되지만, 호스트에서는 165568 으로 매핑됩니다. 사용자 namespace 는 같은 사용자의 프로세스에 대해 표준 UID 격리 기능을 제공합니다. 즉, 호스트의 관점에서 bar 사용자 namespace 내의 www-data 프로세스는 bar 와 구분됩니다.

Docker 도 사용자 namespace 를 지원하지만, 이를 올바르게 구성해야 하며 하나의 사용자 namespace 만 허용됩니다. Podman 은 각 UNIX 사용자가 해당 사용자 namespace 에서 루트리스 컨테이너를 실행합니다.

podman unshare 를 사용하여 컨테이너를 실행하지 않고 사용자의 namespace 에 진입할 수 있습니다. 이를 통해 bar 와 namespace root 간의 관계를 이해할 수 있습니다. bar 의 홈 디렉터리 소유권을 namespace 내부 및 외부 모두에서 비교하여:

bar@debian:~$ ls -ld $HOME
drwx------ 5 bar bar 4096 May 2 22:58 /var/lib/bar
bar@debian:~$ podman unshare ls -ld $HOME
...

Containerfile 을 사용하여 컨테이너 root 에 대한 권한을 이해해야 합니다. apt install 를 사용하여 python3 를 설치할 수 있었습니다. 패키지 설치는 여러 가지 권한이 필요한 작업이 포함되며, bar

host는 root가 아닙니다

?

Privileged operations (권한 부여된 작업)

Podman은 컨테이너 프로세스에 세밀한 root 권한을 부여하기 위해 Linux capabilities를 사용합니다. 이미지를 빌드하고 컨테이너를 실행할 때 이 capabilities를 추가하거나 제거할 수 있습니다.

pscap 을 사용하여

여러 capabilities 가 사용자 bar 를 위한 이미지 빌드 동안 실행되는 apt 프로세스에 부여됨을 관찰할 수 있습니다:

root@debian:~# pscap
ppid pid uid command capabilities
10941 11272 bar apt * chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, sys_chroot, setfcap +

이러한 capabilities 는 Podman 에 의해 설정되며, 이는 namespace 에서 root 가 권한 부여된 작업을 수행할 수 있게 하는 조합입니다. podman build 를 사용하여 --cap-drop=all 을 모든 capabilities 를 제거하는 경우, 이미지 빌드가 권한 부족으로 인해 실패합니다:

bar@debian:~$ podman build -t http-server --cap-drop=all --no-cache .
STEP 1/7: FROM ubuntu:latest
STEP 2/7: RUN apt update && apt install -y python3 && apt clean
...

이 경우 우리는 권한이 필요하므로 root 의 기본 설정을 사용하거나, 모든 capabilities 를 제거한 후 패키지 설치에 필요한 capability 만 설정할 수 있습니다:

bar@debian:~$ podman build -t http-server --cap-drop=all --cap-add=CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER --no-cache .

이를 알면, HTTP 서버 뒤의 python3 프로세스에 부여된 capabilities 를 다시 검토할 수 있습니다:

bar@debian:~$ podman run --rm -it -d --name http-server-1 -p 127.0.0.1:8000:8000/tcp localhost/http-server:latest
bar@debian:~$ podman top http-server-1 user,capeff,args

우리의 HTTP 서버는 많은 capabilities 를 가진 container root 로 실행되고 있습니다. 컨테이너 프로세스가 침해당하는 경우 exploit 할 수 있는 표면적은 상당히 큽니다2.

컨테이너를 시작할 때 모든 capabilities 를 제거하여 상황을 개선할 수 있습니다:

bar@debian:~$ podman run --rm -it -d --name http-server-1 -p 127.0.0.1:8000:8000/tcp --cap-drop=all localhost/http-server:latest
bar@debian:~$ podman top http-server-1 user,capeff,argsUSER EFFECTIVE CAPS COMMAND
root none python3 -m http.server -b 0.0.0.0 8000

훨씬 더 좋습니다! 그러나 우리는 더 나아가야 합니다. capabilities 가 없더라도 root 는 소유한 모든 파일을 수정할 수 있습니다. 서버가 root 아래에서 실행할 필요가 없으므로, 권한 부여되지 않은 사용자가 이를 수행하도록 하십시오.

Rootless non-root

컨테이너 내의 HTTP 서버를 비권한 사용자 (unprivileged user) 로 실행하려면 기본 이미지 (/etc/passwd 파일) 를 확인하여 기존 사용자 (예: www-data) 를 선택하거나, 이미지 빌드 시 자체 사용자를 생성할 수 있습니다. 저의 경우 전용 foo 사용자와 UID 1002 을 사용하여 제공된 파일에 대한 읽기 전용 접근을 선호합니다:

FROM ubuntu:latest
RUN apt update && apt install -y python3 && apt clean
RUN mkdir -p /var/www/html
...

다시 컨테이너를 빌드하고 실행합니다. podman run 을 실행할 때 USER foo:foo 지시를 Containerfile 에 명시하지 않아도 됩니다. Containerfile 의 USER foo:foo 지시는 이후 모든 프로세스에 해당 UID 를 설정하기 때문입니다.

bar@debian:~$ podman run --rm -it -d --name http-server-1 -p 127.0.0.1:8000:8000/tcp --cap-drop=all localhost/http-server:latest
bar@debian:~$ podman top http-server-1 huser,user,capeff,args
HUSER USER EFFECTIVE CAPS COMMAND
...

모두 잘 됩니다! 서버는 컨테이너 내에서 foo 사용자 (UID 166537) 로 실행되고 호스트에는 권한이 없으며 capabilities 는 없습니다.

컨테이너 프로세스는 최소한의 권한으로 실행되어야 하며, 필요할 때만 추가해야 합니다. 예를 들어, python3 를 foo 사용자로 실행하면서 권한 있는 포트 80 에 바인딩하려면 podman run 시 --cap-add=CAP_NET_BIND_SERVICE 로 NET_BIND_SERVICE capability 를 부여해야 합니다.

결론적으로 컨테이너가 실행될 수 있는 방법은 네 가지입니다:

호스트 사용자컨테이너 사용자용어
rootrootroot rootful
...

Podman 은 비권한 (rootless) rootful 컨테이너 실행을 매우 쉽게 만들며, 컨테이너 이미지가 비권한 사용자로 컨테이너 프로세스를 실행할 수 있도록 허용하는 경우 비권한 non-root 컨테이너 실행도 비교적 쉽습니다. 후자는 컨테이너 이미지 빌드 방식에 대한 더 깊은 이해가 필요합니다.

바인드 마운트

Copy Fail 로 넘어가기 전에, 지금까지 본 바인드 마운트 (bind mounts) 개념을 다시 검토해 보겠습니다.

호스트 디렉토리를 컨테이너에 마운트하고, 해당 디렉토리는 호스트 root, 호스트 bar, 네임스페이스 foo 에 의해 소유된 파일이 있습니다. 이 파일들은 각각의 사용자 및 그룹만 읽을 수 있지만, 컨테이너 사용자가 파일을 생성할 수 있도록 누구나 디렉토리에 쓸 수 있습니다:

root@debian:~# mkdir /var/lib/bar/test
root@debian:~# chown bar:bar /var/lib/bar/test
root@debian:~# chmod 0777 /var/lib/bar/test
...

AI 자동 생성 콘텐츠

본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
2

댓글

0