← All posts
Jun 30, 2026

Persistent SSH and Tailscale on Steam Deck / SteamOS Across Updates

TL;DR: SteamOS uses an immutable A/B rootfs and an overlayfs /etc whose upper layer is selectively discarded on updates (SteamOS 3.6+), meaning services enabled naively and configs dropped in /etc disappear after the next OTA without warning. SSH survives because its binary lives in the immutable image — the enable symlink and key-only config need explicit protection via /etc/atomic-update.conf.d/. Tailscale requires the official deck-tailscale installer rather than pacman, which places binaries in /opt/tailscale/ — a path backed by /home/.steamos/offload/opt via bind mount, persistent by design, no whitelist required. Steps: set the deck password, enable sshd, add your public key to ~/.ssh/authorized_keys, create /etc/ssh/sshd_config.d/30-keyonly.conf to disable password auth, register the enable symlink and config in /etc/atomic-update.conf.d/sshd-persistent.conf; then clone and run tailscale.sh, authenticate with tailscale up --qr --operator=deck --ssh (add --login-server for Headscale), and lock in tailscale set --auto-update.

SteamOS’s update model is designed around gaming, not server administration. The same properties that make updates safe and atomic — immutable rootfs, overlayfs /etc, A/B partition swap — create a trap for anyone who follows standard Linux instructions without understanding what actually persists. Getting SSH and Tailscale to survive updates means working with the persistence model, not around it.

You SSH into the Steam Deck. It works. A week later you update SteamOS, try to connect: nothing. No service, no Tailscale node in the tailnet, just silence. You didn’t break anything — the update did exactly what it was designed to do, which is restore the system to a known-good state. You just didn’t know what “known-good” meant for /etc.

This is for homelab operators treating the Steam Deck as a node — remote access, always-on tailnet presence. It assumes familiarity with systemd and SSH key auth. Everything here was done on SteamOS 3.6+ (Arch Linux-based); the overlayfs whitelist mechanism is specific to that version and later.

See also Headscale and Tailscale on OPNSense for the router/subnet side of the same tailnet — this post covers a client, that one covers the gateway.

Table of Contents

Open Table of Contents

The three persistence tiers

Direct answer: SteamOS has three storage tiers with different persistence guarantees, and getting them confused is why setups vanish after updates. The rootfs (A/B partitions, holds /usr and the sshd binary) is replaced wholesale on every update — anything written there via pacman disappears. /etc is an overlayfs whose upper layer survived every update until SteamOS 3.6 added a selective discard pass, so systemd enable symlinks and config drop-ins there now need explicit whitelisting to survive. /home is a separate partition untouched by the A/B swap — authorized_keys there is safe permanently, short of a factory reset. /opt is bind-mounted from /home/.steamos/offload/opt, so anything installed there physically lives in /home and persists automatically, no whitelist needed — this is why the official Tailscale installer targets /opt/tailscale/ instead of following the more conventional pacman route that everything else on a normal Linux box would use.

The immutable rootfs is an A/B partition pair. Updates swap the active partition to a freshly built image. Everything here — system binaries, most of /usr, the sshd binary itself — is replaced wholesale. pacman writes here. This is why pacman -S tailscale is what Tailscale’s own documentation calls a time bomb: it works until the next update swaps the partition, then disappears without warning.

/etc: the overlayfs that selectively forgets

/etc is an overlayfs layered on top of the rootfs /etc:

overlay on /etc type overlay (rw, lowerdir=/sysroot/etc, upperdir=/sysroot/var/lib/overlays/etc/upper, ...)

All modifications to /etc land physically in /var/lib/overlays/etc/upper, not on the rootfs partition. This lets them survive the A/B swap in theory — and they did, until SteamOS 3.6 introduced a selective discard mechanism. The updater now decides which modified /etc files to keep and which to throw away after an update, to prevent stale configs from conflicting with the new image. The systemd enable symlink for sshd lives here. Custom sshd_config.d drop-ins live here. Without explicit registration in the whitelist, they may be gone after the next major update.

