A VNC server on Ubuntu doesn’t put your desktop on the network. TigerVNC, the VNC server Ubuntu packages today, binds port 5901 to 127.0.0.1 and ::1 and nothing else, so a viewer pointed at the server’s LAN address gets Connection refused. That makes the SSH tunnel the normal way in rather than an extra hardening step, because it is the only path the default config leaves open.
This guide installs TigerVNC with Xfce, runs the desktop as a systemd service and connects through SSH. It also covers the three things that tripped us up: a colord password prompt in the first session, the Could not migrate .vnc error and a unit name most tutorials get wrong. The steps below were run on an Ubuntu 26.04.1 LTS lab VM with TigerVNC 1.15 and Xfce 4.20 in October 2026, as written or with scripted equivalents, and the outputs come from that box.
Prerequisites
You need a server you can already reach over SSH, because the tunnel is how the desktop gets to you. The lab VM was small, and that was plenty for one Xfce session:
- Ubuntu 26.04 LTS server, cloud image or minimal install
- 2 vCPU and 4 GB RAM (the lab size, a floor rather than a target)
- About 1 GB of free disk (188 MB of downloads, 745 MB installed)
- A regular user with sudo rights, logged in over SSH
- A VNC viewer on the machine you connect from
One Xfce session used about 300 MB of resident memory after a reboot, so the 4 GB VM had room to spare. If several people will each get their own display, size memory per session and give each user a display number. If you still log in with a password, set up SSH key authentication first, since SSH is the only door this setup has. Ubuntu 24.04 was not part of this test, so treat the paths and unit names below as verified on 26.04 only.
Install TigerVNC as the VNC server on Ubuntu
Install TigerVNC rather than TightVNC. Both are still in the archive, but only TigerVNC ships the systemd unit and the localhost-only default this guide relies on. Xfce is the desktop because it starts inside a VNC session with no extra glue, and dbus-x11 gives that session its message bus.
sudo apt update
sudo apt install xfce4 xfce4-terminal dbus-x11 tigervnc-standalone-server tigervnc-tools
On a minimal server this is a big install, because Xfce pulls in the whole X stack. Ours added 398 packages and took almost 27 minutes, nearly all of it spent waiting on the mirror, since user and system CPU time came to barely over a minute. Plan for the download, not the CPU.
0 upgraded, 398 newly installed, 0 to remove and 4 not upgraded.
Need to get 188 MB of archives.
After this operation, 745 MB of additional disk space will be used.
The package brings vncserver, vncpasswd, the Xtigervnc X server and vncsession. Nothing listens yet, because no display has been started, so there is nothing to firewall at this point either.
Set the VNC password
TigerVNC refuses to start a display until a password file exists, and that file now lives in ~/.config/tigervnc/passwd, not in ~/.vnc as older guides show. In an interactive SSH session, the first vncserver run asks for the password, then asks whether you also want a view-only password, and writes the file for you. We scripted the file instead, so that exchange is not shown here. If you script it, write the file yourself with vncpasswd -f, which reads the password from standard input.
mkdir -p ~/.config/tigervnc
echo "YourVncPass" | vncpasswd -f > ~/.config/tigervnc/passwd
chmod 600 ~/.config/tigervnc/passwd
Replace YourVncPass with your own password. The resulting file is 8 bytes, because classic VNC authentication only uses the first eight characters, so a long passphrase buys you nothing here. That’s one more reason the SSH login in front of it has to be the real gate.
getpassword error: Inappropriate ioctl for device
This is what vncserver prints when it needs a password but has no terminal to ask on. It happens from a script, a CI job or an ssh one-liner without -t.
You will require a password to access your desktops.
getpassword error: Inappropriate ioctl for device
Password:
Nothing is broken. Either run the command from an interactive shell and type the password, or create the file with the vncpasswd -f lines above and start the display again. The second option is the one to use in automation, because it never waits for input.
Start the first VNC session
Display number 1 maps to TCP port 5901 (the display number plus 5900), so start that one. You do not need an xstartup file anymore. Without one, TigerVNC runs /etc/X11/Xtigervnc-session, which hands over to /etc/X11/Xsession, and that starts x-session-manager, the alternatives link that points at whichever session manager has the highest priority. On a server where Xfce is the only desktop, that link resolves to startxfce4, so the old step of editing xstartup is gone. A later section pins Xfce explicitly for boxes that have more than one desktop.
vncserver :1
The server reports the display, the port and the exact viewer command. The xauth warning only appears on the first run, because ~/.Xauthority does not exist until this moment, and it is harmless.
/usr/bin/xauth: file /home/josphat/.Xauthority does not exist
New Xtigervnc server 'vnc01:1 (josphat)' on port 5901 for display :1.
Use xtigervncviewer -SecurityTypes VncAuth -passwd /home/josphat/.config/tigervnc/passwd :1 to connect to the VNC server.
A few seconds later Xfce is up, with no xstartup file anywhere in the home directory. Check that the session processes exist and belong to you.
ps -eo user,cmd | grep -E "[x]fce4-session|[x]fwm4|[x]fdesktop"
All three run as your user, which is what you want. The desktop has your permissions and not root’s, so anything you break in it stays inside your account.
josphat xfce4-session
josphat xfwm4
josphat xfdesktop
The session log sits next to the password file and is named after the host and display (vnc01:1.log here). On our VM it contained Could not find any render nodes and Failed to initialize DRI3 extension, which only mean there is no GPU for hardware rendering. Look for the line Listening for VNC connections on local interface(s), port 5901 instead, because it already tells you where the server listens. The “VNC config” entry you will see in the Xfce taskbar is tigervncconfig -iconic, which Xtigervnc-session starts for every session, and it is harmless.
Confirm VNC listens on localhost only
Check the listening sockets before you try to connect, because this is where most “VNC doesn’t work” reports start. A default TigerVNC session shows port 5901 on the IPv4 and IPv6 loopback addresses and nowhere else.
ss -ltnp | grep -E ":59|:60"
vncserver -list
Both sockets bind 127.0.0.1 and ::1, and vncserver -list shows the one session with its port and process ID.

