Difficulty: Intermediate to High
Have you ever wanted to host a website or other web service without exposing your home IP address or paying for a gigantic VPS (Virtual Private Server)? While VPSs tend to be rather generous on bandwidth they tend to be rather stingy on storage space and general compute power. If I want the VPS with 500GB of storage space to host a clearnet Bitcoin node, I’m going to have to pay a pretty high reoccurring fee. I’m going to explain a method which leverages Firewalld and Wireguard VPN to allow public hosting with a very modestly priced VPS. With this method you’ll be able to leverage your existing home storage and compute resources to do the heavy lifting on your home network while getting the benefit making your application public without exposing your home IP address. This solution will work even if you are stuck behind the dreaded Carrier Grade NAT.
The idea is that you’ll actually run your service from your home network but through the magic of VPN and NAT (Network Address Translation) technology the service can be made available on the public internet. I run a VM (Virtual Machine) host via KVM at my home but this should work equally well with a Raspberry Pi or similar less powerful computer. I would consider this deployment to be of intermediate to high level difficulty depending on your knowledge of Linux and general networking. For this example I’m going to make my BTCPay Server, which I run on my local network, publicly available on the IPv4 internet. I’ll do this without needing an expensive VPS with enough storage for a full Bitcoin node or without exposing my home IP address. See the image above for a high-level overview of what we’ll be configuring.
Here’s what you’ll need to be able to successfully deploy this solution:
- A website or other service that you want to make available on the public internet. Think of something like a Lightning node that you want to be available on Clearnet rather than Tor only.
- A Linux VPS to which you have root access with Wireguard and Firewalld installed.
- Optional – I use the Network Manager utility for configuring a persistent static route on the server at the home location. You can configure this route via another method if you like.
- A server of some kind on your home network running something you want to host publicly. The server could be a large enterprise machine, a Raspberry Pi, or something in between.
Caveat: Depending on how much bandwidth your application requires, you could run into your ISP data cap. For all of the data your VPS uses to service clients on the internet, your home internet connection will use double that amount. All traffic to the service running on the VPS will travel to your home network and back to the VPS so for all accesses to the VPS you’ll receive and transmit that much traffic on your home ISP connection.
From this point forward I’ll refer to the remote side with the public IPv4 address as the “VPS” and the device running the application on the home network as the “VM” since I run my application on a Virtual Machine. For you this could be a Raspberry Pi or other physical device.
First we’ll do a basic Firewalld configuration on the VPS to allow the Wireguard connection from the home VM. This assumes you already have Firewalld installed and running:
$ sudo systemctl start firewalld
$ sudo systemctl enable firewalld
Most likely your public internet connection on the VPS is going to be in the “Public” Zone but we will confirm now. Note the interface name along with the public “inet” address:
$ ip addr
ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:16:3c:bd:75:9b brd ff:ff:ff:ff:ff:ff
altname enp0s3
inet 172.245.91.37/26 brd 172.245.91.63 scope global noprefixroute ens3
Then run this command to confirm that “ens3” is in the Public zone:
$ sudo firewall-cmd --list-all-zones
public (active)
target: default
icmp-block-inversion: no
interfaces: ens3
sources:
services: dhcpv6-client ssh
ports:
protocols:
forward: yes
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
Now I’ll make a permanent change to the Firewalld configuration, reload the firewall so the change takes effect, and then review the configuration to make sure the configuration is as expected:
$ sudo firewall-cmd --zone=public --add-port=51871/udp --permanent
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-all-zones
public (active)
target: default
icmp-block-inversion: no
interfaces: ens3
sources:
services: dhcpv6-client ssh
ports: 51871/udp
protocols:
forward: yes
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
You can now see that UDP port 51871 is allowed. This is the port on the VPS that will allow the incoming Wireguard connection from the home VM.
Now we’re going to configure a basic Wireguard VPN tunnel. I’m not going to go into detail here. A basic Wireguard configuration is pretty well documented on the internet.
However, first there is one important WARNING. As part of the Wireguard configuration we are going to be sending ALL traffic (as in 0.0.0.0/0) from the home VM to the public VPS. If your home VM is running on a different subnet, like a DMZ for instance, than the machine you are using to configure the VM will lose access when you enable the Wireguard connection.
We’ll work around this by configuring a static route to the network of the configuration machine. I use the very fancy “nmtui” utility in Network Manager to accomplish this:

