Harden VNC over VPN by Controlling Where TigerVNC Listens

The VPN protects the path. Where your VNC server listens decides who on that VPN gets to its port, and RFB on its own puts nothing in front of that port but a weak password check.

Keep the TigerVNC listener on localhost and reach it with ssh -L through the tunnel, so the way in is an SSH login. Bind it to the host's VPN address only when you trust every peer on the VPN. Then check the result with ss on the host and ip route get on your side.

How the viewer, the tunnel and the listener fit together

A client laptop on the left connects via a tunnel to a rack-mounted server on the right marked with a shield.

Your viewer connects to an address. If your routing table sends that address into the tunnel, the packets come out at the VNC host's end and hit the listener. RFB has no protection beyond an optional, cryptographically weak password check, and nothing against observation of or tampering with the stream, which is why it belongs inside a secure channel such as IPsec or SSH.

Where the listener is bound is the hardening decision:

  • Localhost only. Nothing on the network reaches the port. You get in through an SSH forward across the VPN, so a peer needs an SSH login on the host before the VNC password matters.
  • The host's VPN address. Every peer that can route to that address gets the VNC password prompt.
  • Every address (0.0.0.0). The LAN and any public interface get the prompt as well.

The commands here are for TigerVNC's standalone server on Debian 13 (trixie), with display :1 on port 5901. <vnc-host-vpn-address> is the address your machine reaches the host on through the tunnel.

Check what the VNC port is listening on now

Two terminal panels compare a localhost VNC listener against an exposed all-interface listening state.

The port is 5900 plus the display number, so :1 is 5901 unless you moved it with -rfbport. On the VNC host:

check what the vnc port is listening on now
ss -tlnp | grep 5901

A stock session shows 127.0.0.1:5901 and [::1]:5901, and, when ss runs as the session user or under sudo, Xtigervnc as the process holding the socket. That's the Debian wrapper's default at work: with no -localhost option it picks VncAuth, and with no TLS, X509 or RSA-AES security type in the list it listens on localhost only. If you see 0.0.0.0:5901 or [::]:5901, the session was started with -localhost no, or its security types include TLS, X509 or RSA-AES without a *None type, and every address on the host is answering.

Route the VNC host into the tunnel

A central routing node with two diverging paths, the purple one threading through a tunnel to reach a server chassis.

From your machine, ask the kernel which way it would send the viewer's packets:

route the vnc host into the tunnel
ip route get <vnc-host-vpn-address>

The output is the route the kernel uses for that destination. If the device after dev is your tunnel (wg0, tun0), you're set. If it's your LAN or default interface, the viewer's traffic bypasses the VPN entirely.

On OpenVPN, add a route line for the LAN that holds the VNC host to the client config. OpenVPN 2.6 adds it once the connection is up and removes it before the TUN/TAP device closes. Leave the gateway off and it's taken from --route-gateway, or with dev tun from the second parameter of --ifconfig:

route the vnc host into the tunnel
route <vnc-lan-network> <netmask>

On WireGuard, the route comes from AllowedIPs in the VNC host's [Peer] section on your side. Put the host's address there (or the LAN behind that peer, as a CIDR range), and wg-quick adds a route for every allowed range when it brings the interface up:

route the vnc host into the tunnel
AllowedIPs = <vnc-host-vpn-address>/32

Reconnect and run ip route get again. It should name the tunnel device now.

If the VNC host is a WireGuard peer behind NAT, the NAT mapping can lapse while the tunnel sits idle, and your packets stop reaching it. Set a keepalive in the [Peer] section of the host's own config. It's off by default, and 25 seconds works with a wide variety of firewalls:

route the vnc host into the tunnel
PersistentKeepalive = 25

Keep the listener on localhost and tunnel in with ssh -L

Four-step vertical diagram showing VNC server restart, loopback listener check, SSH port forwarding command, and viewer connection.

Stock Debian already listens on localhost, but pin it with -localhost yes so that adding a TLS security type later doesn't quietly open the port on every address. Options only apply when the server starts, so restart the session. -kill ends that desktop session; save what's open in it first.

keep the listener on localhost and tunnel in with ssh -l
vncserver -kill :1
vncserver :1 -localhost yes

Run the ss check again: you want 127.0.0.1:5901 and [::1]:5901, and no 0.0.0.0 line.

On your machine, forward a local port to the host's loopback through the VPN. The leading localhost binds the forwarded port for local use only, so nobody else on your network can ride your tunnel. -N skips the remote shell, and the command stays in the foreground, so leave that terminal open:

