Secure servers behind a private network
by Alex Arica

We are going to secure our servers by hiding them behind a private network. Among all servers inside the private network, only one server will be accessible from internet via SSH. And that server will be the "Bastion server", meaning it will allow admins to "SSH jump" to private servers.

The advantage of this approach is, it allows hiding the servers and services (e.g. HTTP Apache server, PostgreSql database, ...) inside a private network. From outside, meaning using a public IP address, it would not be possible to access to those services nor to scan them. This architecture significantly limits the attack surface by only exposing the Bastion server and the external load-balancer to the Internet.

The flow to access to a private server via SSH:

Admin on internet -> via SSH -> Bastion server -> SSH jump to -> private server(s)
                    

The flow to access to a HTTP service (e.g. Nginx) exposed by private server(s):

Client on internet -> via HTTPS -> External load-balancer -> HTTPS -> Nginx on private server(s)
                    

In the flow above, we assumed that the external load-balancer is accessible publicly and has access to the private network. Many hosting companies offer a private network service. For example, OVH offers a "vRack" private network which is accessible via their load-balancers.

Let's set-up this architecture

Let's assume that we have 2 servers: a private server and a public server. They have 2 network interfaces each, as follows:

  • interface eth0: public IP, assigned by the hosting company
  • interface eth1: private IP, manually assigned with range 192.168.0.0/16

At this stage, both servers are publicly accessible. We would like to make changes so that they have the following characteristics:

  • Private server: its network interface eth0 is disabled. It has no public IP and consequently it is not possible to directly access to this server via the internet. It has the network interface eth1 enabled with a static private IP address 192.168.0.2 and a gateway IP 192.168.0.1 .
  • Public server: it is a Bastion server and also a Gateway server. Both of its interfaces eth0 and eth1 are enabled. It has a public IP via eth0 and a static private IP 192.168.0.1 via eth1. As a Bastion server it allows admins to access the Private server (192.168.0.2) via a "SSH jump". As a Gateway server with IP 192.168.0.1, it allows the Private server to access to the internet.

Both servers are shipped with a SSHD service listening to port 22.

Configure the Public server as a Bastion server

Check the Bastion server has SSHD running and listening on port 22. And configure bastion to only accept SSH connections as explained here.

Configure the Public server as a Gateway server

We are going to assign the static IP address 192.168.0.1 to the public server. Later, this IP address will be used by the private server as the gateway IP address.

On Ubuntu

Create this file:

sudo vi /etc/netplan/60-static.yaml
                    

Add the followings:

network:
  version: 2
  ethernets:
    eth1:
      dhcp4: false
      dhcp6: false
      addresses:
       - 192.168.0.1/16
                    

Save the file and apply the changes:

sudo netplan apply
                    

On Debian

Open this file:

sudo vi /etc/network/interfaces
                    

Add the followings:

auto eth1
iface eth1 inet static
address 192.168.0.1
netmask 255.255.0.0
                    

Save the file and apply the changes:

sudo systemctl restart networking
                    

Test the changes

Check Ip address

ip a
                    

We should see the interface eth1 with the IP 192.168.0.1

Reboot the server to make sure that the IP address remains the same:

sudo reboot
                    

After reboot, connect to the server and check the private IP address works:

ping 192.168.0.1
                    

Enable IP forwarding

Check if it is already enabled

sudo sysctl net.ipv4.ip_forward
                    

If the output is "0" then we need to enable it. Open the file:

sudo vi /etc/sysctl.conf
                    

Uncomment the following line to enable packet forwarding for IPv4:

net.ipv4.ip_forward=1
                    

Save the file and apply the changes:

sudo sysctl -p
                    

Check if it is enabled, the output should be "1":

sudo sysctl net.ipv4.ip_forward
                    

Set-up a NAT rule

Set-up a NAT (network-address-translation) to give private network access to the internet.

If a private IP is statically set, please use SNAT (this is our case):

sudo iptables -t nat -A POSTROUTING ! -d 192.168.0.0/16 -o eth0 -j SNAT --to-source [replace this by the public IP address of eth0]
                    

For example:

sudo iptables -t nat -A POSTROUTING ! -d 192.168.0.0/16 -o eth0 -j SNAT --to-source 145.239.7.56
                    

Enable forwarding:

sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o eth1 -m state --state ESTABLISHED,RELATED -j ACCEPT
                    

Persist the new IP rules:

