Proxmox api vncwebsocket

I have a working POC solution. My use case consists of a small custom go web-server which delivers the noVNC client and works as like a websocket reverse proxy. Here is a simple explanation what the go server does:
  1. Receive a new websocket connection from noVNC
  2. Create a new VNCProxy via the ProxmoxAPI ( POST /api2/json/nodes/{node}/qemu/{vmid}/vncproxy )
  3. Create a websocket to the ProxmoxAPI with the vncproxy credentials received earlier
  4. Merge both websockets for bidirectional communication

Things that I found out during development:
  • The POST Request to ".../vncproxy" needs the following Payload: ( generate-password: 1, websocket: 1 )
  • The ProxmoxVNC websocket needs the "Authorization" header with the ProxmoxAPI token. The connection URL looks like this:
    "wss:// <hostname:port> /api2/json/nodes/ $NODE /qemu/ $VMID /vncwebsocket?port= $CONSOLE_PORT &vncticket="+ url.QueryEscape( $CONSOLE_TICKET )

Thanks for this tidbit. Documentation is clear as mud on this, specifically I'm talking about the generate-password and websocket options, as well as having to pass the Authentication header with both vncproxy and vncwebsocket API calls. That is crucial to getting this to work, and yet there is no mention of having to use the Auth header a second time anywhere in the docs that I could find.

My scenario is that I've put proxmox on a private network only. It passes through a public IP to the NGINX VM itself, which also has a private network that can talk to Proxmox. Pinholes in the firewall by source IP and specific destination ports only, etc.

I used NGINX + LUA (openresty) and a Flask router to do this. No native websocket support is required.

The main issues are this:
  • Once the call to vncproxy is made, you have exactly 10 seconds to establish a websocket over HTTPS call to vncwebproxy before that port it assigned is closed.
  • The call to vncproxy gives you a one-time-use password (generate-password), but there's not enough time to actually copy and paste that password in noVNC, or whatever, before that 10 seconds has passed.
  • The entire point of using vncproxy and then vncwebsocket is because Proxmox itself proxies the VNC websocket (wss://) connection over an HTTPS call via the vncwebsocket URL so you don't have to deal with the wss:// protocol.
    • The 5900-5999 VNC port that is opened is used by proxmox itself in vncproxy + vncwebsocket mode. Most of the people asking about vncwebsocket around here seemed just as confused as I was on this.
I'll leave most of the solution up to the reader to figure out, but here are some hints on how to achieve this:
  • vnc.html and vnc_lite.html from the official noVNC package both allow you to stuff the URL-encoded password in the URI.
    • For a reverse proxy setup like I did, and because of the one-time-password, the Connect button in noVNC is useless. vnc_lite.html might be the better choice.
  • vnc.html and vnc_lite.html both support autoconnect=true in the URI. When used with the password, no prompt is required or shown. (You're using this behind a secure 2FA portal, right?)
  • It not difficult to use LUA and Flask to call vncproxy, save the port and ticket it gives you in a local DB (first API call), and then query the DB in order to create a reverse-proxy URL for the vncwebsocket (second call) noVNC will use as its path, with the password URL encoded in the URI.
  • As Jan mentioned, the Authorization header is absolutely required on the call to vncwebsocket or you will get a permission denied error, never mind your ticket is valid and was just given to you less than a second ago by vncproxy.
  • I used an API user and token to create that auth header:

    set $pve_api_token "vncuser@pam!<token_name>=<token_secret>"
    .....lua + flask stuff here
    proxy_set_header Authorization "PVEAPIToken $pve_api_token";

  • Don't forget to set the permissions for your newly created API token. Create a new role for it that has only the perms needed.
  • ChatGPT is your friend. Seriously. When I would get stuck, ChatGPT was super helpful in analyzing the debug logs and the code, and then coming up with fixes.
Example vncproxy URL to get the port, ticket and password looks like this:
https://10.0.0.252:8006/api2/json/nodes/<proxmox node name>/qemu/<vmid>/vncproxy

Example vncwebsocket URL looks like this:
https://10.0.0.252:8006/api2/json/nodes/<proxmox node name>/qemu/<vmid>/vncwebsocket?port=<port from vncproxy call>&vncticket=<url encoded ticket from vncproxy call>

You need the Authorization header with the both calls above!


Example path for noVNC using the vnc_lite.html included. Build this in NGINX+LUA:
https://your.domain.name/noVNC/vnc_lite.html?autoconnect=true&scale=true&path=<URL encoded path to your NGINX location handling the reverse proxy>&password=<one-time-password from vncproxy>"

Final tip: Once noVNC disconnects, you have to start that process all over again from the beginning. The port is closed after that initial 10 seconds after vncproxy was called.
 
Last edited:
  • Like
Reactions: dnk