Skip to main content
RunBook Academy

Proxmox VEXV · Security & HardeningAccess control

The ACL model: paths, propagation and custom roles

Advanced⏱ ~30 minpveum

What you'll learn

  • Read an ACL as the four-part tuple Proxmox stores: path, principal, role, propagate
  • Predict which permission wins when several ACLs match the same object
  • Build a least-privilege custom role with pveum role add and assign it at the narrowest path that works
  • Audit effective permissions for a user, a group and a token rather than inferring them from the ACL list

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.

Every authorisation decision Proxmox makes reduces to one question: does this principal hold a role containing the required privilege, on a path that covers the object being touched?

Everything else — realms, groups, tokens, two-factor — is about establishing who the principal is. This lesson is about what happens after that, and it is the half operators get wrong, because the failure mode is silent. Over-granting produces no error, no log line and no ticket. It produces an account that works, which is exactly what the person who asked for it wanted.

The four fields

An access control entry is a tuple of four things, and Proxmox stores it that way:

FieldWhat it isExample
pathThe object or subtree the entry applies to/vms/100, /storage/pbs-main
principalA user, a group, or an API tokenalice@corp, @backup-ops, svc@pve!nightly
roleA named set of privilegesPVEVMUser, BackupRunner
propagateWhether the entry covers sub-paths1 (default) or 0

pveum acl modify writes one. The path is positional; the rest are options:

Configuration changegrant one user one role on one VM
pveum acl modify /vms/100 --users alice@corp --roles PVEVMUser

The same command takes --groups, --tokens and --propagate. A single invocation can name several of each, and each combination becomes its own stored entry.

The path tree

Paths look like a filesystem and behave like one. The complete set of top-level paths in PVE 9.2:

PathCovers
/Everything. The root of the tree.
/accessThe access-control subsystem itself
/access/groupsGroup administration
/access/realms/{realmid}One authentication realm
/access/users/{userid}One user object
/nodesAll nodes
/nodes/{node}One node: its shell, its services, its firewall
/vmsAll guests, VMs and containers alike
/vms/{vmid}One guest
/storageAll storage definitions
/storage/{storeid}One storage
/poolAll resource pools
/pool/{poolname}One pool, and everything in it
/sdnThe SDN subsystem
/sdn/zones/{zone}One SDN zone
/sdn/vnets/{vnet}One VNet

Two of these are worth pausing on.

/pool/{poolname} is the only path that is indirect. A pool is a membership list of VMs and storages, so an ACL on the pool path reaches objects that are not underneath it in the string sense. This is the mechanism the Proxmox documentation recommends for delegated administration: "Pools can be used to group a set of virtual machines and datastores. You can then simply set permissions on pools (/pool/{poolid}), which are inherited by all pool members." Add a VM to the pool and it inherits the pool ACLs; remove it and it stops.

/nodes/{node} is the path that grants shell access. Sys.Console on a node path is root on that node, because the console it opens is a root shell. There is no non-root node console in PVE. Any role containing Sys.Console assigned at /nodes or / is an administrator role regardless of what else it contains, and regardless of what the role is called.

Which entry wins

Several entries can match one object at once: a role at /, another at /vms, a group membership, a pool. Proxmox resolves them with four rules, quoted from the documentation:

Permissions for individual users always replace group permissions. Permissions for groups apply when the user is member of that group. Permissions on deeper levels replace those inherited from an upper level. NoAccess cancels all other roles on a given path.

Replace, not merge. This is the rule that surprises people, and it cuts both ways. A user with PVEAuditor at / and PVEVMUser at /vms/100 does not have “auditor everywhere plus VM user on 100”. On /vms/100 they have PVEVMUser and only PVEVMUser — the deeper entry replaced the inherited one. If PVEVMUser lacks a privilege that PVEAuditor granted, it is gone on that object.

The practical consequence: a narrow grant on a specific object can remove access the person had a moment ago. Operators tend to reason additively, assign a scoped role to fix one gap, and create a second gap they do not notice until someone reports that a page has stopped loading.