su -
apt-get install iptables-persistent
iptables-save > /etc/iptables/rules.v4
                    

After the steps above, it is highly recommended creating firewall rules on Bastion server, using iptables, in order to limit the external connections to SSH port only, as explained here.

Once you applied the firewalls rules for the NAT rules and the Bastion server rules for ssh, the file /etc/iptables/rules.v4 should contain:

# Generated by iptables-save on Fri May 20 20:35:38 2022
*filter
:INPUT ACCEPT [2130:253524]
:FORWARD ACCEPT [210:21141]
:OUTPUT ACCEPT [4469:572757]
-A INPUT -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i eth0 -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i eth0 -j DROP
-A FORWARD -i eth1 -o eth0 -j ACCEPT
-A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i eth0 -j DROP
COMMIT
# Completed on Fri May 20 20:35:38 2022
# Generated by iptables-save v1.8.7 on Fri May 20 20:35:38 2022
*nat
:PREROUTING ACCEPT [4510:194220]
:INPUT ACCEPT [421:17683]
:OUTPUT ACCEPT [53:3894]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING ! -d 192.168.0.0/16 -o eth0 -j SNAT --to-source [bastion server's public IP]
COMMIT
# Completed on Fri May 20 20:35:38 2022
                    

Configure the private server

We are going to configure the private server with a static IP address 192.168.0.2 .

On Ubuntu

Create this file:

sudo vi /etc/netplan/60-static.yaml
                    

Add the followings:

network:
  version: 2
  ethernets:
    eth1:
      dhcp4: false
      dhcp6: false
      addresses:
       - 192.168.0.2/16
      routes:
       - to: default
         via: 192.168.0.1
      nameservers:
        addresses: [8.8.8.8]
                    

Save the file and apply the changes:

sudo netplan apply
                    

On Debian

Open this file:

sudo vi /etc/network/interfaces
                    

Add the followings:

auto eth1
iface eth1 inet static
address 192.168.0.2
netmask 255.255.0.0
gateway 192.168.0.1
                    

Save the file and apply the changes:

sudo systemctl restart networking
                    

Connect to the private server via SSH

At this stage, on Ubuntu only you would be disconnected because in IP routes the private IP comes before the public IP.

We are going to ssh jump from the Gateway server to the Private server. From a client computer (a laptop or desktop computer), connect via SSH as follows:

ssh -J [username]@[Bastion server's IP address] [username]@[private server IP]
                    

For example:

ssh -J alex@145.239.7.56 alex@192.168.0.2
                    

If the above works, the Bastion server is ready.

Test the changes

Check Ip address

ip a
                    

We should see the interface eth1 with the IP 192.168.0.2 .

Check that the gateway 192.168.0.1 is present in the routing table:

ip route
                    

We should see something similar to:

default via 192.168.0.1 dev eth1 ...
                    

If we do not see the gateway 192.168.0.1 in the routing table, then add it manually:

ip route add default via 192.168.0.1 dev eth1
                    

Disable the public interface eth0 on private server

Now that we can SSH jump to the Private server, it is time to isolate it from the internet. Let's disable the interface eth0 managing a public IP:

sudo ip link set eth0 down
                    

Test:

ip a
                    

The interface eth0 should not have an IP address. Only the interface eth1 should have one: 192.168.0.2

Check default gateway routing:

ip route
                    

We should not see the interface eth0 in the routing table.

Ping:

ping 192.168.0.1
ping reactive-tech.io
                    

The set-up is completed. To add more servers into the private network, follow the steps in section "Configure the private server" by incrementing the private IP address in 192.168.0.[to increment].

Additional actions and readings

On Bastion, configure a firewall to only allow SSH access

Few notes about SNAT and MASQUERADE.

If a private IP is dynamically set by a DHCP, please use MASQUERADE rather than SNAT:

sudo iptables -t nat -A POSTROUTING ! -d 192.168.0.0/16 -o eth0 -j MASQUERADE
                    

For static IPs, SNAT is recommended by the iptables man page:

“This target is only valid in the nat table, in the POSTROUTING chain. It should only be used with dynamically assigned IP (dialup) connections: if you have a static IP address, you should use the SNAT target. Masquerading is equivalent to specifying a mapping to the IP address of the interface the packet is going out, but also has the effect that connections are forgotten when the interface goes down. This is the correct behavior when the next dialup is unlikely to have the same interface address (and hence any established connections are lost anyway).”

Additional discussions on the Web on this topic.