Skip to main content

Permanent SSH tunnel with systemd

· 4 min read
Holger Dörner
PHP Developer & Chapter Lead

From time to time we are in need to access resources on a different server, and if it's only for a short time. The key is here to rely on simple and readily available tools.

Accessing a resource on another server, across the internet, can sometimes be necessary. The problem here is, as always, security. While one can open port 3306, for example, on a server to reach the database from the outside world, it's mostly a bad idea to leave it accessible to everyone. The first measurement should always be to restrict the Port(s) to the IP of the server you want to access the resource with by a firewall rule (like IPSec or ufw).

But just restricting access is just half the story, when data is then sent over the Internet, especially when it's unencrypted. In that case, you need some solution to protect the sensitive Data from unauthorized eyes. While there are numerous solutions out there, they all mostly have their own learning curve and are sometimes not that easy to use if you're not using them on a daily basis. But fortunately, things don't always have to be complicated.

In our most recent case, we needed to access the MySQL database on an old server while moving the application itself to a new one in the first step. The database was already quite large, so we decided to leave it on the old server for the moment and first focus on adapting the application, and its Docker setup, for the new infrastructure.

We generated an SSH Key and installed the public key on the old server to be able to log in via SSH without the need for a password. Establishing a secure tunnel then using the SSH command line tool is pretty easy, but has the drawbacks that You have to do that manually on a terminal and that it doesn't come back up if something happens.

Luckily, most Linux distributions today come with systemd which makes it easy to set up services, and a service can also be a simple shell command. systemd also handles the restart of the service for us in case something went wrong, like network problems or a server restart.

Create service configuration in /etc/systemd/system/ and give it the extension .service, like ssh-tunnel.service. The content of that file should look similar to this example:

[Unit]
Description=Persistent SSH Tunnel from port 3306 on this server to port 3306 on the old one.
After=network.target

[Service]
Restart=on-failure
RestartSec=5
ExecStart=/usr/bin/ssh -i /etc/ssh/YOUR_SSH_PRIVATE_KEY -NTC -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -L 0.0.0.0:3306:xxx.xxx.xxx.xxx:3306 user@host.net

[Install]
WantedBy=multi-user.target
  • Replace YOUR_SSH_PRIVATE_KEY with the SSH key you want to use.
  • 0.0.0.0 binds the tunnel to all network interfaces on the source host, if you want to restrict that just put in the IP address of a specific Interface. When you want only your Docker containers to be able to access the tunnel, find the IP of the network interface docker0 (e.g. with ip a) and use that one instead.
  • :3306: binds the tunnel to this local port, so the application can access MySQL on the default port, but you could use almost any free port number.
  • xxx.xxx.xxx.xxx with the IP of the target server which hosts the resource you want to access.
  • :3306 is the target port on the remote server, in this case, the default Port of MySQL.
  • Replace USER@HOST.net with the Login for the user which corresponds to the SSH Key.

The other switches and options for SSH used in this example are explained here: ssh, ssh_config.

Now just make systemd aware of the new service, enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable ssh-tunnel
sudo systemctl start ssh-tunnel

Now you can access the remote MySQL like it was running locally, for example logging in with mysql -uroot -p.

As you can see, this is very straightforward and easy to maintain, yet very powerful at the same time.