WireGuard End-to-End Encrypted Hub-and-Spoke
This article will show you how to set up a Hub and Spoke WireGuard VPN (Virtual Private Network) with end-to-end encryption (E2EE). In a normal hub-and-spoke configuration, the connection between the hub and each spoke is encrypted, but the connections between the spokes are not — the hub decrypts and then re-encrypts WireGuard traffic as it forwards it from spoke to spoke.
That’s fine if you trust the hub; but if not, you need end-to-end encryption. Fortunately, you can create a separate, E2EE WireGuard tunnel between each pair of spokes, nesting it inside the original hub-and-spoke tunnel. The diagram below illustrates this tunnel-in-tunnel approach:
In this example, we want Endpoint A, behind a NAT (Network Address Translation) router in Site A, to be able to access a private web app hosted by Endpoint B behind another NAT router in Site B. To do so, we first set up Endpoint A to connect over one WireGuard tunnel to Host C in a third site, Site C, with a public IP address of 192.0.2.3
; and do the same for Endpoint B.
Within this first WireGuard VPN, Endpoint A has an IP address of 10.0.0.1
, Endpoint B has an IP address of 10.0.0.2
, and Host C has an IP address of 10.0.0.3
. Through this VPN, Endpoint A can use 10.0.0.2
to connect to Endpoint B, and Endpoint B can respond to Endpoint A at 10.0.0.1
.
Then we set up a second WireGuard VPN nested within the first, where Endpoint A has an IP address of 10.9.9.1
, and Endpoint B has an IP address of 10.9.9.2
. Even though the connection will go through Host C, Host C will not be part of this network; and Host C will not be able to access the plaintext data sent between Endpoint A and Endpoint B, as it is now end-to-end encrypted.
To set up this second WireGuard VPN, we’ll create a second WireGuard interface on Endpoint A and Endpoint B, which we’ll name wg1
on each (we used wg0
for the first VPN). To use this second, nested VPN, to connect to Endpoint B, Endpoint A will use the Endpoint B’s IP address within the second VPN, 10.9.9.2
(and Endpoint B will reply to Endpoint A’s IP address within the second VPN, 10.9.9.1
).
These are the steps we’ll follow:
Set Up a Normal Hub and Spoke VPN
First, follow the Hub and Spoke Configuration Guide to set up a normal hub-and-spoke WireGuard VPN. But unlike the directions of the “extra” section in that guide, do not add any additional firewall rules restricting the forwarding of WireGuard connections from spoke to spoke (see the Extra: Configure Firewall on Host C section of this guide for instructions on a firewall for Host C that will work for this scenario).
Once you’re able to connect from Endpoint A to Endpoint B through that VPN (ie, you’re able to run curl 10.0.0.2
on Endpoint A to connect to the webserver on Endpoint B), we can start setting up the inner, E2EE VPN.
Calculate the Inner MTU
Each network interface available to a host, including WireGuard interfaces, needs to be configured with a MTU (Maximum Transmission Unit) setting, to let the host OS know how much data can be sent through the interface within a single Ethernet frame. For a physical Ethernet interface, this is usually 1500
bytes (but may differ depending on the parameters of the physical network to which it’s connected).
Because data sent through a WireGuard interface is wrapped by WireGuard with an additional UDP packet plus some WireGuard metadata, the MTU for a WireGuard interface should be slightly less than the MTU of the physical interface through which it will ultimately be sent. You can use this formula to calculate the MTU for a WireGuard interface:
(MTU of physical interface) - ((IP header size) + (UDP header size) + (WireGuard metadata size))
If the host has multiple physical interfaces, use the smallest MTU of any physical interface through which WireGuard traffic may be sent (or if you’re going to send WireGuard traffic through another virtual network interface, use the MTU of that other virtual interface).
All but the first part of the above formula are constants: IPv4 headers are always 20
bytes; IPv6 headers are always 40
bytes. UDP headers are always 8
bytes; and the WireGuard metadata size is always 32
bytes (split evenly between a header and trailer). So we can simplify this formula down to the following, using 60
(20 + 8 + 32
) if you know you’re only going to use IPv4 to transport WireGuard packets, and 80
(40 + 8 + 32
) otherwise:
(MTU of physical interface) - (IPv4 only ? 60 : 80)
For example, for a host that has one physical Ethernet interface with a MTU of 1500
, if you know you’re always going to connect to WireGuard endpoints over IPv4, you’d set the MTU of the WireGuard interface to 1440
:
1500 - 60 = 1440
WireGuard will normally do this calculation for you, and assume that the connection to other WireGuard endpoints may sometimes use IPv6; so if you don’t specify an MTU
setting in the [Interface]
section of a WireGuard config file, WireGuard will normally set the interface’s MTU to 1420
(ie 1500 - 80
).
However, this auto-calculated value is good only for the outer interface — for the inner interface, we need to perform the calculation a second time, starting with the MTU of the outer WireGuard interface instead of the physical interface. Run the ip address
command on the host to see what the MTU has been set for the outer interface (wg0
):
$ ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 06:ea:80:ec:11:87 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.11/24 brd 192.168.1.255 scope global dynamic eth0
valid_lft 3226sec preferred_lft 3226sec
3: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 10.0.0.1/32 scope global wg0
valid_lft forever preferred_lft forever
In the above example, the MTU for wg0
is 1420
. If we’re only going to use IPv4 addresses for the inner tunnel’s endpoints, our MTU calculation for the inner interface will produce a value of 1360
:
1420 - 60 = 1360
If we decide to use IPv6 addresses for the inner tunnel’s endpoints, our MTU calculation will produce a value of 1340
:
1420 - 80 = 1340
I’m going to use 1340
as the MTU for the examples below (so that they would work equally well for both IPv4 and IPv6), but be sure to check the MTU size of the outer interface on each host, so you can set the MTU size of the inner interface appropriately.
Generate a Second Set of WireGuard Keys
For the second, inner VPN, you could re-use the same WireGuard keys you used for the outer VPN — the hub wouldn’t be able to decrypt the inner VPN’s traffic even if you did. But the best practice is to use a separate key pair for each WireGuard interface, even for interfaces on the same machine, as it allows keys to be rotated separately on each separate network.
Run the following commands to generate a new key pair for Endpoint A:
$ wg genkey > endpoint-a-wg1.key
$ wg pubkey < endpoint-a-wg1.key > endpoint-a-wg1.pub
And the following commands to generate a new key pair for Endpoint B:
$ wg genkey > endpoint-b-wg1.key
$ wg pubkey < endpoint-b-wg1.key > endpoint-b-wg1.pub
This will generate four files: endpoint-a-wg1.key
, endpoint-a-wg1.pub
, endpoint-b-wg1.key
, and endpoint-b-wg1.pub
. The *.key
files contain the private keys, and the *.pub
files contain the public keys. The content of each will be 44 characters of Base64-encoded text:
$ cat endpoint-a-wg1.key
0F11111111111111111111111111111111111111110=
$ cat endpoint-a-wg1.pub
hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=
$ cat endpoint-b-wg1.key
2G22222222222222222222222222222222222222220=
$ cat endpoint-b-wg1.pub
777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI=
Configure a Second Interface on Endpoint A
So with those new keys in hand, configure a second WireGuard interface for Endpoint A at /etc/wireguard/wg1.conf
(the first WireGuard interface we created was named wg0.conf
, in the same directory):
# /etc/wireguard/wg1.conf # local settings for Endpoint A [Interface] PrivateKey = 0F11111111111111111111111111111111111111110= Address = 10.9.9.1/32 ListenPort = 59999 MTU = 1340 # remote settings for Endpoint B [Peer] PublicKey = 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI= Endpoint = 10.0.0.2:59999 AllowedIPs = 10.9.9.2/32
Notice that via the Address
setting of the [Interface]
section we give this interface (wg1
) an IP address of 10.9.9.1
; whereas we gave the outer interface (wg0
) an IP address of 10.0.0.1
. We’ll do the same thing on Endpoint B, where the inner interface will have an IP address of 10.9.9.2
, and the outer interface an IP address of 10.0.0.2
.
In the [Peer]
section for Endpoint B, we’ll use Endpoint B’s inner interface IP address in the AllowedIPs
setting, and its outer address in the Endpoint
setting. This means that on Endpoint A, any traffic directed to 10.9.9.2
will be sent first through the inner interface (wg1
), wrapped in one layer of encryption for its “direct” connection to Endpoint B, and then sent as a new set of packets to 10.0.0.2
. But because the AllowedIPs
setting for the outer interface (wg0
) includes 10.0.0.2
, this new set of packets will also be sent through the outer interface, and encrypted a second time, and wrapped in another set of packets to be sent through Endpoint A’s physical Ethernet interface, on to Host C (where Host C will forward those packets to Endpoint B).
Also notice that we’ve set the ListenPort
setting of the [Interface]
section to be 59999
. This means that any packets that Endpoint A receives on UDP port 59999
, including packets received by the outer WireGuard interface, will be sent through this inner interface for decryption and reassembly into their original state. We’ll do the same thing on Endpoint B (which is why we’re using the same port in the Endpoint
setting for Endpoint B). When Endpoint A receives packets on the listen port of its outer WireGuard interface (wg0
) from Host C, and decrypts and reassembles those packets, and sees the reassembled packets are themselves directed to UDP port 59999
, it will send them through this interface (wg1
) for one last layer of decryption and reassembly.
One last thing to notice: we need to manually set the MTU
setting in the [Interface]
section to the value we calculated in the Calculate the Inner MTU section.
Configure a Second Interface on Endpoint B
Next, configure a second WireGuard interface for Endpoint B at /etc/wireguard/wg1.conf
, mirroring the second interface on Endpoint A:
# /etc/wireguard/wg1.conf # local settings for Endpoint B [Interface] PrivateKey = 2G22222222222222222222222222222222222222220= Address = 10.9.9.2/32 ListenPort = 59999 MTU = 1340 # remote settings for Endpoint A [Peer] PublicKey = hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k= Endpoint = 10.0.0.1:59999 AllowedIPs = 10.9.9.1/32
This second interface on Endpoint B will have an IP address of 10.9.9.2
(configured via its Address
setting), and will route any packets addressed to 10.9.9.1
to the second interface on Endpoint A (configured via its AllowedIPs
setting). It uses Endpoint A’s 10.0.0.1
IP address as the Endpoint
setting for its connection to Endpoint A, so as to tunnel this inner WireGuard connection (10.9.9.2
to 10.9.9.1
) through the outer WireGuard connection (10.0.0.2
to 10.0.0.1
by way of Host C at 10.0.0.3
).
For this inner tunnel, we’ll use the same ListenPort
setting of 59999
as we used on Endpoint A. While we could use a different port on each different spoke if we wanted to, using the same port will make it easier to set up firewall rules on Host C to block all non-tunnel-in-tunnel traffic from being sent through the outer tunnel (see Extra: Configure Firewall on Host C).
Make sure you customize the MTU
setting for Endpoint B with the value you calculated for it using the formula from the Calculate the Inner MTU section.
Start Up WireGuard
On both Endpoint A and Endpoint B, start the wg-quick
service for new second interface (wg1
). On Linux with systemd, run the following commands:
$ sudo systemctl enable wg-quick@wg1.service
$ sudo systemctl start wg-quick@wg1.service
On systems without systemd, run this command instead:
$ sudo wg-quick up wg1
If you run wg-quick up
directly, you’ll see output like the following:
$ sudo wg-quick up /etc/wireguard/wg1.conf
[#] ip link add wg1 type wireguard
[#] wg setconf wg1 /dev/fd/63
[#] ip -4 address add 10.9.9.1/32 dev wg1
[#] ip link set mtu 1340 up dev wg1
[#] ip -4 route add 10.9.9.2/32 dev wg1
If you ran systemctl start
instead, you can see the same output by running the journalctl
tool, like so:
$ journalctl -u wg-quick@wg1.service
systemd[1]: Starting WireGuard via wg-quick(8) for wg1...
wg-quick[271288]: [#] ip link add wg1 type wireguard
wg-quick[271288]: [#] wg setconf wg1 /dev/fd/63
wg-quick[271288]: [#] ip -4 route add 10.9.9.1/32 dev wg1
wg-quick[271288]: [#] ip link set mtu 1340 up dev wg1
wg-quick[271288]: [#] ip -4 address add 10.9.9.2/32 dev wg1
systemd[1]: Started WireGuard via wg-quick(8) for wg1.
Make sure you also keep the first interface (wg0
) up on all hosts. If you run the wg show
command, you should see both interfaces listed on Endpoint A and Endpoint B:
$ sudo wg show
interface: wg0
public key: /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
private key: (hidden)
listening port: 51821
peer: jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
endpoint: 192.0.2.3:51823
allowed ips: 10.0.0.0/24
latest handshake: 7 minutes, 42 seconds ago
transfer: 748 B received, 932 B sent
interface: wg1
public key: hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k=
private key: (hidden)
listening port: 59999
peer: 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI=
endpoint: 10.0.0.2:59999
allowed ips: 10.9.9.2/32
Test Out the Tunnel
Test out the new E2EE inner tunnel the same way as you tested the outer tunnel — but this time, use the IP address of Endpoint B’s inner interface. If you start up a webserver on port 80 of Endpoint B, you should be able to connect to it using an IP address of 10.9.9.2
from Endpoint A:
$ curl 10.9.9.2
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...
If you see any HTML output from this, your E2EE WireGuard tunnel works!
Extra: Allow Endpoint B to Initiate Access to Endpoint A
The directions for setting up the outer interfaces in the Hub and Spoke Configuration Guide instruct you to add a PersistentKeepalive
setting to the configuration for Endpoint B, but not for Endpoint A. This keeps the outer tunnel between Host C and Endpoint B open at all times, so Host C can relay traffic initiated from Endpoint A through it to Endpoint B.
This is ideal for a scenario where Endpoint A always initiates connections to Endpoint B, but Endpoint B never needs to initiate connections to Endpoint A — like our example, where Endpoint B hosts a web app of which Endpoint A is a client. But if you have a scenario where sometimes Endpoint B also needs to initiate connections to Endpoint A, make sure you add a PersistentKeepalive
setting to the configuration for Endpoint A’s outer interface (wg0
):
# /etc/wireguard/wg0.conf # local settings for Endpoint A [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1/32 ListenPort = 51821 # remote settings for Host C [Peer] PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw= Endpoint = 192.0.2.3:51823 AllowedIPs = 10.0.0.0/24 PersistentKeepalive = 25
A PersistentKeepalive
setting of 25
(seconds) should do the trick in most cases.
You do not need to add a PersistentKeepalive
setting to any inner interface configuration — if you’ve set PersistentKeepalive
on the outer interface, adding a PersistentKeepalive
setting to the inner interface will just add extra traffic for no benefit.
Also, if you set up a firewall on Endpoint A, you’ll need to configure it to allow new connections on its inner interface (wg1
), similar to the directions below for the firewall for Endpoint B.
Extra: Configure Firewall on Endpoint A
On Endpoint A, which only initiates outbound connections, and never needs to receive new inbound connections, you don’t need anything special — the default settings for a generic firewall are good for this (accept traffic from established connections, block new inbound connections, block connection forwarding).
If you don’t have a firewall set up on Endpoint A yet, and Endpoint A is running Linux, you can add add the following iptables commands to your WireGuard configuration to block new connections from other members of the WireGuard VPN (while still allowing two-way traffic through connections that Endpoint A has initiated):
# /etc/wireguard/wg1.conf # local settings for Endpoint A [Interface] PrivateKey = 0F11111111111111111111111111111111111111110= Address = 10.9.9.1/32 ListenPort = 59999 MTU = 1340 PreUp = iptables -A INPUT -i %i -m state --state ESTABLISHED,RELATED -j ACCEPT PreUp = iptables -A INPUT -i %i -j REJECT PostDown = iptables -D INPUT -i %i -m state --state ESTABLISHED,RELATED -j ACCEPT PostDown = iptables -D INPUT -i %i -j REJECT # remote settings for Endpoint B [Peer] PublicKey = 777Ntbnv/AA9Fvd53yVR6Fsrd02CNCM2ySXgzXbbSUI= Endpoint = 10.0.0.2:59999 AllowedIPs = 10.9.9.2/32
You can add these same commands to both the inner interface (wg1
) and the outer interface (wg0
) — wg-quick will substitute the name of the interface for the %i
token when it executes the PreUp
or PostDown
commands.
If you want to add these commands to a running interface, shut down the interface first (eg sudo wg-quick down wg1
), make the change, and then start the interface back up (eg sudo wg-quick up wg1
). This will make sure that the PostDown
commands, executed when you shut the interface down, match the PreUp
commands that were executed when you started the interface up.
Extra: Configure Firewall on Endpoint B
On Endpoint B, the default settings for a generic firewall (accept traffic from established connections, block new inbound connections, block connection forwarding) are good for its outer WireGuard interface and physical network interface — but for its inner WireGuard interface, the firewall needs to allow new HTTP connections from Endpoint A.
If you don’t have a firewall set up on Endpoint B yet, and Endpoint B is running Linux, you can add add the following iptables commands to your inner WireGuard configuration to block new connections from other members of the inner WireGuard VPN — except for connections to Endpoint B’s TCP port 80
:
# /etc/wireguard/wg1.conf # local settings for Endpoint B [Interface] PrivateKey = 2G22222222222222222222222222222222222222220= Address = 10.9.9.2/32 ListenPort = 59999 MTU = 1340 PreUp = iptables -A INPUT -i wg1 -m state --state ESTABLISHED,RELATED -j ACCEPT PreUp = iptables -A INPUT -i wg1 -m state --state NEW -p tcp --dport 80 -j ACCEPT PreUp = iptables -A INPUT -i wg1 -j REJECT PostDown = iptables -D INPUT -i wg1 -m state --state ESTABLISHED,RELATED -j ACCEPT PostDown = iptables -D INPUT -i wg1 -m state --state NEW -p tcp --dport 80 -j ACCEPT PostDown = iptables -D INPUT -i wg1 -j REJECT # remote settings for Endpoint A [Peer] PublicKey = hN+4fjPihcKbEDFQhqrMmW8oPqqaT4CUQYtyg3FUx3k= Endpoint = 10.0.0.1:59999 AllowedIPs = 10.9.9.1/32
For its outer WireGuard interface, you can block all new connections except for to its inner WireGuard listen port (59999
):
# /etc/wireguard/wg0.conf # local settings for Endpoint B [Interface] PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA= Address = 10.0.0.2/32 ListenPort = 51822 PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT PreUp = iptables -A INPUT -i wg0 -m state --state NEW -p udp --dport 59999 -j ACCEPT PreUp = iptables -A INPUT -i wg0 -j REJECT PostDown = iptables -D INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT PostDown = iptables -D INPUT -i wg0 -m state --state NEW -p udp --dport 59999 -j ACCEPT PostDown = iptables -D INPUT -i wg0 -j REJECT # remote settings for Host C [Peer] PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw= Endpoint = 192.0.2.3:51823 AllowedIPs = 10.0.0.0/24 PersistentKeepalive = 25
If you’re using nftables on Endpoint B instead of iptables, use a ruleset like this:
#!/usr/sbin/nft -f flush ruleset define pub_iface = "eth0" define outer_wg_iface = "wg0" define outer_wg_port = 51822 define inner_wg_port = 59999 table inet filter { chain input { type filter hook input priority 0; policy drop; # accept all loopback packets iif "lo" accept # accept all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # accept all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # drop new connections over rate limit ct state new limit rate over 1/second burst 10 packets drop # accept all DHCPv6 packets received on link-local address ip6 daddr fe80::/64 udp dport dhcpv6-client accept # accept all SSH packets received on a public interface iifname $pub_iface tcp dport ssh accept # accept all outer WireGuard packets received on a public interface iifname $pub_iface udp dport $outer_wg_port accept # accept all inner WireGuard packets received on the outer WireGuard interface iifname $outer_wg_iface udp dport $inner_wg_port accept # accept all HTTP packets received on the inner WireGuard interface iifname $inner_wg_iface tcp dport http accept # reject with polite "port unreachable" icmp response reject } chain forward { type filter hook forward priority 0; policy drop; reject with icmpx type host-unreachable } }
See WireGuard Nftables Configuration Guide for an explanation of the various rules — and notice that this is ruleset is nearly identical to the nftables point-to-point configuration for Endpoint B, but with the addition of a rule to accept inner WireGuard packets on the outer WireGuard interface, and to adjust the rule that accepts HTTP packets to use the inner interface instead of the outer one.
If you’re using UFW for the firewall on Endpoint B, you minimally just need to open up UDP port 599999
on the outer WireGuard interface (wg0
), and TCP port 80
on the inner WireGuard interface (wg1
):
$ sudo ufw allow in on wg0 proto udp to any port 59999
Rules updated
Rules updated (v6)
$ sudo ufw allow in on wg1 proto tcp to any port 80
Rules updated
Rules updated (v6)
$ sudo ufw status
To Action From
-- ------ ----
59999/udp on wg0 ALLOW Anywhere
80/tcp on wg1 ALLOW Anywhere
59999/udp (v6) on wg0 ALLOW Anywhere (v6)
80/tcp (v6) on wg1 ALLOW Anywhere (v6)
And if you’re using firewalld for the firewall on Endpoint B, you might want to start by putting your physical network interface (eth0
) into the public
zone, and the two WireGuard interfaces (wg0
and wg1
) into the trusted
zone:
$ sudo firewall-cmd --zone=public --add-interface=eth0
success
$ sudo firewall-cmd --zone=trusted --add-interface=wg0
success
$ sudo firewall-cmd --zone=trusted --add-interface=wg1
success
$ sudo firewall-cmd --get-active-zones
public
interfaces: eth0
trusted
interfaces: wg1 wg0
$ sudo firewall-cmd --runtime-to-permanent
success
However, if you want to further lock down access to these interfaces, use the instructions for the firewalld point-to-point configuration for Endpoint B. If you do that, add wg1
(the inner interface) instead of wg0
(the outer interface) to the custom mywg
zone, and add an additional custom zone for your wg0
interface:
$ sudo firewall-cmd --permanent --new-zone=myouter
success
$ sudo firewall-cmd --reload
success
$ sudo firewall-cmd --zone=myouter --add-port=59999/udp
success
$ sudo firewall-cmd --zone=myouter --add-interface=wg0
success
$ sudo firewall-cmd --info-zone=myouter
mysite (active)
target: default
icmp-block-inversion: no
interfaces: wg0
sources:
services:
ports: 59999/udp
protocols:
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
$ sudo firewall-cmd --runtime-to-permanent
success
Extra: Configure Firewall on Host C
If you’re using an iptables firewall, the instructions from the extra firewall section of the Hub and Spoke Configuration Guide directs you to allow new HTTP connections to be forwarded through Endpoint B’s outer tunnel, and to block all other new connections. However, with the new inner, E2EE tunnel between Endpoint A and Endpoint B, Host C won’t ever see this HTTP traffic — it will only see encrypted WireGuard traffic from the inner tunnel passing through its outer tunnel.
So instead of allowing TCP port 80
traffic directed to Endpoint B through its WireGuard tunnel, we need to adjust the firewall to instead allow traffic directed to Endpoint B’s inner WireGuard listen port, UDP port 59999
. Also, since a) the traffic will be end-to-end encrypted, so making a distinction between new and established connections with it won’t really add much security; and b) we’re using the same inner WireGuard port for all spokes; we can simplify the two original ACCEPT
rules into one: allow all traffic forwarded through Host C’s WireGuard tunnel destined for UDP port 59999
:
# /etc/wireguard/wg0.conf # local settings for Host C [Interface] PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGA= Address = 10.0.0.3/32 ListenPort = 51823 PreUp = sysctl -w net.ipv4.ip_forward=1 PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT PreUp = iptables -A INPUT -i wg0 -j REJECT PreUp = iptables -A FORWARD -i wg0 -p udp --dport 59999 -j ACCEPT PreUp = iptables -A FORWARD -i wg0 -j REJECT PostDown = iptables -D INPUT -i wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT PostDown = iptables -D INPUT -i wg0 -j REJECT PostDown = iptables -D FORWARD -i wg0 -p udp --dport 59999 -j ACCEPT PostDown = iptables -D FORWARD -i wg0 -j REJECT # remote settings for Endpoint A [Peer] PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= AllowedIPs = 10.0.0.1/32 # remote settings for Endpoint B [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= AllowedIPs = 10.0.0.2/32
If you’re using nftables on Host C instead of iptables, use the same ruleset laid out for Host C in the Hub and Spoke section of the WireGuard Nftables Configuration Guide, except change this rule that allows HTTP traffic to be forwarded to Endpoint B:
# forward all HTTP packets for Endpoint B
ip daddr 10.0.0.2 tcp dport http accept
To a rule that allows all E2EE WireGuard traffic to be forwarded between any two spokes:
# forward all E2EE WireGuard packets
udp dport 59999 accept
(Note that neither UFW nor firewalld is a good fit for the hub role — consider using iptables or nftables instead.)