Proxmox VEX · LXC ContainersSecurity
Privileged vs unprivileged containers
What you'll learn
- Compare privileged and unprivileged containers in detail
- Choose the right mode for the workload
- Identify workloads that require privileged mode
- Recognise the security implications
- Audit a cluster for privileged containers and migrate one off privileged mode
Prerequisites
Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12
Why this matters in production
Choosing the wrong container mode exposes the host to unnecessary risk. The default exists for a reason.
Stated as plainly as it can be: a privileged container is closer to a process on the host than it is to a virtual machine. Its root user is the host’s root user. Files it creates are owned by host UID 0. If it can reach a kernel interface, it reaches it with the host’s authority. The namespaces still limit what it sees; they do not limit what it is.
An unprivileged container is a different proposition, and the difference is one piece of arithmetic.
Privileged vs unprivileged — the distinction
| Aspect | Privileged | Unprivileged |
|---|---|---|
| UID 0 in container | UID 0 on host | UID 100000+ on host |
| Can mount host filesystems | Yes | No |
| Can load kernel modules | Yes | No |
| Can access host devices | Yes (with care) | No |
| Use case | Special (Docker-in-LXC, legacy software) | Default for all workloads |
flowchart LR
subgraph UP[Unprivileged]
UC[Container root] -->|UID 100000+| UH[Host]
end
subgraph PR[Privileged]
PC[Container root] -->|UID 0| PH[Host]
end
Reading the mode off a running container
The config field records intent. uid_map records what the kernel did, and it
is the answer to trust when they might differ.
# pct exec 200 -- cat /proc/self/uid_map
pct exec 201 -- cat /proc/self/uid_map 0 100000 65536
0 0 4294967295Illustrative output
Container 200 maps its ID 0 to host 100000 for 65536 IDs: unprivileged. Container 201 identity-maps the entire ID space: privileged, and every file it writes lands on disk owned by real host UIDs.
When unprivileged is required
Use unprivileged (the default) for:
- Almost everything.
- Standard Linux services.
- Multi-tenant workloads.
- Any workload processing untrusted input.
When privileged is required
Some workloads cannot run in unprivileged mode:
- Docker-in-LXC: the Docker daemon requires certain capabilities that user namespaces strip.
- Software that hard-codes UID 0 ownership of files.
- Legacy kernel module loading inside the container.
- Custom iptables/netfilter rules that unprivileged mode blocks.
The unprivileged toggle is permanent
You cannot change unprivileged after creation. The only path is destroy + recreate.
flowchart LR
A[Unprivileged container] -->|destroy| B[Recreate as privileged]
A -->|destroy| C[Recreate as unprivileged]
A -. cannot toggle .-> A
Workarounds that avoid privileged mode
Most “needs privileged” claims are really “needs one specific thing that unprivileged blocks”, and there is a targeted flag for it.
| The claim | What is actually needed | The targeted answer |
|---|---|---|
| “Docker needs privileged” | procfs and sysfs for nested runtimes | features: nesting=1 |
| “It needs to mount an NFS share” | The mount syscall for one fs type | Mount on the host, bind it in |
| “It needs FUSE” | The FUSE device and syscalls | features: fuse=1 |
| “It needs to create device nodes” | mknod | features: mknod=1, or better, dev[n] |
| “It needs the GPU” | One device node plus a cgroup rule | dev0: /dev/dri/renderD128,... |
| “It needs to write to files owned by host UIDs” | An ID translation, not privilege | idmap= on the mount point |
| “It needs the kernel keyring” | keyctl syscalls | features: keyctl=1 |
set -euo pipefail
CTID=200
pct config "$CTID" | grep '^features:' || echo 'no features currently set'
pct set "$CTID" --features nesting=1,keyctl=1
pct reboot "$CTID"
pct config "$CTID" | grep -E '^(unprivileged|features):'The point of the table is not the flags. It is that “privileged” is never the requirement - it is a superset that happens to include the requirement, chosen because narrowing it takes five minutes of reading. Ask what specifically fails, and the answer is nearly always one row of that table.
Security implications of privileged mode
In privileged mode, the container can:
- Mount arbitrary filesystems (including the host root).
- Access host block devices.
- Communicate with most host kernel interfaces.
A kernel exploit (CVE-2022-0185 and similar) running as root in a privileged container typically grants host root.
Production considerations
Auditing an estate
The risk-register argument only works if you know the answer. On most clusters nobody does, because the field is not on any default column list.
set -euo pipefail
for CONF in /etc/pve/nodes/*/lxc/*.conf; do
CTID=$(basename "$CONF" .conf)
NODE=$(basename "$(dirname "$(dirname "$CONF")")")
NAME=$(grep -m1 '^hostname:' "$CONF" | awk '{print $2}')
if grep -q '^unprivileged: 1' "$CONF"; then
:
else
FEAT=$(grep -m1 '^features:' "$CONF" | cut -d' ' -f2- || true)
printf 'PRIVILEGED ct:%-6s node=%-10s host=%-20s features=%s\n' \
"$CTID" "$NODE" "${NAME:-?}" "${FEAT:-none}"
fi
doneRun it quarterly and put the output in the risk register verbatim. Two columns matter: the container, and whether anybody can still say why.
Common mistakes
- Defaulting to privileged because “the documentation said so.”
- Running Docker-in-LXC without enabling features first (privilege escalation).
- Believing unprivileged mode is “as secure as a VM.” It is not; it is more secure than privileged mode, but neither provides kernel isolation.
- Reaching for privileged mode to fix a permissions error that is really an ID mapping problem.
- Assuming a container is unprivileged because it was created recently. Read
uid_map, not the age of the config.
Key takeaways
- Default to unprivileged. The default exists for a reason.
- Privileged is for specific, documented cases (Docker-in-LXC, legacy).
- Use features (nesting, fuse, mknod) before resorting to privileged mode.
Knowledge check
Knowledge check · 5 questions
Q1. Which is the default container mode for new Proxmox containers?
Q2. Moving a container between privileged and unprivileged means destroying and recreating it.
Q3. Name one feature that must be enabled to run Docker inside an unprivileged container.
Q4. A service in an unprivileged container cannot write to a bind-mounted directory owned by host UID 1000. A colleague proposes recreating the container as privileged, which does fix it. What is the objection?
Q5. You are converting a privileged container to unprivileged by backing it up and restoring into a new ID. Which of these need explicit attention? Select all that apply.
Passing score: 75%. Answers are checked in this browser.