If you see 0.0.0.0 on port 5901 instead, someone has set localhost=no or passed -localhost no, and the desktop is exposed to everything that can route to the box. TigerVNC’s own defaults file, /etc/tigervnc/vncserver-config-defaults, documents the behavior. Localhost stays on unless you switch to a TLS or X509 security type, and with localhost on, plain VncAuth is the only security type offered. That pairing is deliberate, because VncAuth protects the password exchange but doesn’t encrypt the screen or your keystrokes, so TigerVNC only allows it where something else carries the traffic.
Connect through an SSH tunnel
The connection starts from your workstation, not the server. Our client was a Linux box, and the same commands work from macOS. Set the server address and your SSH user once in that shell, then prove the port really is closed to the network before you build the tunnel, so you know the tunnel is the only thing that works.
export HOST_IP=10.0.1.50
export SSH_USER=josphat
nc -zv -w3 "${HOST_IP}" 5901
A refused connection is the healthy result. A timeout would point at a firewall in between, and a success would mean port 5901 is open to the LAN, which you should fix before going further.
nc: connect to 10.0.1.50 port 5901 (tcp) failed: Connection refused
Open the tunnel with your normal SSH login. The -L 5901:localhost:5901 option forwards port 5901 on your workstation to port 5901 on the server’s loopback, -N runs no remote command and -f sends SSH to the background once you have authenticated. Our older write-up on creating SSH tunnels covers the other forwarding modes. On Windows, the built-in OpenSSH client takes the plain foreground form, ssh -L 5901:localhost:5901 [email protected] (replace with your user and server address), which keeps the tunnel open while that window stays open. We did not test a Windows client.
ssh -fN -L 5901:localhost:5901 "${SSH_USER}@${HOST_IP}"
Now point your VNC viewer at the local end of the tunnel and enter the VNC password. Viewers accept two forms that look alike: localhost:1 means display 1, which the viewer turns into port 5901, and localhost::5901 with a double colon means the TCP port itself. Our client used localhost::5901, could reach the server only through this tunnel, and the Xfce desktop came straight up.

