OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 011

Rootless containers: whose file permissions count?

Trace UID mappings through bind mounts, persistent data, and network boundaries to explain Podman's permission checks.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSContainer permissions and lifecycles ↗Podman container workflows ↗
About 7 min readStart reading

Rootless containers let container processes run within an ordinary user’s permission boundaries. UID 0 inside a container can still manage its own filesystem view, but this does not make it a host administrator. In practice, the common stumbling blocks are less about “whether the container has root” and more about who owns the host files, which UID the application uses, and where its data ultimately remains.

First establish who is running Podman#

This article consolidates existing Podman notes and corrects them against the stable documentation and v6.1.3 source checked in October 2026. The examples target local rootless Podman on Linux. Paths used by remote clients or Podman Machine belong to the server or virtual machine and cannot be interpreted in the same way. The commands below are reproducible illustrations; no container experiments were run for this article.

First query the environment as the same ordinary user who will run the containers:

id
podman version
podman info --format 'rootless={{.Host.Security.Rootless}} remote={{.Host.ServiceIsRemote}}'
podman info --format '{{.Store.GraphRoot}}'
podman ps -a

Check rootless mode, whether the connection is remote, and the storage root together. Switching to sudo opens root’s Podman storage and management view; an empty listing does not prove that the original containers are gone. Different ordinary users also manage separate containers. [1][5]

Root inside the container goes through UID translation#

The default rootless user namespace usually maps the caller’s host UID to container UID 0, then maps other container UIDs into the allocated subordinate ID range. If the host user has UID 1000 and the allocated subuid range starts at 100000, a conceptual mapping might be:

# 解释用例,不是本机输出
# 容器起始 UID   宿主起始 UID   连续数量
0                1000            1
1                100000          65536

The first line maps container root to the ordinary host user; the second line supplies the additional UIDs. In this mapping, for example, container UID 1000 maps to host UID 100999, not 101000. The actual mapping depends on the userns mode, existing configuration, and the pod containing the container. This table is not a fixed answer for every container.

podman unshare cat /proc/self/uid_map /proc/self/gid_map
# 若已有运行中的容器,用实际容器名替换
podman top demo pid user hpid huser

unshare shows the base rootless namespace; keep-id or auto containers may create different mappings on top of it. In top, user is the container’s view and huser is the host’s view, and the two can differ. Seeing root in the container’s id output, or observing a failed mknod, is not enough by itself to establish the overall isolation state. [2][6]

/etc/subuid and /etc/subgid record the additional ID ranges allocated to users; helpers such as newuidmap/newgidmap set up the mappings. You can query just the records for the current username. If ranges are missing, an administrator should check for non-overlapping allocations and the distribution’s configuration. On NixOS, handle this in the system declaration rather than copying a range into host files. Changing ranges also affects existing container namespaces and file ownership; logging in again is not a universal migration method.

Mounting a directory does not mean the process can write to it#

A bind mount only exposes a path inside the container. The kernel still checks the actual host UID/GID, permission bits, ACLs, and SELinux labels when enabled. Root in a default rootless container can often write to directories already writable by the calling user. An application running as a different UID in the image may map to a subuid and be unable to write to a directory owned by the host user without the necessary permissions.

First identify which user the application needs. keep-id maps the caller to the same numeric UID/GID in the container and starts under that identity by default. If the image requires a fixed container UID, you can use the keep-id:uid=… ,gid=… mapping form and explicitly check --user. This changes the identity mapping; it does not automatically satisfy the image’s requirements for a username, home, or writable runtime directories. [3]

ro makes the mount read-only; z/Z control SELinux relabeling, with z for a shared label and Z for a private label. Neither corrects Unix ownership. :U recursively changes ownership of the source directory, making persistent changes visible on the host. Do not treat it, recursive chown, or broad chmod as a fix to add whenever permissions fail. Nor should shared home or system directories be relabeled wholesale just for container convenience. [4]

A minimal example that preserves host file ownership#

The following uses mktemp -d to create a unique demonstration directory in temporary storage, then writes a file. It assumes that Podman is installed and the public BusyBox image is accessible. run pulls the image if missing, then creates and starts a disposable container; it does not connect to a personal account. On SELinux hosts, you may change the mount option from rw to rw,Z after confirming that this directory is dedicated to the example.

demo_dir=$(mktemp -d "${TMPDIR:-/tmp}/podman-permission-demo.XXXXXX")
printf 'demo directory: %s\n' "$demo_dir"
printf 'host input\n' > "$demo_dir/input.txt"

podman run --rm --network=none \
  --userns=keep-id --user "$(id -u):$(id -g)" \
  --volume "$demo_dir:/work:rw" \
  docker.io/library/busybox:1.37 \
  sh -c 'id; cat /work/input.txt; printf "container output\n" > /work/output.txt'

stat -c '%u:%g %a %n' "$demo_dir/input.txt" "$demo_dir/output.txt"

