Standalone Browser Console¶
Purpose¶
netbox-proxbox provides a Console tab directly on each synchronized NetBox VirtualMachine. It does not navigate to or require another management application. Other applications may provide their own consoles as parallel downstream consumers, but they are not part of this request path.
The feature supports:
- QEMU graphical consoles through noVNC;
- QEMU terminal consoles through xterm.js; and
- LXC terminal consoles through xterm.js.
LXC graphical consoles are rejected at the browser, NetBox, and proxbox-api boundaries.
Trust boundaries¶
Browser
| authenticated NetBox page + CSRF-protected session request
v
netbox-proxbox
| exact restricted VM + open_console endpoint permission
| typed sync-state identity + authenticated service request
v
proxbox-api
| encrypted, origin-bound, short-lived, one-use relay state
| Proxmox ticket acquisition and WebSocket authentication
v
Proxmox vncwebsocket
netbox-proxbox owns NetBox object authorization and the user interface. proxbox-api owns Proxmox authentication and the WebSocket transport. This split is required because stock NetBox exposes Django plugin HTTP views but does not terminate plugin WebSockets.
The browser receives only:
- the configured public proxbox-api WebSocket authority plus a fixed relay path;
- one short-lived opaque relay token;
- the relay expiry timestamp; and
- the selected console type.
The browser never receives the Proxmox hostname, port, VNC ticket, authentication header or cookie, upstream WebSocket URL, endpoint ID, API key, or TLS verification policy. None of those values may appear in browser DOM attributes, JavaScript logs, Django error responses, or proxbox-api close reasons.
Authorization and identity¶
The tab requires netbox_proxbox.open_console_proxmoxendpoint. Session creation repeats authorization server-side and never trusts fields supplied by JavaScript.
ProxboxVMConsoleSessionView performs these checks in order:
- the caller is authenticated;
- the caller has the global console permission;
VirtualMachine.objects.restrict(user, "view")returns the exact requested VM;- its
ProxboxVirtualMachineSyncStatebelongs to that VM; - the typed
ProxmoxEndpointis enabled and remains visible through the caller'sopen_consoleobject restriction; - the typed
ProxmoxNodebelongs to that endpoint, and its current name matches the recorded synchronized name; - any typed Proxmox cluster belongs to the same endpoint;
- VMID is positive and the synchronized guest type is exactly
qemuorlxc; - exactly one trusted
FastAPIEndpointmaps the typed endpoint to a current proxbox-api endpoint row; and - a recorded raw backend endpoint ID, when present, matches the resolved backend ID.
Missing, ambiguous, partial, or drifted identity fails closed. The view does not fall back to the VM name, a custom field, the first Proxmox endpoint, or an unscoped proxbox-api request.
The tab view keeps the two object types separate inside NetBox's generic view
machinery. Its permission hook checks the endpoint-owned open_console
permission together with any inherited additional_permissions,
without allowing ObjectPermissionRequiredMixin to apply that
action to the VirtualMachine queryset. The queryset is independently
restricted with the caller's VM view permission, and the resolved synchronized
endpoint is then checked through its own open_console restriction. Removing
that explicit separation turns every endpoint-authorized VM lookup into a 404
because open_console is not a VirtualMachine action.
Session protocol¶
The browser sends only the selected console_type to the plugin session route. The plugin derives every target field server-side and calls:
POST /proxmox/console/browser-sessions
Content-Type: application/json
X-Proxbox-API-Key: <server-side only>
X-Proxbox-Actor: <NetBox username>
{
"endpoint_id": 31,
"vmid": 101,
"node": "pve-01",
"vm_type": "qemu",
"console_type": "novnc",
"origin": "https://netbox.example.com"
}
proxbox-api returns a short-lived opaque token and a relative browser relay path to the NetBox server. The plugin validates the response and returns a browser-safe projection:
{
"websocket_url": "wss://relay.example.com/proxmox/console/browser-stream",
"stream_token": "<opaque>",
"expires_at": "2026-09-14T13:00:00+00:00",
"console_type": "novnc"
}
The FastAPIEndpoint must have Use WebSocket enabled, and its configured websocket_domain and WebSocket port must identify the public TLS endpoint that serves proxbox-api. Only an enabled WebSocket authority is bound to the stored backend target, so the console never uses one from an endpoint where the option is disabled. The page and relay must use HTTPS/WSS. Redirects are disabled for the authenticated server request.
The browser WebSocket automatically supplies the NetBox page Origin. It offers
binary plus a dedicated token subprotocol, while proxbox-api negotiates only
binary. The token never appears in the URI, query string, DOM, console output,
or access log. proxbox-api atomically consumes the token only when that origin
matches the value bound during creation. Expired, replayed, malformed, or
foreign-origin tokens cannot open a stream.
Graphical console¶
The graphical client uses the vendored noVNC 1.7.0 ES-module runtime and connects without a password. The runtime includes the upstream noVNC and pako licenses and is pinned by a complete file-list and SHA-256 tree-digest test. proxbox-api performs the bounded RFB 3.8 authentication exchange with Proxmox server-side, answers the VNC challenge using the private ticket, and presents no-auth RFB to noVNC only after that upstream authentication succeeds.
The UI enables scaleViewport and resizeSession. Disconnect, reconnect, page hide, and page unload paths destroy the RFB object and remove listeners before another session can start.
Terminal console¶
The terminal client uses the existing vendored xterm.js assets. It follows Proxmox termproxy framing:
- terminal output frames begin with
dand the prefix is removed before writing to xterm; - resize uses
1:<rows>:<cols>:0; - the keepalive frame is
0followed by NUL; and - keyboard input is forwarded only while the current WebSocket is open.
A ResizeObserver updates the terminal dimensions. The observer, socket, xterm instance, and event listeners are all released during disconnect and page teardown.
Endpoint SSH credential chain¶
The endpoint-level SSH terminal uses the server-side credential facade. New
configuration defaults to a ServiceSSHCredential attached one-to-one to a
ProxmoxEndpointService, which is bound to the endpoint's IPAddress and TCP
SSH port. The endpoint detail page shows every link in that chain and directs
operators to the SSH tab when one is missing. Existing dedicated credentials
and endpoint-password reuse remain supported.
The host and pinned host-key fingerprint continue to come from the
ProxmoxEndpoint; moving credential material to the application service does
not change connection approval or host-key verification. The browser and
rendered pages receive only readiness metadata. Plaintext material is resolved
only by the protected endpoint SSH secrets API and its response shape is
unchanged.
Migration from the external handoff¶
Migration 0084_proxboxpluginsettings_console_url.py remains immutable release history. Migration 0095_standalone_vm_console.py removes ProxboxPluginSettings.console_url and adds the open_console_proxmoxendpoint permission. The settings form, settings API, template extension button, and external-link template no longer expose the retired handoff.
Upgrade proxbox-api first so the browser relay is available before the NetBox plugin exposes the Console tab. Deploying the plugin first fails closed with an unavailable-session response, but leaves operators without a working console until the backend is upgraded.
After upgrading proxbox-api:
- upgrade netbox-proxbox and run the normal NetBox migration process;
- grant
netbox_proxbox.open_console_proxmoxendpointonly to roles allowed to interact with guest consoles; - configure the
FastAPIEndpointHTTP and public WebSocket authorities with HTTPS/WSS and certificate verification; and - verify the Console tab with a synchronized QEMU and LXC guest.
No external console URL setting is required or accepted.
Failure behavior¶
Console identity errors include a stable diagnostic code, a plain-language cause, and the exact operator action. The page offers Repair all enabled endpoints when the failure can be repaired by a full sync and the current user has the core Job add permission used by normal Proxbox sync enqueue actions. The button is a CSRF-protected POST to the existing guarded repair workflow; it reconciles compatibility fields, re-pushes endpoint configuration, and queues one full sync across every enabled Proxmox endpoint. That reconciliation can update or remove stale synchronized NetBox inventory outside the displayed VM, so the page states the scope beside the button. It does not grant permissions, enable endpoints, expose secrets, guess a missing endpoint, or bypass an active repair job.
| Symptom or code | Required action |
|---|---|
| Console tab is absent | A NetBox administrator must grant netbox_proxbox.open_console_proxmoxendpoint for the required endpoint and ensure the user can view the VM. Users cannot grant themselves console access. |
SYNC_STATE_MISSING |
Review the disclosed estate-wide scope, then select Repair all enabled endpoints when offered. Otherwise ask an administrator with Job add permission to run Proxbox → Repair / Rebuild Proxbox sync-state. Return after the job completes. |
SYNC_ENDPOINT_LINK_MISSING |
The proxbox-api endpoint can be healthy while the VM's typed NetBox endpoint or node relation is missing. A full sync repairs it only when the current-run endpoint map and stamp are corroborated by the VM's protected native-cluster ownership. Review the disclosed estate-wide scope, then select Repair all enabled endpoints when offered, or ask an administrator with Job add permission to run the same recovery action. If the row remains unbound, inspect the job's sync_state_relation_resolution reasons described below. |
SYNC_ENDPOINT_DISABLED |
A Proxbox administrator must enable the linked NetBox ProxmoxEndpoint, run a full sync, and retry. The console does not enable endpoints inline because enablement changes the endpoint's estate-wide synchronization posture. |
SYNC_NODE_IDENTITY_DRIFTED or SYNC_CLUSTER_IDENTITY_DRIFTED |
Run a full sync. The post-sync resolver replaces a stale same-endpoint node only when the VM has a native device, exactly one node in the endpoint links to that device, the recorded name selects that same node, and its tracking cluster agrees. Correct contradictory inventory instead of guessing. |
Job summary reports native_cluster_missing |
Assign the VM to its correct native NetBox cluster, run the endpoint's protected cluster synchronization, then run the full VM sync again. |
Job summary reports native_cluster_not_owned_by_endpoint |
Run protected cluster synchronization for the intended endpoint. Review its authoritative netbox_cluster and linked tracking row if synchronization refuses the claim. |
Job summary reports native_cluster_ownership_ambiguous |
Review and remove the conflicting endpoint or tracking claim through the supported cluster ownership workflow. Do not choose an owner by matching names. Run cluster synchronization and then the full VM sync again. |
Job summary reports node_native_device_not_found or node_native_device_ambiguous |
Run a full synchronization so the VM's native device and the node's device link are current. If the reason remains, correct the missing or duplicate same-endpoint device link before retrying. |
Job summary reports node_name_contradicts_native_device |
Run a full synchronization so the recorded VM node name and native device placement are current. Correct the node tracking or VM placement if they still select different nodes. |
Job summary reports node_unverified_drift_without_native_device |
Run a full synchronization so proxbox-api restores the VM's native device. The resolver preserves the existing node relation until device placement corroborates a replacement. A null node relation may still bind from one exact same-endpoint name; that fallback cannot cross endpoints. |
Job summary reports a cluster-name/link or node_contradicts_cluster contradiction |
Correct the tracking relation or native VM placement, then run a full sync. The resolver does not replace the node while its cluster graph disagrees. |
| Tab shows another incomplete or drifted identity code | Run a full Proxbox sync and confirm the VM sidecar links one enabled endpoint and node. Do not repair ambiguous identity by guessing an endpoint. |
| Session reports no trusted backend | Confirm the endpoint has been synchronized to exactly one enabled FastAPI backend and its stored target still matches. The FastAPI endpoint must also have Use WebSocket enabled and resolve a browser WebSocket base (websocket_domain, else its IP address, else its domain); a backend without both is not browser-capable and is skipped. |
| Session requires HTTPS | Serve NetBox over HTTPS and configure a public WSS proxbox-api endpoint. |
| WebSocket closes as invalid or expired | Create a new session; tokens are short-lived, origin-bound, and one-use. |
| QEMU graphical console fails before display | Check proxbox-api and Proxmox console permissions; the browser will not receive the private failure detail. |
| LXC graphical option is absent | Expected; LXC supports terminal only. |
Verification¶
Run the focused plugin tests:
uv run pytest -p no:django tests/test_vm_console_server.py tests/test_vm_console_frontend.py
Run the real-Django migration and view tests plus the complete repository gates before release. Validate in an authenticated browser that:
- the Console tab stays inside NetBox;
- QEMU graphical and terminal sessions exchange frames;
- LXC terminal sessions exchange frames;
- replay and expiry fail;
- disconnect and reconnect create a fresh session; and
- browser requests, rendered HTML, console logs, and network responses contain no external management URL or Proxmox ticket/authentication material.