Access your home linux box from anywhere with SSH tunnels
....and other things! Recently, I bought a Raspberry Pi 3. Now that the rest of the components have arrived, I've got a rather nice little home server that's got a 1 terabyte WD PiDrive attached to it to provide lots of lovely shared storage, which is rather nice.
However, within a few weeks I was faced with a problem. How do I access my new box to configure it from my internship when I'm on lunch? Faced with such a challenge, I did what anyone would, and took to the internet to find a solution.
It didn't take long. A while ago I heard about these things called 'SSH tunnels', which, while not designed for a high throughput, are more than adequate for a low-intensity SSH connection that runs a few kilobytes a second in either direction. After reading this excellent answer by erik on the Unix & Linux StackExchange, I had an understanding of how SSH tunnels work, and was ready to put together a solution. You should go and read that answer if you'd like to understand SSH tunnels too - it explains it much better than I ever could :P
With that knowledge in hand, I went about planning the SSH tunnel. I already have a server a public IP address (it's hosting this website!), so I needed a reverse tunnel to allow me to access a port local to my linux box at home (called
elessar - a virtual cookie for anyone who gets the reference!) from
Important! Ask yourself whether it's moral and ethical to set up an ssh tunnel before you think about following along with this article! If you find yourself behind a firewall or something similar, then the chances are that it's there for a good reason - and you might get into trouble if you try and circumvent it. I won't be held responsible for any loss or damages of any description caused by the reading of this post.
First job: create a limited account on
elessar to SSH into. That's easy:
sudo useradd --system ssh-tunnel
Then, with a few quick lines in
Match User ssh-tunnel ForceCommand echo 'This account can only be used for ssh tunnelling.'
....we can prevent the
ssh-tunnel user from being abused to gain shell access to the server (let me know if there are any further measures I can put in place here).
Now that I had a user account to ssh in as, I could set up a public / private keypair to authenticate with
starbeamrainbowlabs.com, and cook up an SSH command for
elessar that would set up the appropriate tunnel. After fiddling around a bit, I came up with this that did the job:
ssh -TN -R30582:localhost:5724 firstname.lastname@example.org
Very cool. So with that command executing on
elessar, I could ssh into
starbeamrainbowlabs.com! In short, it sets up a tunnel that will make port
starbeamrainbowlabs.com tunnel through to port
5724 on elessar - the port on elessar that has SSH running on it, without allocating a pseudo-tty to save resources. explainshell.com can, well, explain it in more detail if you're interested.
Having an SSH command that would set up the tunnel is nice, but it's not very useful, since I have to execute it first before I can actually SSH into
elessar from afar.
The solution was actually a little bit complicated. First, I wrote a simple
systemd service file (
systemd is what I have installed, since it's vanilla raspbian - this should be easily adaptable to other systems and setups) to start the SSH tunnel automagically on boot:
[Unit] Description=SSH tunnel from starbeamrainbowlabs.com to local ssh server. [Service] Type=simple ExecStart=/usr/bin/ssh -TN -R30582:localhost:5724 email@example.com [Install] WantedBy=network-online.target
I quickly realised that there were a few flaws with this approach. Firstly, it tried to start the SSH connection before my router had connected to the internet, since my router starts faster than the box that initialises the fibre connection to my ISP. Secondly, it fails to retry when the connection dies.
The first problem can be solved relatively easily, by wrapping the ssh command in a clever bit of shell scripting:
/bin/sh -c 'until ping -c1 starbeamrainbowlabs.com &>/dev/null && sleep 5; do :; done && /usr/bin/ssh -TN -R30582:localhost:5724 firstname.lastname@example.org
The above tries to ping
starbeamrainbowlabs.com every 5 seconds until it succeeds, and only then does it attempt to open the SSH connection. This solves the first problem. To solve the second, we need to look at autossh. Autossh is a small tool that monitors an ssh connection in a variety of configurable ways and restarts the connection if ever dies for whatever reason. You can install it with your favourite package manager:
sudo apt install autossh
apt with whatever package manager you use on your system. With it installed, we can use a command like this:
autossh -o "UserKnownHostsFile /home/ssh-tunnel/.ssh/known_hosts" -o "IdentityFile /home/ssh-tunnel/.ssh/ssh-tunnel_ed25519" -o "PubkeyAuthentication=yes" -o "PasswordAuthentication=no" -o "ServerAliveInterval 900" -TN -R30582:localhost:5724 -p 7261 email@example.com
to automatically start our ssh tunnel, and restart it if anything goes wrong. Note all the extra settings I had to specify here. This is because even though I had many of them specified in
~/.ssh/config for the
ssh-tunnel user, because of systemd's weird environment when it starts a service, I found I had to specify everything in the command line with absolute paths (ugh).
Basically, the above tells autossh where the known_hosts file is (important for automation!), that it should only attempt public / private keypair authentication and not password authentication, that it should check the server's still there every 15 minutes, and all the other things we figured out above.
Finally, I combined the solutions I came up with for both problems, which left me with this:
[Unit] Description=SSH tunnel from starbeamrainbowlabs.com to local ssh server. [Service] Type=simple ExecStart=/bin/sh -c 'until ping -c1 starbeamrainbowlabs.com &>/dev/null && sleep 5; do :; done && /usr/bin/autossh -o "UserKnownHostsFile /home/pi/.ssh/known_hosts" -o "IdentityFile /home/pi/.ssh/ssh-tunnel_ed25519" -o "PubkeyAuthentication=yes" -o "PasswordAuthentication=no" -o "ServerAliveInterval 900" -TN -R30582:localhost:5724 -p 7261 firstname.lastname@example.org' [Install] WantedBy=network-online.target
Here's a version that utilises the
-f parameter of autossh to put the
autossh into the background, which eliminates the
sh parent process:
[Unit] Description=SSH tunnel from starbeamrainbowlabs.com to local ssh server. [Service] Type=forking Environment=AUTOSSH_PIDFILE=/var/run/sbrl-ssh-tunnel/ssh-tunnel.pid PIDFile=/var/run/sbrl-ssh-tunnel/ssh-tunnel.pid ExecStartPre=/bin/mkdir -p /var/run/sbrl-ssh-tunnel ExecStartPre=-/bin/chown ssh-tunnel:ssh-tunnel /var/run/sbrl-ssh-tunnel ExecStart=/bin/sh -c 'until ping -c1 starbeamrainbowlabs.com &>/dev/null && sleep 5; do :; done && /usr/bin/autossh -f -o "UserKnownHostsFile /home/pi/.ssh/known_hosts" -o "IdentityFile /home/pi/.ssh/ssh-tunnel_ed25519" -o "PubkeyAuthentication=yes" -o "PasswordAuthentication=no" -o "ServerAliveInterval 900" -TN -R30582:localhost:5724 -p 7261 email@example.com' [Install] WantedBy=network-online.target
I ended up further modifying the above to set up an additional tunnel to allow
elessar to send emails via the postfix email server that's running on starbeamrainbowlabs.com. Let me know if you'd be interested in a tutorial on this!