m4nifest0-tech

Search the site

Virtualization

Unprivileged LXC containers on Proxmox: bind mounts and UID mapping

Why a bind mount in an unprivileged LXC container shows nobody-owned files, and how to fix it with UID math or with lxc.idmap, subuid and subgid.
Published on 8 min readLevel: Advanced

An unprivileged LXC container with a bind mount often shows files and folders owned by nobody:nogroup, even when the permissions on the host look correct. This is not a Proxmox defect: it depends on how the kernel translates user identifiers between container and host. Understanding the mechanism lets you choose knowingly between two solutions, computing permissions on the host and custom mapping with lxc.idmap, each with different security trade-offs.

Privileged and unprivileged containers

In a privileged container the identifiers coincide with the host's: the container's root is the host's root and, in case of a breakout, only the isolation mechanisms (namespaces, AppArmor, capabilities) are left to limit the damage. In an unprivileged container the kernel uses a user namespace: the range of UIDs and GIDs seen by the container is shifted to a different range on the host, so the container's root is an unprivileged user on the host.

On Proxmox VE the default shift starts at 100000 and covers 65536 identifiers, as declared by the root:100000:65536 entry in /etc/subuid and /etc/subgid. The rule is host UID = 100000 + container UID.

User in the containerUID in the containerUID on the host
root0100000
www-data33100033
first user created1000101000
nobody65534165534

The kernel checks permissions using the host's IDs: the container only sees a translation of them. An ID with no match in the namespace appears as 65534 (nobody), and that is what you observe with a bind mount when the folder belongs to a host ID outside the mapped range, for example 1000. Giving up the unprivileged container to work around the problem moves the risk to the host: it is better to fix the permissions, not the isolation.

The bind mount

A bind mount exposes a host directory inside the container. For container 101:

On the node hosting the container
pct set 101 -mp0 /tank/dati,mp=/mnt/dati

The general form is pct set <vmid> -mp0 /host/path,mp=/container/path. The command adds the line mp0: /tank/dati,mp=/mnt/dati to /etc/pve/lxc/101.conf; the mount point becomes active at the next container start. A few rules:

  • The directory must exist on the host and, in a cluster, on every node where the container can be started or migrated (see Three-node Proxmox VE cluster).
  • Do not mount system directories such as /, /var or /etc: the documentation flags them as a serious risk for the host.
  • The contents of bind mounts are not included in vzdump backups: the data must be protected separately, as described in 3-2-1 backup strategy with Proxmox Backup Server.

Computing permissions on the host

With the default mapping, it is enough to give the directory to the host ID that corresponds to the container user. For UID and GID 1000:

On the host
chown -R 101000:101000 /tank/dati

Verify with numeric IDs, because names depend on the namespace in which they are resolved.

On the host
ls -ln /tank/dati
On the node, inside the container
pct exec 101 -- ls -ln /mnt/dati
Host view
-rw-r--r-- 1 101000 101000 0 Apr  8 10:00 nota.txt
Container view
-rw-r--r-- 1 1000 1000 0 Apr  8 10:00 nota.txt

The method requires no changes to the container configuration. The limit is that on the host the data ends up owned by an unusual ID: this is inconvenient if the same directory is shared with other services, for example Samba or NFS, or if the files already exist with a different owner. By default all unprivileged containers use the same range, so a bind mount shared between several containers shows the same IDs everywhere.

Custom mapping with lxc.idmap

When the files already have a real owner on the host, you can let a single ID cross the namespace without translation: the container's UID 1000 becomes the host's UID 1000, while all the others stay shifted. For each type (u for UIDs, g for GIDs) three ranges are needed, which must cover 0 to 65535 with no gaps or overlaps.

/etc/pve/lxc/101.conf (lines to add)
lxc.idmap = u 0 100000 1000
lxc.idmap = g 0 100000 1000
lxc.idmap = u 1000 1000 1
lxc.idmap = g 1000 1000 1
lxc.idmap = u 1001 101001 64535
lxc.idmap = g 1001 101001 64535

Each line has the form lxc.idmap = <type> <first ID in the container> <first ID on the host> <count>. Here IDs 0 to 999 go to 100000-100999, ID 1000 stays 1000 and IDs 1001 to 65535 go to 101001-165535: 1000 + 1 + 64535 makes 65536.

LXC starts the container as root and applies the mapping with newuidmap and newgidmap, which only accept the IDs delegated to the user in /etc/subuid and /etc/subgid. The default entry covers 100000-165535, so ID 1000 must be delegated to root as well.

On the host
echo 'root:1000:1' >> /etc/subuid
echo 'root:1000:1' >> /etc/subgid

The format is user:first-id:count. Without these entries the container does not start and the error mentions newuidmap or newgidmap; to diagnose, it helps to start it in the foreground with lxc-start -F -n 101. The mapping is read at every start, so the container must be restarted.

On the node
pct shutdown 101
pct start 101

Warning

Mapping an ID without translation reduces isolation: a process running as UID 1000 in the container acts on the host as UID 1000, with access to everything that user can read or write, not just the shared directory. It is better to map an ID reserved for the purpose, for example a host service account with no login shell, and not that of a real user. The host ID does not have to match the container one: just replace the second number in the u 1000 1000 1 and g 1000 1000 1 lines (and the entries in subuid and subgid) with the chosen ID, as long as it is outside the 100000-165535 range.

Important

In a container already in use, the files of its filesystem that belong to the remapped ID (for example the home of user 1000, stored on disk as 101000) show up as nobody after the change: Proxmox does not realign them. It is better to apply the mapping to a new container, or to fix ownership from the host with the container stopped.

Tip

If several containers only need to share a directory, it is often enough to map a GID and work with group permissions (directory with the setgid bit and g+rwX): UIDs stay shifted and no host user is exposed.

Note

The documentation for the most recent Proxmox VE versions (9.x series) also describes an idmap option for a single mount point, which avoids remapping the whole container. Check that the version you use supports it; lxc.idmap remains valid on Proxmox VE 8 and 9.

Verifying the mapping

On the node
pct exec 101 -- cat /proc/self/uid_map
Expected output (the second column shows the host IDs)
         0     100000       1000
      1000       1000          1
      1001     101001      64535

The definitive check is to create a file from the container and read its numeric owner on the host.

On the node
pct exec 101 -- su -s /bin/sh -c 'id && touch /mnt/dati/prova' utente-esempio
ls -ln /tank/dati/prova
Expected output
uid=1000(utente-esempio) gid=1000(utente-esempio) groups=1000(utente-esempio)
-rw-r--r-- 1 1000 1000 0 Apr  8 10:05 prova

With the custom mapping the UID is 1000 on both sides; with the default one it would be 101000 on the host. For a quick diagnosis:

  • Owner nobody in the container: the host ID is not within the mapped range.
  • Container that won't start after the change: missing entry in subuid or subgid, or ranges that don't add up to 65536.
  • Permission denied with the correct IDs: check the permissions of the parent directories; with bind mounts in unprivileged containers, ACLs are not usable.

Related articles