Repository navigation
Remote GUI
Running GUI programs on a remote host and showing them locally, when ssh -X isn't enough. Covers three shapes: individual GUI apps, full remote desktops, and game-quality streaming.
Pick in this order. Stop at the first match.
- Is the task dev / shell / code? → Use the already-set-up stack: Zed (SSH-remote), Windsurf (fallback), Claude Code over SSH, tmux. Thin local footprint, all the cost on the remote. Don't reach for anything else.
-
Need to run one or a few GUI apps remotely? →
wp <host> <app>(awaypipe --no-gpu sshwrapper shipped indot_bash_aliases). Works out of the box on every Linux host in the fleet — no per-host setup, no credentials, no keyring, no firewall rule, no GRD configuration. Just needs thewaypipebinary on both ends (installed by default via our apt/dnf lines on all Linux hosts) and passwordless SSH (already set up). ~100–200 MB local per app window. This is the fleet-wide baseline; reach for it first. Works for browser, file manager, text editor, terminal, anything up through moderate GPU use. -
Need a full remote desktop (not just one app)? →
xfreerdp3(client) +gnome-remote-desktop(server on GNOME hosts). ~50 MB local regardless of remote complexity — strictly cheaper than waypipe for seeing many apps at once, but requires per-host setup on the server (credentials, TLS cert, possibly a keyring unlock). Worth it only when #2 is ergonomically wrong for the workload (many windows, window-manager-level interaction, seeing the full desktop). See Server setup below. - Latency-critical (video scrubbing, high-refresh scrolling) and RDP isn't cutting it? → Sunshine (server) + Moonlight (client). ~80 MB client with hardware H.264/H.265 decode. Heavier server setup; use only if #3 falls short.
-
Session needs to survive SSH drops / attach-detach across devices? →
xpra. Niche; covered as a footnote below.
Skip: xrdp (GRD does the job on every GNOME host we have when GRD works), VNC (RDP is strictly better), Guacamole in a browser (browser is the fat part on a low-RAM client).
The practical upshot: wp covers the common case with zero per-host state. Full-desktop RDP is the upgrade path when per-app forwarding isn't enough. On Ubuntu 25.10 the full-desktop path had a known bug (see below), so wp did the heavy lifting fleet-wide for a while; Ubuntu 26.04 with GRD 50 fixed it, and wp is still the right first move for one app.
Both installed on GUI hosts only (.hasGui = true):
| Tool | Linux package | macOS | Purpose |
|---|---|---|---|
xfreerdp3 |
freerdp3-x11 (Ubuntu 24+/Debian 13+), freerdp2-x11 (older), freerdp (Fedora) |
brew freerdp
|
Full remote desktop client |
waypipe |
waypipe (apt + dnf, in the general list) |
— (Wayland-only, not applicable) | Per-app Wayland forwarding |
On macOS, Windows, and iPad, use a native RDP client (Microsoft Remote Desktop) instead of xfreerdp if it's already installed.
xfreerdp3 /u:"$USER" /v:remote-host +clipboard +dynamic-resolution /cert:ignoreKey flags:
-
/u:<user>— GRD username on the remote host, not a Unix login. By default GRD picks your remote Unix username, which usually matches$USERon the client — that's why"$USER"is a safe default. If you run the client as a different account than the one GRD is advertising on the remote, pass the GRD username explicitly (/u:eugene). -
/v:<host>— host or IP.xfreerdp3does NOT read~/.ssh/config, so SSH aliases don't resolve here; pass the hostname or IP directly. -
+clipboard— shared clipboard in both directions (needs GRD 46+; earlier versions only do remote→local). -
+dynamic-resolution— resize the session window and it resizes the remote desktop in real time. -
/cert:ignore— don't fail on the self-signed cert GRD generates. Fine for LAN; on WAN, pin the fingerprint with/cert:tofu(trust-on-first-use) or provision a real cert.
Authentication: GRD uses a username + password pair, stored in the remote's GNOME keyring. This is NOT your Unix password and NOT SSH keys — it's a separate credential GRD manages. Set both from Settings → System → Remote Desktop on the remote. Any local Unix account that knows the GRD credentials can connect (including non-sudo users); the sudo group has nothing to do with it.
waypipe ssh remote-host nautilusAny app the remote can run, shown as a native local window. Works only when both ends are Wayland. Needs waypipe on the remote too (installed by chezmoi on hasGui Linux hosts; present on GNOME/KDE Wayland distros by default).
Not installed by chezmoi. Install on-demand if you need session persistence across SSH drops:
# Server (remote):
xpra start ssh://<host>/100 --start=<app>
# Client (local):
xpra attach ssh://<host>/100Set the remoteDesktop role during chezmoi init --apply (defaults to client on GUI hosts):
# First-time setup: answer "server" or "both" at the Remote-desktop role prompt.
# Override later by editing ~/.config/chezmoi/chezmoi.toml:
# remoteDesktop = "server"
chezmoi init --promptString remoteDesktop=server uhop/dotfilesWith remoteDesktop set to server or both on a GNOME host, chezmoi targets the --system daemon — not the user-scope daemon. Rationale: user-mode stores credentials in the GNOME keyring (libsecret), and the default collection is locked over SSH without a live GUI login. The --system daemon stores credentials in /etc/gnome-remote-desktop/ (root:gnome-remote-desktop, mode 0600, TPM-encrypted when a TPM is present, GKeyFile otherwise) — no keyring dance. It's also semantically the right match for "accept RDP connections including when nobody's logged in."
If passwordless sudo is available to chezmoi apply, the automation runs the full setup:
-
Generates a self-signed TLS cert+key (10-year) at
/etc/gnome-remote-desktop/rdp-tls.{crt,key}. -
sudo grdctl --system rdp enable / disable-view-only / set-port 3389 / set-tls-cert / set-tls-key. -
sudo systemctl enable --now gnome-remote-desktop.service(system scope). -
Disables any stray user-mode daemon (
systemctl --user disable --now gnome-remote-desktop.service+grdctl rdp disable) to free port 3389. -
Opens UFW port 3389 via the
-grd-firewallscript where UFW is installed and active. Prints hints forfirewalld/iptables/nftablesotherwise.
If passwordless sudo is not available (default, since /etc/sudoers.d/chezmoi doesn't allowlist grdctl), the script writes a helper to /tmp/finish-grd-setup.sh and tells you to run it. The helper is idempotent, prompts for the GRD username and password in a TTY (read -rp / read -rsp), and supplies them to grdctl directly — no <grd-user> / <grd-password> placeholder strings you could accidentally paste as literals:
bash /tmp/finish-grd-setup.shSubsequent chezmoi applies see the daemon already running and no-op.
Credentials are never baked into chezmoi source. The same /tmp/finish-grd-setup.sh helper is also generated when the system daemon is configured but credentials are still unset (e.g., sudo was available on apply but no TTY to prompt). Run it the same way. For automation, pass credentials via env (avoid shell history — read from a secret store):
GRD_USER=... GRD_PASSWORD=... bash /tmp/finish-grd-setup.shTest from a client once credentials are set (substitute your chosen username and the target host):
xfreerdp3 /u:YOUR-GRD-USER /v:YOUR-HOST +clipboard +dynamic-resolution /cert:tofuThe GRD username is GRD-specific — doesn't have to match any Unix account. The password lives under /etc/gnome-remote-desktop/, encrypted at rest (TPM-backed when available else GKeyFile).
Non-GNOME hosts (KDE, XFCE, ...) fall through — the automation gates on .detect.desktop == "gnome". KDE's equivalent is krdp (Plasma 6.1+); xrdp covers non-GNOME Linux. Not automated today; manual setup, or file a follow-up.
Resolved. Verified 2026-09-16: with gnome-remote-desktop 50.2-0ubuntu0.1 on Ubuntu 26.04.1, an xfreerdp3 /v:box /cert:tofu login (credentials at the prompt) reaches the desktop through the system-mode handover, and journalctl -b | grep -i libei-connection on the host is empty. The chezmoi automation needed no change. The rest of this section stays for any host still on 25.10.
gnome-remote-desktop 48.0-4ubuntu0.2 on Ubuntu 25.10 has an assertion failure in the remote-login handover path. After the system daemon accepts auth and tries to hand the client off to a fresh GDM session, the per-session daemon crashes with:
gnome-remote-desktop-daemon: ../src/libei-connection.c:75:
ei_connection_get_context: Assertion `connection' failed.
Symptom from the client: blank black RDP window for ~10 seconds, then Connection reset by peer (ERRCONNECT_CONNECT_TRANSPORT_FAILED). Confirmed 2026-04-17 on one host; reproducible on any Ubuntu 25.10 + GNOME host.
Workaround — user-mode GRD attached to an existing graphical session:
- Log in graphically on the target host's console (physical monitor or remote KVM) with your password — this unlocks the GNOME keyring via PAM. An existing text/tty session doesn't unlock the keyring; graphical login is required.
-
Configure user-mode GRD in that session (open a terminal on the GNOME desktop, not SSH):
grdctl rdp enable grdctl rdp disable-view-only grdctl rdp set-port 3389 grdctl rdp set-tls-cert ~/.local/share/gnome-remote-desktop/rdp-tls.crt grdctl rdp set-tls-key ~/.local/share/gnome-remote-desktop/rdp-tls.key grdctl rdp set-credentials <grd-user> <grd-password> systemctl --user restart gnome-remote-desktop.service
-
Stop the broken system daemon so it doesn't fight for port 3389:
sudo systemctl disable --now gnome-remote-desktop.service
- Leave the graphical session logged in. User-mode GRD attaches to that live session (shared-desktop model), so the session must exist when a client connects. If the host reboots, repeat steps 1-2.
Downside: needs a live user session; no handover to a fresh session. Upside: avoids the libei crash entirely, keyring-unlock side effect included. Good fit for trusted home / LAN hosts where someone's logged in anyway.
That is what happened: the fixed gnome-remote-desktop arrived with Ubuntu 26.04, and the system-mode automation in the chezmoi script resumed working without changes (it's idempotent).
Don't want to log in graphically on every server just to RDP into it? For single-app workflows, waypipe forwards a remote app's Wayland surface over SSH. No handover, no keyring, no GRD involvement. Works on every host-pair where both ends run Wayland (GNOME on Ubuntu 24+). Already installed on every .hasGui host via the apt general list.
Use --no-gpu for SSH'd server sessions — by default waypipe tries to share GPU buffers via DMA-BUF at /dev/dri/renderD128, which is mode 0660 group render and governed by logind's seat0 ACL. SSH sessions aren't granted that ACL (no seat), so waypipe fails with S...: Failed to open drm fd: Permission denied and hangs. --no-gpu falls back to shared-memory transport — slightly slower on GPU-heavy apps (video, 3D), unnoticeable on everything else (calculator, file manager, text editor, terminal, browser).
The dotfiles dot_bash_aliases defines a wp function that does this:
wp() { waypipe --no-gpu ssh "$@"; }Usage from a Wayland terminal (so $WAYLAND_DISPLAY is set):
wp <host> <app> [args...]
# Examples
wp box gnome-calculator
wp box nautilus
wp box firefox
wp box -- bash -c 'cd ~/some-project && code .'Each invocation = one local window running a single remote app. When the remote app exits, the local window closes. No session state.
Want GPU sharing too? (For large apps where software rendering is noticeable.) Add yourself to the render group on the target host, then log out + back in fully:
ssh <host> 'sudo gpasswd -a $USER render'
# Log out of the host and re-ssh so group membership picks up.After that, waypipe ssh <host> <app> (without --no-gpu) works. The wp helper still forces --no-gpu for consistency across hosts where the group add hasn't been done; drop the flag manually on a per-invocation basis when you want GPU sharing.
Caveats:
- Both ends need
waypipe(yes — shipped via our apt general list on all.hasGuiLinux hosts). -
macOS and Windows clients can't use
wp. Wayland doesn't exist on macOS (Quartz/Cocoa) or Windows (Win32). No remote Wayland surface can render locally. For a Mac/Windows client needing remote Linux GUI: use RDP (MS Remote Desktop) against a GRD host, or SSH+XQuartz for one-off X11 apps, or run a Linux VM andwpfrom inside it. - Large/GPU-heavy apps still want native; waypipe forwards the compositor surface but GPU rendering happens remotely then blits locally — fine for most GUIs, limited for video scrubbing.
- Ignore
libEGL warning: failed to get driver name/MESA: error: ZINK/ similar on--no-gpu— the app probed for GPU, didn't get it, fell back to software. No impact on usability.
If you want a remote app that persists across disconnects (laptop sleep, Wi-Fi flap), install xpra on-demand (not shipped by default):
sudo apt install xpra # on both ends
# Server (remote):
xpra start ssh://eugene@box/100 --start=firefox
# Client (local):
xpra attach ssh://eugene@box/100
# (Detach any time with Ctrl+C; the remote app keeps running.)
# Re-attach later from the same or a different client:
xpra attach ssh://eugene@box/100Niche — waypipe covers most per-app needs without the session-persistence overhead. Reach for xpra only when you specifically need attach/detach.
Older distro (Ubuntu 22.04, Debian 12): you have freerdp2-x11 and the binary is xfreerdp (no 3 suffix). Use xfreerdp instead.
You're running xfreerdp3 from an SSH session that has no attached X server or Wayland compositor. RDP clients need a local display to render into. Open a terminal on the client host's physical desktop session (not via SSH) and re-run. Alternatively, ssh -X <client-host> forwards X11 over SSH if you really need to invoke it remotely, but that's a weird workflow — the RDP window would be double-tunneled.
IPv6 stall — same as other bootstrap hangs. If the remote resolves an AAAA record but doesn't route public IPv6, RDP stalls. Fix either end's /etc/gai.conf to prefer IPv4 (precedence ::ffff:0:0/96 100).
Clipboard relay requires GRD 46+ (GNOME 46+). Earlier versions only support remote → local direction. Upgrade GNOME or use a drag-and-drop workaround.
Some distros ship an AppArmor profile for waypipe that blocks its socket. Check journalctl -xe | grep -i apparmor. Fix: sudo aa-disable /etc/apparmor.d/usr.bin.waypipe if you're sure about the security tradeoff, or write a custom profile.
xrdp (the non-GNOME-native alternative to GRD) creates a SEPARATE Xorg session, not attaching to your existing Wayland session. If you need to see what's on the physical console, use GRD, not xrdp.
GRD's user+password is NOT your Unix credentials. Set via Settings → System → Remote Desktop. The GRD username defaults to your Unix username; the password is a fresh one GRD manages in the GNOME keyring.
grdctl rdp set-credentials fails with BIO_new failed for certificate or RDP server certificate is invalid
GRD needs a TLS cert + key pair before it can accept any configuration that spins up the RDP server. The chezmoi automation generates a self-signed pair at ~/.local/share/gnome-remote-desktop/rdp-tls.{crt,key} on first apply. If you hit this error outside the automated path, run:
mkdir -p ~/.local/share/gnome-remote-desktop
openssl req -newkey rsa:4096 -nodes -x509 -days 3650 \
-keyout ~/.local/share/gnome-remote-desktop/rdp-tls.key \
-out ~/.local/share/gnome-remote-desktop/rdp-tls.crt \
-subj "/CN=$(hostname)"
grdctl rdp set-tls-cert ~/.local/share/gnome-remote-desktop/rdp-tls.crt
grdctl rdp set-tls-key ~/.local/share/gnome-remote-desktop/rdp-tls.keyClients connect with /cert:ignore or /cert:tofu anyway; no trusted-CA cert needed.
The GNOME keyring's default collection is locked. libsecret can't write the credential until it's unlocked. Two paths:
- Log in once on the GNOME desktop locally. A graphical login unlocks the keyring for the session; subsequent SSH commands inherit the unlocked state via D-Bus.
-
Headless-only host, no GUI login available: unlock from the shell. Requires the keyring password, which matches your Unix login password on a fresh GNOME install unless you changed it:
Or via
echo -n 'YOUR_LOGIN_PASSWORD' | gnome-keyring-daemon --unlock grdctl rdp set-credentials <grd-user> <grd-password>
secret-tool:(The query text is irrelevant;secret-tool search --all --unlock service whatever 2>/dev/null || true
--unlocktriggers the unlock prompt.)
-
KDE / xrdp server automation — current automation gates on
.detect.desktop == "gnome". If a KDE host enters the fleet, add a KDE branch (Plasma 6.1+ shipskrdp); xrdp for non-GNOME, non-KDE. -
Moonlight client / Sunshine server — server-side install deferred. Known target workflow: streaming games from a GPU workstation to Moonlight on Android TV / iPad (clients already installed outside dotfiles; App Store / sideload, not chezmoi-managed). When the first workstation needs streaming, extend the
remoteDesktoprole to cover Sunshine + add arun_onchange_after_install-sunshine.sh.tmpl(systemd user unit + ports 47984/47989/48010 TCP + 47998-48000 UDP +loginctl enable-lingerfor headless). Today:detect::has_sunshineprobes for Sunshine presence (a hint on hosts where someone's installed it manually); nothing is wired through the resolver. -
petsnippets — per the CLI Help Terseness convention, promote the one-liners above topetonly after observing them in daily use. Today: wiki-only.
Initial setup
General information
- Shell Environment
- Git Configuration
- Application Notes
- Remote GUI
- Utilities
- Playbash Server Management
Day-to-day operations