Proxmox VE Security Testing: API Tokens, Web Console, Cluster Management, and VM Access

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.

Table of Contents

  1. Authentication and API Token Testing
  2. VM and Container Enumeration
  3. Web Console Access
  4. Proxmox Security Hardening

Authentication and API Token Testing

# 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

VM and Container Enumeration

# 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

Web Console Access

# 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

Proxmox VE Security Hardening

Proxmox VE Security Hardening Checklist:
Security TestMethodRisk
API token with root-equivalent permissionsPOST /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=1Critical
VM console access via vncproxyPOST /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 requiredCritical
LXC container shell access via termproxyPOST /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 hostCritical
Cluster node credential sharingRead /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 nodesCritical
ACME/Let's Encrypt credential exposureGET /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 plaintextHigh

Automate Proxmox VE Security Testing

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