Xorg from unprivileged user

Abstractly.
There is some software that needs X’s.
Download, install, launch - enjoy.
But here’s the problem: I don’t want to run software (absolutely everything that is not included in the standard debian repository.) like this on:

  1. My HOST.
  2. From my user.
  3. Under my user’s Xorg.
  4. Allow into my networks, including 127.0.0.0

In addition, a browser for regular web surfing and a browser for client banking are not the same browser, user, and sometimes even system.
We will not consider points 1, 2, 4 now; we will talk about X.

In debian, with standard system settings, LightDM is used as the default display manager.
You can enable listen tcp in it, but it runs Xorg processes as root.
In gdm3, on the contrary, by default, it launches Xorg from the user who logs into the environment, but the ability to enable listen tcp was broken.
More precisely, they left the ability to disable nolisten tcp,
but not enable listen tcp.

To do this, you need to edit the wrapper over X.

First, install gdm3 as the default DM, if it is not there, install it via apt.
dpkg-reconfigure gdm3

Allow connection via tcp


File /etc/gdm3/custom.conf, if it doesn’t exist, create it

1
2
3
4
5
6
[security]
DisallowTCP=false
AllowTCP=true

[xdmcp]
ServerArguments=-listen tcp


In /etc/gdm3/daemon.conf add lines

1
2
3
4
5
6
[security]
DisallowTCP=false
AllowTCP=true

[xdmcp]
ServerArguments=-listen tcp


DisallowTCP=false will actually disable the -nolisten tcp,
but neither AllowTCP=true,
nor ServerArguments won’t turn on -listen tcp

To do this, edit the wrapper script /usr/bin/X, «It’s perfectly safe, I assure you».
However, please note that when you update the xorg package, this file will be overwritten.
And besides the necessary -listen tcp, let’s add a little more code.

Debian “bookworm”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh
#
# Execute Xorg.wrap if it exists otherwise execute Xorg directly.
# This allows distros to put the suid wrapper in a separate package.

basedir=/usr/lib/xorg
runuser=`/usr/bin/whoami`
if [ -x "$basedir"/Xorg.wrap ]; then
    if [ "$runuser" = "myuser" ]; then
        exec "$basedir"/Xorg.wrap -listen tcp :0 "$@"
    elif [ "$runuser" = "vmuser" ]; then
        exec "$basedir"/Xorg.wrap -listen tcp :5 "$@"
    elif [ "$runuser" = "unprivilegeduser" ]; then
        exec "$basedir"/Xorg.wrap -listen tcp :25 "$@"
    elif [ "$runuser" = "anotheruser" ]; then
        exec "$basedir"/Xorg.wrap -listen tcp :325 "$@"
    else
        exec "$basedir"/Xorg.wrap -listen tcp "$@"
    fi
else
    exec "$basedir"/Xorg -listen tcp "$@"
fi

Debian “trixie”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh
#
# Execute Xorg.wrap if it exists otherwise execute Xorg directly.
# This allows distros to put the suid wrapper in a separate package.

basedir=/usr/lib/xorg
runuser=`/usr/bin/whoami`
if [ -x "$basedir"/Xorg.wrap ]; then
    if [ "$runuser" = "myuser" ]; then
        exec "$basedir"/Xorg.wrap "$@" -listen tcp ':0'
    elif [ "$runuser" = "vmuser" ]; then
        exec "$basedir"/Xorg.wrap "$@" -listen tcp ':5'
    elif [ "$runuser" = "unprivilegeduser" ]; then
        exec "$basedir"/Xorg.wrap "$@" -listen tcp ':25'
    elif [ "$runuser" = "anotheruser" ]; then
        exec "$basedir"/Xorg.wrap "$@" -listen tcp ':325'
    else
        exec "$basedir"/Xorg.wrap "$@" -listen tcp
    fi
else
    exec "$basedir"/Xorg "$@" -listen tcp
fi


I think it’s clear, my user, myuser, gets -listen tcp :0 - the standard port :0
It is worth clarifying here that if you have, for example, three users, the main one is always in the system.
And two others that can alternate in use (login/logout).
Then, with the standard setup, the main one will get port :0, the next one who logs in, for example user2, will get xorg accepting connections on :1 (:6001), the next user3 - on port :2 (:6002), but if user2 (:1) logs out and logs in again, xorg will get the next free port :3 .
This means that the alternation of ports will be random and you will have to constantly specify the required port when launching the required application.

And this is how we achieve for each specific user the launch of xorg with -listen tcp on his own port.

  1. myuser - :0
  2. vmuser - :5
  3. unprivilegeduser - :25
  4. anotheruser - :325
  5. rest users - -listen tcp, and the next free port, that is 1,2,3,4,6, and so on.


The point is that in kvm, in which the environment is already configured, you can now run an application and it will connect to the exactly xorg/user you need.
For example: DISPLAY=10.1.1.1:25.0 /usr/bin/gedit
Access is controlled at the iptables level, and the X of a specific user, for example, for unprivilegeduser it is port 6025.

1
  iptables -A INPUT   -s 10.1.1.125/32 -p tcp -m tcp --dport 6025 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT    # Xorg access


In addition to iptables, the user’s xorg session must allow network connections
/usr/bin/xhost + 10.1.1.125, you can automate it as you want, or via ~/.profile, I have it set up in ~/.config/autostart/init.sh.desktop

1
2
3
4
5
[Desktop Entry]
Exec=/usr/local/bin/unprivilegeduser-init.sh
Name=unprivilegeduser-init.sh
Terminal=False
Type=Application


The unprivilegeduser-init.sh script contains all the necessary commands,
including xhost + 10.1.1.125
In kvm, in .bash_profile for the required user, add a line
export DISPLAY=10.1.1.1:25.0
Now, when you run an application in kvm, it will be displayed in the X session of the unprivilegeduser user.

Not enough? Let’s ban all networks for this user


1
2
3
4
5
6
7
8
9
10
11
12
13
id unprivilegeduser
uid=12345(unprivilegeduser) gid=12345(unprivilegeduser) groups=12345(unprivilegeduser)

  # 12345 unprivilegeduser
  iptables -I OUTPUT -o tun11   --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o tun12   --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o tun15   --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o kvmbr0  --match owner --uid-owner 12345 -j ACCEPT               # xorg
  iptables -I OUTPUT -o wlan0   --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o eth0    --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o eth1    --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o wg99    --match owner --uid-owner 12345 -j DROP
  iptables -I OUTPUT -o lo      --match owner --uid-owner 12345 -j DROP


Important Notes


Let me remind you that you currently have two users in your area of ​​attention:
unprivilegeduser - at the host level that Xorg is running from, which should not have access to any networks other than kvmbr0.
And some user, for example, insidekvmuser from which gedit is launched in kvm.
Next, to redirect all traffic to the tunnel, you need to configure the network specifically for the insidekvmuser user.

This example does not guarantee complete isolation of these users’ processes, but it allows you not to worry about the clipboard buffer, about information on the display from other applications and access to system and user directories.

Why not use vncviewer/virt-viewer/spice?


Yes, these options exclude the host system, leaving all processes in the virtual kvm/lxc environment, but the rendering speed of vncviewer or spice cannot be compared with a network connection to xorg, it is only slightly behind the native one in speed. But here it’s up to you to decide.