Table of Contents

Exposing a Local Server to the Public Internet Safely

These are the steps I took in order to securely expose a local server on my network to the wild west of the public internet, allowing me to free myself from cloud providers and properly self-host everything (with a small exception).

In this log/tutorial you will notice that I don't talk about how to secure the network that a “semi public-facing” device is in. The reason for this is that everyone's network is different, and in my specific case, all devices that are going to be proxied to the outside world will be on a physically separate network (5G modem in my case), thus I don't have to worry about having a compromised device on my home's network.

Bridge VPN

In order to ensure that I don't have any open ports on my home router and no public internet traffic flowing through my home network, I've decided to use a VPN and a cloud VPS (the only exception to the self-hosting everything rule) to provide a secure tunnel for traffic to flow through. This was done using the amazing SoftEther VPN project, which apart from Wireguard and Tailscale, is in my opinion the best VPN software in the market.

Setting up the Server

To set the VPN software up on a Hetzner VPS in Local Bridge mode in order to avoid the bottleneck of SecureNAT, I've followed the following tutorial. Since Hetzner VPSes by default use DHCP for IPv4 address attribution, this will conflict with the DHCP server we need to install in order to run the local bridge, so following this Hetzner tutorial for static IP configuration is required in order for the setup to work.

Since I wasn't able to get the DHCP server using dnsmasq to work no matter what I tried, I had to enable SecureNAT on the server, but disabling the Virtual NAT which is the extremely slow layer that greatly impacts performance, thus only using the DHCP server. The configuration used was the following:

SecureNAT Configuration

Since we are using a DHCP server to hand out IPs in our VPN we need to ensure that the server also has an IP assigned to the TAP interface that it's using to communicate within the VPN's network. This can be tested by running dhclient tap_vpn, where tap_vpn is the VPN's TAP interface name, and later making this change permanent by adding the following to your /etc/network/interfaces file (assuming Debian):

auto tap_vpn
allow-hotplug tap_vpn
iface tap_vpn inet dhcp

The last step in the server side of things is to ensure that systemd can properly start the server upon a system start. Since all tutorials on the internet are still using initd, here I have the systemd unit file that I used, which was taken from the softether-vpnserver Debian package and slightly modified:

[Unit]
Description=SoftEther VPN Server
After=network.target auditd.service
 
[Service]
Type=forking
TasksMax=16777216
ExecStart=/root/vpnserver/vpnserver start
ExecStop=/root/vpnserver/vpnserver stop
KillMode=process
Restart=on-failure
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_SYS_NICE CAP_SYSLOG CAP_SETUID
 
[Install]
WantedBy=multi-user.target

After all of these changes a reboot should be done on the server just to ensure that everything comes up automatically and works.

Securing the VPN Network

Since there will be different types of users on this VPN, it's important to create some groups, at least one for your public-facing servers and another one for all your local servers. This will make ACLs much easier to create and manage. Also ensure that your servers are all authenticating with extremely strong passwords or client certificates.

It's also important to ensure that, if a system on your VPN's network becomes compromised, that an attacker isn't able to use it to jump to other nodes on your VPN, specially the internet facing server. For this, a simple but effective measure would be to setup some Access List rules blocking traffic that could be used to compromise other systems.

Reverse Proxy

Since we are not exposing our local network directly to the open internet by means of port forwarding, we will require a reverse proxy server that is exposed to the internet. In this case I have a Hetzner VPS running Debian and nginx, this is the simplest and most flexible setup for a reverse proxy. Even though I have many years of experience with Apache and have been a bit reluctant to move to nginx, this is the perfect use for it, after all it was initially developed as a reverse proxy. It's super performant as a reverse proxy, easy to configure, integrates well with certbot, and can proxy raw TCP/UDP streams.

Setup for HTTP(S)

Setting up nginx for HTTP(S) traffic is super simple, all we need is nginx itself and certbot installed, after these are installed it's a simple matter of configuring virtual hosts for each site we wish to serve. For each site you want to configure you need to create a new file under /etc/nginx/sites-available/ with the following content:

server {
    listen 80;
    listen [::]:80;
 
    server_name domain.tld www.domain.tld;
 
    location / {
        proxy_pass http://ip_vpn_local:80;
        include proxy_params;
    }
}

To enable this virtual host we need to symlink it to the /etc/nginx/sites-enabled/ and restart the web server:

ln -sf /etc/nginx/sites-available/yourdomain /etc/nginx/sites-enabled/
systemctl restart nginx

This is all it takes to reverse proxy an HTTP website. Now for enabling HTTPS so that modern, pedantic and useless, browsers can access our website without complaining:

apt install certbot python3-certbot-nginx
certbot --nginx --no-redirect
systemctl reload nginx

Follow the wizard for certbot and you should now have a website that has HTTPS active and won't automatically redirect to it, so that older browsers are still able to access your website without requiring useless encryption.

Setup for TCP

Setting up nginx for proxying TCP streams is even easier than for HTTP. This is useful for things like Gopher. To set this up you need to install the stream module for nginx:

apt install libnginx-mod-stream

Next you should edit your /etc/nginx/nginx.conf and append the following at the end of the file:

stream {
    server {
        listen 70;
        listen [::]:70;
 
        proxy_pass ip_vpn_local:70;
    }
}

It's important to note that this needs to go in your /etc/nginx/nginx.conf below the http {} directive and that, as usual, a restart of the web server is required.