루트리스 컨테이너와 Copy Fail 취약점 분석
이 글은 4월 29일 공개된 'Copy Fail' 취약점(CVE-2026-31431)이 Podman의 루트리스(Rootless) 컨테이너 환경에 미치는 영향을 분석합니다. 취약점 악용 시 컨테이너 내부에서 루트 권한을 획득할 수 있으나, Podman의 구조적 특성상 호스트 시스템에 미치는 피해 반경은 매우 제한적입니다. 실무자 관점에서 리눅스 컨테이너의 격리 원리와 보안 심층 방어의 중요성을 잘 보여주는 중요한 사례입니다.
4월 29일, CVE-2026-31431이 https://copy.fail/ 웹사이트를 통해 공개되었습니다. 이 취약점은 권한이 없는 일반 로컬 사용자가 작성자가 공유한 파이썬 스크립트를 실행하여 루트 셸(root shell)을 획득할 수 있게 합니다. 이 익스플로잇은 퍼블릭 서비스, 개발 환경, CI/CD(지속적 통합) 작업 등 다양한 용도로 널리 사용되는 리눅스 컨테이너를 공격하는 데 사용될 수 있습니다. Copy Fail에 의해 해킹된 컨테이너는 다양한 종류의 공격에 매우 효과적으로 악용될 수 있습니다.
이 CVE는 제가 컨테이너 실행을 Docker에서 Podman으로 전환한 지 약 1년이 된 시점에 나와서 특히 흥미로웠습니다. 이러한 변경을 결정한 데는 여러 이유가 있었지만, 무엇보다 Podman의 강력한 보안 태세(security posture)가 결정적이었습니다. Podman은 권한 없는 일반 사용자로 컨테이너를 실행하는 것을 매우 간단하게 만들며, 이를 소위 '루트리스(rootless)' 컨테이너 실행이라고 부릅니다.
Docker와 달리 Podman은 fork/exec 모델을 사용하므로 컨테이너 프로세스는 결국 컨테이너를 실행하는 데 사용된 podman run 프로세스의 하위 프로세스가 됩니다. 결과적으로 표준 UID 분리를 활용하여 컨테이너 프로세스를 시스템의 루트나 다른 사용자로부터 격리할 수 있습니다.
Copy Fail에 대한 글을 읽으면서도 루트리스 컨테이너에서의 구체적인 활용 사례에 대한 정보는 거의 찾지 못했습니다. 몇 가지 간단한 테스트를 수행한 후, 저는 Copy Fail이 실제로 루트리스 컨테이너에서 악용되어 컨테이너 루트 셸을 얻을 수 있다는 것을 확인했습니다. 하지만 Podman의 여러 기능을 통해 그 피해 반경(blast radius)이 상당히 제한된다는 사실도 확인했습니다.
발행 시점을 기준으로 컨테이너 탈출(container escape)에 대한 정보는 많지 않습니다. 근본 원인, scatterlist 다이어그램, 2011 → 2015 → 2017의 연혁 및 익스플로잇 연구는 Xint 블로그에서 확인할 수 있습니다. 파트 2(쿠버네티스 컨테이너 탈출)는 향후 공개될 예정입니다.
제가 테스트한 결과, 컨테이너 루트 권한은 여전히 컨테이너를 실행하는 권한 없는 일반 사용자가 호스트 수준에서 할 수 있는 작업에만 제한됩니다. 결론적으로 Copy Fail은 Podman의 루트리스 컨테이너 구현을 설명할 때 참고할 수 있는 아주 훌륭한 사례로 입증되었습니다.
이 글에서는 다양한 컨테이너 구성에 걸쳐 익스플로잇을 재현하여 루트리스 컨테이너가 침해되었을 때 노출되는 범위를 이해해 보고자 합니다. 글이 다소 길어졌으므로, 필요한 경우 관련 부분으로 바로 건너뛰셔도 좋습니다:
- 루트리스 컨테이너, 사용자 네임스페이스 및 리눅스 기능(Capabilities)에 대한 실용적인 검토
- 루트리스 컨테이너에서 Copy Fail 사용하기
- 침해 발생 시 노출을 추가로 제한하기 위한 심층 방어(Defence in depth) 실천
루트리스 컨테이너 개요 HTML을 서비스하기 위해 HTTP 서버를 실행해야 한다고 가정해 보겠습니다. 이 서버는 UID가 1001인 'bar'라는 권한 없는 일반 사용자가 소유한 컨테이너 내에서 실행될 것입니다. 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:~$ cat > Containerfile <<EOF FROM ubuntu:latest RUN apt update && apt install -y python3 && apt clean RUN mkdir -p /var/www/html WORKDIR /var/www/html RUN cat > index.html <<HTML HTML EXPOSE 8000 CMD ["python3", "-m", "http.server", "-b", "0.0.0.0", "8000"] EOF
bar@debian:$ podman build -t http-server .
bar@debian:$ podman run --rm -it --name http-server-1 -d -p 127.0.0.1:8000:8000/tcp localhost/http-server:latest
이제 서버가 요청에 정상적으로 응답해야 합니다: bar@debian:~$ curl localhost:8000
루트리스(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 모델을 사용합니다. 'bar' 사용자가 podman run 명령을 실행했습니다.