Press "Enter" to skip to content

Accessing an SSH server that is behind a VPN

If you own an SSH server and that machine is behind a VPN, then apparently, without any special setup, you are not able to SSH to it with its original public IP from a client outside the VPN. An easy solution is to disconnect the machine from the VPN, so clients outside the VPN can access it. But what if you really really want to use the VPN on that machine?

Disclaimer: I am no expert, so I am not confident that all the procedures I did or all my understanding are correct. What I am sure is only that it works on my machine. That is to say, if you want to try my solution, please use with cautions. Also, any correction is welcome! I hope to learn from real experts!

The underlying issue of the SSH server behind a VPN is that, when a client connects to a server with the server’s public IP, the client expects to get server’s return traffic from the same public IP. But the server is behind a VPN. In other words, when the server tries to return SSH signals, it uses the VPN. Then the client gets the returned signals from VPN’s IP, not the server’s public IP that it’s expecting. So the client declines to accept the signals and drops the SSH connection.

The idea of the solution is to route server’s return SSH signal through its original IP. And this routing is allowed only when the server is responding to the traffic coming to the original IP, not other IPs (i.e., not the traffic coming to VPN’s IP).

Step 1: obtain subnet and mask information

There are many ways to achieve this step. Here is a newbie’s approach.

If you are using NetworkManager, then find the inet4 information from the output of the command nmcli. For example, the output may be something like this:

$ nmcli
--------------------------------------------------------
ch-us-01.protonvpn.com.udp1194 VPN connection
        content hidden

docker0: connected to docker0
        content hidden

enp8s0: connected to Wired connection 1
        "Intel I211"
        ethernet (igb), xx:xx:xx:xx:xx:xx, hw, mtu 1500
        inet4 123.123.123.123/23
        route4 0.0.0.0/0
        route4 x.x.x.x/24

tun0: connected to tun0
        content hidden

eno1: unavailable
        content hidden

lo: unmanaged
        content hidden

DNS configuration:
        content hidden

The interface enp8s0 is the one connecting to the internet in this example. And the line inet4 123.123.123.123/23 is the public IPv4 address and the mask. If you don’t know how to calculate the subnet, use any IP calculator on the internet. For example, using this calculator with the IP address (123.123.123.123) and the mask (23), in the output, the line Network: 123.123.122.0/23 represents the subnet and the mask.

Step 2: obtain gateway information

Next, we need the gateway address. The following command outputs the traffic routes related to the internet interface [INTERFACE].

$ ip route show dev [INTERFACE]

Again, let’s use the previous example, in which enp8s0 is the name of the interface. The output may look like:

$ ip route show dev enp8s0
------------------------------------------------------------------------
default via 123.123.123.254 proto dhcp metric 100 
123.123.122.0/23 proto kernel scope link src 123.123.123.123 metric 100 
123.123.123.123 proto static scope link metric 100 

The IP 123.123.123.254 in the line default via ... is the gateway.

Step 3: add a routing table and rules

First, we add a table for traffic coming to the original public IP and name the able 128:

# ip rule add from [PUBLIC IP] table 128

To my understanding, the above command says, whenever there’s traffic coming to the public IP, the system will look up the routing rules in the table 128.

Next, we add routing rules to the table:

# ip route add table 128 to [SUBNET]/[MASK] dev [INTERFACE]
# ip route add table 128 default via [GATEWAY]

To be honest, I don’t really know what these two commands are doing. I have almost zero knowledge about network stuff. I don’t even really know what are subnet and gateway. Hope someone here can help me to understand.

But what I do know is that after this step, when there’s traffic coming to the public IP, the machine will accept them. And if the machine has to respond to/return the traffics, it will use the default IP, instead of VPN. But the problem is, we may not want the connections other than SSH to go/come through the public IP. So we have to block other connections through a firewall.

Step 4: allow firewall to only accept SSH traffic coming to the original public IP

In this example, I use iptables to control the kernel-level linux firewall directly. I think other user-friendly interfaces should also work if you have one.

When there is traffic coming to the original public IP and to SSH server’s listening port, we want the system to accept it.

# iptables -A INPUT -d [PUBLIC IP] -p tcp --dport [SSH LISTENING PORT] -j ACCEPT

And for all other traffics coming to the original public IP, but not to SSH server’s port, we want the machine to drop them:

# iptables -A INPUT -d [PUBLIC IP] -j DROP

Now, clients outside the VPN should be able to SSH to the machine, while the machine is still using the VPN.

3 Comments

  1. Anonymous Anonymous

    This doesn’t work if the device is behind NAT 🙁

    • Probably. If an SSH server using VPN is also behind NAT, solutions may depend on the configuration of the NAT.

      But the key is still the same: let the SSH server return its message to clients from the true IP, instead of the VPN’s IP. Though the presence of NAT may cause some troubles when configuring traffic routing.

  2. Hey thanks for this. I’ve played around with this idea off-and-on for years, but your instructions worked. Now just gotta figure out how it works. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.