If you've ever automated anything against Proxmox VE — a Terraform plan, an Ansible playbook, a Python script that spins up a lab VM every night — you've probably started by hardcoding your root@pam password into a config file somewhere. It works, right up until you rotate that password, enable two-factor authentication, or hand the script to a teammate, and everything breaks at once. API tokens exist specifically to get you out of that trap: they're separate credentials, scoped to exactly the permissions you choose, that you can revoke individually without touching the human account they're attached to.
This guide walks through creating and using API tokens in Proxmox VE 9.x, from the two-minute GUI version to the pveum command-line workflow you'll actually want for repeatable automation. You'll learn how privilege separation works (and why it trips people up), how to scope a token down to just the resources it needs, and how to wire the token into curl, Ansible, and Terraform. We'll close with the security practices that keep an automation token from becoming the easiest way into your cluster.
What You Will Learn
- What an API token actually is in Proxmox VE, and how it differs from a regular user login
- How privilege separation (
--privsep) changes what a token can do - Creating a token through the web UI and through
pveumon the command line - Scoping a token's permissions with roles and ACLs instead of granting it full access
- Calling the REST API directly with curl using the
PVEAPITokenheader - Using a token with Ansible and with Terraform/OpenTofu providers
- Rotating, expiring, and revoking tokens safely
- Common authentication errors and what actually causes them
Prerequisites
- A working Proxmox VE 9.x host with web UI or SSH access as a user who can manage permissions (typically
root@pamor an account with theAdministratorrole) - Basic comfort with the command line — we'll use
pveum, Proxmox's user/permission management CLI curlinstalled on whatever machine you're testing from (it ships by default on almost every Linux distro and macOS)- Optional: Ansible or Terraform/OpenTofu installed if you want to follow those sections
Why Not Just Use a Username and Password?
You technically can authenticate to the Proxmox REST API with a username, password, and a ticket/CSRF token pair — that's how the web UI itself logs in. But that approach has three problems for automation specifically:
- Tickets expire fast. Web UI login tickets are valid for about two hours, so any long-running automation has to re-authenticate constantly, which means storing the password somewhere it can be reused.
- It doesn't play well with 2FA. If the user account has two-factor authentication enabled (and it should, for anything with real privileges), password-based API login either fails outright or needs awkward workarounds.
- You can't scope it down. A password grants everything that account can do, with no way to say "this specific script can only manage VMs in this specific resource pool."
API tokens solve all three: they don't expire on a two-hour cycle (though you can set a hard expiration date), they're unaffected by whether the parent user has 2FA turned on, and — this is the important part — they support their own, separate ACL entries that get intersected with the user's permissions.
Understanding Privilege Separation
Every API token in Proxmox VE belongs to a user, and every token has a single setting that determines how much of that user's access it inherits: privilege separation, or --privsep.
--privsep 1(the default): The token starts with zero permissions of its own. Proxmox calculates what the token can actually do by intersecting the user's permissions with the token's own ACL entries. In practice this means you create the token, then explicitly grant it access to specific paths and roles — nothing works until you do.--privsep 0: The token inherits the full permission set of its parent user, with no separate ACLs needed. This is simpler to set up but means a leaked token is just as dangerous as a leaked password for that account.
For anything beyond a quick throwaway test, leave privilege separation on (the default) and grant the token only the roles it needs on the specific paths it needs them on. If the automation only manages VMs in one resource pool, the token should not be able to touch storage configuration, other pools, or user management — even though the parent user might be able to.
Step 1: Decide Who the Token Belongs To
Resist the temptation to create every automation token under root@pam. Root already has unrestricted access, which means a root-owned token with privilege separation disabled is a master key to the entire cluster. Instead, create a dedicated user for each automation purpose:
pveum user add automation@pve --comment "Service account for CI/CD pipeline"
Using the pve realm (rather than pam, which maps to real Linux system accounts) keeps automation accounts entirely inside Proxmox's own user database, separate from anything on the underlying OS.
Step 2: Create a Role Scoped to What the Automation Needs
Proxmox VE ships with several built-in roles — PVEAdmin, PVEVMAdmin, PVEVMUser, PVEAuditor, PVEDatastoreAdmin, and others — that bundle common sets of privileges. Check what's available and what each one grants:
pveum role list
If a built-in role is close enough, use it. If you want tighter control — say, a token that can start, stop, and snapshot VMs but never delete them or touch storage — define a custom role from individual privileges:
pveum role add VMOperator --privs "VM.Audit,VM.PowerMgmt,VM.Snapshot,VM.Monitor"
You can list every available privilege string with pveum role add --help or by checking the Permissions section of the admin guide; common ones include VM.Allocate, VM.Clone, VM.Config.Disk, VM.Console, Datastore.AllocateSpace, and Sys.Modify.
Step 3: Create the API Token
Using the Web UI
- Log in to the Proxmox VE web interface and go to Datacenter → Permissions → API Tokens.
- Click Add.
- Select the user (
automation@pve), give the token an ID such asci-runner, and optionally set an expiration date. - Leave Privilege Separation checked unless you have a specific reason to disable it.
- Click Add. Proxmox will display the token's secret value exactly once — copy it immediately into a password manager or secrets store. There is no way to retrieve it again later; if you lose it, you have to regenerate the token and update every place that uses it.
Using the Command Line
The CLI version is faster to script and easier to reproduce across environments:
pveum user token add automation@pve ci-runner --comment "CI/CD pipeline token" --expire 1798761600
The --expire value is a Unix epoch timestamp, not a date string — convert with date -d '2027-01-01' +%s if you're working from a calendar date. Omit --expire (or pass 0) for a token that never expires, though for anything long-lived you'll usually want to set one and rotate on a schedule instead.
The command's output includes a full-tokenid and a value field. The value is the secret — again, shown only this once:
┌──────────────┬──────────────────────────────────────┐
│ key │ value │
├──────────────┼──────────────────────────────────────┤
│ full-tokenid │ automation@pve!ci-runner │
│ info │ {"privsep":1} │
│ value │ 1a2b3c4d-5e6f-7890-abcd-ef1234567890 │
└──────────────┴──────────────────────────────────────┘
Step 4: Grant the Token Its Own Permissions
With privilege separation on, the token above can currently do nothing — it needs its own ACL entry. Grant the custom role on the path the automation should manage, in this case a specific resource pool:
pveum acl modify /pool/ci-lab --tokens 'automation@pve!ci-runner' --roles VMOperator
Note the path: ACLs in Proxmox apply to a hierarchy (/, /nodes/<node>, /vms/<vmid>, /pool/<poolid>, /storage/<storageid>, and so on), and permissions propagate downward by default. Scoping to a pool means the token can manage every VM assigned to that pool without needing a separate ACL entry per VM. If you only want the token to touch one specific VM, target /vms/<vmid> instead and set --propagate 0 if you don't want it to affect anything nested underneath.
You can confirm what a token can actually do at any time with:
pveum acl list
Step 5: Call the API With curl
The Authorization header format for token auth is PVEAPIToken=USER@REALM!TOKENID=SECRET — all as one value, no spaces around the equals signs:
curl -s -k \
-H "Authorization: PVEAPIToken=automation@pve!ci-runner=1a2b3c4d-5e6f-7890-abcd-ef1234567890" \
https://pve.example.com:8006/api2/json/nodes
The -k flag skips TLS certificate verification, which is fine for a quick test against a self-signed cert but should be replaced with a real certificate check (or your internal CA's bundle) in anything that runs unattended. See the Let's Encrypt for the Proxmox web interface guide if you haven't set up trusted certificates yet.
A few more examples using the same token:
# List VMs on a node
curl -s -k -H "Authorization: PVEAPIToken=automation@pve!ci-runner=1a2b3c4d-..." \
https://pve.example.com:8006/api2/json/nodes/pve1/qemu
# Start a VM
curl -s -k -X POST \
-H "Authorization: PVEAPIToken=automation@pve!ci-runner=1a2b3c4d-..." \
https://pve.example.com:8006/api2/json/nodes/pve1/qemu/101/status/start
# Create a snapshot
curl -s -k -X POST \
-H "Authorization: PVEAPIToken=automation@pve!ci-runner=1a2b3c4d-..." \
-d "snapname=pre-update" \
https://pve.example.com:8006/api2/json/nodes/pve1/qemu/101/snapshot
Notice there's no separate CSRF token needed for API-token authentication — that requirement only applies to ticket-based (username/password) sessions. This is one of the reasons token auth is simpler to script than replicating what the web UI does under the hood.
If you want to explore what endpoints exist before scripting against them, the built-in API viewer at https://your-host:8006/pve-docs/api-viewer/ documents every route, parameter, and required permission interactively.
Using the Token With Ansible
The community.general collection's Proxmox modules (and the newer community.proxmox collection) accept token authentication directly, which avoids storing a password in your inventory or vault at all:
- name: Create a VM from template
community.general.proxmox_kvm:
api_host: pve.example.com
api_user: automation@pve
api_token_id: ci-runner
api_token_secret: "{{ vault_proxmox_token_secret }}"
node: pve1
clone: ubuntu-2404-template
name: web-01
full: true
state: present
Keep the secret itself out of plain text — pull it from Ansible Vault, a HashiCorp Vault lookup, or your CI system's secret store, and reference it as a variable like the example above.
Using the Token With Terraform / OpenTofu
Both major community providers for Proxmox (bpg/proxmox and Telmate/proxmox) support API token authentication in their provider block:
provider "proxmox" {
endpoint = "https://pve.example.com:8006/"
api_token = "automation@pve!ci-runner=1a2b3c4d-5e6f-7890-abcd-ef1234567890"
insecure = false
}
As with any Terraform credential, avoid committing the token secret to version control — pass it via an environment variable (most providers read something like PROXMOX_VE_API_TOKEN) or a Terraform Cloud/OpenTofu variable set instead of hardcoding it in a .tf file.
Rotating and Revoking Tokens
Because a token is a separate credential from the user's password, you can kill one without disabling the account it belongs to — useful if a CI secret leaks in a build log or a laptop with a cached token gets lost.
To regenerate a token's secret while keeping the same token ID (and its existing ACL entries):
pveum user token modify automation@pve ci-runner --regenerate 1
To remove a token entirely:
pveum user token remove automation@pve ci-runner
To see every token that exists for a given user, including expiration dates, before deciding what to clean up:
pveum user token list automation@pve
A reasonable habit for anything with real access is to set an expiration date when you create the token, put a calendar reminder to rotate it before that date, and treat "token about to expire and nobody renewed it" as the automation quietly turning itself off rather than a surprise outage — which is exactly the point of setting an expiration in the first place.
Troubleshooting Common Errors
401 Unauthorized
Usually a malformed Authorization header. Double-check the format: PVEAPIToken=USER@REALM!TOKENID=SECRET, with no space after PVEAPIToken= and no quotes embedded in the header value itself. A common mistake is copying the token ID and secret with a stray newline from a terminal wrap.
403 Forbidden / Permission Check Failed
This almost always means privilege separation is on (correctly) but no ACL has been granted to the token yet, or the ACL was granted on the wrong path. Run pveum acl list and confirm there's an entry matching the token's full ID (user@realm!tokenid) on a path that covers the resource you're calling. Remember that the token's effective permission is the intersection of the user's permission and the token's permission — if the parent user's own access was later reduced, the token's ceiling drops too, even if the token's ACL entry hasn't changed.
Token Works for Reads but Not Writes
Check the specific privilege, not just the role name. A role like PVEAuditor is intentionally read-only; if the automation needs to change VM state, it needs a role that includes privileges like VM.PowerMgmt or VM.Config.*, not just VM.Audit.
"Invalid Parameter" on --expire
The value has to be a Unix timestamp, not a human-readable date. Generate it with date -d '2027-06-01T00:00:00' +%s on Linux, or date -j -f '%Y-%m-%d' '2027-06-01' +%s on macOS's BSD date.
Token Stopped Working With No Config Change
Check whether it simply passed its expiration date with pveum user token list <userid> — this is by far the most common cause of a token that "just stopped working" for no apparent reason.
FAQ
Can an API token do everything a user can do in the web UI?
No. Tokens cannot open a VM console (noVNC/SPICE) or access a few other session-bound features, regardless of privilege separation settings — those require an authenticated user ticket. For everything else — VM lifecycle management, storage operations, backups, configuration — a token with sufficient privileges can do the same things the API allows a logged-in user to do.
Do I need a different token for every automation tool?
Not strictly, but it's good practice. A separate token per tool (one for your CI pipeline, one for a monitoring exporter, one for a Terraform workspace) means you can revoke or rotate one without breaking the others, and your audit trail clearly shows which integration made which change.
Is disabling privilege separation ever the right call?
Occasionally — for a single-purpose service account where the user itself already has exactly the narrow permissions you want, disabling privsep just saves you from duplicating the same ACL entries on both the user and the token. But for anything using a broadly-privileged user (especially root@pam), leave it enabled.
Can I see which token made a particular change?
Yes. The task log and, for many actions, the audit-relevant entries in /var/log/pve/tasks/ record the acting user/token identity, so token-driven changes are traceable back to user@realm!tokenid rather than showing up as an anonymous action.
What happens if I regenerate a token's secret?
The token ID and its existing ACL entries stay exactly as they were — only the secret value changes. Anything still using the old secret will start getting 401 errors immediately, so coordinate the rotation with updating whatever stores the secret (vault, CI variable, provider config) at the same time.
Wrapping Up
API tokens turn Proxmox automation from "a script that knows the root password" into something you can actually reason about: who has access, to what, until when, and how to cut it off the moment something looks wrong. The setup takes a few extra minutes compared to hardcoding a password — creating a dedicated user, defining a scoped role, granting an ACL entry — but that's exactly the work that turns a script you're nervous about into infrastructure you can hand to someone else, log, and revoke without a second thought. Start with one automation, scope its token as narrowly as the task allows, and expand from there as you find what your actual workflows need.