MJUN Tech Note

Always Port Forward with autossh and systemd/launchd on Mac and Ubuntu

Hello. This time we’ll combine autossh, which maintains SSH connections as much as possible, with systemd and launchd, which manage services on Linux and Unix, to create an environment that always performs SSH port forwarding.

This configuration eliminates the need to manually make SSH or VPN connections every time when viewing sites that can only be viewed or accessed under specific networks.

What is autossh

autossh is a CLI command that maintains SSH connections as much as possible. Install it with the following methods:

# Ubuntu
sudo apt install autossh
# macOS
brew install autossh

Command usage is almost the same as ssh:

autossh -M 0 -f -L 12022:remote:22

The -M option is for monitor port. If there’s nothing special, you can disable it with 0. Details are described in man autossh.

The -f option runs autossh in the background. Note that you cannot perform input like password requests, so when using this option, you need to be able to log into the remote without a password.

Combining autossh with systemd and launchd

systemd is Ubuntu’s service management tool, and launchd is macOS’s service management tool. By registering autossh with systemd and launchd:

  • If SSH drops, autossh reconnects
  • If autossh drops, systemd/launchd reconnects
  • At system startup, systemd/launchd connects

This way, you can maintain SSH connections more robustly. This time, we’ll port forward using autossh based on settings written in ssh config.

Preparation

First, as preparation, set up passwordless SSH connection to the remote. It’s good to set SSH authentication to public key authentication.

Note that on Ubuntu, if you set a passphrase on the private key, while on macOS you can set UseKeychain in ~/.ssh/config so that the passphrase is saved in Keychain when using SSH and automatically loaded into ssh-agent, Ubuntu doesn’t have Keychain so you need to do it yourself.

As mentioned earlier, autossh can’t handle input during SSH, so when using autossh on Ubuntu, you need to prepare a private key without a passphrase. On Mac, Keychain handles this nicely so it’s not necessary. You can set/remove private key passwords with the following command:

ssh-keygen -f ~/.ssh/id_rsa -p

After resolving the above, perform an initial normal SSH connection. This is because fingerprint confirmation occurs during initial SSH and requires a response, to prevent this from happening when using autossh. This time we’ll SSH with settings written in ssh config:

Host remote
  HostName hoge
  User hoge
  LocalForward 6006 localhost:6006
ssh remote
The authenticity of host 'hoge (::1)' can't be established.
ED25519 key fingerprint is SHA256:hogehogehogehoge.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

Success if initial login completes without password input other than yes. Let’s proceed.

Making autossh a Service

Next, we’ll write configs to make autossh a service.

Mac: Launchd Case

First, let’s check the full path location of autossh:

$ which autossh
/opt/homebrew/bin/autossh

This time it appears to be in /opt/homebrew/bin. On Intel Mac it might be in /usr/local/bin. There are several locations for Launchd configuration files, but this time we’ll have it execute when the user logs in. This way, the contents of ~/.ssh/config written earlier can be used. Place a file with the following contents in ~/Library/LaunchAgents/remote.autossh.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>KeepAlive</key>
  <true/>
  <key>RunAtLoad</key>
  <true/>
  <key>EnvironmentVariables</key>
  <dict>
    <key>AUTOSSH_GATETIME</key>
    <integer>0</integer>
  </dict>
  <key>UserName</key>
  <string>[username]</string>
  <key>Label</key>
  <string>remote.autossh</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/bin</string>
    <string>-M</string>
    <string>0</string>
    <string>-N</string>
    <string>remtote</string>
  </array>
</dict>
</plist>

Once the file is written, register it with launchd. Please write the plist path as an absolute path:

launchctl load /Users/[username]/Library/LaunchAgents/remote.autossh.plist

This time we added RunAtLoad to the plist, so execution should occur when loaded. Let’s check:

$ launchctl list | grep remote.autossh
57892	0	remote.autossh

From left: PID, exit code, and Label are displayed. This time there’s a PID and exitcode=0, so it’s successful. To remove from service, use unload:

launchctl unload /Users/[username]/Library/LaunchAgents/remote.autossh.plist

When ssh config is changed, restart with stop, start:

launchctl stop iplab.autossh
launchctl start iplab.autossh

Ubuntu: Systemd Case

On Ubuntu, we’ll configure systemd. CentOS and Amazon Linux etc. (RedHat family) also use the same systemd, so with slight changes you should be able to configure similarly (unconfirmed).

Write configuration in /etc/systemd/system/remote.autossh.service:

[Unit]
Description=keep ssh
After=network-online.target ssh.service

[Service]
User=[username]
Type=simple
RestartSec=3
Restart=always
TimeoutStopSec=10
Environment="AUTOSSH_GATETIME=0"
ExecStart=/usr/bin/autossh -M 0 -N remote

[Install]
WantedBy=multi-user.target

Finally, start the service, check status to see if it’s working normally, and set it for auto-start:

sudo systemctl start remote.autossh.service
sudo systemctl status remote.autossh.service

sudo systemctl enable remote.autossh.service

That completes the setup. Good work!