How to keep an SSH tunnel alive with autossh and systemd

Run the tunnel as a systemd service with Restart=always, and use autossh -M 0 -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes as the command. The keepalive options make ssh notice a dead connection, autossh restarts it, and systemd restarts everything after a crash or boot. Use a dedicated key on an account that can only open that one tunnel.

Updated . autossh 1.4g, systemd 257 and OpenSSH 10.0 on Debian 13 in privileged throwaway containers.

1. Create a key for the tunnel

On the machine that starts the tunnel, create a local account and a key without a passphrase, because the service starts unattended. Then save the server's host key, so that the first connection does not stop to ask:

sudo apt install autossh
sudo useradd -m -s /usr/sbin/nologin tunnel
sudo -u tunnel mkdir -m 700 /home/tunnel/.ssh
sudo -u tunnel ssh-keygen -t ed25519 -N "" -C tunnel -f /home/tunnel/.ssh/id_ed25519
ssh-keyscan -t ed25519 server | sudo -u tunnel tee /home/tunnel/.ssh/known_hosts

Compare that host key with the server's real fingerprint before you rely on it. See known_hosts explained.

2. Restrict the account on the server

A key with no passphrase on a remote machine should open only this tunnel. On the server, create an account named tunnel and put the public key in its ~/.ssh/authorized_keys:

restrict,port-forwarding ssh-ed25519 AAAA... tunnel

Then add this to the end of /etc/ssh/sshd_config:

Match User tunnel
  AllowTcpForwarding remote
  PermitListen localhost:2222
  X11Forwarding no
  ForceCommand /usr/sbin/nologin
sudo sshd -t && sudo systemctl reload ssh

We tested the result with this key:

AttemptResult
-R 2222:localhost:22Works
-R 2223:localhost:22remote port forwarding failed for listen port 2223
Running a command or a shellThis account is currently not available.
-L 9000:localhost:22Connection closed, nothing gets through

The last line needs the Match block. With only permitlisten="localhost:2222" in authorized_keys, a -L forward still worked in our test, which let the key reach services on the server's loopback. More on key options is in how to restrict an SSH key.

3. Create the service

Save this as /etc/systemd/system/autossh-tunnel.service:

[Unit]
Description=Reverse SSH tunnel to server
Wants=network-online.target
After=network-online.target

[Service]
User=tunnel
Environment=AUTOSSH_GATETIME=0
ExecStart=/usr/bin/autossh -M 0 -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -i /home/tunnel/.ssh/id_ed25519 -R 2222:localhost:22 tunnel@server
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now autossh-tunnel.service
systemctl status autossh-tunnel

What each part does

  • -M 0 turns off autossh's own monitor port, so it restarts ssh only when ssh exits. The autossh README suggests the keepalive options as a replacement for the monitor port.
  • ServerAliveInterval=30 and ServerAliveCountMax=3 make ssh exit after about 90 to 120 seconds of silence from the server. In our test, a 5 second interval with the same count took 20 seconds, and a 100 second freeze with these values did not trigger it.
  • ExitOnForwardFailure=yes makes ssh exit if the port cannot be opened. Without it, you can get a live connection with no tunnel.
  • AUTOSSH_GATETIME=0 makes autossh keep trying even if the first attempt fails, which matters at boot.
  • Restart=always brings the service back if autossh itself dies.
  • Do not use autossh's -f here, since systemd expects the process to stay in the foreground.

4. See it recover

What we didWhat happened
Killed the session on the serverautossh started a new ssh at once
Froze the server for 28 seconds, with a 5 second intervalTimeout, server server not responding. after 20 seconds, then a new connection once the server was back
Killed autossh with kill -9, and its sshsystemd restarted the service, and NRestarts went to 1

Watch this yourself with journalctl -u autossh-tunnel -f. On the server, ss -ltn | grep 2222 shows the port.

If the port stays taken after a network failure

After a real outage, the server can still hold the old connection and its port, so the new one fails with remote port forwarding failed until the old one times out. In the Match block on the server, add:

  ClientAliveInterval 30
  ClientAliveCountMax 3

The server then drops dead clients after about 90 seconds, and the retries succeed.

For other reasons the port can be taken, see SSH tunnel: Address already in use and remote port forwarding failed.

Do you need autossh at all

With systemd and the keepalive options, plain ssh also works. ssh exits, and Restart=always starts it again. Replace the command in the unit with:

ExecStart=/usr/bin/ssh -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -i /home/tunnel/.ssh/id_ed25519 -R 2222:localhost:22 tunnel@server

In our test this recovered from a killed session with one systemd restart, after the 10 second RestartSec. autossh restarts sooner, and it is the option if you have no systemd.

One difference we saw: systemctl restart logged status=1/FAILURE for autossh as it stopped. The service still started again normally.

Questions

What does autossh -M 0 do?

It turns off the monitor port. autossh then restarts ssh only when ssh exits, and the ServerAliveInterval and ServerAliveCountMax options make ssh exit when the connection dies.

Do I need autossh if I use systemd?

Not always. Plain ssh with the keepalive options and Restart=always also comes back. autossh restarts sooner and works without systemd.

Why does my reverse tunnel fail after a network outage?

The server may still hold the old connection and its port. Add ClientAliveInterval 30 and ClientAliveCountMax 3 on the server so it drops dead clients after about 90 seconds.

Sources

More in Tunnels and jump hosts

All guides