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:
| Attempt | Result |
|---|---|
-R 2222:localhost:22 | Works |
-R 2223:localhost:22 | remote port forwarding failed for listen port 2223 |
| Running a command or a shell | This account is currently not available. |
-L 9000:localhost:22 | Connection 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 0turns off autossh's own monitor port, so it restartssshonly whensshexits. The autossh README suggests the keepalive options as a replacement for the monitor port.ServerAliveInterval=30andServerAliveCountMax=3makesshexit 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=yesmakessshexit if the port cannot be opened. Without it, you can get a live connection with no tunnel.AUTOSSH_GATETIME=0makes autossh keep trying even if the first attempt fails, which matters at boot.Restart=alwaysbrings the service back if autossh itself dies.- Do not use autossh's
-fhere, since systemd expects the process to stay in the foreground.
4. See it recover
| What we did | What happened |
|---|---|
| Killed the session on the server | autossh started a new ssh at once |
| Froze the server for 28 seconds, with a 5 second interval | Timeout, server server not responding. after 20 seconds, then a new connection once the server was back |
Killed autossh with kill -9, and its ssh | systemd 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.