/home: the partition that always survives

/home always persists. It’s a separate partition, untouched by the A/B swap. ~/.ssh/authorized_keys lives here. Set it once and it survives every update — factory reset excepted.

/opt: bind-mounted into persistence by design

/opt is bind-mounted from /home/.steamos/offload/opt. This is the key insight behind the deck-tailscale installer. Installing binaries to /opt/tailscale/ means they physically live at /home/.steamos/offload/opt/tailscale/, automatically bind-mounted to /opt at boot. Persistent by design, no whitelist required. The SteamOS team built this offload mechanism specifically for third-party software that needs to survive updates.

SSH: key-only, update-safe

Direct answer: SSH survives updates in five steps: set the deck user’s password (lands in /etc/shadow, part of the persisted overlay); systemctl enable --now sshd (the binary is in the immutable image and always present, but the enable symlink lives in the discardable overlay); drop your public key into ~/.ssh/authorized_keys in /home, which is permanent; disable password auth via a sshd_config.d drop-in rather than editing the main config, since SteamOS already includes that directory; and — the step that actually matters — register both the enable symlink and the config drop-in in /etc/atomic-update.conf.d/, which the updater reads before discarding the overlay’s upper layer and explicitly preserves. Skip that last step and a SteamOS 3.6+ update silently deletes the symlink, the config, or both, and SSH access is gone until you redo the setup by hand — no warning, no log entry pointing at the cause, just a Deck that no longer answers on port 22.

Set the deck password

The deck user has no password by default. Without one, sudo won’t work interactively. Set it from the desktop mode terminal:

passwd

The password lands in /etc/shadow, part of the overlayfs upper layer. It’s not on the rootfs, so it survives the A/B swap. A SteamOS update won’t reset it — though a factory reset will.

Enable the SSH daemon

sudo systemctl enable --now sshd

The sshd binary (/usr/lib/systemd/system/sshd.service) is part of the immutable image and survives every update. What systemctl enable creates is a symlink:

/etc/systemd/system/multi-user.target.wants/sshd.service → /usr/lib/systemd/system/sshd.service

This symlink lives in the /etc overlayfs upper layer and is precisely what the SteamOS 3.6+ updater may discard.

Verify the daemon is up:

systemctl status sshd

Expected output:

● sshd.service - OpenSSH Daemon
     Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: disabled)
     Active: active (running) since ...

If it’s enabled but inactive, check journalctl -u sshd -b for the reason before continuing.

Add your public key

On the Steam Deck:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "ssh-ed25519 AAAA... you@machine" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Or from your client machine, if password auth is still on:

ssh-copy-id deck@<deck-ip>

This file lives at /home/deck/.ssh/authorized_keys — the persistent /home partition. Once here, it never needs to be recreated across updates.

Confirm key auth works before disabling passwords:

ssh -o PasswordAuthentication=no deck@<deck-ip>

If this drops you into a shell, proceed. If it prompts for a password, key auth is not working — don’t continue to the next step until it is.

Disable password authentication