NoAccess is the deny primitive. It is a built-in role containing no privileges, and the rule above makes it absolute on the path where it is set. It is how you carve an exception out of a broad grant: PVEAdmin propagating from /, NoAccess on /vms/900 for the guest nobody should touch.

Configuration changea broad grant with a carved-out exception
pveum acl modify /vms --groups ops --roles PVEVMAdmin
pveum acl modify /vms/900 --groups ops --roles NoAccess

--propagate 0 scopes an entry to exactly one object. The default is 1. A non-propagating entry on /vms applies to the collection endpoint and not to any individual guest, which is almost never what someone means when they set it. Use it deliberately or leave it alone.

Roles are just named privilege sets

There are seventeen built-in roles in PVE 9.2. The ones worth memorising:

RoleGrantsReach for it when
AdministratorEvery privilegeNever, on a path other than the one root already covers
NoAccessNothing, and cancels everything else on the pathCarving an exception out of a broad grant
PVEAuditorRead-only across the treeMonitoring, inventory, a new joiner
PVEVMUserConsole, power, backup on a guestA tenant who operates a guest but does not configure it
PVEVMAdminFull guest configurationA team that owns a set of guests
PVEDatastoreUserAllocate space, audit a storageAnything that needs to write backups
PVEPoolUserAudit a poolRarely alone; pair it with a guest role
PVESysAdminNode-level: ACLs, audit, console, syslogNode operators — note the Sys.Console
PVEUserAdminCreate and modify usersAn identity team that should not touch guests

The rest — PVETemplateUser, PVEDatastoreAdmin, PVEPoolAdmin, PVESDNAdmin, PVESDNUser, PVEMappingAdmin, PVEMappingUser, PVEAdmin — are narrower slices of the same privilege list.

Privileges themselves are grouped by what they act on. The full set in PVE 9.2:

Node and system: Group.Allocate, Mapping.Audit, Mapping.Modify, Mapping.Use, Permissions.Modify, Pool.Allocate, Pool.Audit, Realm.AllocateUser, Realm.Allocate, SDN.Allocate, SDN.Audit, Sys.Audit, Sys.Console, Sys.Incoming, Sys.Modify, Sys.PowerMgmt, Sys.Syslog, User.Modify.

Guest: SDN.Use, VM.Allocate, VM.Audit, VM.Backup, VM.Clone, VM.Config.CDROM, VM.Config.CPU, VM.Config.Cloudinit, VM.Config.Disk, VM.Config.HWType, VM.Config.Memory, VM.Config.Network, VM.Config.Options, VM.Console, VM.GuestAgent.Audit, VM.GuestAgent.FileRead, VM.GuestAgent.FileSystemMgmt, VM.GuestAgent.FileWrite, VM.GuestAgent.Unrestricted, VM.Migrate, VM.PowerMgmt, VM.Replicate, VM.Snapshot, VM.Snapshot.Rollback.

Storage: Datastore.Allocate, Datastore.AllocateSpace, Datastore.AllocateTemplate, Datastore.Audit.

The guest-agent privileges are split five ways for a reason. VM.GuestAgent.FileRead reads any file inside the guest as the agent user, which on a Linux guest is root. VM.GuestAgent.Unrestricted permits arbitrary command execution in the guest. Neither is implied by VM.Console, and a monitoring role that only needs to ask the agent for the guest IP wants VM.GuestAgent.Audit and nothing else.

Building a custom role

The workflow is: name the job, list the API calls it makes, map those to privileges, create the role, assign it at the narrowest path that covers the job.

Take a monitoring collector that needs to read cluster and guest status and nothing else.

Configuration changea read-only monitoring role
pveum role add MonitoringCollector \
--privs 'Sys.Audit VM.Audit Datastore.Audit VM.GuestAgent.Audit'

