Security & Multi-Tenancy: Roles, Pools, API Tokens, and Isolation

A single admin with root access works for a homelab. It doesn’t work when multiple people or teams share the same Proxmox cluster. Who can see what? Who can modify what? What happens when someone leaves?

Access control isn’t a feature you enable. It’s a product you design. Every permission is a decision: who needs this access, why, and what’s the blast radius if it’s misused?

Proxmox has robust RBAC (Role-Based Access Control). The question is whether you use it intentionally or let it grow organically into chaos.

Access Control Model

Proxmox permissions combine:

Permission = User/Group + Role + Path
Example:
- User: developer@pve
- Role: PVEVMUser
- Path: /pool/dev-team
Result: developer can use VMs in dev-team pool

Users and Groups

Users: Individual accounts. Can be in multiple groups.

Terminal window
# Create user in Proxmox realm
pveum user add developer@pve --password <password>
# Create user in PAM realm (Linux user)
pveum user add admin@pam
# List users
pveum user list

Groups: Collections of users. Simplify permission management.

Terminal window
# Create group
pveum group add developers --comment "Development team"
# Add user to group
pveum user modify developer@pve --groups developers
# List groups
pveum group list

Authentication Realms

RealmUse CaseNotes
pamLinux admins who need SSHSystem users
pveWeb UI only usersProxmox internal
ldapEnterprise integrationExternal directory
adActive DirectoryWindows integration

For multi-tenancy, usually:

  • Admins: PAM (SSH + Web UI)
  • Regular users: PVE realm (Web UI only)

Built-in Roles

Proxmox includes these roles:

RolePermissions
AdministratorEverything (dangerous)
PVEAdminAlmost everything (no system access)
PVEAuditorRead-only access
PVEDatastoreAdminManage datastores
PVEDatastoreUserUse datastores
PVEPoolAdminManage pools
PVEPoolUserUse pools
PVEVMAdminFull VM control
PVEVMUserUse VMs (console, start/stop)
PVETemplateUserClone templates
PVEUserAdminManage users
NoAccessExplicit deny

Custom Roles

Create roles for specific needs:

Terminal window
# Create role with specific privileges
pveum role add VMOperator --privs "VM.Console VM.PowerMgmt VM.Monitor"
# List available privileges
pveum privilege list
# View role
pveum role list

Common custom roles:

Terminal window
# Developer: Can create/manage own VMs
pveum role add Developer --privs "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.Memory VM.Config.Network VM.Console VM.Migrate VM.Monitor VM.PowerMgmt VM.Snapshot VM.Snapshot.Rollback Datastore.AllocateSpace"
# Observer: Can view, nothing else
pveum role add Observer --privs "VM.Audit Datastore.Audit"
# Backup Operator: Can backup/restore
pveum role add BackupOperator --privs "VM.Backup VM.Snapshot Datastore.AllocateSpace"

Resource Pools

Pools group resources (VMs, storage, nodes) for delegation:

Terminal window
# Create pool
pveum pool add dev-team --comment "Development team resources"
# Add VM to pool
qm set 100 --pool dev-team
# Add storage to pool
pveum pool modify dev-team --storage local-lvm
# List pools
pveum pool list

Pool-Based Permissions

Grant access to pool, not individual VMs:

Terminal window
# Developers can manage VMs in their pool
pveum acl modify /pool/dev-team --users developer@pve --roles Developer
# Or by group
pveum acl modify /pool/dev-team --groups developers --roles Developer

New VMs in the pool automatically inherit permissions.

Pool Strategy

Organize by:

Option 1: By team
/pool/dev-team
/pool/qa-team
/pool/production
Option 2: By environment
/pool/development
/pool/staging
/pool/production
Option 3: By project
/pool/project-alpha
/pool/project-beta

Match your organization structure.

API Tokens

API tokens are better than passwords for automation:

  • Separate from user password
  • Can have different permissions
  • Easily revoked without changing user password
  • Audit trail shows token ID

Creating Tokens

Terminal window
# Create token for user
pveum user token add developer@pve automation --privsep 0
# Output shows token secret (save it!)
# Token: developer@pve!automation
# Secret: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# With privilege separation (token can have fewer privs than user)
pveum user token add admin@pam ci-cd --privsep 1
pveum acl modify /pool/production --tokens admin@pam!ci-cd --roles BackupOperator

Token Best Practices

# Good: Specific tokens for specific purposes
admin@pam!terraform # Infrastructure automation
admin@pam!ansible # Configuration management
admin@pam!monitoring # Read-only metrics
developer@pve!ci-build # CI pipeline builds
# Bad: Generic tokens with admin access
admin@pam!api # Too broad, no purpose documented

Using Tokens in Automation

Terminal window
# API call with token
curl -k -H "Authorization: PVEAPIToken=developer@pve!automation=xxxx-xxxx-xxxx" \
https://proxmox:8006/api2/json/version
# Terraform provider
provider "proxmox" {
pm_api_url = "https://proxmox:8006/api2/json"
pm_api_token_id = "terraform@pve!automation"
pm_api_token_secret = var.proxmox_token
}
# Ansible
proxmox_kvm:
api_host: proxmox
api_user: ansible@pve
api_token_id: automation
api_token_secret: "{{ vault_proxmox_token }}"

Permission Paths

