- CVE-2026-31431 Copy Fail은 로컬 비권한 사용자가
root셸을 얻을 수 있게 하며, Podman 루트리스 컨테이너 안에서도 컨테이너 내부root권한 상승이 가능함 - Podman 루트리스 컨테이너는 사용자 네임스페이스, UID 분리, Linux capabilities를 조합해 컨테이너 내부
root를 호스트의 비권한 사용자로 매핑하고 호스트 권한을 제한함 - 테스트에서 rootless non-root 컨테이너의
foo사용자는 Copy Fail 실행 후 컨테이너 내부root가 될 수 있었지만, 권한은 호스트 비권한 사용자bar가 가능한 범위로 제한됐고 호스트root소유 파일은 읽지 못함 --security-opt=no-new-privileges나--cap-drop=all을 적용하면 Copy Fail 실행 후에도 셸이foo와 capabilitiesnone상태로 유지되어 즉각적인root셸 획득과 capability 상승을 막을 수 있음- Copy Fail의 효과는 컨테이너 생명주기를 넘어 남을 수 있어 커널 패치와 재부팅이 필요하며, 읽기 전용 루트 파일시스템, cgroups 리소스 제한, 얇은 런타임 이미지, 방화벽 같은 심층 방어를 함께 적용해야 함
Copy Fail과 Podman 루트리스 컨테이너의 노출 범위
- CVE-2026-31431은 4월 29일 copy.fail에서 공개됐으며, 공개된 Python 스크립트를 실행하면 로컬 비권한 사용자가
root셸을 얻을 수 있음 - Copy Fail은 Linux 컨테이너 안에서도 악용 가능하며, Podman 루트리스 컨테이너에서도 컨테이너 내부
root셸 획득이 가능함 - 테스트에서는 컨테이너
root가 호스트 수준에서 컨테이너를 실행한 비권한 사용자bar의 권한 범위로 제한됨 - Podman의 루트리스 구현은 사용자 네임스페이스, UID 분리, Linux capabilities를 조합해 컨테이너 프로세스의 호스트 권한을 제한함
- Copy Fail은 루트리스 컨테이너도 취약점에 면역이 아니지만, Podman 설정으로 침해 후 공격 범위를 줄일 수 있음을 보여줌
루트리스 컨테이너의 동작 방식
-
기본 예제: 비권한 사용자
bar가 HTTP 서버 실행- 예제 환경은 UID
1001인 비권한 사용자bar가 Podman으로ubuntu:latest기반 이미지를 빌드하고,python3 -m http.server를 실행하는 구성임 - 호스트에서
ps로 보면python3프로세스는 사용자bar소유로 실행됨 - Podman은 fork/exec 모델을 사용하므로 컨테이너 프로세스는
podman run프로세스의 후손이 되며, 일반적인 UID 분리로 컨테이너 프로세스를 호스트root나 다른 사용자와 분리할 수 있음 - 일반적인 Docker 구성에서는 비권한 사용자가
docker run을 실행해도 Docker 클라이언트가 루트 권한 데몬과 통신하고, 데몬이 최종적으로 컨테이너 프로세스를 생성하므로 호스트에서 컨테이너 프로세스가root로 보일 수 있음
- 예제 환경은 UID
-
루트리스 rootful
- 컨테이너 이미지는 명시적인
USER지시어나--user플래그가 없으면 보통 컨테이너 명령을 내부root로 실행함 podman top출력에서 HTTP 서버 프로세스는 호스트 사용자1001로 매핑되지만, 컨테이너 내부 사용자로는root로 실행됨- 이 구성은 호스트에서는 비권한 사용자로 실행되지만 컨테이너 내부에서는
root인 rootless rootful 상태임
- 컨테이너 이미지는 명시적인
-
사용자 네임스페이스
- Podman 루트리스 컨테이너는 사용자 네임스페이스를 사용해 컨테이너 안팎의 UID/GID를 다르게 매핑함
- 예제에서 컨테이너 내부 UID
0인root는 호스트의 UID1001인bar로 매핑됨 /etc/subuid의bar:165536:65536설정은bar의 네임스페이스 프로세스에 할당 가능한 UID 범위를 정함- 예제에서는
bar의 UID1001외에165536부터231072까지의 UID가bar프로세스에 할당될 수 있음 - 컨테이너 내부 사용자
www-data로sleep을 실행하면 내부에서는www-data지만 호스트에서는165568로 표시됨 podman unshare로 사용자 네임스페이스에 들어가면, 호스트에서bar:bar소유인 홈 디렉터리가 네임스페이스 내부에서는root:root로 보임- Docker도 사용자 네임스페이스를 지원하지만 별도 설정이 필요하고 하나의 사용자 네임스페이스만 허용되는 반면, Podman은 각 UNIX 사용자의 루트리스 컨테이너를 해당 사용자 네임스페이스에서 실행함
-
권한 작업과 Linux capabilities
- Podman은 Linux capabilities를 사용해 컨테이너 프로세스에 세분화된 루트 권한을 부여함
- 이미지 빌드 중
apt install같은 작업은chown,dac_override,fowner,setgid,setuid,net_bind_service,sys_chroot같은 capability 조합으로 가능해짐 podman build --cap-drop=all로 모든 capability를 제거하면apt가setgroups,setegid,seteuid,chown등에 실패해 이미지 빌드가 실패함- 필요한 capability만 추가하는 방식도 가능하며, 예제에서는
CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER를 추가해 패키지 설치를 수행함 - 기본 실행 상태의 HTTP 서버는 컨테이너 내부
root로 실행되며CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT같은 많은 effective capabilities를 가짐 - HTTP 서버에는 이런 권한이 필요 없으므로
podman run --cap-drop=all로 모든 capability를 제거할 수 있으며, 이때podman top에서는 effective capabilities가none으로 표시됨
-
루트리스 non-root
- 컨테이너 내부에서도 비권한 사용자로 HTTP 서버를 실행하려면 기존
/etc/passwd의 사용자, 예를 들어www-data를 쓰거나 이미지 빌드 중 전용 사용자를 만들 수 있음 - 예제에서는 UID
1002인foo사용자와 그룹을 만들고/var/www/html에 읽기 권한을 부여한 뒤USER foo:foo를 설정함 - 이 이미지를
--cap-drop=all로 실행하면 프로세스는 컨테이너 내부foo, 호스트 UID166537, effective capabilitiesnone상태가 됨 - 컨테이너 프로세스는 필요한 최소 권한으로 실행해야 하며, 예를 들어
foo가 권한 포트80에 바인딩해야 한다면--cap-add=CAP_NET_BIND_SERVICE를 추가해야 함 - 컨테이너 실행 방식은 네 가지로 구분됨
root호스트 사용자 + 컨테이너root: root rootfulroot호스트 사용자 + 컨테이너 비권한 사용자: root non-root- 비권한 호스트 사용자 + 컨테이너
root: rootless rootful - 비권한 호스트 사용자 + 컨테이너 비권한 사용자: rootless non-root
- Podman은 rootless rootful 컨테이너 실행을 쉽게 만들며, 컨테이너 프로세스를 비권한 사용자로 실행할 수 있다면 rootless non-root 구성도 비교적 쉽게 만들 수 있음
- 컨테이너 내부에서도 비권한 사용자로 HTTP 서버를 실행하려면 기존
바인드 마운트와 UID 격리
- 호스트 디렉터리를 컨테이너에 마운트하면 호스트
root, 호스트bar, 네임스페이스foo가 소유한 파일의 접근 가능 여부가 UID 매핑에 따라 달라짐 - 예제에서는
/var/lib/bar/test디렉터리에 호스트root소유root.txt, 호스트bar소유bar.txt를 만들고, 컨테이너에서/test로 읽기/쓰기 마운트함 - 컨테이너를
foo로 실행하면 호스트bar소유 파일은 컨테이너 내부에서root:root로 보이고, 호스트root소유 파일은 네임스페이스에 매핑되지 않아nobody:nogroup으로 보임 - 컨테이너 내부
foo는bar.txt와root.txt를 읽지 못하며, rootless non-root가 rootless rootful보다 추가 격리를 제공함 foo가 마운트 디렉터리에 만든foo.txt는 호스트에서 UID166537소유로 표시되고, 호스트 사용자bar는 해당 파일 내용을 읽지 못함- 컨테이너를 내부
root로 실행하면 네임스페이스root는 호스트bar소유 파일과foo소유 파일을 읽을 수 있지만, 호스트root소유 파일은 읽지 못함 - 내부
root로 실행하면서--cap-drop=all을 적용하면foo파일도 읽지 못하고, 호스트bar소유 파일만 읽을 수 있음
Copy Fail 테스트
-
테스트 조건
- Copy Fail 테스트에는 원래 공개된 커밋
8e918b5의 익스플로잇 버전이 사용됨 - 예제 컨테이너 이미지는 기존 HTTP 서버 이미지에
curl을 추가해 컨테이너 안에서 익스플로잇 스크립트를 내려받을 수 있게 함 - 이미지 이름은
copyfail로 빌드됨 - 테스트 커널은 Debian의
6.12.74+deb13+1-amd64이며, Debian 기준 최근 버전 중6.12.85미만이면 아직 패치되지 않은 커널로 사용 가능하다고 봄 - 일반적으로 비권한 사용자
foo가su를 호출하면root비밀번호를 요구함 - 각 테스트에서는 컨테이너 사용자가
/tmp에 Copy Fail 스크립트를 다운로드하고 실행한 뒤,root셸을 얻으면sleep을 호출함 - Copy Fail은 컨테이너 생명주기를 넘어 지속되므로 각 테스트 전에 VM을 재부팅함
- Copy Fail 테스트에는 원래 공개된 커밋
-
루트리스 rootful에서의 결과
--user=root로 컨테이너를 실행하면 컨테이너 내부 프로세스는 이미root임- 이 상태에서 Copy Fail 스크립트를 실행하고
su를 호출하면uid=0(root)셸을 얻지만,root사용자는 원래 비밀번호 없이su로 다른 root 셸을 열 수 있으므로 Copy Fail이 실질적으로 추가하는 바는 없음 podman top에서는/bin/bash,python3 copy_fail_exp.py,su,sleep모두 컨테이너 내부root, 호스트 사용자1001로 표시됨- 동일한 capability 집합이 유지되며
CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT가 보임 - 내부
root는 마운트된/test에서bar.txt와foo.txt는 읽지만, 호스트root소유root.txt는 읽지 못함
-
루트리스 non-root에서의 결과
- 컨테이너를
foo로 실행한 뒤 Copy Fail 스크립트를 실행하고su를 호출하면 컨테이너 내부root로 권한 상승됨 - 결과 셸의
id는uid=0(root) gid=1002(foo) groups=1002(foo)로 표시됨 podman top에서는 초기/bin/bash, 익스플로잇 실행 프로세스,su호출은 호스트 UID166537, 컨테이너 사용자foo, capabilitiesnone상태로 보임- 권한 상승 후
[sh]와sleep은 호스트 사용자1001, 컨테이너 사용자root로 표시되고, rootless rootful과 같은 capability 집합을 얻음 - 권한 상승된 컨테이너
root도 호스트root소유root.txt는 읽지 못함 - 이 상태에서는 컨테이너가 침해됐지만, 공격 범위는 컨테이너와 호스트 비권한 사용자
bar가 가능한 범위로 제한됨
- 컨테이너를
-
no-new-privileges적용 시 결과- Podman은
--security-opt=no-new-privileges로 컨테이너 프로세스가 시작 시점보다 더 많은 권한을 얻지 못하게 할 수 있음 - rootless non-root 컨테이너에 이 옵션을 적용하고 Copy Fail을 실행하면 셸은 열리지만 여전히
uid=1002(foo)상태임 podman top에서도 모든 프로세스가 호스트 UID166537, 컨테이너 사용자foo, capabilitiesnone으로 유지됨- 마운트된
/test에서도foo는 자기 파일만 읽을 수 있고bar.txt와root.txt는 읽지 못함 - 컨테이너는 침해됐지만, 내부 비권한 사용자
foo와 capability 없는 상태로 제한됨
- Podman은
-
--cap-drop=all적용 시 결과- rootless non-root 컨테이너를
--cap-drop=all로 실행해도foo는 원래 capability가 없음 - 이 상태에서 Copy Fail을 실행하고
su를 호출하면 열린 셸은uid=1002(foo)로 유지됨 podman top에서도/bin/bash, 익스플로잇 실행,su, 셸,sleep모두foo와 capabilitiesnone상태임- 익스플로잇은
root셸 획득에는 실패하고,foo는/test에서 자기 파일만 읽을 수 있음 - 이 결과는
no-new-privileges테스트와 유사하며, 두 조치는 함께 사용해 capability 노출을 효과적으로 줄일 수 있음
- rootless non-root 컨테이너를
-
익스플로잇의 지속성
- 즉각적인
root셸과 capability 획득은no-new-privileges나--cap-drop=all로 막을 수 있었지만, 익스플로잇 자체의 효과는 남음 - 이후 capability 제한 없이 새 컨테이너를 실행하면 비권한 컨테이너 사용자
foo가su만 호출해도 컨테이너root가 될 수 있음 - 따라서 커널 패치와 재부팅이 여전히 필요함
- 즉각적인
심층 방어 전략
-
읽기 전용 이미지
podman run에--read-only를 추가하면 컨테이너 루트 파일시스템이 읽기 전용으로 마운트됨- Podman은 기본적으로
/tmp,/run,/var/tmp같은 일부 디렉터리를 쓰기 가능하게 마운트하므로, 완전히 읽기 전용으로 만들려면--read-only-tmpfs=false도 추가해야 함 - 읽기 전용 컨테이너가 침해되면 시스템에 쓰기 작업이 허용되지 않아 익스플로잇 이후 일부 공격을 제한할 수 있음
- 다만
curl출력을python3로 파이프할 수 있으므로 읽기 전용 설정만으로 익스플로잇 실행 자체를 막지는 못함 - 예제의
python3HTTP 서버는 파일시스템 쓰기가 필요 없어 이 옵션을 안전하게 사용할 수 있음 - 많은 사전 빌드 이미지는 특정 디렉터리에 쓰기 접근을 전제로 하므로 읽기 전용 루트 파일시스템에서 제대로 동작하지 않을 수 있음
- 읽기 전용 루트 파일시스템은 컨테이너에 연결된 쓰기 가능 볼륨과 독립적이며, 침해 시 해당 마운트 디렉터리에는 여전히 쓸 수 있음
-
리소스 제한
- Docker와 Podman은 cgroups를 사용해 컨테이너에 제공되는 리소스를 제한할 수 있음
- 컨테이너에는 무제한 메모리, CPU, PID가 필요하지 않음
podman stats로 컨테이너 리소스 사용량을 확인한 뒤 그에 맞춰 제한을 적용할 수 있음
-
사용 가능한 바이너리 제한
- 예제는 단순화를 위해
ubuntu이미지를 사용했지만,ubuntu이미지는 침해 시 공격자가 사용할 수 있는 많은 바이너리를 포함함 - HTTP 서버 실행에는 이런 바이너리 대부분이 필요하지 않음
- 런타임 이미지는 가능한 한 얇게 구성하는 편이 좋음
- 멀티스테이지 빌드를 사용해 빌드 타임 환경과 런타임 환경을 분리할 수 있음
- python3 같은 목적별 이미지, Debian의
-slim변형,alpine같은 더 작은 배포판을 기반으로 사용할 수 있음 - 컨테이너 프로세스와 호환된다면 distroless images나
scratch를 사용해 셸, 패키지 관리자, 시스템 유틸리티가 없는 런타임을 만들 수 있음
- 예제는 단순화를 위해
-
방화벽
iptables나nftables를 사용해 컨테이너 프로세스를 방화벽으로 제한할 수 있음- 컨테이너 프로세스에 꼭 필요한 수신·송신 연결만 허용해야 함
- HTTP 서버 예제에서는 DNS나 로컬·원격 서버로의 연결이 필요하지 않으므로, 성립된 수신 연결에서 오는
tcp패킷만 허용하는 식으로 제한할 수 있음
운영상 의미
- 표준 Podman 루트리스 컨테이너는 표준 Docker 컨테이너 구성보다 더 나은 격리 수단을 기본적으로 제공함
- Docker도 rootless 실행과 비권한 사용자 네임스페이스 사용이 가능하지만, Podman보다 더 많은 설정 노력이 필요하며 아키텍처 차이도 영향을 줌
- Docker는 여전히 널리 사용되며 Dokku, Kamal, Coolify, Dokploy 같은 셀프호스팅 도구도 기본적으로 Docker를 사용함
- Docker Hub 이미지를 충분히 검토하지 않고 실행하거나 잠금 조치를 적용하지 않으면 필요한 것보다 넓은 공격 표면으로 서비스가 실행될 수 있음
- 컨테이너 이미지의 구현 세부사항을 이해해야 함
- 어떤 사용자 또는 사용자들이 컨테이너 프로세스를 실행하는지 알아야 함
- 컨테이너 프로세스가 루트 파일시스템의 어떤 디렉터리에 의존하는지 알아야 함
- 필요한 Linux capabilities와 필요하지 않은 capabilities를 구분해야 함
- Podman과 컨테이너가 제공하는 여러 메커니즘을 조합하면 컨테이너를 강화하고 침해 시 폭발 반경을 줄일 수 있음
- 워크로드에 따라 컨테이너를 유일한 보안 경계로 의존해서는 안 됨
- 컨테이너와 별도 물리·가상 머신을 함께 사용하면 효과적으로 분리할 수 있음
- Podman은 같은 호스트 안에서도 각 워크로드를 별도의 비권한 사용자와 자체 사용자 네임스페이스로 실행해 격리하는 방법을 제공함