Proxmox API Tokens for Secure Automation Without Root
Stop putting root credentials in Terraform and Ansible. Learn to create scoped Proxmox API tokens with least-privilege ACLs you can revoke in seconds.
On this page
If you are using root@pam credentials in your Terraform provider, Ansible inventory, or curl scripts against the Proxmox API, you have a ticking time bomb buried in your config files. Proxmox VE 7+ introduced scoped API tokens that let you grant exactly the permissions an automation tool needs — and revoke them instantly if a secret leaks. By the end of this guide you will have a dedicated service user, a scoped token with its own ACL assignments, and a tested workflow that keeps root credentials out of every script, pipeline, and .env file.
Key Takeaways
- Token format: Token IDs always follow
user@realm!token-name; the secret is shown once at creation and never again - Three CLI commands:
pveum user add,pveum acl modify, andpveum user token addis all it takes from the Proxmox shell - Privilege separation: Enable
--privsep 1so the token's ACLs are scoped independently from the user's — narrower blast radius on leak - PVE realm only: Use
automation@pveoverautomation@pamfor service accounts — no Linux shadow entry means no pivot path to shell - Revocation is instant:
pveum user token removekills the token immediately with no grace period and no other credentials affected
Why Root Credentials in Automation Are a Real Risk
Most quick-start tutorials reach for root@pam plus a password because it works immediately. Then that credential ends up in a Terraform tfvars file, a .env committed to a private repo, or an Ansible vault that three team members share. When you need to rotate the root password — or when someone leaves the team — you break every integration simultaneously.
The safer pattern is a dedicated service account with an API token scoped to exactly what the automation needs. If a token leaks, you run one command to revoke it. The root account and every other integration is untouched.
With Proxmox VE 9.1 running on most production and homelab nodes now, the per-token ACL system is mature and well-tested. This pairs naturally with the network and host-level controls covered in the Proxmox firewall, fail2ban, and SSH hardening guide — API token hygiene is the equivalent layer for the management plane.
Understanding the Proxmox Permission Model
Before creating tokens, understand the three-layer structure that controls what any principal can do:
- Users — authenticated identities such as
root@pamorautomation@pve - Roles — named permission bundles such as
PVEVMAdmin,PVEAuditor, orAdministrator - ACLs — bindings of
(path, principal, role)that express "this user has this role at this path"
Paths are hierarchical. / covers the entire cluster, /vms covers all VMs and containers, /vms/100 covers only VM 100, /nodes/pve covers only the node named pve, and /storage/local-lvm covers only that pool.
API tokens layer on top of users. By default a token inherits all of its owning user's ACLs. If you enable Privilege Separation, the token's effective permissions become the intersection of the user's ACLs and the token's own ACLs — so you can restrict a token below what the user has, but never grant it more.
| Privilege Separation | Token ACL | Effective Permission |
|---|---|---|
| Disabled | — | Identical to owning user |
| Enabled | PVEAuditor on / |
User's ACLs ∩ PVEAuditor on / |
| Enabled | PVEVMAdmin on /vms |
User's ACLs ∩ PVEVMAdmin on /vms |
| Enabled | No ACL assigned | Zero permissions |
The intersection model means you must assign ACLs to both the user and the token when privilege separation is on.
How to Create a Dedicated Service User
Create the account from the Proxmox shell. Using the pve realm keeps it internal to Proxmox — no PAM entry, no shadow file, no SSH login path:
# Create a local PVE user — no password needed, token-only auth
pveum user add automation@pve --comment "Terraform/Ansible service account"
For multi-system setups, create one user per automation system: terraform@pve, ansible@pve, monitoring@pve. That way revoking the Terraform token does not affect Ansible, and you can audit API activity per-system in the Proxmox task log.
Assign Roles with Least Privilege
Choose roles that match what the automation actually does — not the widest role that makes it work:
| Role | Grants |
|---|---|
PVEVMAdmin |
Full VM/container lifecycle: create, configure, start, stop, delete |
PVEVMUser |
Start, stop, and console only — no create or delete |
PVEDatastoreAdmin |
Manage storage, backups, snapshots, upload ISOs |
PVEDatastoreUser |
Allocate disk space for VM disks only |
PVEAuditor |
Read-only across all paths |
PVESysAdmin |
Node-level operations: certs, services, network config |
For a Terraform workflow that provisions and destroys VMs:
# VM lifecycle access across all VMs
pveum acl modify /vms --users automation@pve --roles PVEVMAdmin
# Disk allocation on the target storage pool
pveum acl modify /storage/local-lvm --users automation@pve --roles PVEDatastoreUser
# ISO and template upload if Terraform manages cloud-init images
pveum acl modify /storage/local --users automation@pve --roles PVEDatastoreAdmin
For an Ansible read-only audit pass before automating full VM provisioning playbooks with Ansible:
pveum acl modify / --users automation@pve --roles PVEAuditor
For a K3s cluster on Proxmox VMs where Terraform manages both nodes and their storage:
pveum acl modify /vms --users automation@pve --roles PVEVMAdmin
pveum acl modify /storage/local-lvm --users automation@pve --roles PVEDatastoreAdmin
Create the API Token
Create the token with privilege separation enabled:
pveum user token add automation@pve terraform --privsep 1 --comment "Terraform provider 2026-05"
Proxmox prints a table with the token ID and secret:
┌──────────────────┬──────────────────────────────────────┐
│ key │ value │
╞══════════════════╪══════════════════════════════════════╡
│ full-tokenid │ automation@pve!terraform │
│ info │ {"privsep":"1"} │
│ value │ a1b2c3d4-e5f6-7890-abcd-ef1234567890 │
└──────────────────┴──────────────────────────────────────┘
The value field is the token secret. It is displayed exactly once. Store it in your secrets manager — HashiCorp Vault, Bitwarden Secrets, a GitHub Actions secret, or a locally-encrypted .env file — before closing the terminal. If you lose it, delete the token and create a new one.
Now assign ACLs directly to the token. With privilege separation enabled, the token has zero permissions until you do this:
# Grant token access to VM management
pveum acl modify /vms \
--users automation@pve \
--tokens automation@pve!terraform \
--roles PVEVMAdmin
# Grant token access to the storage pool
pveum acl modify /storage/local-lvm \
--users automation@pve \
--tokens automation@pve!terraform \
--roles PVEDatastoreUser
You can scope the token more narrowly than the user. The user might have /vms admin access, but a specific token can be restricted to /vms/100 through individual VM ACL assignments — useful for a deployment token that should only ever touch its own VMs.
Test the Token Before Wiring It Into Anything
Verify the token works with a raw API call before touching Terraform or Ansible:
TOKEN_ID="automation@pve!terraform"
TOKEN_SECRET="a1b2c3d4-e5f6-7890-abcd-ef1234567890"
PROXMOX_HOST="192.168.1.10"
curl -s -k \
-H "Authorization: PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}" \
"https://${PROXMOX_HOST}:8006/api2/json/nodes" \
| python3 -m json.tool
A successful response returns a JSON array of cluster nodes. A 401 means the token ID or secret is wrong. A 403 means the token authenticated but lacks permissions at that path — recheck your pveum acl modify commands and confirm the ACL was applied to the token ID, not just the user.
To list all current ACLs for debugging:
pveum acl list
Using the Token in Terraform
The bpg/proxmox Terraform provider accepts API tokens natively. Configure your provider block:
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.66"
}
}
}
provider "proxmox" {
endpoint = "https://192.168.1.10:8006/"
api_token = var.proxmox_api_token
insecure = false
}
In terraform.tfvars — add this file to .gitignore before your first commit:
proxmox_api_token = "automation@pve!terraform=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Or export it as an environment variable so it never touches the filesystem at all:
export TF_VAR_proxmox_api_token="automation@pve!terraform=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Using the Token in Ansible
The community.general.proxmox_kvm module accepts token credentials directly:
- name: Provision a VM
community.general.proxmox_kvm:
api_host: 192.168.1.10
api_user: automation@pve
api_token_id: terraform
api_token_secret: "{{ proxmox_token_secret }}"
node: pve
name: my-vm
cores: 2
memory: 2048
state: present
Store proxmox_token_secret in an Ansible Vault file, not in a plaintext group_vars file. Use ansible-vault encrypt_string to inline-encrypt the value if you prefer a single-file setup over a separate vault.
Rotating and Revoking Tokens
List all tokens for a user:
pveum user token list automation@pve
Revoke a token immediately — effective in seconds, no grace period:
pveum user token remove automation@pve terraform
For a zero-downtime rotation, create the replacement first, then remove the old one:
# 1. Create replacement token
pveum user token add automation@pve terraform-v2 --privsep 1 --comment "Rotated 2026-05-05"
# 2. Assign ACLs to the new token
pveum acl modify /vms \
--users automation@pve \
--tokens automation@pve!terraform-v2 \
--roles PVEVMAdmin
pveum acl modify /storage/local-lvm \
--users automation@pve \
--tokens automation@pve!terraform-v2 \
--roles PVEDatastoreUser
# 3. Update secrets manager, test new token, update all consumers
# 4. Remove old token
pveum user token remove automation@pve terraform
The gotcha here: token ACLs are attached to the token ID, not the user. When you delete and recreate a token — even with the identical name — all ACLs are gone and must be reassigned from scratch. Keep a documented list of which ACLs each token needs, or script the reassignment as part of your rotation runbook so you do not rediscover this at 2am.
Why the PVE Realm Is Safer for Service Accounts
A automation@pve user has no Linux system account. There is no /etc/passwd entry, no shadow file entry, and no ability to SSH into the Proxmox host using those credentials directly. If the API token is compromised, the attacker has API access scoped to the token's ACLs — they cannot pivot to an interactive shell on the hypervisor.
A automation@pam user maps to a real Linux account that does have a shadow entry. Depending on your SSH configuration, that user may be able to authenticate to the host directly. The LOLPROX analysis of Proxmox hypervisor exploit paths makes this point clearly: reducing the identity attack surface at the API layer is one of the highest-leverage hardening steps available.
Use the PVE realm for every service account. Reserve PAM accounts for humans who need both shell access and web UI access.
Conclusion
Replacing root credentials with scoped API tokens is a 20-minute change that pays back every time you rotate a secret, offboard a team member, or recover from a credential leak without touching your root account. Create automation@pve, assign narrowly-scoped ACLs, enable privilege separation on the token, store the secret in a proper secrets manager, and document the ACL assignments for your rotation runbook. The natural next step is putting this token to work in a structured Ansible workflow — the Ansible VM automation guide walks through a production-ready inventory approach that pairs directly with what you set up here.