Chapter 68: Podman in Depth: Install, Rootless Containers, Pods, Compose, Kube YAML and Alternatives
68.2 Rootless mode: user namespaces, subuid ranges, networking, volumes and limits
Why: the problem
Rootless means you start containers as a normal Linux user, with no sudo and no root daemon anywhere. Inside the container the process can still believe it is root, install packages and own files as UID 0, but on the host it is just your user. If an attacker breaks out of a rootless container, they land as ravi, not as root, and can only damage what ravi can damage. That single property is why Red Hat, the Kubernetes community and security teams at banks like HDFC and ICICI push rootless containers so hard. Why is this possible? Linux user namespaces let a process see a different set of user IDs from the rest of the system. Podman creates a user namespace where UID 0 inside maps to your own UID outside, such as 1000, and UIDs 1 to 65536 inside map to a private range of host UIDs reserved for you in /etc/subuid and /etc/subgid, such as 100000 to 165535. A file created by root inside the container appears on the host as owned by you; a file created by UID 999 inside, like the postgres user, appears as 100998. Nobody else on the host has those IDs, so your containers cannot touch other users' files. What changes compared with rootful containers? Networking is done in user space by pasta, so ports below 1024 need a sysctl change and container-to-container traffic on the default network goes through a user-mode stack. Volumes you mount from your home folder may show unexpected owners, which the :U flag or podman unshare fixes. Each user has a separate image store under ~/.local/share/containers, so images are not shared with root or other users. Resource limits through cgroups v2 need systemd delegation, which modern Fedora, RHEL 9 and Ubuntu 24.04 provide by default. How do we learn it? We check the subuid ranges, prove the UID mapping from inside and outside, compare rootless and rootful containers, run Postgres with a bind mount and fix ownership the right way, open a low port safely, set memory and CPU limits, and keep containers running after logout with lingering. No chmod tricks are needed anywhere; correct ownership and the :U or :Z flags are enough.
What: every term in plain English
- Rootless container — Container started by a normal user with no root daemon.
- User namespace — Kernel feature that maps UIDs inside a container to different host UIDs.
- /etc/subuid — File listing the subordinate UID range each user may use.
- /etc/subgid — Same for group IDs.
- newuidmap — Setuid helper that writes the UID map for rootless users.
- podman unshare — Runs a command inside your user namespace, to fix file owners.
- :U volume flag — Changes ownership of a mounted folder to match the container user.
- :Z volume flag — Relabels the folder for SELinux so only this container can use it.
- pasta — User-mode network stack for rootless containers.
- loginctl enable-linger — Keeps a user's services and containers running after logout.
How: step by step
- Check your subuid and subgid ranges. Range present.
- Run a container and compare id inside and ps outside. Mapping visible.
- Read /proc/self/uid_map. 0 maps to your UID.
- Create a file as root inside a bind mount. Owned by you on host.
- Run Postgres with a bind mount and the :U flag. No permission errors.
- Inspect ownership with podman unshare. Correct mapping.
- Allow port 80 for unprivileged users or use 8080. Port opens.
- Set memory and CPU limits. cgroup v2 limits.
- Enable lingering. Survives logout.
It helps to think of your subuid range as a private apartment block inside the host. Your own UID is the front door, numbered 1000; every other room is numbered from 100000 upwards and belongs to you alone. Whatever happens inside the block, including root inside a container, can only move furniture around in your rooms. This is why shared build servers at companies like Zoho and Thoughtworks give each developer or CI agent its own Linux user running rootless Podman: two teams on the same server cannot see or change each other's images, containers or files, and nobody needs sudo. When you design such a server, give each user a non-overlapping range of 65536 IDs in /etc/subuid and /etc/subgid, keep their home folders on a disk with enough space for images, and set lingering only for users that run long-lived services.
Example 1: See the UID mapping from both sides
Goal. See the UID mapping from both sides.
map.sh
grep "^$USER:" /etc/subuid /etc/subgid
podman unshare cat /proc/self/uid_map
podman run -d --name sleeper docker.io/library/alpine:3.22 sleep 600
podman exec sleeper id
ps -o user,pid,cmd -C sleep
mkdir -p ~/lab/shared
podman run --rm -v ~/lab/shared:/data docker.io/library/alpine:3.22 sh -c 'touch /data/by-root; adduser -D -u 1001 app; su app -c "touch /tmp/x"; chown 1001 /data/by-root; ls -ln /data'
ls -ln ~/lab/shared
podman rm -f sleeper
What each line does
/etc/subuid— ravi:100000:65536 gives ravi 65536 private host UIDs starting at 100000.uid_map— Line 0 1000 1 maps root inside to UID 1000 outside; line 1 100000 65536 maps the rest.id inside— The container sees uid=0(root).ps outside— The host sees the sleep process owned by ravi.chown 1001— Inside UID 1001 becomes host UID 101000, which is inside ravi's private range.
Green output
$ bash map.sh
/etc/subuid:ravi:100000:65536
/etc/subgid:ravi:100000:65536
0 1000 1
1 100000 65536
uid=0(root) gid=0(root) groups=0(root)
USER PID CMD
ravi 52310 sleep 600
-rw-r--r-- 1 1001 0 0 Oct 25 03:40 by-root
-rw-r--r--. 1 101000 1000 0 Oct 25 09:10 by-root
Expected result. Root inside the container is ravi outside, and UID 1001 inside is 101000 outside. Remember this formula: host UID equals 100000 plus inside UID minus 1, for every inside UID above 0.
Example 2: Postgres with a bind mount: permission errors fixed the right way
Goal. Postgres with a bind mount: permission errors fixed the right way.
pg.sh
mkdir -p ~/lab/pgdata
podman run -d --name pg-bad -e POSTGRES_PASSWORD=REPLACE_WITH_DB_PASSWORD \
-v ~/lab/pgdata:/var/lib/postgresql/data docker.io/library/postgres:17-alpine
sleep 3; podman logs pg-bad | tail -2
podman rm -f pg-bad
podman run -d --name pg -e POSTGRES_PASSWORD=REPLACE_WITH_DB_PASSWORD \
-v ~/lab/pgdata:/var/lib/postgresql/data:U,Z -p 5432:5432 docker.io/library/postgres:17-alpine
sleep 5; podman logs pg | tail -1
ls -ln ~/lab | grep pgdata
podman unshare ls -ln ~/lab | grep pgdata
podman exec pg psql -U postgres -c 'select version();' | head -3
What each line does
First run— The postgres user, UID 70 in alpine, cannot write into a folder owned by ravi.:U— Podman chowns the folder to the container user, mapped into ravi's range; no world-writable modes needed.:Z— Relabels the folder for SELinux on Fedora and RHEL; harmless elsewhere.ls -ln on host— Shows owner 100069, inside ravi's private range.podman unshare ls— Shows the same folder as UID 70 from the container's point of view.
Green output
$ bash pg.sh
chmod: /var/lib/postgresql/data: Operation not permitted
initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permitted
pg-bad
d1e2f3...
LOG: database system is ready to accept connections
drwx------. 19 100069 100069 4096 Oct 25 09:12 pgdata
drwx------. 19 70 70 4096 Oct 25 09:12 pgdata
version
--------------------------------------------------------------------
PostgreSQL 17.6 on x86_64-pc-linux-musl, compiled by gcc ...
Expected result. The :U flag fixed ownership with a safe 0700 data folder, which is what Postgres demands. Never solve these errors by opening permissions to everyone; fix the owner instead.
Example 3: Ports, limits and lingering
Goal. Ports, limits and lingering.
ops.sh
podman run -d --name web80 -p 80:80 docker.io/library/nginx:1.27-alpine 2>&1 | tail -1
printf 'net.ipv4.ip_unprivileged_port_start=80\n' | sudo tee /etc/sysctl.d/90-podman-ports.conf
sudo sysctl --system >/dev/null
podman run -d --name web80 -p 80:80 --memory 128m --cpus 0.5 docker.io/library/nginx:1.27-alpine
podman inspect web80 --format 'mem={{.HostConfig.Memory}} cpus={{.HostConfig.NanoCpus}}'
podman stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}'
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
loginctl enable-linger $USER
loginctl show-user $USER --property=Linger
What each line does
Port 80 fails first— Rootless users cannot bind below 1024 by default.ip_unprivileged_port_start=80— Lowers the limit for the whole host; using 8080 behind a load balancer is often simpler.--memory and --cpus— Same flags as Docker, enforced by cgroups v2 through systemd delegation.cgroup.controllers— Must list cpu and memory for limits to work rootless.enable-linger— Without it, systemd stops your containers when you log out.
Green output
$ bash ops.sh
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf
net.ipv4.ip_unprivileged_port_start=80
7a8b9c...
mem=134217728 cpus=500000000
web80 3.1MB / 134.2MB 0.00%
cpuset cpu io memory pids
Linger=yes
Expected result. Port 80 works after one sysctl, limits are enforced rootless, and lingering keeps the containers alive after you close SSH. That is a production-ready rootless setup on one VM.
Every command, step by step (Podman lab)
-
Ranges
grep $USER /etc/subuidWhat it does: Subuid.
ravi:100000:65536 -
Map
podman unshare cat /proc/self/uid_mapWhat it does: Mapping.
0 1000 1 -
Inside
podman exec sleeper idWhat it does: Root inside.
uid=0(root) -
Outside
ps -o user,cmd -C sleepWhat it does: You outside.
ravi sleep 600 -
Mount
podman run -v ~/lab/pgdata:/var/lib/postgresql/data:U,Z ...What it does: Owner fix.
d1e2f3... -
Unshare
podman unshare ls -ln ~/labWhat it does: Inside view.
70 70 pgdata -
Port
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80What it does: Low ports.
= 80 -
Limits
podman run --memory 128m --cpus 0.5 ...What it does: cgroups.
7a8b9c... -
Stats
podman stats --no-streamWhat it does: Usage.
3.1MB / 134.2MB -
Linger
loginctl enable-linger $USERWhat it does: After logout.
Linger=yes
When it is red 1
cannot find UID/GID for user ravi: no subuid ranges found
Cause. User created without subuid entries.
Fix, step by step
- sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 ravi; podman system migrate.
When it is red 2
Operation not permitted on a bind mount
Cause. Container user cannot write a folder owned by you.
Fix, step by step
- Use :U or podman unshare chown.
When it is red 3
Permission denied with SELinux enforcing
Cause. Missing SELinux label.
Fix, step by step
- Add :Z or :z to the mount.
When it is red 4
Error: OCI runtime error: the requested cgroup controller cpu is not available
Cause. No systemd delegation.
Fix, step by step
- Enable Delegate=yes for user@.service; reboot.
When it is red 5
containers stop after logout
Cause. Lingering off.
Fix, step by step
- loginctl enable-linger.
Practice tasks
Easy Task 1: Ranges
Why: Check.
What to do: Read your subuid lines.
- grep.
Solution
grep $USER /etc/subuidEasy Task 2: Map
Why: View.
What to do: Print uid_map.
- unshare.
Solution
podman unshare cat /proc/self/uid_mapEasy Task 3: Compare
Why: Inside/outside.
What to do: id inside, ps outside.
- Two views.
Solution
ps -C sleep -o userMedium Task 4: Mount
Why: Owner.
What to do: Create files in a bind mount.
- ls -ln.
Solution
ls -ln ~/lab/sharedMedium Task 5: Postgres
Why: Fix.
What to do: Run Postgres with :U,Z.
- Ready.
Solution
podman logs pgMedium Task 6: Limits
Why: cgroups.
What to do: Set memory and CPU.
- inspect.
Solution
podman stats --no-streamHard Task 7: Port
Why: 80.
What to do: Open port 80 safely.
- sysctl.
Solution
curl -s localhostHard Task 8: HDFC host
Why: Main task.
What to do: Full hardened rootless setup.
- Linger yes.
Solution
loginctl show-user $USERHomework: try at home
For HDFC Bank-style hardened RHEL hosts, run Postgres and nginx rootless as user ravi with a bind-mounted data folder fixed by :U,Z, nginx on port 80 via the sysctl, 128 MB and half a CPU limits, and lingering on, then prove the host UIDs with ls -ln.
Recap
- Rootless containers run as your user with no root daemon.
- User namespaces map inside UIDs to your private subuid range.
- Fix volume permissions with :U, :Z or podman unshare, never with open modes.
- Low ports need a sysctl; limits need cgroups v2 delegation.
- Lingering keeps rootless containers running after logout.
FAQ
Q. Is rootless slower?
Slightly for networking, because packets go through pasta in user space; disk and CPU are the same. For most web apps the difference is not measurable; for heavy network loads use rootful containers on a dedicated host.
Q. Can Docker run rootless too?
Yes, Docker has a rootless mode with dockerd-rootless-setuptool.sh, but it still runs a per-user daemon. In Podman rootless is the default and needs no setup.
Q. Why does ls on the host show strange owners like 100069?
They are the mapped UIDs from your subuid range. Use podman unshare ls to see the container view.
Q. What is the difference between :z and :Z?
Lowercase :z gives the folder a shared SELinux label, so several containers can use it. Uppercase :Z gives a private label for one container only. Use :Z for a database data folder and :z for a folder of static files read by two web containers. On Ubuntu without SELinux both flags are ignored, so it is safe to keep them in scripts that run on every distribution.
Q. How do I give a new user subuid ranges?
Modern useradd adds them automatically. For older users run sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 username, make sure ranges never overlap between users, and run podman system migrate as that user so Podman picks up the change.
Q. Can rootless containers use GPUs?
Yes, with the NVIDIA Container Toolkit and CDI device specs, used by many ML teams on shared servers.
Interview questions
-
Explain how rootless Podman works.
Model answer: A user namespace maps container root to the user and other UIDs to the subuid range; newuidmap writes the map.
-
Why are rootless containers safer?
Model answer: An escape gives only the user's rights, not host root.
-
How do you fix permission denied on a rootless bind mount?
Model answer: Use :U to chown into the mapped range, :Z for SELinux, or podman unshare chown.
-
What are the limitations of rootless?
Model answer: Low ports, user-mode network speed, per-user image stores, some kernel features.
Quick quiz
Q1. What does root inside map to?
Your own UID.
Q2. Where are subordinate ranges stored?
/etc/subuid and /etc/subgid.
Q3. Which flag fixes mount ownership?
:U.
Q4. Which flag fixes SELinux labels?
:Z.
Q5. What keeps containers after logout?
loginctl enable-linger.
Samajla ka?