Permissions apply to paths in the resource tree:

/ # Root (everything)
├── /access # User/group management
├── /nodes # All nodes
│ ├── /nodes/pve1 # Specific node
├── /pool # All pools
│ └── /pool/dev-team # Specific pool
├── /storage # All storage
│ └── /storage/local # Specific storage
└── /vms # All VMs (by ID)
└── /vms/100 # Specific VM

Permission Inheritance

Permissions cascade down:

Terminal window
# Grant access to all VMs in a pool
pveum acl modify /pool/dev-team --users developer@pve --roles PVEVMUser
# Developer can now access:
# - /pool/dev-team (pool itself)
# - All VMs in that pool
# - Storage assigned to that pool

Explicit Deny

NoAccess role blocks inheritance:

Terminal window
# User has pool access
pveum acl modify /pool/dev-team --users developer@pve --roles Developer
# But NOT this specific VM
pveum acl modify /vms/105 --users developer@pve --roles NoAccess

Multi-Tenant Architecture

Example: Web Hosting Provider

Tenants: customer-a, customer-b, customer-c
Structure:
├── /pool/customer-a
│ ├── VMs 100-199
│ └── Storage quota
├── /pool/customer-b
│ ├── VMs 200-299
│ └── Storage quota
└── /pool/customer-c
├── VMs 300-399
└── Storage quota
Users:
- customer-a-admin@pve → /pool/customer-a (PVEVMAdmin)
- customer-a-user@pve → /pool/customer-a (PVEVMUser)
- customer-b-admin@pve → /pool/customer-b (PVEVMAdmin)
...
Isolation:
- Network: Separate VLANs per customer
- Storage: Pool quotas, separate datastores
- Compute: Resource limits on pools

Example: Corporate IT

Departments: dev, qa, production, infrastructure
Structure:
├── /pool/development
│ └── All non-prod VMs
├── /pool/qa
│ └── Test environments
├── /pool/production
│ └── Production workloads (restricted)
└── /pool/infrastructure
└── DNS, monitoring, etc.
Groups and roles:
- developers → /pool/development (Developer)
- qa-engineers → /pool/qa (Developer)
- sre-team → /pool/production (PVEVMUser)
- sre-leads → /pool/production (PVEVMAdmin)
- infra-admins → / (PVEAdmin)

Audit Logging

Track who did what:

Task History

Terminal window
# Recent tasks (via API)
pvesh get /cluster/tasks
# Node-specific tasks
pvesh get /nodes/pve1/tasks

Web UI: Datacenter → Tasks → Filter by user

System Logs

Terminal window
# Auth logs
journalctl -u pveproxy | grep auth
# API access logs
tail -f /var/log/pveproxy/access.log

External Audit

For compliance, forward logs:

Terminal window
# Syslog forwarding
echo "*.* @syslog-server:514" >> /etc/rsyslog.d/remote.conf
systemctl restart rsyslog

Quotas and Limits

Prevent resource exhaustion:

Pool Quotas

Not built-in, but enforceable via custom roles and monitoring:

Terminal window
# Create role without VM.Allocate
pveum role add PoolUser --privs "VM.Console VM.PowerMgmt VM.Monitor"
# Users can use VMs but not create new ones
# Admins create VMs, assign to pool

VM Resource Limits

Terminal window
# Limit CPU
qm set 100 --cpulimit 2 # Max 2 cores worth
# Limit memory
qm set 100 --memory 4096 --balloon 2048
# Limit disk I/O
qm set 100 --bwlimit "backup=10240,restore=10240"

Storage Quotas

Ceph/ZFS can enforce quotas:

Terminal window
# ZFS quota
zfs set quota=100G rpool/data/customer-a
# Ceph quota
ceph osd pool set-quota customer-pool max_bytes 107374182400

Security Checklist

User Management:
[ ] No shared accounts
[ ] Each person has individual user
[ ] Users in appropriate groups
[ ] Unused users disabled/deleted
Roles:
[ ] Custom roles for common use cases
[ ] No one uses Administrator role directly
[ ] Principle of least privilege applied
Pools:
[ ] Resources organized into pools
[ ] Permissions at pool level (not individual VMs)
[ ] Clear ownership per pool
API Tokens:
[ ] Automation uses tokens, not passwords
[ ] Tokens have specific purposes
[ ] Tokens documented
[ ] Unused tokens revoked
Audit:
[ ] Logs retained appropriately
[ ] Regular review of access
[ ] Alerts on sensitive operations

Token Rotation

Regular token rotation:

Terminal window
# Create new token
pveum user token add admin@pam ansible-v2 --privsep 0
# Update automation to use new token
# Verify new token works
# Remove old token
pveum user token remove admin@pam ansible-v1

Schedule this quarterly or when personnel changes.

The Lesson

Access control is a product. It needs to be designed.

The lazy approach:

  • Everyone is admin
  • One shared account
  • Permissions “we’ll figure out later”

The result:

  • No audit trail
  • Blast radius is entire cluster
  • Personnel change = security nightmare

The designed approach:

  • Users in groups
  • Groups have roles
  • Roles are minimal
  • Resources in pools
  • Permissions at pool level
  • Automation uses tokens
  • Regular access reviews

Access control isn’t overhead — it’s what makes multi-tenancy possible. Design it upfront, enforce it consistently, and review it regularly.