Abstract
The Linux, and more generally Unix, ecosystems have prided themselves on not needing graphical applications to do most tasks. While that is true generally, there are times in which it might be nice to have a graphical application or entire desktop running on a remote machine. This could be useful for things like remote development or running a graphical environment on a headless system such as a raspberry pi.
This blog post is going to cover how to setup and run X11 applications over SSH as well as how to tunnel VNC connections over SSH making using VNC over a hostile network (like the Internet) less risky. While everything that will be covered in this is probably possible one way or another on Wayland, I will not be covering or even speculating on how one might go about it; however, that would be a great topic for someone to cover in their own blog!
Setting Up
Before getting started, we will have to install some sort of a
Unix-like machine with X11. I went with Slackware for this, but
ultimately it should not really matter as long as Xorg and the OpenSSH
server are installed. Then enable X11 forwarding in
sshd_config
, which may be disabled by default:
diff -u /home/fac3/Desktop/ssh/sshd_config.old /home/fac3/Desktop/ssh/sshd_config
--- /home/fac3/Desktop/ssh/sshd_config.old 2025-07-27 14:31:39.175870567 -0400
+++ /home/fac3/Desktop/ssh/sshd_config 2025-07-27 14:31:49.717583900 -0400
@@ -91,7 +91,7 @@
#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no-#X11Forwarding no
+X11Forwarding yes
#X11DisplayOffset 10
#X11UseLocalhost yes #PermitTTY yes
After that, restart the sshd
service:
# For systemD users:
systemctl restart sshd
# For Runit users:
sv restart sshd
# For FreeBSD users
service sshd restart
# For Slackware
/etc/rc.d/rc.sshd restart
In addition, make sure the user is in the video
group;
this can be checked by typing groups
as the relevant user.
If the user is not in that group:
# For Linux
/sbin/usermod -aG video ${USER}
# For FreeBSD
pw groupmod video -m ${USER}
The logout and log back in for the changes to take effect. Now the
only thing that really needs to be done is logging in correctly, and
setting the DISPLAY
environment variable.
# SSH to other machine allowing X forwarding
ssh -X ${USER}@192.168.122.218
# Setting $DISPLAY variable so there is a 'screen' for the applications to run on
export DISPLAY=:0
# Launching graphical application
firefox
A few points on this, while most documentation will use the
-X
flag when forwarding X11 applications over SSH, there is
also the -Y
flag that will work as well, from the ssh man
page the -X
flag:
Enables X11 forwarding. This can also be specified on a per-host basis in a configuration file. X11 forwarding should be enabled with caution. Users with the ability to bypass file permissions on the remote host (for the user’s X authorization database) can access the local X11 display through the forwarded connection. An attacker may then be able to perform activities such as keystroke monitoring.
While the -Y
flag:
Enables trusted X11 forwarding. Trusted X11 forwardings are not subjected to the X11 SECURITY extension controls.
I have even seen some posts on things like Stackoverflow that use
both flags in the same command; in general, I would recommend sticking
with just the -X
flag though as completely disabling the
limited security that X11 has is just not a great idea, and modern
computers are powerful enough to be able to work through the overhead
without noticing much if at all.
SSH’s X11 forwarding will also not allow for running remote desktops,
so even if we manually run something like startxfce4
, it
will eventually fail because X11 does not detect a screen of any kind.
For doing an entire remote desktop, another tools needs to be used, such
as VNC.
VNC Over SSH
There are times in which it could be beneficial to run an entire GUI session on a remote headless machine, and while VNC does this well, it is not necessarily a secure protocol. So, if needing to VNC into a remote machine over the Internet or some other untrusted network, it will need to be done through a security layer. SSH is a good choice for this because it is a well known and secure protocol that is available on most every platform; additionally, for Unix machines, there is a decent chance that SSH is already configured, enabled, and hardened to be able to do this securely.
Before actually being able to run VNC over SSH though, we will need
to get a VNC server. The full Slackware install comes with TigerVNC
pre-installed so that is what I will be using. In the event that you are
not using Slackware, installing it is as easy as downloading it from
your distro’s package manager. It should be called something like
tigervnc
or tigervnc-server
. From there, we
can setup our SSH connection to port forward the port for the VNC
server.
The SSH command to do this is rather straightforward.
ssh -C -L 5900:localhost:5900 ${USER}@192.168.122.218
And technically only the -L
flag is required, however,
the -C
flag can help with performance. From the ssh man
page The -C
flag does:
Requests compression of all data (including stdin, stdout, stderr, and data for forwarded X11, TCP and UNIX-domain connections). The compression algorithm is the same used by gzip(1). Compression is desirable on modem lines and other slow connections, but will only slow down things on fast networks. The default value can be set on a host-by-host basis in the configuration files; see the Compression option in ssh_config(5).
and the -L
flag does:
Specifies that connections to the given TCP port or Unix socket on the local (client) host are to be forwarded to the given host and port, or Unix socket, on the remote side. This works by allocating a socket to listen to either a TCP port on the local side, optionally bound to the specific bind_address, or to a Unix socket. Whenever a connection is made to the local port or socket, the connection is forwarded over the secure channel, and a connection is made to either host port hostport, or the Unix socket remote_socket, from the remote machine.
Port forwardings can also be specific in the configuration file. Only the superuser can forward privileged ports. IPv6 addresses can be specified by enclosing the address in square brackets.
By default, the local port is bound in accordance with the GatewayPorts setting. However, an explicit bind_address may be used to bind the connection to a specific address. The bind_address of “localhost” indicates that the listening port be bound for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces.
On the remote machine, make sure the VNC server is running, the
command looks like vncserver :0
on the Slackware machine I
am working with, but that may vary depending on the VNC server and
whether it is running as a service. Then on the VNC viewer, connect to
localhost:5900
, type in the password and enjoy the
connection. Something worth noting is that VNC works on port 5900+n
where ‘n’ is the number of “screens”. So if we ran
vncserver :1
instead of vncserver :0
, we would
need to forward port 5901 on our SSH command, which would look as
follows:
ssh -C -L 5901:localhost:5901 ${USER}@192.168.122.218
This would continue up for each additional screen.
The final note on this, at least in Slackware during my testing for
this blog post, if the .xinitrc
file was not executable,
than the VNC server would ignore the file, meaning that nothing in the
.xinitrc
would actually be relevant in the VNC session.
After changing the execution mode, it began applying the commands in
that file to the VNC session (such as changing from xfce to KDE or
Fluxbox).