freedzhost.cloud

SSH for an agent workstation: instant, safe, and four traps

Level IntermediateEstimated time 30 minRead first A second-hand laptop as a comfortable Linux workstation (Omarchy)A word you don't know? Glossary

From the laptop, everything goes over SSH: to the server, the guard machine, the virtual machines. You type a few commands an hour; an agent sends hundreds. At that rate, each slow handshake and each quoting mistake adds up, and some mistakes run code you never meant to run. Four settings and rules, each one paid for once.

What you get. Commands to your machines that answer in about 12 ms instead of 130 to 340, text that can never be executed by accident, windows opened remotely that do not take your game down when closed.

1. Share one connection per machine

# ~/.ssh/config
Host server
    HostName server.lan
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

The first connection opens a master; the next ones ride on it for ten minutes. Measured here: first call 130 to 340 ms, the following ones about 12 ms.

Put it on your aliases only. Long-lived tunnels (autossh) should connect by address, so they never become the shared master that everyone else depends on.

2. Free text travels by standard input, never in the command line

The command you pass to ssh is read again by the remote shell. Put a pull request body or an agent's output in it, and its backticks and $( ) are executed on the other side. It happened to us: the backticks of a PR description ran as commands. They did not exist, by luck.

# no
ssh server "open-pr --body '$BODY'"
# yes: the text on stdin, a script on the other side builds the arguments itself
printf '%s' "$BODY" | ssh server 'python3 ~/bin/open_pr_stdin.py'

3. Open a window on another machine, properly

A graphical app started over SSH needs the session's whole environment, read from a program already running in that session:

tr '\0' '\n' < /proc/$(pgrep -u "$USER" -x steam | head -1)/environ \
  | grep -E '^(WAYLAND_DISPLAY|DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION_BUS_ADDRESS|XAUTHORITY)='

Then start it detached, with that environment:

setsid nohup env WAYLAND_DISPLAY=... DISPLAY=... XDG_RUNTIME_DIR=... \
  DBUS_SESSION_BUS_ADDRESS=... XAUTHORITY=... lutris >/tmp/lutris.log 2>&1 </dev/null & disown

XAUTHORITY is not optional: without it, anything going through XWayland (Wine games included) crashes at start. Its file name changes at each session: read it, never hard-code it.

Do not use systemd-run --user --collect for this. When the app is closed, systemd cleans up its whole group of processes: we lost a game launcher, a running game and an 84 GB download half-way.

4. Never kill by name pattern

pkill -f something matches every process whose command line contains "something". Over SSH, that includes the remote shell running your command, and the SSH server process carrying it: your session dies mid-work. It happened twice in one day here; we now block it mechanically in the agent's hooks.

pgrep -f "[m]y-server"      # the brackets: the pattern no longer matches its own command line
kill $(pgrep -f "[m]y-server")

Traps

A user service does not have ~/.local/bin in its PATH. Give full paths in your units.

A shared connection survives a network change badly. If commands hang after you change Wi-Fi, close the master: ssh -O exit server.

Found this useful? Tell us what to measure next.