Proxmox VE is the most widely deployed open-source hypervisor for self-hosted virtualization, running the underlying infrastructure for all VMs and LXC containers in a home lab or small datacenter. Its security impact is fundamentally different from application-layer tools: Proxmox runs as root and all API operations execute with root-level host privileges, meaning a compromised Proxmox API token gives an attacker root on the bare metal and control over every VM and container on the host. Key security assessment areas include: API tokens created in Datacenter > Permissions > API Tokens are long-lived and stored in /etc/pve/user.cfg; the vncproxy and termproxy endpoints provide interactive console access to any VM; cluster nodes share the /etc/pve distributed filesystem giving each node full visibility into all API tokens and user configurations; and Proxmox's default self-signed certificate often causes users to disable certificate verification in automation scripts, enabling MITM. This guide covers systematic Proxmox VE security assessment.
# Proxmox VE REST API — authentication via ticket or API token
PROXMOX_URL="https://proxmox.example.com:8006"
# Authenticate with username/password to get a ticket and CSRF token
AUTH_RESPONSE=$(curl -sk -X POST "${PROXMOX_URL}/api2/json/access/ticket" \
-d "username=root@pam&password=yourpassword" 2>/dev/null)
echo "$AUTH_RESPONSE" | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
print(f'Ticket: {data.get(\"ticket\",\"FAILED\")[:40]}...')
print(f'CSRF token: {data.get(\"CSRFPreventionToken\",\"FAILED\")}')
print(f'Username: {data.get(\"username\")}')
" 2>/dev/null
TICKET=$(echo "$AUTH_RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['ticket'])" 2>/dev/null)
CSRF=$(echo "$AUTH_RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['CSRFPreventionToken'])" 2>/dev/null)
# Test API token authentication (format: USER@REALM!TOKENID=SECRET)
# API tokens are created in Datacenter > Permissions > API Tokens
API_TOKEN="root@pam!mytoken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
curl -sk -H "Authorization: PVEAPIToken=${API_TOKEN}" \
"${PROXMOX_URL}/api2/json/nodes" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
nodes = d.get('data',[])
print(f'Cluster nodes: {len(nodes)}')
for n in nodes:
print(f' {n.get(\"node\")} status={n.get(\"status\")} cpu={n.get(\"cpu\"):.2f} mem={n.get(\"mem\")}')
" 2>/dev/null
# List all users and API tokens (requires Datacenter.Audit permission)
curl -sk -b "PVEAuthCookie=${TICKET}" -H "CSRFPreventionToken: ${CSRF}" \
"${PROXMOX_URL}/api2/json/access/users?full=1" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('data',[])
print(f'Users: {len(users)}')
for u in users:
print(f' {u.get(\"userid\")} enable={u.get(\"enable\")} tokens={len(u.get(\"tokens\",{}))}')
for tok_id, tok in u.get('tokens',{}).items():
print(f' Token: {tok_id} expire={tok.get(\"expire\",\"never\")}')
" 2>/dev/null
# Proxmox VM and LXC enumeration — full inventory with resource allocation
PROXMOX_URL="https://proxmox.example.com:8006"
TICKET="your-pve-auth-cookie"
CSRF="your-csrf-token"
NODE="pve"
# Enumerate all VMs on a node
curl -sk -b "PVEAuthCookie=${TICKET}" -H "CSRFPreventionToken: ${CSRF}" \
"${PROXMOX_URL}/api2/json/nodes/${NODE}/qemu" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
vms = d.get('data',[])
print(f'VMs on node {\"$NODE\"}: {len(vms)}')
for vm in sorted(vms, key=lambda x: x.get('vmid',0)):
print(f' VM {vm.get(\"vmid\")}: {vm.get(\"name\",\"unnamed\")} status={vm.get(\"status\")} mem={vm.get(\"mem\",0)//1024//1024}GB')
" 2>/dev/null
# Enumerate all LXC containers
curl -sk -b "PVEAuthCookie=${TICKET}" -H "CSRFPreventionToken: ${CSRF}" \
"${PROXMOX_URL}/api2/json/nodes/${NODE}/lxc" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
ctrs = d.get('data',[])
print(f'LXC containers on node: {len(ctrs)}')
for c in sorted(ctrs, key=lambda x: x.get('vmid',0)):
print(f' CT {c.get(\"vmid\")}: {c.get(\"name\",\"unnamed\")} status={c.get(\"status\")}')
" 2>/dev/null
# Get VM configuration — reveals disks, network interfaces, boot order
VMID=100
curl -sk -b "PVEAuthCookie=${TICKET}" -H "CSRFPreventionToken: ${CSRF}" \
"${PROXMOX_URL}/api2/json/nodes/${NODE}/qemu/${VMID}/config" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
cfg = d.get('data',{})
print(f'VM config:')
print(f' Memory: {cfg.get(\"memory\")}MB')
print(f' CPUs: {cfg.get(\"cores\")}')
for key, val in cfg.items():
if key.startswith('net'):
print(f' {key}: {val}')
if key.startswith('scsi') or key.startswith('virtio') or key.startswith('ide'):
print(f' Disk {key}: {val}')
" 2>/dev/null
# Proxmox VNC/SPICE console — interactive access to any VM
PROXMOX_URL="https://proxmox.example.com:8006"
TICKET="your-pve-auth-cookie"
CSRF="your-csrf-token"
NODE="pve"
VMID=100
# Request a VNC proxy ticket for a VM — this opens a noVNC web console
VNC_RESPONSE=$(curl -sk -X POST \
-b "PVEAuthCookie=${TICKET}" \
-H "CSRFPreventionToken: ${CSRF}" \
"${PROXMOX_URL}/api2/json/nodes/${NODE}/qemu/${VMID}/vncproxy" \
-d "websocket=1&generate-password=1" 2>/dev/null)
echo "$VNC_RESPONSE" | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
print(f'VNC ticket: {data.get(\"ticket\",\"FAILED\")}')
print(f'VNC port: {data.get(\"port\")}')
print(f'VNC password: {data.get(\"passwd\")}')
# This ticket can be used to open a WebSocket VNC connection to the VM
" 2>/dev/null
# Request terminal proxy for LXC container (shell access)
CTID=101
curl -sk -X POST \
-b "PVEAuthCookie=${TICKET}" \
-H "CSRFPreventionToken: ${CSRF}" \
"${PROXMOX_URL}/api2/json/nodes/${NODE}/lxc/${CTID}/termproxy" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
print(f'Term ticket: {data.get(\"ticket\",\"FAILED\")}')
print(f'Term port: {data.get(\"port\")}')
" 2>/dev/null
| Security Test | Method | Risk |
|---|---|---|
| API token with root-equivalent permissions | POST /api2/json/access/ticket with root@pam credentials — ticket gives full cluster control; API tokens created with Datacenter Administrator role have equivalent access and are long-lived; list tokens via GET /api2/json/access/users?full=1 | Critical |
| VM console access via vncproxy | POST /nodes/{node}/qemu/{vmid}/vncproxy — returns a VNC ticket that opens an interactive console to the VM; equivalent to physical keyboard/monitor access; no separate VM-level authentication required | Critical |
| LXC container shell access via termproxy | POST /nodes/{node}/lxc/{ctid}/termproxy — returns a terminal proxy ticket for direct shell access to the LXC container; equivalent to running lxc-attach on the host | Critical |
| Cluster node credential sharing | Read /etc/pve/user.cfg on any cluster node — contains all API token IDs, hashed passwords, and permission assignments for the entire cluster; compromise of one cluster node exposes credentials for all nodes | Critical |
| ACME/Let's Encrypt credential exposure | GET /api2/json/cluster/acme/plugins — returns configured ACME DNS plugin credentials (Cloudflare API tokens, Route53 keys, etc.) used for certificate issuance; DNS provider credentials stored in plaintext | High |
Ironimo tests Proxmox VE deployments for API token enumeration and privilege assessment, default root@pam credential validation, VM and LXC console access via vncproxy and termproxy, cluster node credential sharing in /etc/pve/user.cfg, ACME DNS plugin credential exposure, firewall configuration review, and TLS certificate validation.
Start free scan