Skip to main content
RunBook Academy

Proxmox VEX · LXC ContainersSecurity

Privileged vs unprivileged containers

Intermediate⏱ ~22 minpct

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

Not yet marked complete on this device.

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

AspectPrivilegedUnprivileged
UID 0 in containerUID 0 on hostUID 100000+ on host
Can mount host filesystemsYesNo
Can load kernel modulesYesNo
Can access host devicesYes (with care)No
Use caseSpecial (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.

Read-only / Safetwo containers, two answers
# pct exec 200 -- cat /proc/self/uid_map
pct exec 201 -- cat /proc/self/uid_map
         0     100000      65536
       0          0 4294967295

Illustrative 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 claimWhat is actually neededThe targeted answer
“Docker needs privileged”procfs and sysfs for nested runtimesfeatures: nesting=1
“It needs to mount an NFS share”The mount syscall for one fs typeMount on the host, bind it in
“It needs FUSE”The FUSE device and syscallsfeatures: fuse=1
“It needs to create device nodes”mknodfeatures: mknod=1, or better, dev[n]
“It needs the GPU”One device node plus a cgroup ruledev0: /dev/dri/renderD128,...
“It needs to write to files owned by host UIDs”An ID translation, not privilegeidmap= on the mount point
“It needs the kernel keyring”keyctl syscallsfeatures: keyctl=1
Configuration changegrant the specific feature instead of full privilege
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.

Read-only / Safeevery privileged container on every node
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
done

Run 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

  1. Q1. Which is the default container mode for new Proxmox containers?

  2. Q2. Moving a container between privileged and unprivileged means destroying and recreating it.

  3. Q3. Name one feature that must be enabled to run Docker inside an unprivileged container.

  4. 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?

  5. 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.