# Access Control

This page describes go-pve-qemu's three guards: origin-scoped CORS, the `ALLOW_IPS` whitelist, and per-VM state and lock checks.

## Layer 1: CORS

Every request passes the CORS middleware:

| `Origin` | Response |
|---|---|
| A private IP (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) in the server's own `/24` | `Access-Control-Allow-Origin` echoes that origin |
| Any other origin | No `Access-Control-Allow-Origin` |
| No `Origin` (curl, server-side callers) | Unaffected |

The server subnet comes from the first non-loopback private IPv4 interface, falling back to `192.168.0.*`. Allowed methods are `GET`, `POST`, `OPTIONS`; `OPTIONS` preflights get `204` directly.

CORS only constrains browsers; it is not authentication.

## Layer 2: `ALLOW_IPS`

Every mutating endpoint (install, start, stop, shutdown, reboot, destroy, set/*) first matches the client IP:

| `ALLOW_IPS` | Behavior |
|---|---|
| `0.0.0.0` | Allow all |
| `192.168.0.10,192.168.0.20` | Allow only the listed IPs |
| unset or empty | Deny all |

The client IP comes from Gin's `c.ClientIP()`. The service sets no trusted proxies, so Gin honors `X-Forwarded-For` / `X-Real-IP` by default, and a client talking to the API directly can spoof its IP; expose the service only inside a controlled network.

Rejections look like this:

| Endpoint type | Response |
|---|---|
| Sync endpoints | `403 this IP is not allowed to perform this action` |
| SSE endpoints | `200` with a single `data: {"message": "...this IP is not allowed to perform this action"}` line |

Query endpoints (health, list, status) do not check `ALLOW_IPS`.

## Layer 3: VM State and the Disabled List

Mutating endpoints that target a VM check it against `pvesh get /cluster/resources` and [`.go_qemu_disabled`](/state-files):

| Check | Failure response |
|---|---|
| VM is on the disabled list | `400 this IP is not allowed to be controlled` |
| Requires running, VM is stopped | `400 VM is not running` |
| Requires stopped, VM is running | `400 VM is running` |
| Cluster data or disabled list unreadable | `500 failed to get VM list: ...` |

| Requires a running VM | Requires a stopped VM |
|---|---|
| stop, shutdown, reboot | start, destroy, set/cpu, set/memory, set/disk, set/node |

SSE endpoints report check failures as a single `data:` line as well.
