Access your home linux box from anywhere with SSH tunnels
(Header by GDJ from openclipart.org. Source page)
....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 starbeamrainbowlabs.com
.
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 starbeamrainbowlabs.com
for elessar
to SSH into. That's easy:
sudo useradd --system ssh-tunnel
Then, with a few quick lines in /etc/ssh/sshd_config
:
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 ssh-tunnel@starbeamrainbowlabs.com
Very cool. So with that command executing on elessar
, I could ssh into elessar
from starbeamrainbowlabs.com
! In short, it sets up a tunnel that will make port 30582
on 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 ssh-tunnel@starbeamrainbowlabs.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 ssh-tunnel@starbeamrainbowlabs.com
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
Substitute 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 ssh-tunnel@starbeamrainbowlabs.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 ssh-tunnel@starbeamrainbowlabs.com'
[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 ssh-tunnel@starbeamrainbowlabs.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!
Sources and Futher Reading
- SSH Tunnelling for Fun and Profit: Autossh by cytopia over at Everything CLI