pveum acl modify / --tokens 'monitor@pve!prometheus' \
--roles MonitoringCollector --propagate 1

Now a backup operator who runs vzdump jobs against a pool and needs somewhere to write them.

Configuration changea scoped backup role
pveum role add BackupRunner --privs 'VM.Audit VM.Backup VM.Snapshot'
pveum role add BackupTarget --privs 'Datastore.Audit Datastore.AllocateSpace'

pveum acl modify /pool/production --tokens 'svc-backup@pve!nightly' \
--roles BackupRunner
pveum acl modify /storage/pbs-main --tokens 'svc-backup@pve!nightly' \
--roles BackupTarget

Splitting into two roles is not ceremony. BackupRunner on the pool and BackupTarget on one storage means the token cannot write to any other storage, and cannot back up a guest that is not in the pool. One role containing all five privileges, assigned at /, would do the same job and also permit writing to every datastore in the cluster.

VM.Snapshot is in the list because vzdump in snapshot mode takes one. A job configured for stop mode does not need it, and leaving it out is a real reduction — VM.Snapshot combined with VM.Snapshot.Rollback is a data-destroying pair, and the second is correctly absent here.

Auditing what you actually granted

The ACL list tells you what was written. It does not tell you what a principal can do, because of inheritance, group membership, replacement and token intersection. Ask Proxmox directly.

Read-only / Safeeffective permissions for a user
$ pveum user permissions alice@corp --path /vms/100
┌──────────────────┬──────┐
│ privilege        │ prop │
╞══════════════════╪══════╡
│ VM.Audit         │ 1    │
├──────────────────┼──────┤
│ VM.Backup        │ 1    │
├──────────────────┼──────┤
│ VM.Console       │ 1    │
├──────────────────┼──────┤
│ VM.PowerMgmt     │ 1    │
└──────────────────┴──────┘

Illustrative output

Omit --path and it returns the whole resolved tree, which is the right call during an access review. For tokens there is a separate subcommand, and it is the one that matters most:

Read-only / Safeeffective permissions for an API token
pveum user token permissions svc-backup@pve nightly

Reading the raw entries is still useful for spotting an ACL nobody remembers adding:

Read-only / Safethe stored entries
pveum acl list
pveum user list
pveum role list

Common mistakes

  • Reasoning additively. Deeper entries replace inherited ones. A scoped grant can remove access, and the symptom appears on an unrelated page.
  • Auditing the ACL list instead of effective permissions. The list is the input. pveum user permissions and pveum user token permissions are the output, and for a privilege-separated token they are the only place the intersection is visible.
  • Custom roles that contain Sys.Console. The role is an administrator role. Name it accordingly or remove the privilege.
  • Granting at / because the narrower path did not work. It usually did not work because the job touches two subtrees. Write two entries.
  • Using Administrator as a shortcut during an incident and leaving it. Every incident-time grant needs an expiry in the same ticket that created it.

Key takeaways

  • An ACL is path, principal, role, propagate. Everything else resolves into those four fields.
  • Deeper entries replace inherited ones; NoAccess cancels everything on its path. Neither behaviour is additive.
  • Sys.Console, Permissions.Modify, and VM.Config.Disk with Datastore.AllocateSpace are administrator-equivalent whatever the role is called.
  • Build roles from the API calls a job makes, then assign at the narrowest path that covers the job — which is often two paths, not one.
  • Verify with pveum user permissions and pveum user token permissions. The ACL list is not the answer.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A user holds PVEAuditor at / with propagation, and PVEVMUser at /vms/100. What are their effective permissions on VM 100?

  2. Q2. Which of these make a custom role administrator-equivalent regardless of what the role is named? Select all that apply.

  3. Q3. A privilege-separated API token can hold permissions on a path that its owning user does not have, provided the token is given an explicit ACL there.

  4. Q4. A backup token needs to run vzdump against guests in the production pool and write to the pbs-main datastore. What is the correct scoping?

Passing score: 75%. Answers are checked in this browser.