Now on to the Wireguard configuration:
Public VPS side:
[Interface]
Address = 192.168.231.1/24
ListenPort = 51871
PrivateKey = [REDACTED]
[Peer]
PublicKey = [REDACTED]
AllowedIPs = 192.168.231.0/24
Home VM side:
[Interface]
Address = 192.168.231.2/24
ListenPort = 51993
PrivateKey = [REDACTED]
[Peer]
PublicKey = [REDACTED]
AllowedIPs = 192.168.231.0/24, 0.0.0.0/0
Endpoint = 172.245.91.37:51871
PersistentKeepalive = 25
The really important part of this configuration is the 0.0.0.0/0 “AllowedIPs” on the Home VM side. This allows my BTCPay Server running on my home network to return traffic from web requests coming to the public IP address of the VPS. In fact, all traffic originating on the home VM will go toward the VPS Wireguard interface EXCEPT for any static routes you have set (like we did with “nmtui” above).
Another important thing to note is that we don’t specify an endpoint on the VPS side of the Wireguard VPN configuration. The Wireguard VPN tunnel has to be initiated from the home VM side but this provides a large benefit in that the home VM side doesn’t require a public IP. This configuration will work even if your home network is stuck behind Carrier Grade Double NAT!
Don’t forget to start and enable your wireguard service with Systemd on both the VPS and VM sides of the tunnel. The name of my Wireguard service is “btcpay”:
$ sudo systemctl start wg-quick@btcpay
$ sudo systemctl enable wg-quick@btcpay
Make sure that your Wireguard tunnel is working correctly by pinging between the home VM and the VPS on the 192.168.231.0/24 subnet. You can run “sudo wg” to see that traffic has been both transmitted and received:

