Introduction

SSH natively provides the ability to forward sockets, though in the following we focus on network sockets. Using SSH, users can pull a port from a remote host (ssh -L ..; local port forwarding) or push a port to a remote host (ssh -R ..; remote port forwarding).

SSH also allows for using bastion hosts (also referred to as — in my opinion more suitably — jumphosts), i.e. systems that the connection muss pass through in order to reach the target system.

The combination of these two features allows for arbitrarily complex port forwarding through various networks, all the while keeping the strong authentication and encryption properties of SSH.

Accessing a system behind NAT via SSH

Similarly to how a VPN would connect various systems at OSI-Layer 3, we can connect devices at Layer 4 (with port granularity) using TCP-Sockets and a publicly reachable jumphost through the use of SSH.

System 1

The system we want to expose can push its SSH port to the jumphost (port 2222).

ssh -R 22:127.0.0.1:2222 jumphost@<IP-OR-HOSTNAME>


System 2

Now we can access System 1 from System 2.

ssh -L 127.0.0.1:2222:127.0.0.1:2222 jumphost@<IP-OR-HOSTNAME> # Pull the port to our system
ssh username@127.0.0.1 # Access the system


It turns out that this pattern can be simplified, by specifying the jumphost-System as, well, a jumphost.

ssh -J jumphost@<IP-OR-HOSTNAME> 127.0.0.1:2222


Essentially, by providing the J-flag, we can think of the following SSH-access as happening from the jumphost itself. And since System 1 pushed its SSH-socket onto the jumphost, we can access it at port 2222.

A template for secure SSHjumphosts

This is great already (and — for all intents and purposes — a simple alternative to what many people use ngrok for), but also quite verbose. Plus, do you really want to allow System 1 and System 2 full access to your jumphost?

The sane answer is no and thus I tend to stick to the following pattern for securing (and sharing!) the jumphost.

Configuration of the jumphost

The following configuration must happen on the jumphost.

Creating a jumphost-<SERVICE-NAME> user

By giving the user an explanatory name and comment, it’ll be easier to identify its purpose.

useradd -g jumphosts -s /bin/false -m -c "Jumphost user to publish and access the SSH-socket of system01" jumphost-ssh-system01


Restricting port forwarding

Inside /etc/ssh/sshd_config we add the following lines:

Match User jumphost-ssh-system01
PermitOpen 127.0.0.1:2222
X11Forwarding no
AllowAgentForwarding no
ForceCommand /bin/false


Now, the following limitations are enforced for the new user:

• The user cannot use a shell (-s /bin/false)
• The user’s ability for port forwarding is restricted to a single host:port combination (Match User.. + PermitOpen 127.0.0.1:2222)
• No AgentForwarding or X11Forwarding can take place

Configuring access using public keys

It is a good idea to only ever use pubkey-authentication (or, in enterprise environments, certificate-based authentication). Thus, the ~/.ssh/authorized_keys-file of userjumphost-ssh-system01 must be populated with the publickeys of all systems that are to be connected via SSH and via the jumphost.

The publishing system can now push its socket onto the jumphost (authorized port only), and subscribing systems may pull the port onto their system, or configure the jumphost accordingly (CLI or in their ~/.ssh/config).

Configuration using .ssh/config

Instead of pulling ports manually, or specifying the J-flag, this verbosity can be outsourced into the .ssh/config-file.

Host jumphost
HostName <IP-OR-HOSTNAME>
User jumphost-ssh-system01
Host system01
HostName <IP-OR-HOSTNAME>

If system01 is only accessible via another jumphost, this can easily be achieved by adjusting the ProxyJump-option: ProxyJump jumphost,username@<IP-OR-HOSTNAME>:PORT. This pattern allows for arbitrarily deep nesting, to jump through various networks. If downstream jumphosts are to be access-controlled, this requires further care though.