Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,428 words · 6 segments analyzed
May 4, 2026ContentsAn overview of rootless containersRootless rootfulUser namespacesPrivileged operationsRootless non-rootBind mountsCopy FailRootless rootfulRootless non-rootRootless non-root while disabling new privilegesRootless non-root while dropping capabilitiesThe exploit persistsDefence in depthRead-only imagesResource constraintsLimit available binariesFirewallingConclusionFurther readingOn April 29th CVE-2026-31431 was publicly disclosed at https://copy.fail/. This vulnerability allows a local unprivileged user to obtain a root shell by running the Python script shared by the author.This exploit can be used to exploit Linux containers, which are widely used to run all sorts of things: public-facing services, development environments, continuous integration jobs, etc. A container exploited with Copy Fail can used quite effectively for many kinds of attacks.This CVE is quite interesting to me as it’s been about a year since I moved away from Docker to Podman to run containers. Several reasons motivated this change, but chief among them was Podman’s security posture 1.Podman makes it trivial to run containers as an unprivileged user, and this is known as running a container “rootless”. Unlike Docker, Podman uses a fork/exec model such that the container process is ultimately a descendant of the podman run process that is used to run the container. As a result, you can rely on standard UID separation to isolate your container processes from root or other users in the system.As I read about Copy Fail I did not find much information about its use in rootless containers specifically. After performing some simple tests I confirmed that Copy Fail is indeed exploitable in rootless containers to obtain a container root shell, but the blast radius of this is severaly limited using several features in Podman.At the time of publishing, there is not a lot of information about container escapes:Root cause, scatterlist diagrams, the 2011 → 2015 → 2017 history, and the exploit walkthrough are on the Xint blog. Part 2 (Kubernetes container escape) is forthcoming.In my testing, the container root is still limited to what the unprivileged user running the container can do at the host level.
All in all, Copy Fail has proven to be a great example to refer to when writing about Podman’s implementation of rootless containers. In this note I reproduce the exploit across distinct container configurations to try to understand the exposure of a compromised rootless container.This article ended up being a bit long so feel free to jump ahead to the relevant parts if you need to:A practical review of rootless containers, user namespaces and Linux capabilitiesUsing Copy Fail in rootless containersPracticing defence in depth to further limit exposure in the event of a compromiseAn overview of rootless containersLet’s assume that I need to run an HTTP server to serve some HTML. The server will run in a container owned by an unprivileged user bar whose UID is 1001.I install Podman, create the user bar, and switch to it. Then, I build the image using podman build and run the container using 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 <!DOCTYPE html><html lang="en"></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 The server should now be responding to requests:bar@debian:~$ curl localhost:8000 <!DOCTYPE html><html lang="en"></html> Rootless rootfulLet’s examine what this container process looks like.
Using ps I can confirm that this python3 process is owned by the user 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 As mentioned in the introduction, Podman uses a fork/exec model to run containers. User bar executed the podman run command, and the container command python3 descended from that process. This is in contrast to the standard Docker setup, in which running docker run as an unprivileged user executes a Docker client that interacts with a rootful daemon that ultimately spawns the container: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 root 5198 5175 5 19:20 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000 bar@debian:~$ docker container top http-server-1 UID PID PPID C STIME TTY TIME CMD root 4844 4820 0 14:51 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000 Now, containers also have users and groups to determine permissions inside the container. Most images default to running the container commands as root in the absence of an explicit USER instruction in the Containerfile or a --user flag when running the container.Using podman top I can confirm that the python3 container process is running as root as I did not declare which user executes the process: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 Remember that containers share the kernel with the host. What does being root inside the container mean?
Surely this is not the same as host root given that we’re using an unprivileged user?User namespacesPodman uses user namespaces for rootless containers. User namespaces allow processes to have different a UID/GID inside and outside the container. In our previous example, the python3 process has a UID of 0 (i.e container root) inside the namespace while being mapped to UID 1001 (i.e host bar) outside it.The range of UIDs that can be allocated to namespaced processes of user bar is determined in /etc/subuid:bar@debian:~$ grep bar /etc/subuid bar:165536:65536 Besides UID 1001, there are 65,537 UIDs can be allocated to processes of bar, starting with 165536 and ending with 231072 (165536 + 65537).Our current image is based off of ubuntu, which brings its own set of users: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 bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin _apt:x:42:65534::/nonexistent:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash Processes and objects from these users have the UIDs shown above within the bar user namespace. Outside the namespace these are mapped to a UID within the 165537-231072 range, with the exception of root which is mapped to host UID 1001.For example, let’s have bar run sleep in the container as user www-data: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 165568 www-data sleep 60 The sleep process is running as www-data inside the user namespace but is mapped to 165568 on the host. The user namespace affords standard UID isolation across processes of the same user. That is to say, from the host’s perspective, a process of www-data in the bar user namespace is separate from one of bar.Docker does support using user namespaces, but it must be configured accordingly and only one user namespace is allowed. With Podman, each UNIX user has its rootless containers running in the corresponding user namespace.You can use podman unshare to enter the user’s namespace without having to run a container.
We can use this to understand the relationship between bar and the namespace root by comparing the ownership of bar’s home directory, both inside and outside the 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 drwx------ 5 root root 4096 May 2 22:58 /var/lib/bar The last thing to understand about the container root is privileges. Per the Containerfile that we’re using, root was able to install python3 in the container using apt install. How was this possible given that installing packages involves multiple privileged operations and bar is not the host root?Privileged operationsPodman uses Linux capabilities to grant granular root privileges to a container process. You can drop or add these capabilities both when building the image and running the container.By using pscap we can observe that multiple capabilities are granted to the apt processes that runs during the image build for user bar: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 + These capabilities are set by Podman and it is the combination of these what allows root in the namespace to perform privileged operations. Should we drop all capabilities during podman build using --cap-drop=all, the image will fail to build due to lack of permissions: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 WARNING: apt does not have a stable CLI interface.