That capture is from the systemd-managed session at 1600×900, which the next sections set up. Our first manual session came up at 1920×1200 instead, and it greeted us with a password dialog before anything else.
Authentication is required to create a color managed device
This polkit dialog sat on top of an otherwise empty desktop in our very first session. It comes from colord, the color management daemon that Xfce pulls in, trying to register the virtual display, and it asks for an admin password before the desktop is usable.

The reason shows up in the policy for that action. Polkit allows it without a password only for an active local session, and a VNC session is not a local seat, so it falls into the “any” class, which demands admin authentication.
pkaction --action-id org.freedesktop.color-manager.create-device --verbose
Read the three implicit lines. Any session needs auth_admin, inactive ones get no, and only an active local session gets yes.
org.freedesktop.color-manager.create-device:
description: Create a color managed device
message: Authentication is required to create a color managed device
vendor: System Color Manager
vendor_url: https://www.freedesktop.org/projects/system-color-manager/
icon: application-vnd.iccprofile
implicit any: auth_admin
implicit inactive: no
implicit active: yes
annotation: org.freedesktop.policykit.owner -> unix-user:colord
Cancelling the dialog does no harm, and a polkit rule grants the action so the password is not needed. The rule below allows the six colord device and profile actions for members of the sudo group, which is the admin group on Ubuntu, so normal users still get asked. Create the rule file.
sudo vim /etc/polkit-1/rules.d/45-allow-colord.rules
Paste this content, which is the exact file we tested.
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.color-manager.create-device" ||
action.id == "org.freedesktop.color-manager.create-profile" ||
action.id == "org.freedesktop.color-manager.delete-device" ||
action.id == "org.freedesktop.color-manager.delete-profile" ||
action.id == "org.freedesktop.color-manager.modify-device" ||
action.id == "org.freedesktop.color-manager.modify-profile") &&
subject.isInGroup("sudo")) {
return polkit.Result.YES;
}
});
Polkit watches its rules directories and reloads them when a file changes, so the restart is only there to make the load immediate and certain.
sudo systemctl restart polkit
You can prove the rule works without waiting for the dialog. An SSH session is just as non-local as the VNC one, so ask polkit directly from it.
pkcheck --action-id org.freedesktop.color-manager.create-device --process $$
Before the rule, the answer was polkit\56result=auth_admin with Authorization requires authentication and -u wasn't passed. and exit code 2. With the rule in place it changes to a plain yes and exit code 0.
polkit\56result=yes
Be clear about the scope, though. In our lab the dialog appeared on the first session only, and we couldn’t bring it back afterwards, not with the rule removed, not after restarting colord, and not after moving colord’s mapping.db and storage.db aside. So we cannot promise you’ll see it at all. If you do see it, the pkcheck result above proves the rule grants the action.
Pin the Xfce session and resolution
Relying on x-session-manager works only while Xfce is the sole desktop on the box. Install GNOME next to it and the VNC session starts whichever session manager has the highest alternatives priority, which may not be the one you want. Check what the link points at now:
update-alternatives --display x-session-manager
On our server Xfce was the only candidate, so the link sat on startxfce4 in auto mode:
x-session-manager - auto mode
link best version is /usr/bin/startxfce4
link currently points to /usr/bin/startxfce4
link x-session-manager is /usr/bin/x-session-manager
slave x-session-manager.1.gz is /usr/share/man/man1/x-session-manager.1.gz
/usr/bin/startxfce4 - priority 50
Pin the session in ~/.config/tigervnc/config so the choice no longer depends on that link. The file takes one key=value per line, using the same names as the vncserver options, and session=xfce names the xfce.desktop entry in /usr/share/xsessions. Set the geometry in the same file instead of passing -geometry on every start, because the systemd unit in the next step reads it too:
printf "session=xfce\ngeometry=1600x900\n" > ~/.config/tigervnc/config
That file is not the only source. The wrapper also reads /etc/tigervnc/vncserver-config-defaults and /etc/tigervnc/vncserver-config-mandatory, and a ~/.config/tigervnc/config.pl takes priority over the plain config file, as the Debian TigerVNC Config.pm shows. If a setting seems ignored, look there first. The change takes effect on the next session start, not the running one, and our capture through the tunnel after the change came out at exactly 1600×900. Pick a size that fits your local screen without scaling, because a scaled VNC desktop blurs text badly.
Run TigerVNC as a systemd service
A display you start by hand dies with the server, so hand it to TigerVNC’s own unit instead. Stop the manual session first, because the unit wants the same display number and fails if it is taken.
vncserver -kill :1
The unit does not take a user name. It looks up which user owns which display in /etc/tigervnc/vncserver.users, so map display 1 to your account (the shell expands $USER before sudo runs), then enable and start it.
echo ":1=$USER" | sudo tee -a /etc/tigervnc/vncserver.users
sudo systemctl enable --now tigervncserver@:1
The enable step links the unit into multi-user.target, so the desktop comes back on every boot without anyone logging in.
:1=josphat
Created symlink '/etc/systemd/system/multi-user.target.wants/tigervncserver@:1.service' → '/usr/lib/systemd/system/[email protected]'.
Check the X server the unit started. It should run as your user, and its command line should carry -localhost=1, the password file from your home directory and VncAuth.
systemctl is-active tigervncserver@:1
ps -eo user,cmd | grep "[X]tigervnc" | cut -c1-200
We captured this block before adding session=xfce, so the session script line carries no argument yet. Those three flags are the whole security model of this setup in one line. The server stays on loopback, checks your password file and offers plain VNC authentication, which is fine only because the tunnel wraps it. The grep also matches the session script, and cut stops each line at 200 characters, which is why the first one ends on -geometry 1.
active
josphat /usr/bin/Xtigervnc :1 -localhost=1 -desktop vnc01:1 (josphat) -rfbport 5901 -PasswordFile /home/josphat/.config/tigervnc/passwd -SecurityTypes VncAuth -auth /home/josphat/.Xauthority -geometry 1
josphat /bin/sh /etc/X11/Xtigervnc-session
To confirm the unit honours session=xfce, look at the session processes after a restart of the unit:
sudo systemctl restart tigervncserver@:1
ps -eo user,cmd | grep -E "[x]fce4-session|[X]tigervnc-session|startxfce4" | cut -c1-150
The session script now receives startxfce4 as its argument instead of the default. We trimmed the output to the session lines, because the unbracketed startxfce4 in the pattern also matches the grep command itself:
josphat /bin/sh /etc/X11/Xtigervnc-session startxfce4
josphat xfce4-session
josphat /usr/bin/ssh-agent startxfce4
We rebooted the VM with the unit enabled to make sure it survives. After the reboot the unit was active, port 5901 was back on 127.0.0.1 and ::1 only, and Xfce was running. The whole VM used 609 MB of its 3.9 GB, and four desktop processes held most of the session’s share.
| Process | Resident memory (KB) |
|---|---|
| xfwm4 | 112268 |
| Xtigervnc | 102936 |
| xfdesktop | 47468 |
| xfce4-panel | 41992 |
That is about 300 MB for one idle desktop, with the window manager and the VNC X server costing about the same. Don’t pick Xfce for memory alone, because our desktop environment RAM comparison found it within a few MiB of GNOME on a full Ubuntu desktop install. We’d pick it for VNC because it started in the session with no xstartup file and no extra configuration, while GNOME over VNC wasn’t part of this test.
No files found for vncserver@:1.service
Most tutorials, including the old version of this one, use vncserver@ as the unit name. On Ubuntu the packaged unit is [email protected], and the old name does not exist.
systemctl cat vncserver@:1
systemd answers with a plain miss, because no unit file or alias by that name is installed.
No files found for vncserver@:1.service.
Use tigervncserver@ in every systemctl command. You also don’t need a hand-written unit with User= lines anymore, because the packaged one reads vncserver.users and runs /usr/libexec/tigervncsession-start for the mapped user. A copied unit from an old guide only adds a second definition to keep in sync.
Fix “vncserver: Could not migrate .vnc to .config/tigervnc”
Current TigerVNC moves an old ~/.vnc directory to ~/.config/tigervnc the first time it starts, and that move fails when the user has no ~/.config directory at all. We reproduced it with a fresh user whose only VNC file was an old-style ~/.vnc/passwd.
vncserver: Could not migrate /home/vnctest/.vnc to /home/vnctest/.config/tigervnc.
No display starts, and running vncserver again gives the same error, because nothing creates the missing ~/.config in between. That’s common on server images, where nothing graphical has ever run for the account. Root is the usual victim, since /root on our cloud image had no .config directory either, which explains the /root/.vnc to /root/.config/tigervnc variant of this error. Create the parent directory and start the display again (display 3 in our test, so use your own number).
mkdir -p ~/.config
vncserver :3
This time the migration runs and the display starts:
/usr/bin/xauth: file /home/vnctest/.Xauthority does not exist
New Xtigervnc server 'vnc01:3 (vnctest)' on port 5903 for display :3.
Use xtigervncviewer -SecurityTypes VncAuth -passwd /home/vnctest/.config/tigervnc/passwd :3 to connect to the VNC server.
List both directories to see what the migration did:
ls -la ~/.vnc ~/.config/tigervnc
The password file has moved into the new directory and ~/.vnc is now a symlink to it, so old scripts that still point at ~/.vnc keep working.
lrwxrwxrwx 1 vnctest vnctest 16 Oct 21 09:58 /home/vnctest/.vnc -> .config/tigervnc
/home/vnctest/.config/tigervnc:
total 20
drwxr-xr-x 2 vnctest vnctest 4096 Oct 21 09:58 .
drwxr-xr-x 5 vnctest vnctest 4096 Oct 21 09:58 ..
-rw------- 1 vnctest vnctest 8 Oct 21 09:58 passwd
-rw-r--r-- 1 vnctest vnctest 750 Oct 21 09:58 vnc01:3.log
-rw-r--r-- 1 vnctest vnctest 6 Oct 21 09:58 vnc01:3.pid
If you hit the error as root, the same mkdir fixes it, but the better fix is not to run a desktop as root at all. Give the desktop to a normal sudo user, as this guide does, and keep root for sudo. On Debian the same layout change applies, and our TigerVNC on Debian guide covers that side.
Keep port 5901 closed and tunnel over SSH
Leave the default alone. TigerVNC already keeps 5901 on loopback, and after a reboot the systemd unit put it back on 127.0.0.1 and ::1 without any help, so the secure setup is the one you get by changing nothing. Every connection then rides inside SSH, which encrypts the session and makes an attacker get through your SSH login before VNC is even reachable. The tunnel command from earlier is the whole client setup.
ssh -fN -L 5901:localhost:5901 "${SSH_USER}@${HOST_IP}"
Opening 5901 to the network instead swaps that for a much weaker gate. We didn’t test an exposed port, and we wouldn’t run one, for these reasons:
- VncAuth only uses the first eight password characters
- The screen and every keystroke cross the network unencrypted
- Port 5901 is a common target for VNC scanners
- Turning localhost off also changes the security types on offer
Spend the effort on SSH instead, since it’s the one port this setup needs. Keep 5901 off the allow list in your UFW firewall rules, and turn off SSH password logins once your key works. Close the backgrounded tunnel when you’re done with the desktop, because it stays up after the viewer exits. Check what is running first:
pgrep -af "^ssh .*-L 5901:localhost:5901"
Only the tunnel process matches:
808524 ssh -o BatchMode=yes -fN -L 5901:localhost:5901 [email protected]
Then kill it with the same anchored pattern:
pkill -f "^ssh .*-L 5901:localhost:5901"
The ^ssh anchor is what makes this safe. An unanchored -f pattern matches the full command line of every process, so it would also kill any shell or script whose command line happens to contain the string. In our test pkill returned 0, a fresh nc -z -w2 localhost 5901 then failed with exit code 1, and the same pgrep found nothing, so the forward was gone.
If several people need desktops from a browser without SSH clients, a gateway from our open source remote desktop tools roundup fits better than opening VNC. The TigerVNC project site documents the TLS and X509 security types for cases where a tunnel is impossible. For one admin reaching one server, though, the SSH tunnel is the setup we’d use, and it’s the one TigerVNC already expects.