keep the listener on localhost and tunnel in with ssh -l
ssh -N -L localhost:5901:localhost:5901 <user>@<vnc-host-vpn-address>

In a second terminal, connect. With TigerVNC's viewer, two colons mean a port number, while one colon followed by a number under 100 means a display number:

keep the listener on localhost and tunnel in with ssh -l
xtigervncviewer localhost::5901

You still get the VNC password prompt. SSH is the extra gate in front of it.

Bind TigerVNC to its VPN address when every peer may connect

An administrator at a host server slots a VPN address card into the listener port while peer clients connect through a tunnel.

When the VNC host runs the VPN itself and you want peers to connect without an SSH login, bind the listener to the host's VPN address instead of every address. Xtigervnc's -interface option takes the address, and vncserver passes it through:

bind tigervnc to its vpn address when every peer may connect
vncserver -kill :1
vncserver :1 -interface <vnc-host-vpn-address> -localhost no

Order matters here. The Debian wrapper resets the localhost setting when it reads -interface, so with -localhost no first you end up on localhost again. Done right, ss shows <vnc-host-vpn-address>:5901 and no 0.0.0.0 line.

The VPN address has to exist when the server starts. Start it with the tunnel down and Xtigervnc exits at once with status 1. vncserver points you at its log in ~/.config/tigervnc/, which ends with vncExtInit: No path or port configured for incoming connections. Bring the tunnel up first.

Then connect straight to the address:

bind tigervnc to its vpn address when every peer may connect
xtigervncviewer <vnc-host-vpn-address>::5901

Every peer that can route to that address now meets the VNC password and nothing else, so keep this for a VPN where you'd trust every peer with that prompt.

When the viewer can't connect

A troubleshooting diagram with a wg show terminal panel on the left branching to three smaller fix cards on the right.

On a WireGuard tunnel, start with the tunnel itself. wg show prints the configuration and runtime information for each interface:

when the viewer can’t connect
sudo wg show
  • The peer has no latest handshake line: the two ends have never completed a handshake. Check that peer's Endpoint and PublicKey lines, then run sudo wg-quick down <interface> and sudo wg-quick up <interface>.
  • ip route get names your LAN or default interface: the packets bypass the tunnel. Add the OpenVPN route line or the AllowedIPs entry and reconnect.
  • ss on the host shows nothing on 5901: the session isn't running. Start it with the vncserver :1 line for your path and read its log in ~/.config/tigervnc/.
  • ss shows 127.0.0.1:5901 after you bound the VPN address: -localhost no came before -interface. Swap them and restart the session.

On a Mac host

A Mac laptop displaying a settings panel with an active VNC viewer toggle and a warning marker.

On a Mac, the setting that lets a non-Apple VNC viewer in with a VNC password sits under System Settings, General, Sharing, the Info button next to Remote Management: VNC viewers may control screen with password. Turning it on requires a VNC password. Leave it off unless a non-Apple viewer has to connect. Third-party viewers might not encrypt keystrokes sent over the network, and VNC control of the screen is nearly unrestricted access.

Default to localhost plus ssh -L. A peer on the VPN then needs an SSH account on the host before the VNC password even comes up, and a routing mistake on your side can't expose the port. Move to the VPN-address bind only on a VPN where every peer is someone you'd give the VNC password to anyway.

FAQs

Should OpenVPN carry VNC traffic over UDP or TCP?

UDP. It's OpenVPN's default protocol, and OpenVPN is built to run best over it; TCP is there for networks where UDP can't get through. If you're stuck on TCP, add tcp-nodelay to the server config. It turns off Nagle's algorithm so packets go out immediately, and in server mode the server pushes the setting to connecting clients.

should openvpn carry vnc traffic over udp or tcp
tcp-nodelay

Can the TigerVNC viewer open the SSH tunnel itself?

Yes. -via runs SSH local port forwarding to the gateway you name and connects through it, which saves you the separate ssh -L terminal. Name the VNC host as the gateway sees it, which for a localhost listener is localhost:

can the tigervnc viewer open the ssh tunnel itself
xtigervncviewer -via <user>@<vnc-host-vpn-address> localhost:1
community thread

Discuss this in the community

Questions about this, or a setup it does not cover? Ask in Security and Remote Access.

Security and Remote Access group

last active 2 Sep 2026, 11:07