Before we get into the interesting part we will enable IP routing on the VPS. This will be required to route traffic from the home VM, through the BTCPay VM, and out to the internet as well as in the other direction.
On my Arch Linux system I will now enable IP Routing by creating the file “/etc/sysctl.d/99-sysctl.conf” and adding the single line “net.ipv4.ip_forward = 1”. Now reboot the VPS to confirm that IP routing is enabled in a persistent fashion. Run the following command to confirm that IP Routing is persistently enabled. We are hoping to get a “1” as the result:
$ sudo sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
The way that you enable IPv4 Routing will vary depending on your Linux distribution but IP Routing must be enabled for this setup to work.
Now we get to the interesting part. We are going to configure the VPS to route traffic from the home VM out to the internet and port forward traffic from the internet back to the BTCPay VM.
Before we start, let’s enable a constant ping from our VM to 1.1.1.1. Hopefully you will get a result of “Packet filtered” because the firewalld instance on the VPS is dropping the traffic. We are going to change that now!
At first thought you might consider enabling masquerade on the public Firewalld zone and that will work… until you have more than one connection at a time. This would probably be fine for my very lightly used BTCPay Server but if you run a server that receives even a modest amount of traffic you will run into problems. Instead we will use a Firewalld feature that is rarely mentioned and there seems to be little documentation on its actual usage. We are going to use Firewalld Policies. If you are familiar with the concept of a Zone Based Firewall then the Firewalld Policy configuration will make sense to you. The Firewalld Policy tied together with the port forwarding on the VPS public zone will give us the desired result of traffic flowing between our home VM and the internet like it has the public IPv4 address directly tied to an interface.
This is a useful page detailing Firewalld policy commands if you want to learn more about this useful feature:
https://firewalld.org/documentation/man-pages/firewall-cmd.html
The Firewalld configuration will take place entirely on the VPS side. The first step is assigning the Wireguard interface we just created to the internal zone, reloading Firewalld for the change to go into effect, and then verifying that the configuration is as expected:
$ sudo firewall-cmd --permanent --zone=internal --add-interface=btcpay
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-all-zones
internal (active)
target: default
icmp-block-inversion: no
interfaces: btcpay
We will now add a “rich rule” so that Firewalld will NAT the traffic from our home VM (all traffic from 192.168.231.2) out to the internet through our VPS:
$ sudo firewall-cmd --permanent --zone=public --add-rich-rule='family="ipv4" source address="192.168.231.2/32" masquerade'
$ sudo firewall-cmd --reload
We will also disable intra zone forwarding as we don’t need this feature and disabling it is better for security:
$ sudo firewall-cmd --zone=public --permanent --remove-forward
$ sudo firewall-cmd --reload
We are going to create a new policy called “outbound”:
$ sudo firewall-cmd --new-policy=outbound --permanent
Then we will add interfaces ingress and egress zones to the policy:
$ sudo firewall-cmd --permanent --policy=outbound --add-ingress-zone=internal
$ sudo firewall-cmd --permanent --policy=outbound --add-egress-zone=public
We will then set the target to ACCEPT meaning that all traffic received from the internal zone will be forwarded to the public zone. After making the change we’ll reload Firewalld for the change to go into effect:
$ sudo firewall-cmd --permanent --policy=outbound --set-target=ACCEPT
$ sudo firewall-cmd --reload
I had to stop my constant ping and then restart it but I’m now able to ping 1.1.1.1 through my VPS! You can stop the Wireguard service on the home VM for a moment to notice the difference in latency when routing through the VPS vs just going directly to 1.1.1.1:
$ ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=55 time=21.3 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=55 time=21.1 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=55 time=21.2 ms
$ sudo systemctl stop wg-quick@btcpay
S ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=54 time=11.5 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=54 time=7.70 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=54 time=8.36 ms
Make sure to restart Wireguard after this test:
$ sudo systemctl start wg-quick@btcpay
The final step is to allow the traffic to our external ports and forward that traffic from the VPS to the home VM. In the case of BTCPay we’ll forward http (80/tcp) and https (443/tcp). On the VPS we’ll run several commands. These allow the traffic into the public zone:
$ sudo firewall-cmd --permanent --zone=public --add-service=http
$ sudo firewall-cmd --permanent --zone=public --add-service=https
Now we will port forward these to our VM:
$ sudo firewall-cmd --permanent --zone=public --add-forward-port=port=80:proto=tcp:toport=80:toaddr=192.168.231.2
$ sudo firewall-cmd --permanent --zone=public --add-forward-port=port=443:proto=tcp:toport=443:toaddr=192.168.231.2
Finally we’ll reload Firewalld one last time to make the changes take effect:
sudo firewall-cmd --reload
You can now access the login page of my BTCPay Server that is being run via this method right now! The VM running BTCPay Server as well as my Bitcoin full node are running on my home network and this inexpensive VPS is allowing you to access it!
https://btcpay.bitcoinlizard.net
If you found this guide useful you can throw a couple Sats my way via this BTCPay Server instance using the donation widget below.
2 replies on “VPS Passthrough Hosting
Host a large website or application with a modest VPS”
And what IP would the service see on the VM as incoming? I have been thinkering with this but it keeps appearing as incoming IP the wireguard server.
You will see the machine accessing your service’s public IP. I an electrumx and a Core Lightning node via this method and they won’t work right without seeing the other end’s public IP as the incoming connection on the VM. This is the start of the section of the guide that would relate to making this work correctly:
“At first thought you might consider enabling masquerade on the public Firewalld zone and that will work…”