Create a drop-in config. SteamOS ships with Include /etc/ssh/sshd_config.d/*.conf in the main sshd_config, so any .conf file in that directory is picked up automatically without touching the main file.

sudo tee /etc/ssh/sshd_config.d/30-keyonly.conf << 'EOF'
PasswordAuthentication no
PubkeyAuthentication yes
EOF

Reload:

sudo systemctl reload sshd

Verify the directives took effect — sshd -T prints the merged runtime config:

sudo sshd -T | grep -E 'passwordauthentication|pubkeyauthentication'

Expected:

passwordauthentication no
pubkeyauthentication yes

If passwordauthentication still shows yes, the drop-in directory may not be included. Check whether Include /etc/ssh/sshd_config.d/*.conf appears in /etc/ssh/sshd_config, and whether it appears before any conflicting PasswordAuthentication directive.

Register both files with the atomic update whitelist

This is the step that makes SSH survive updates.

sudo mkdir -p /etc/atomic-update.conf.d
sudo tee /etc/atomic-update.conf.d/sshd-persistent.conf << 'EOF'
/etc/systemd/system/multi-user.target.wants/sshd.service
/etc/ssh/sshd_config.d/30-keyonly.conf
EOF

The updater reads atomic-update.conf.d before discarding the overlayfs upper layer and explicitly preserves the listed paths in the new system state. Without this, a SteamOS 3.6+ update may delete the enable symlink (making sshd not start at boot), delete the key-only config (restoring password authentication), or both — silently.

The atomic-update.conf.d directory itself lives in the /etc overlayfs, but the updater processes it before discarding the overlay, so its entries are applied to the incoming update. The listed paths are preserved; everything else in the overlay upper layer is at the updater’s discretion.

Tailscale: install via the official deck script

Direct answer: pacman -S tailscale installs to /usr/bin/, part of the immutable rootfs — it works right up until the next SteamOS update swaps the partition and the binaries vanish, silently, with no error. The fix is the official deck-tailscale installer, which puts the tailscale/tailscaled binaries in /opt/tailscale/ (bind-mounted from /home, so it survives) and writes its systemd unit files directly to /etc/systemd/system/ rather than the wants/ symlink subdirectory the updater discards more aggressively. Unlike SSH, Tailscale’s install doesn’t strictly require atomic-update.conf.d entries — but adding them anyway, using the same whitelist pattern, costs nothing and removes any doubt. After running the script: authenticate with tailscale up --qr (add --login-server for Headscale), then lock in tailscale set --auto-update so the binaries update themselves independently of SteamOS’s own update cycle, so a new Tailscale release never depends on rerunning the installer manually.

Why does pacman fail here?

pacman -S tailscale installs the binaries to /usr/bin/ — the immutable rootfs. The node joins the tailnet, the service appears to run, everything looks fine. Then SteamOS updates, swaps the rootfs partition, and the binaries are gone. The service definition might still exist in the overlayfs (pointing to a path that no longer exists), the node vanishes from the tailnet, and there’s no error — just absence.

This isn’t a corner case. It happens after every SteamOS update.

Clone and run the deck installer

git clone https://github.com/tailscale-dev/deck-tailscale.git ~/deck-tailscale
cd ~/deck-tailscale
bash tailscale.sh

The script writes to several locations, each chosen to survive updates:

PathContentsWhy it persists
/opt/tailscale/tailscale and tailscaled binaries/opt is bind-mounted from /home/.steamos/offload/opt — lives in /home
/etc/systemd/system/tailscale.servicesystemd unit file/etc overlayfs — unit files in system/ (not wants/) survive the updater’s discard pass more consistently
/etc/systemd/system/tailscaled.service.d/override.confBinary path override pointing to /optSame
/etc/default/tailscaledPORT and FLAGS variablesSame
/etc/profile.d/tailscale.shAdds /opt/tailscale/ to PATHSame

The key difference from SSH: the tailscale.sh script does not require entries in atomic-update.conf.d. Tailscale’s service files land directly in /etc/systemd/system/ (not in the wants/ symlink subdirectory), and the updater treats those more conservatively. If you want to be certain — add them to the whitelist anyway using the same pattern as the SSH config. The principle is identical.

After the script finishes, verify:

systemctl status tailscaled

Expected: active (running).

Authenticate

For Tailscale’s own control server:

tailscale up --qr --operator=deck --ssh

For a self-hosted Headscale instance:

tailscale up --qr --operator=deck --ssh --login-server=https://your.headscale.host

--qr generates a QR code in the terminal — scan it with the Tailscale app to authenticate without copying URLs manually in desktop mode. --operator=deck allows the deck user to run tailscale status, tailscale ping, and similar commands without sudo; without it, only root can control the daemon. --ssh enables Tailscale SSH, an identity-based access layer the Tailscale daemon handles directly, parallel to and independent of the OpenSSH setup.

Confirm the node is in the tailnet:

tailscale status

Expected: Steam Deck listed with a 100.64.0.0/10 address and state active.

Enable auto-update

sudo tailscale set --auto-update

The binaries are in /opt/tailscale/ — persistent, outside the rootfs update cycle. Tailscale’s auto-update mechanism replaces them in-place, independently of SteamOS. Without this, you’d need to re-run tailscale.sh for every new Tailscale release. The only reason to re-run the script is if a SteamOS update somehow discards the /etc overlayfs entries — not the normal update path, but possible in an aggressive reset scenario.

Two independent access paths — what happens if one fails?

Direct answer: After this setup, OpenSSH and Tailscale SSH are two independent paths into the Deck that don’t share a failure mode. OpenSSH on port 22 needs only key auth and the network path to be open — it works whether or not tailscaled is running. Tailscale SSH is identity-based and routed through the tailnet regardless of NAT or port filtering, but it needs tailscaled up and the node authenticated. If Tailscale auth lapses or tailscaled crashes, OpenSSH is unaffected. If something blocks inbound port 22, Tailscale SSH still gets through. For a self-hosted Headscale setup specifically, Tailscale SSH also fails if the Headscale instance itself is down — which is the one scenario where OpenSSH over a separate network path is the only way in — the two paths are complementary precisely because their failure modes don’t overlap.

After this setup there are two SSH access paths that don’t depend on each other:

OpenSSH on port 22 — key-only auth, available over the local network and over the Tailscale IP. Works regardless of whether tailscaled is running.

Tailscale SSH — identity-based, routed through the tailnet regardless of port filtering or NAT. Requires tailscaled to be up and the node to be authenticated.

They’re complementary. If Tailscale authentication lapses or tailscaled crashes, OpenSSH is still there. If you’re behind something that blocks inbound port 22, Tailscale SSH gets through anyway. For the Headscale case, Tailscale SSH also fails if the Headscale instance itself is unreachable — so OpenSSH over a different path remains the fallback.

After an update, what should you check?

Direct answer: Run systemctl is-active sshd, systemctl is-active tailscaled, and tailscale status after every SteamOS update. If sshd shows inactive or not enabled, the atomic-update.conf.d whitelist didn’t take — check that /etc/atomic-update.conf.d/sshd-persistent.conf exists and lists both the enable symlink and the config drop-in. If tailscaled is inactive, check journalctl -u tailscaled -b; the binary at /opt/tailscale/tailscaled should still be present since /opt persists through normal updates. The one case none of this protects against is a factory reset: it wipes /home entirely, taking authorized_keys, Tailscale’s state, and the /opt/tailscale/ binaries with it, and also clears the /etc overlay upper layer. A factory reset isn’t an update — it’s intentional destruction of user state, and everything in this post needs to be redone from scratch afterward — treat it as a fresh install, not an update to recover from.

After a SteamOS update, run:

systemctl is-active sshd
systemctl is-active tailscaled
tailscale status

If sshd is inactive or not enabled after the update, the atomic-update.conf.d whitelist didn’t apply. Check that /etc/atomic-update.conf.d/sshd-persistent.conf exists and contains both paths. If tailscaled is inactive, check journalctl -u tailscaled -b — the binary at /opt/tailscale/tailscaled should still be present since /opt persists through updates.

One hard exception: a factory reset wipes /home. ~/.ssh/authorized_keys disappears along with Tailscale’s state files and the /opt/tailscale/ binaries (since those are physically in /home/.steamos/offload/opt). A factory reset is not an update — it’s intentional destruction of user state. Everything needs to be redone. The /etc overlayfs upper layer is also cleared. The password is gone. Start from scratch.

References


Breno Zanato Detomini
Breno Zanato Detomini

Embedded systems and network engineer based in Brazil.

← All posts