The printed path is the location of the demonstration file; use that actual path to confirm any later cleanup. The output file is expected to belong to the host UID/GID running Podman, but verify this with stat. --network=none restricts container networking at runtime; image pulls happen on the host side and may still access a registry. --rm deletes the container after it exits, not this bind-mounted data. Record the image digest when reproducing the experiment.

Storage drivers, images, and volumes have distinct locations#

The container writable layer, image layers, and named volumes are separate objects. The default rootless graphroot is usually $HOME/.local/share/containers/storage, but XDG_DATA_HOME and storage.conf can override it; runroot holds runtime state. Do not use the rootful path /var/lib/containers/storage/volumes to guess where an ordinary user’s data resides.

podman info --format '{{.Store.GraphDriverName}}'
podman info --format '{{.Store.GraphOptions}}'
podman info --format '{{.Store.GraphRoot}}'
podman info --format '{{.Store.RunRoot}}'
podman system df -v
# 只查询已有卷;使用实际卷名
podman volume inspect demo-data --format '{{.Mountpoint}}'

overlay is the storage driver name; fuse-overlayfs can serve as its mount_program. graphDriverName=overlay alone does not establish that native OverlayFS is in use. Also check graphOptions, storage.conf, the kernel, and the underlying filesystem. Filesystems such as NFS that do not understand user namespaces are unsuitable for a rootless graphroot; a network-hosted home can be paired with a local storage directory. [1][8]

Podman manages named volumes, which are suitable for persistent application data. First use may initialize ownership and copy image contents. Inspecting a volume’s NeedsChown, NeedsCopyUp, and Mountpoint is more reliable than modifying its storage directory directly. Deleting a container does not mean deleting its named volumes, and prune is not unconditionally safe housekeeping. Confirm usage relationships and backups before deciding which data to clean up.

Assess port publishing separately from rootless networking#

Whether an ordinary user can bind low ports depends on settings such as net.ipv4.ip_unprivileged_port_start; not every system imposes a fixed prohibition on ports below 1024. The demonstration service uses a high port and explicitly binds loopback:

# 加入实际服务的 podman run 参数
--publish 127.0.0.1:8080:8080

The application must listen on 8080 inside the container. The 8080 on the left belongs to the host, and the one on the right to the container. Omitting the host IP generally means binding all interfaces; accessing a service through localhost in a browser does not prove that it is exposed only to the local machine. [4]

Podman 5.0 changed the default networking for standalone rootless containers to pasta; 6.0 also removed slirp4netns and CNI support. In 6.1.3, the network backend is Netavark, while pasta handles the corresponding rootless networking path. Port forwarding for rootless custom bridges still defaults to rootlessport; pasta/pesto forwarding is a separate experimental option. The network backend, userspace networking, and port forwarding are three different layers of information. Do not copy slirp4netns troubleshooting flags from older tutorials into 6.x. [7]

Narrow down permission errors layer by layer using evidence#

A consistent troubleshooting sequence is: confirm the running user and version; identify the container application’s UID; read the mapping; check the host target directory; then investigate SELinux, ACLs, and the filesystem. For a Permission denied error, first establish whether it occurs during image extraction, container startup, or an application file write.

# 宿主:仅看目标目录与文件属性
stat -c '%u:%g %a %n' /path/to/demo-data
namei -l /path/to/demo-data
# 若系统提供这些工具,进一步看 ACL 与 SELinux
getfacl /path/to/demo-data
ls -Zd /path/to/demo-data

Even if the directory itself is writable, its parent directories still require traversal permission. If access relies on a supplementary group, check whether the runtime preserves that group. Rootless reduces the risk associated with host administrator privileges, but an application can still damage personal data it has been authorized to access. Keeping sensitive directories out of mounts, mounting inputs read-only, and limiting writable data to dedicated directories make this boundary meaningful in practice.

References & further reading

  1. Podman rootless mode and storage ↗
  2. podman unshare and UID mapping ↗
  3. Podman 6.1.3 user namespace options source ↗
  4. podman run: mounts, users, and published ports ↗
  5. Current-environment fields in podman info ↗
  6. Host-user fields in podman top ↗
  7. Podman 6.0.0 networking changes ↗
  8. podman volume inspect ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

Container permissions and lifecycles

  1. 01Rootless containers: whose file permissions count?
  2. 02How does Quadlet turn container declarations into services?

Podman container workflows

  1. 01Podman fundamentals
  2. 02A basic Podman usage guide
  3. 03Rootless containers: whose file permissions count?
  4. 04Rootless configuration: mappings, ICMP, and registries
  5. 05Pods and Kubernetes manifests
  6. 06How does Quadlet turn container declarations into services?
  7. 07Pods and multi-service configuration with Quadlet
  8. 08Compose providers and interoperability with Docker tools
  9. 09Managing Podman on NixOS
  10. 10Advanced Podman operations and tuning
  11. 11Docker and Podman: architecture, compatibility, and coexistence
CONTINUE READING / NEXT ARTICLEHow does Quadlet turn container declarations into services?

Follow source files, systemd generation, startup verification, persistent data, and readiness checks, then diagnose service failures.