WireGuard Access Control With Iptables

WireGuard allows you to securely access one host (ie computer, mobile phone, internet-of-things device, etc) from another. For a given host, you can control which other hosts can connect through WireGuard to the first host by specifying the public keys of the other hosts in the first host’s WireGuard configuration. However, WireGuard doesn’t have a built-in mechanism for controlling what services or other hosts you can connect to through that WireGuard connection. To control that, you need to use firewall software on the host itself.

Iptables is the most common firewall software on Linux. This article will show you how to use iptables to apply ACLs (Access-Control Lists) to the network services exposed through WireGuard, for each of the primary WireGuard topologies:

Example Network

For this article we’ll use the following example network, where we need to connect six particular hosts together, three of which are on the same LAN (Local Area Network), which I’ll call Site A, and three of which are remote:

Allowed Network Access
Figure 1. Allowed network access

Alice’s Workstation, located on the LAN, needs to be able to connect to TCP ports 22 (SSH), 25 (SMTP, to send email), and 143 (IMAP, to check her email) on the Mail Server; TCP ports 22 (SSH, for secure shell access), 80 (the main web app), and 8080 (a secondary “admin” web app) on the Web Server; and TCP port 5900 on the VNC Server (Virtual Network Computing, to access a desktop application running on the server).

Bob’s Workstation, located at a remote office, needs to be able to connect to TCP ports 25 (SMTP) and 143 (IMAP) on the Mail Server; TCP ports 80 (main HTTP app) and 8080 (“admin” HTTP app) on the Web Server; and TCP port 5900 on the VNC Server.

Cindy’s Laptop, located at her home office or on the road, needs to be able to connect to TCP ports 25 (SMTP) and 143 (IMAP) on the Mail Server; and TCP port 80 on the Web Server.

The VNC Server is located at a remote cloud environment. It needs to access TCP port 25 on the Mail Server (to send email), and TCP port 80 on the Web Server (to communicate with the main web app’s web API).

The Mail and Web Servers are located on the LAN. The Web Server needs to access TCP port 25 on the Mail Server, to send email.

In tabular form, our ACLs would look like this:

Table 1. Network ACLs
Destination Host Destination Port Source Host Access

Mail Server

22

Alice’s Workstation

Allow

Mail Server

25

Alice’s Workstation

Allow

Mail Server

25

Bob’s Workstation

Allow

Mail Server

25

Cindy’s Laptop

Allow

Mail Server

25

VNC Server

Allow

Mail Server

25

Web Server

Allow

Mail Server

143

Alice’s Workstation

Allow

Mail Server

143

Bob’s Workstation

Allow

Mail Server

143

Cindy’s Laptop

Allow

Web Server

22

Alice’s Workstation

Allow

Web Server

80

Alice’s Workstation

Allow

Web Server

80

Bob’s Workstation

Allow

Web Server

80

Cindy’s Laptop

Allow

Web Server

80

VNC Server

Allow

Web Server

8080

Alice’s Workstation

Allow

Web Server

8080

Bob’s Workstation

Allow

VNC Server

5900

Alice’s Workstation

Allow

VNC Server

5900

Bob’s Workstation

Allow

Everything else

Everything else

Everything else

Deny

For simplicity, we’ll set up and tear down our iptables rules via PreUp and PostDown settings in the configuration file for the WireGuard interface on each host; and we’ll name the WireGuard interface on each host wg0 (using a config file named /etc/wireguard/wg0.conf on each host). Also, we’ll only use the IPv4 version of iptables. The IPv6 version of iptables works the same way, except you substitute the ip6tables command for the iptables command.

Point to Point

If we used WireGuard to connect our example hosts with a point to point topology, the network diagram would look like this:

Point to Point Network
Figure 2. Point to point network

We’d use port forwarding (DNAT, Destination Network Address Translation) on the Internet router at Site A to allow Internet access to to the Mail Server on port 51823, and to the Web Server on port 51824. The VNC Server would be accessible to the Internet via a static IP address at 203.0.113.50. Alice’s Workstation, Bob’s Workstation, and Cindy’s Laptop would not have fixed Internet IP addresses (and as such, they would have to be the initiator of any WireGuard connection to another host).

To help keep all these addresses straight, here’s a table of each host’s public endpoint IP address and port, its endpoint IP address and port within the Site A LAN, and its internal WireGuard VPN (Virtual Private Network) IP address:

Table 2. Point to Point Addresses
Host Internet Address LAN Address WireGuard Address

Alice’s Workstation

dynamic

dynamic

10.0.0.2

Mail Server

198.51.100.10:51823

192.168.1.63:51823

10.0.0.3

Web Server

198.51.100.10:51824

192.168.1.44:51824

10.0.0.4

VNC Server

203.0.113.50:51825

N/A

10.0.0.5

Bob’s Workstation

dynamic

N/A

10.0.0.6

Cindy’s Laptop

dynamic

N/A

10.0.0.7

Since each host is connected directly to the other hosts via WireGuard, we’ll use their private WireGuard IP addresses for our iptables rules, and we’ll apply the rules to the INPUT iptables chain.

The firewalls for the three users’ workstations are easy, since they allow only outbound-initiated connections. Since they’re simple, we’ll just add a couple of rules directly to the INPUT chain. Each of these hosts’ settings will mirror those from the “Endpoint A” firewall in the point to point configuration tutorial — except that in this guide, we’ll assume that you may also adjust your default iptables chain policies to drop all connections by default, as described by the Chain Policies recommendation at the end of this article. When you deny access by default, you need an additional firewall rule to explicitly accept connections on your WireGuard interface’s listen port (51820 below):

# local settings for Alice's Workstation
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51820

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# local firewall
PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -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 -j REJECT

# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.168.1.63:51823
AllowedIPs = 10.0.0.3/32

# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
Endpoint = 192.168.1.44:51824
AllowedIPs = 10.0.0.4/32

# remote settings for VNC Server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
Endpoint = 203.0.113.50:51825
AllowedIPs = 10.0.0.5/32
# local settings for Bob's Workstation
[Interface]
PrivateKey = EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA=
Address = 10.0.0.6/32
ListenPort = 51820

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# local firewall
PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -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 -j REJECT

# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 198.51.100.10:51823
AllowedIPs = 10.0.0.3/32

# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
Endpoint = 198.51.100.10:51824
AllowedIPs = 10.0.0.4/32

# remote settings for VNC Server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
Endpoint = 203.0.113.50:51825
AllowedIPs = 10.0.0.5/32
# local settings for Cindy's Laptop
[Interface]
PrivateKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGA=
Address = 10.0.0.7/32
ListenPort = 51820

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# local firewall
PreUp = iptables -A INPUT -i wg0 -m state --state ESTABLISHED,RELATED -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 -j REJECT

# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 198.51.100.10:51823
AllowedIPs = 10.0.0.3/32

# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
Endpoint = 198.51.100.10:51824
AllowedIPs = 10.0.0.4/32

The Mail Server is the most complicated; let’s do it next. It accepts connections from all the other hosts in our WireGuard network, but needs to limit access to certain ports for certain hosts (for example, to allow SSH access from Alice’s Workstation only; see the Network ACLs table for the full list).

We could add all our iptables rules directly to the INPUT chain; but to keep things better organized, we’ll add four custom chains (wg0-input, wg0-input-ssh, wg0-input-smtp, and wg0-input-imap), and direct incoming packets from our wg0 WireGuard interface through them:

# local settings for Mail Server
[Interface]
PrivateKey = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCGA=
Address = 10.0.0.3/32
ListenPort = 51823

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51823 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51823 -j ACCEPT

# local firewall
PreUp = iptables -N wg0-input
PreUp = iptables -N wg0-input-ssh
PreUp = iptables -N wg0-input-smtp
PreUp = iptables -N wg0-input-imap

PreUp = iptables -I INPUT -i wg0 -j wg0-input

PreUp = iptables -A wg0-input -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-input -p tcp --dport  22 -j wg0-input-ssh
PreUp = iptables -A wg0-input -p tcp --dport  25 -j wg0-input-smtp
PreUp = iptables -A wg0-input -p tcp --dport 143 -j wg0-input-imap
PreUp = iptables -A wg0-input -j REJECT

PreUp = iptables -A wg0-input-ssh  -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A wg0-input-smtp -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.4 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.7 -j ACCEPT

PreUp = iptables -A wg0-input-imap -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input-imap -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A wg0-input-imap -s 10.0.0.7 -j ACCEPT

PostDown = iptables -D INPUT -i wg0 -j wg0-input

PostDown = iptables -F wg0-input
PostDown = iptables -F wg0-input-ssh
PostDown = iptables -F wg0-input-smtp
PostDown = iptables -F wg0-input-imap

PostDown = iptables -X wg0-input
PostDown = iptables -X wg0-input-ssh
PostDown = iptables -X wg0-input-smtp
PostDown = iptables -X wg0-input-imap

# remote settings for Alice's Workstation
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 10.0.0.2/32

# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
Endpoint = 192.168.1.44:51824
AllowedIPs = 10.0.0.4/32

# remote settings for VNC Server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
Endpoint = 203.0.113.50:51825
AllowedIPs = 10.0.0.5/32

# remote settings for Bob's Workstation
[Peer]
PublicKey = af24MfZ5LzUedF5WlpK2+O8602g2fmiKO8dYdv8dUyI=
AllowedIPs = 10.0.0.6/32

# remote settings for Cindy's Laptop
[Peer]
PublicKey = QHy/1+olsMKePzg/044wWUunKs/IfWzy4Ub/iJbzJ00=
AllowedIPs = 10.0.0.7/32

Let’s break the above rules down, step by step. The very first block ensures that when the WireGuard interface is brought up, the port on which it’s listening for encrypted UDP packets (51823, configured via its ListenPort setting) is open:

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51823 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51823 -j ACCEPT

The PreUp command uses the -I (aka --insert) flag, instead of the -A (aka --append) flag, to make sure that the rule is first in the INPUT chain, so as to come before any other iptables rules already set up for the system’s INPUT chain. The PostDown command removes the rule when the interface is brought down.

The next block uses the iptables -N command (-N for --new-chain) to create four new chains, wg0-input, wg0-input-ssh, wg0-input-smtp, and wg0-input-imap:

PreUp = iptables -N wg0-input
PreUp = iptables -N wg0-input-ssh
PreUp = iptables -N wg0-input-smtp
PreUp = iptables -N wg0-input-imap

The next command creates the link between the main INPUT chain and the wg0-input chain, sending all packets incoming on the wg0 WireGuard interface to the wg0-input chain:

PreUp = iptables -I INPUT -i wg0 -j wg0-input

The next set of rules apply to packets directed to the wg0-input chain. The first rule accepts all packets from all connections initiated by this host itself (for example, if the Mail Server made an HTTP request to the Web Server, this rule would allow the corresponding response from the Web Server back into the Mail Server). The second, third, and forth rules direct SSH, SMTP, and IMAP packets to the wg0-input-ssh, wg0-input-smtp, and wg0-input-imap chains, respectively. The last rule rejects all other packets:

PreUp = iptables -A wg0-input -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-input -p tcp --dport  22 -j wg0-input-ssh
PreUp = iptables -A wg0-input -p tcp --dport  25 -j wg0-input-smtp
PreUp = iptables -A wg0-input -p tcp --dport 143 -j wg0-input-imap
PreUp = iptables -A wg0-input -j REJECT

For packets directed to the wg0-input-ssh chain, the next rule allows it if its source IP address is 10.0.0.2 (Alice’s Workstation):

PreUp = iptables -A wg0-input-ssh  -s 10.0.0.2 -j ACCEPT

Since there are no other rules for the wg0-input-ssh chain, SSH packets from all other sources will return to the wg0-input chain, and be rejected by the last rule in it.

For packets directed to the wg0-input-smtp chain, the next set of rules allows it if its source IP address is 10.0.0.2, 10.0.0.4, 10.0.0.5, 10.0.0.6, or 10.0.0.7 (all the other hosts in this WireGuard VPN):

PreUp = iptables -A wg0-input-smtp -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.4 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A wg0-input-smtp -s 10.0.0.7 -j ACCEPT

For packets directed to the wg0-input-imap chain, the next set of rules allows it if its source IP address is 10.0.0.2, 10.0.0.6, or 10.0.0.7 (Alice’s Workstation, Bob’s Workstation, or Cindy’s Laptop):

PreUp = iptables -A wg0-input-imap -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input-imap -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A wg0-input-imap -s 10.0.0.7 -j ACCEPT

As with the wg0-input-ssh chain, if a packet doesn’t match any of the rules in the wg0-input-smtp or wg0-input-imap chains, it returns back to the wg0-input chain, where it’s rejected by the last rule in that chain.

When the WireGuard interface is brought down, the link between the main INPUT chain and our custom wg0-input chain is deleted:

PostDown = iptables -D INPUT -i wg0 -j wg0-input

And then we delete (with the -F flag, for --flush) all the rules in each of the wg0-input, wg0-input-ssh, wg0-input-smtp, and wg0-input-imap chains:

PostDown = iptables -F wg0-input
PostDown = iptables -F wg0-input-ssh
PostDown = iptables -F wg0-input-smtp
PostDown = iptables -F wg0-input-imap

And then finally we can delete each of our custom wg0-input, wg0-input-ssh, wg0-input-smtp, and wg0-input-imap chains themselves (with the -X flag, short for --delete-chain):

PostDown = iptables -X wg0-input
PostDown = iptables -X wg0-input-ssh
PostDown = iptables -X wg0-input-smtp
PostDown = iptables -X wg0-input-imap

If we later added a new host to our WireGuard VPN — say Dave’s Laptop, at IP address 10.0.0.8 — we could grant SMTP and IMAP access to that new host by making the following changes to the WireGuard configuration of the Mail Server:

  1. Add a [Peer] entry for Dave’s Laptop (include at minimum the public key for the peer, and the line AllowedIPs = 10.0.0.8/32). This grants Dave’s Laptop access to connect to the Mail Server’s WireGuard interface.

  2. Add a PreUp = iptables -A wg0-input-smtp -s 10.0.0.8 -j ACCEPT entry. This grants the WireGuard connection from Dave’s Laptop access to the Mail Server’s SMTP port.

  3. Add a PreUp = iptables -A wg0-input-imap -s 10.0.0.8 -j ACCEPT entry. This grants the WireGuard connection from Dave’s Laptop access to the Mail Server’s IMAP port.

The Web Server’s firewall rules are going to be similar in structure to the Mail Server’s, just with a different set of ports. We’ll again use the wg0-input-ssh chain for port 22, but now use wg0-input-http for port 80 and wg0-input-admin for port 8080. Note that these custom chain names are completely arbitrary (only INPUT is a built-in name) — I’ve just chosen some names that will help me remember what they’re for when I look at them again in two months:

# local settings for Web Server
[Interface]
PrivateKey = CDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDHA=
Address = 10.0.0.4/32
ListenPort = 51824

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51824 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51824 -j ACCEPT

# local firewall
PreUp = iptables -N wg0-input
PreUp = iptables -N wg0-input-ssh
PreUp = iptables -N wg0-input-http
PreUp = iptables -N wg0-input-admin

PreUp = iptables -A INPUT -i wg0 -j wg0-input

PreUp = iptables -A wg0-input -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-input -p tcp --dport   22 -j wg0-input-ssh
PreUp = iptables -A wg0-input -p tcp --dport   80 -j wg0-input-http
PreUp = iptables -A wg0-input -p tcp --dport 8080 -j wg0-input-admin
PreUp = iptables -A wg0-input -j REJECT

PreUp = iptables -A wg0-input-ssh   -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A wg0-input-http  -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input-http  -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A wg0-input-http  -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A wg0-input-http  -s 10.0.0.7 -j ACCEPT

PreUp = iptables -A wg0-input-admin -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input-admin -s 10.0.0.6 -j ACCEPT

PostDown = iptables -D INPUT -i wg0 -j wg0-input

PostDown = iptables -F wg0-input
PostDown = iptables -F wg0-input-ssh
PostDown = iptables -F wg0-input-http
PostDown = iptables -F wg0-input-admin

PostDown = iptables -X wg0-input
PostDown = iptables -X wg0-input-ssh
PostDown = iptables -X wg0-input-http
PostDown = iptables -X wg0-input-admin

# remote settings for Alice's Workstation
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 10.0.0.2/32

# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.168.1.63:51823
AllowedIPs = 10.0.0.3/32

# remote settings for VNC Server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
Endpoint = 203.0.113.50:51825
AllowedIPs = 10.0.0.5/32

# remote settings for Bob's Workstation
[Peer]
PublicKey = af24MfZ5LzUedF5WlpK2+O8602g2fmiKO8dYdv8dUyI=
AllowedIPs = 10.0.0.6/32

# remote settings for Cindy's Laptop
[Peer]
PublicKey = QHy/1+olsMKePzg/044wWUunKs/IfWzy4Ub/iJbzJ00=
AllowedIPs = 10.0.0.7/32

And finally, the VNC Server is a little simpler than the Mail and Web Servers. It just has one port (5900) for which we want to grant access. Because it’s simpler, I’ll just use one custom chain (wg0-input), and add the rules that grant access to Alice’s and Bob’s Workstations (10.0.0.2 and 10.0.0.6) directly to it:

# local settings for VNC Server
[Interface]
PrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=
Address = 10.0.0.5/32
ListenPort = 51825

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51825 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51825 -j ACCEPT

# local firewall
PreUp = iptables -N wg0-input
PreUp = iptables -A INPUT -i wg0 -j wg0-input

PreUp = iptables -A wg0-input -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-input -p tcp --dport 5900 -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A wg0-input -p tcp --dport 5900 -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A wg0-input -j REJECT

PostDown = iptables -D INPUT -i wg0 -j wg0-input
PostDown = iptables -F wg0-input
PostDown = iptables -X wg0-input

# remote settings for Alice's Workstation
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 10.0.0.2/32

# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.168.1.63:51823
AllowedIPs = 10.0.0.3/32

# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
Endpoint = 192.168.1.44:51824
AllowedIPs = 10.0.0.4/32

# remote settings for Bob's Workstation
[Peer]
PublicKey = af24MfZ5LzUedF5WlpK2+O8602g2fmiKO8dYdv8dUyI=
AllowedIPs = 10.0.0.6/32

Note that while the iptables rules for the VNC Server don’t allow inbound access from the Mail Server and the Web Server, we’ve still included peer entries for them. That’s because the VNC Server needs to make outbound connections to the Mail Server to send email, and to the Web Server to make web API calls. Our iptables rules don’t restrict outbound access in any way, and they allow inbound responses to connections the VNC Server itself has initiated over WireGuard via the PreUp = iptables -A wg0-input -m state --state ESTABLISHED,RELATED -j ACCEPT rule.

Hub and Spoke

If we used WireGuard to connect our example hosts with a hub and spoke topology, the network diagram would look like this:

Hub and Spoke Network
Figure 3. Hub and spoke network

For our hub, we’d add a dedicated WireGuard Server to Site A, to which the Internet router at Site A would forward UDP port 51821. All the other hosts in the WireGuard network would connect to it, instead of directly to each other: The remote WireGuard hosts would connect to it through Site A’s Internet address of 198.51.100.10, and the local Site A hosts would connect to it through its LAN address of 192.168.1.101.

Here’s a table of each host’s public endpoint IP address and port, its endpoint IP address and port within the Site A LAN, and its internal WireGuard VPN IP address:

Table 3. Hub and Spoke Addresses
Host Internet Address LAN Address WireGuard Address

WireGuard Server

198.51.100.10:51821

192.168.1.101:51821

10.0.0.1

Alice’s Workstation

N/A

dynamic

10.0.0.2

Mail Server

N/A

192.168.1.63:51823

10.0.0.3

Web Server

N/A

192.168.1.44:51824

10.0.0.4

VNC Server

203.0.113.50:51825

N/A

10.0.0.5

Bob’s Workstation

dynamic

N/A

10.0.0.6

Cindy’s Laptop

dynamic

N/A

10.0.0.7

Having added this dedicated WireGuard Server to our example network, let’s also add some access-control rules for it, too. We’ll allow Alice’s Workstation to SSH into it, and allow it to send email via the Mail Server. Conceptually, this would add two additional rows to our master Network ACLs table from above:

Table 4. Additional Hub ACLs
Destination Host Destination Port Source Host Access

WireGuard Server

22

Alice’s Workstation

Allow

Mail Server

25

WireGuard Server

Allow

Now, since all traffic goes through our hub WireGuard Server, it’s an ideal place to enforce centralized access control. We’ll implement all the ACLs for our WireGuard VPN right in the WireGuard configuration on the WireGuard Server:

# local settings for hub WireGuard Server
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# packet forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51821 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51821 -j ACCEPT

# hub firewall
PreUp = iptables -N wg0-filter
PreUp = iptables -N to-wg-server-ssh
PreUp = iptables -N to-mail-server-ssh
PreUp = iptables -N to-mail-server-smtp
PreUp = iptables -N to-mail-server-imap
PreUp = iptables -N to-web-server-ssh
PreUp = iptables -N to-web-server-http
PreUp = iptables -N to-web-server-admin
PreUp = iptables -N to-vnc-server

PreUp = iptables -I INPUT   -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -o wg0 -j wg0-filter
PreUp = iptables -I OUTPUT  -o wg0 -j wg0-filter

PreUp = iptables -A wg0-filter -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-filter -d 10.0.0.1 -p tcp --dport   22 -j to-wg-server-ssh
PreUp = iptables -A wg0-filter -d 10.0.0.3 -p tcp --dport   22 -j to-mail-server-ssh
PreUp = iptables -A wg0-filter -d 10.0.0.3 -p tcp --dport   25 -j to-mail-server-smtp
PreUp = iptables -A wg0-filter -d 10.0.0.3 -p tcp --dport  143 -j to-mail-server-imap
PreUp = iptables -A wg0-filter -d 10.0.0.4 -p tcp --dport   22 -j to-web-server-ssh
PreUp = iptables -A wg0-filter -d 10.0.0.4 -p tcp --dport   80 -j to-web-server-http
PreUp = iptables -A wg0-filter -d 10.0.0.4 -p tcp --dport 8080 -j to-web-server-admin
PreUp = iptables -A wg0-filter -d 10.0.0.5 -p tcp --dport 5900 -j to-vnc-server
PreUp = iptables -A wg0-filter -j REJECT

PreUp = iptables -A to-wg-server-ssh    -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A to-mail-server-ssh  -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A to-mail-server-smtp -s 10.0.0.1 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.4 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.7 -j ACCEPT

PreUp = iptables -A to-mail-server-imap -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-mail-server-imap -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A to-mail-server-imap -s 10.0.0.7 -j ACCEPT

PreUp = iptables -A to-web-server-ssh   -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A to-web-server-http  -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.7 -j ACCEPT

PreUp = iptables -A to-web-server-admin -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-web-server-admin -s 10.0.0.6 -j ACCEPT

PreUp = iptables -A to-vnc-server       -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-vnc-server       -s 10.0.0.6 -j ACCEPT

PostDown = iptables -D INPUT   -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -o wg0 -j wg0-filter
PostDown = iptables -D OUTPUT  -o wg0 -j wg0-filter

PostDown = iptables -F wg0-filter
PostDown = iptables -F to-wg-server-ssh
PostDown = iptables -F to-mail-server-ssh
PostDown = iptables -F to-mail-server-smtp
PostDown = iptables -F to-mail-server-imap
PostDown = iptables -F to-web-server-ssh
PostDown = iptables -F to-web-server-http
PostDown = iptables -F to-web-server-admin
PostDown = iptables -F to-vnc-server

PostDown = iptables -X wg0-filter
PostDown = iptables -X to-wg-server-ssh
PostDown = iptables -X to-mail-server-ssh
PostDown = iptables -X to-mail-server-smtp
PostDown = iptables -X to-mail-server-imap
PostDown = iptables -X to-web-server-ssh
PostDown = iptables -X to-web-server-http
PostDown = iptables -X to-web-server-admin
PostDown = iptables -X to-vnc-server

# remote settings for Alice's Workstation
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
AllowedIPs = 10.0.0.2/32

# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = 192.168.1.63:51823
AllowedIPs = 10.0.0.3/32

# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
Endpoint = 192.168.1.44:51824
AllowedIPs = 10.0.0.4/32

# remote settings for VNC Server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
Endpoint = 203.0.113.50:51825
AllowedIPs = 10.0.0.5/32

# remote settings for Bob's Workstation
[Peer]
PublicKey = af24MfZ5LzUedF5WlpK2+O8602g2fmiKO8dYdv8dUyI=
AllowedIPs = 10.0.0.6/32

# remote settings for Cindy's Laptop
[Peer]
PublicKey = QHy/1+olsMKePzg/044wWUunKs/IfWzy4Ub/iJbzJ00=
AllowedIPs = 10.0.0.7/32

(If this example seems a bit too dense, check out the “Host C” firewall from the hub and spoke configuration tutorial, which shows a much simplified version of this with a single network service.)

Let’s walk through all these iptables rules. The very first block ensures that the WireGuard interface’s listen port (51821) is accessible, even if other rules configured elsewhere on the system have blocked it off by default:

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51821 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51821 -j ACCEPT

The next block creates a bunch of custom chains that we’ll use to enforce our ACLs:

PreUp = iptables -N wg0-filter
PreUp = iptables -N to-wg-server-ssh
PreUp = iptables -N to-mail-server-ssh
PreUp = iptables -N to-mail-server-smtp
PreUp = iptables -N to-mail-server-imap
PreUp = iptables -N to-web-server-ssh
PreUp = iptables -N to-web-server-http
PreUp = iptables -N to-web-server-admin
PreUp = iptables -N to-vnc-server

The next block sets up the basic rules for all packets incoming and outgoing through the host’s wg0 WireGuard interface (the traffic entering and exiting the WireGuard tunnel). Iptables automatically sends all incoming packets with a local destination address to its built-in INPUT chain, and all incoming packets with a non-local destination address to its built-in FORWARD chain. Outbound packets generated on the host itself are sent through the built-in OUTPUT chain.

So the first rule applies to all incoming packets from the WireGuard interface destined for the hub itself; the second rule applies to all incoming packets from the WireGuard interface destined for other hosts; the third rule applies to outgoing packets to the WireGuard interface forwarded from other hosts; and the fourth rule applies to all outgoing packets to the WireGuard interface from the host itself. Each of these rules sends the matching packets through the wg0-filter chain (where we’ll implement our centralized access-control logic):

PreUp = iptables -I INPUT   -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -o wg0 -j wg0-filter
PreUp = iptables -I OUTPUT  -o wg0 -j wg0-filter

We use the -I (aka --insert) flag in the above rules to add them to the head of their respective chains, evaluated before any other rules that have been set up elsewhere for the chain. While most of the packets the WireGuard Server encounters in this scenario will both come in and go out the wg0 interface, potentially matching both the second and third rules, because our wg0-filter chain always makes either an ACCEPT or REJECT decision, no packets will actually be matched by both rules. Having both rules in place ensures that our wg0-filter chain also captures the case where a remote WireGuard endpoint tries to connect through the hub to a non-WireGuard endpoint, or vice versa.

The next block is where we start to build our centralized access-control logic. The first rule in it allows all packets that are part of an existing connection (for example, the packets carrying an HTTP response from an HTTP request that was previously allowed). The middle rules send off packets to per-service chains based on their destination IP address and port. The last rule denies any packets sent to this chain that haven’t matched a previous rule (therefore blocking all traffic through our WireGuard network not explicitly allowed by the per-service chains):

PreUp = iptables -A wg0-filter -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-filter -d 10.0.0.1 -p tcp --dport   22 -j to-wg-server-ssh
PreUp = iptables -A wg0-filter -d 10.0.0.3 -p tcp --dport   22 -j to-mail-server-ssh
PreUp = iptables -A wg0-filter -d 10.0.0.3 -p tcp --dport   25 -j to-mail-server-smtp
PreUp = iptables -A wg0-filter -d 10.0.0.3 -p tcp --dport  143 -j to-mail-server-imap
PreUp = iptables -A wg0-filter -d 10.0.0.4 -p tcp --dport   22 -j to-web-server-ssh
PreUp = iptables -A wg0-filter -d 10.0.0.4 -p tcp --dport   80 -j to-web-server-http
PreUp = iptables -A wg0-filter -d 10.0.0.4 -p tcp --dport 8080 -j to-web-server-admin
PreUp = iptables -A wg0-filter -d 10.0.0.5 -p tcp --dport 5900 -j to-vnc-server
PreUp = iptables -A wg0-filter -j REJECT

The remaining PreUp blocks enforce access control for a particular service. The next two rules allow SSH access to the WireGuard Server and to the Mail Server from Alice’s Workstation (10.0.0.2):

PreUp = iptables -A to-wg-server-ssh    -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A to-mail-server-ssh  -s 10.0.0.2 -j ACCEPT

And the next rules allow the WireGuard Server (10.0.0.1), Alice’s Workstation (10.0.0.2), the Web Server (10.0.0.4), the VNC Server (10.0.0.5), Bob’s Workstation (10.0.0.6), and Cindy’s Laptop (10.0.0.7) to send email via the Mail Serever:

PreUp = iptables -A to-mail-server-smtp -s 10.0.0.1 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.4 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.7 -j ACCEPT

And the next rules allow the Alice’s Workstation (10.0.0.2), Bob’s Workstation (10.0.0.6), and Cindy’s Laptop (10.0.0.7) to check mail on the Mail Server:

PreUp = iptables -A to-mail-server-imap -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-mail-server-imap -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A to-mail-server-imap -s 10.0.0.7 -j ACCEPT

The next set of rules allow Alice’s Workstation (10.0.0.2) to SSH into the Web Server; and allow Alice’s Workstation, the VNC Server (10.0.0.5), Bob’s Workstation (10.0.0.6), and Cindy’s Laptop (10.0.0.7) to connect to the main web app on the Web Server; and allow Alice’s and Bob’s Workstations to connect to the admin web app on the Web Server:

PreUp = iptables -A to-web-server-ssh   -s 10.0.0.2 -j ACCEPT

PreUp = iptables -A to-web-server-http  -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.5 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.6 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.7 -j ACCEPT

PreUp = iptables -A to-web-server-admin -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-web-server-admin -s 10.0.0.6 -j ACCEPT

And the final set of iptables rules allow Alice’s Workstation and Bob’s Workstation to connect to the VNC Server:

PreUp = iptables -A to-vnc-server       -s 10.0.0.2 -j ACCEPT
PreUp = iptables -A to-vnc-server       -s 10.0.0.6 -j ACCEPT

The PostDown blocks tear down the custom chains set up above. The first block removes the rules that send incoming and outgoing packets from and to the WireGuard interface through the wg0-filter chain:

PostDown = iptables -D INPUT   -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -o wg0 -j wg0-filter
PostDown = iptables -D OUTPUT  -o wg0 -j wg0-filter

And the next PostDown block removes all the rules in our custom chains:

PostDown = iptables -F wg0-filter
PostDown = iptables -F to-wg-server-ssh
PostDown = iptables -F to-mail-server-ssh
PostDown = iptables -F to-mail-server-smtp
PostDown = iptables -F to-mail-server-imap
PostDown = iptables -F to-web-server-ssh
PostDown = iptables -F to-web-server-http
PostDown = iptables -F to-web-server-admin
PostDown = iptables -F to-vnc-server

And once the custom chains are empty, they can be deleted by the final PostDown block:

PostDown = iptables -X wg0-filter
PostDown = iptables -X to-wg-server-ssh
PostDown = iptables -X to-mail-server-ssh
PostDown = iptables -X to-mail-server-smtp
PostDown = iptables -X to-mail-server-imap
PostDown = iptables -X to-web-server-ssh
PostDown = iptables -X to-web-server-http
PostDown = iptables -X to-web-server-admin
PostDown = iptables -X to-vnc-server

You may also want to set up a separate firewall on each of the other hosts in this WireGuard network (in particular, to reject any non-WireGuard traffic), but you don’t need to do so to enforce access control from WireGuard connections. For example, on Alice’s Workstation, you may set up the firewall to reject all incoming traffic from all sources (see the Chain Policies tip at the end of this article); and then in the WireGuard configuration for Alice’s Workstation, insert an iptables rule to allow incoming UDP packets on the WireGuard listen port while the WireGuard interface is up:

# local settings for Alice's Workstation
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51820

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# remote settings for WireGuard Server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Endpoint = 192.168.1.101:51821
AllowedIPs = 10.0.0.0/24

You won’t need any additional WireGuard-specific iptables rules on Alice’s Workstation, because of the iptables rules we added on the hub WireGuard server.

If we later added a new host to our WireGuard VPN — say Dave’s Laptop, at IP address 10.0.0.8 — we could grant SMTP and IMAP access to that new host simply by making the following changes to the WireGuard configuration of the WireGuard Server:

  1. Add a [Peer] entry for Dave’s Laptop (include at minimum the public key for the peer, and the line AllowedIPs = 10.0.0.8/32). This grants Dave’s Laptop access to connect to the WireGuard Server’s WireGuard interface.

  2. Add a PreUp = iptables -A to-mail-server-smtp -s 10.0.0.8 -j ACCEPT entry. This grants the forwarded WireGuard connection from Dave’s Laptop access to the Mail Server’s SMTP port.

  3. Add a PreUp = iptables -A to-mail-server-imap -s 10.0.0.8 -j ACCEPT entry. This grants the forwarded WireGuard connection from Dave’s Laptop access to the Mail Server’s IMAP port.

Point to Site

If we used WireGuard to connect our example hosts with a point to site topology, the network diagram would look like this:

Point to Site Network
Figure 4. Point to site network

We’d also add a dedicated WireGuard Server to Site A in this scenario to connect to the remote endpoints. The Internet router in Site A would forward UDP port 51821 to it, allowing the remote endpoints to connect to it with Site A’s Internet address of 198.51.100.10. The dedicated WireGuard Server would masquerade (SNAT, Source Network Address Translation) the remote WireGuard endpoints on Site A’s LAN, so that connections from any of the remote endpoints (the VNC server, Bob’s Workstation, or Cindy’s Laptop) would appear to hosts on the LAN (Alice’s Workstation, the Mail Server, or the Web Server) to be coming from the dedicated WireGuard Server itself.

Also, to allow Alice’s Workstation to connect to the VNC server, the dedicated WireGuard server would also have to forward (DNAT) TCP port 5900 from Alice’s Workstation to the VNC server through its WireGuard tunnel.

Here’s a table of each host’s public endpoint IP address and port, its internal WireGuard VPN IP address, the IP address that can be used to access it through the WireGuard VPN, and the IP address that it can be accessed from the Site A LAN:

Table 5. Point to Site Addresses
Host Internet Address WireGuard Address Access from VPN Access from LAN

WireGuard Server

198.51.100.10:51821

10.0.0.1

10.0.0.1

192.168.1.101

Alice’s Workstation

N/A

N/A

192.168.1.12

192.168.1.12

Mail Server

N/A

N/A

192.168.1.63

192.168.1.63

Web Server

N/A

N/A

192.168.1.44

192.168.1.44

VNC Server

203.0.113.50:51825

10.0.0.5

10.0.0.5

192.168.1.101:5900

Bob’s Workstation

dynamic

10.0.0.6

10.0.0.6

192.168.1.101 (dynamic port)

Cindy’s Laptop

dynamic

10.0.0.7

10.0.0.7

192.168.1.101 (dynamic port)

Because WireGuard would not be involved in the connections among Alice’s Laptop, the Mail Server, and the Web Server, we’d have to set up individual firewalls on each of those hosts to enforce access control among them. But we could still use the dedicated WireGuard server to enforce access control among the other connections in our WireGuard network.

The point to site configuration tutorial covers the basic WireGuard configuration for this scenario in detail. Here we’ll just focus on the firewall for the dedicated WireGuard Server — which will look a lot like the hub WireGuard Server in the Hub and Spoke scenario above, except that here we use the LAN addresses of the hosts in Site A, instead of their WireGuard addresses (since they don’t have WireGuard addresses in this scenario). Also, in this scenario we need a stanza for masquerading the packets from the WireGuard network when forwarded to the Site A LAN; and a stanza for forwarding port 5900 from the Site A LAN to the VNC Server (10.0.0.5):

# local settings for Site A WireGuard Server
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# packet forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1

# masquerading
PreUp = iptables -t mangle -A PREROUTING -i wg0 -j MARK --set-mark 0x200
PreUp = iptables -t nat -A POSTROUTING ! -o wg0 -m mark --mark 0x200 -j MASQUERADE
PostDown = iptables -t mangle -D PREROUTING -i wg0 -j MARK --set-mark 0x200
PostDown = iptables -t nat -D POSTROUTING ! -o wg0 -m mark --mark 0x200 -j MASQUERADE

# port forwarding
PreUp = iptables -t nat -A PREROUTING -d 192.168.1.101 -p tcp --dport 5900 -j DNAT --to-destination 10.0.0.5
PostDown = iptables -t nat -D PREROUTING -d 192.168.1.101 -p tcp --dport 5900 -j DNAT --to-destination 10.0.0.5

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51821 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51821 -j ACCEPT

# site firewall
PreUp = iptables -N wg0-filter
PreUp = iptables -N to-mail-server-smtp
PreUp = iptables -N to-mail-server-imap
PreUp = iptables -N to-web-server-http
PreUp = iptables -N to-web-server-admin
PreUp = iptables -N to-vnc-server

PreUp = iptables -I INPUT   -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -o wg0 -j wg0-filter
PreUp = iptables -I OUTPUT  -o wg0 -j wg0-filter

PreUp = iptables -A wg0-filter -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-filter -d 192.168.1.63  -p tcp --dport   25 -j to-mail-server-smtp
PreUp = iptables -A wg0-filter -d 192.168.1.63  -p tcp --dport  143 -j to-mail-server-imap
PreUp = iptables -A wg0-filter -d 192.168.1.44  -p tcp --dport   80 -j to-web-server-http
PreUp = iptables -A wg0-filter -d 192.168.1.44  -p tcp --dport 8080 -j to-web-server-admin
PreUp = iptables -A wg0-filter -d 10.0.0.5      -p tcp --dport 5900 -j to-vnc-server
PreUp = iptables -A wg0-filter -j REJECT

PreUp = iptables -A to-mail-server-smtp -s 10.0.0.6     -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 10.0.0.7     -j ACCEPT

PreUp = iptables -A to-mail-server-imap -s 10.0.0.6     -j ACCEPT
PreUp = iptables -A to-mail-server-imap -s 10.0.0.7     -j ACCEPT

PreUp = iptables -A to-web-server-http  -s 10.0.0.5     -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.6     -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 10.0.0.7     -j ACCEPT

PreUp = iptables -A to-web-server-admin -s 10.0.0.6     -j ACCEPT

PreUp = iptables -A to-vnc-server       -s 192.168.1.12 -j ACCEPT
PreUp = iptables -A to-vnc-server       -s 10.0.0.6     -j ACCEPT

PostDown = iptables -D INPUT   -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -o wg0 -j wg0-filter
PostDown = iptables -D OUTPUT  -o wg0 -j wg0-filter

PostDown = iptables -F wg0-filter
PostDown = iptables -F to-mail-server-smtp
PostDown = iptables -F to-mail-server-imap
PostDown = iptables -F to-web-server-http
PostDown = iptables -F to-web-server-admin
PostDown = iptables -F to-vnc-server

PostDown = iptables -X wg0-filter
PostDown = iptables -X to-mail-server-smtp
PostDown = iptables -X to-mail-server-imap
PostDown = iptables -X to-web-server-http
PostDown = iptables -X to-web-server-admin
PostDown = iptables -X to-vnc-server

# remote settings for VNC Server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
Endpoint = 203.0.113.50:51825
AllowedIPs = 10.0.0.5/32

# remote settings for Bob's Workstation
[Peer]
PublicKey = af24MfZ5LzUedF5WlpK2+O8602g2fmiKO8dYdv8dUyI=
AllowedIPs = 10.0.0.6/32

# remote settings for Cindy's Laptop
[Peer]
PublicKey = QHy/1+olsMKePzg/044wWUunKs/IfWzy4Ub/iJbzJ00=
AllowedIPs = 10.0.0.7/32

Note that the masquerading will happen after the firewall filtering (so the filter rules will see the original 10.0.0.x source address of masqueraded packets), whereas the port forwarding will happen before the firewall filtering (so the filter rules will see the re-written 10.0.0.5 destination address on forwarded packets).

Also note that just like the firewall for the hub WireGuard Server of the Hub and Spoke scenario, in this scenario we also run the direct INPUT and OUTPUT chains (for packets that come in or go out the WireGuard interface directly to or from the dedicated WireGuard Server), in addition to the FORWARD chain, through our wg0-filter chain. This ensures that we enforce our ACLs on direct connections between the dedicated WireGuard Server and the remote WireGuard endpoints (for example, preventing the WireGuard Server from SSHing into Bob’s Workstation, and vice-versa) — even if we don’t have any explicit access grants for direct access to or from the WireGuard Server itself.

Outside of the iptables rules we set up here for the WireGuard Server’s WireGuard interface, we would probably also want to set up some iptables rules that would enforce access control to the WireGuard Server from the Site A LAN (for example, to allow SSH access only from Alice’s Workstation). We won’t set them up here, however, since we’d want these rules to be in effect all the time, not just when the WireGuard interface is up.

For the remote WireGuard hosts, such as the VNC Server or Bob’s Workstation, that need to allow access to both hosts in the WireGuard network itself as well as the Site A LAN, we’ll want to specify both the subnet for the WireGuard network and the Site A LAN in the AllowedIPs setting for the WireGuard Server peer in their WireGuard configuration:

# local settings for VNC Server
[Interface]
PrivateKey = EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=
Address = 10.0.0.5/32
ListenPort = 51825

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51825 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51825 -j ACCEPT

# remote settings for WireGuard Server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Endpoint = 198.51.100.10:51821
AllowedIPs = 10.0.0.0/24, 192.168.1.0/24
# local settings for Bob's Workstation
[Interface]
PrivateKey = EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA=
Address = 10.0.0.6/32
ListenPort = 51820

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# remote settings for WireGuard Server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Endpoint = 198.51.100.10:51821
AllowedIPs = 10.0.0.0/24, 192.168.1.0/24

Hosts that only need access to the Site A LAN, like Cindy’s Laptop, only need its subnet specified for their AllowedIPs setting:

# local settings for Cindy's Laptop
[Interface]
PrivateKey = GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGA=
Address = 10.0.0.7/32
ListenPort = 51820

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# remote settings for WireGuard Server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Endpoint = 198.51.100.10:51821
AllowedIPs = 192.168.1.0/24

Site to Site

If we adjusted our example scenario a little, such that the VNC Server, Bob’s Workstation, and Cindy’s Laptop were all were on the same LAN at a remote office (call it Site B), we could use a site to site topology to connect the two sites with WireGuard. The network diagram in that case would look like this:

Site to Site Network
Figure 5. Site to site network

We’d add a dedicated WireGuard Server A to Site A and a dedicated WireGuard Server B to Site B. The Internet router at Site A would forward UDP port 51821 to WireGuard Server A, and the Internet router at Site B would forward UDP port 51822 to WireGuard Server B. The router for the Site A LAN would route traffic for the Site B LAN through WireGuard Server A, and the router for the Site B LAN would route traffic for the Site A LAN through WireGuard Server B.

Here’s a table of each host’s public endpoint IP address and port, its IP address within its LAN, and its internal WireGuard VPN IP address:

Table 6. Site to Site Addresses
Host Internet Address LAN Address WireGuard Address

WireGuard Server A

198.51.100.10:51821

192.168.1.101

10.0.0.1

Alice’s Workstation

N/A

192.168.1.12

N/A

Mail Server

N/A

192.168.1.63

N/A

Web Server

N/A

192.168.1.44

N/A

WireGuard Server B

203.0.113.2:51822

192.168.200.22

10.0.0.2

VNC Server

N/A

192.168.200.50

N/A

Bob’s Workstation

N/A

192.168.200.86

N/A

Cindy’s Laptop

N/A

192.168.200.27

N/A

Since the two WireGuard servers would touch only cross-LAN traffic, we could use them to enforce only the cross-LAN part of our ACLs. This would cut down the Network ACLs listed at the top of this article to this much more modest list:

Table 7. Cross Site ACLs
Destination Host Destination Port Source Host Access

Mail Server

25

Bob’s Workstation

Allow

Mail Server

25

Cindy’s Laptop

Allow

Mail Server

25

VNC Server

Allow

Mail Server

25

WireGuard Server B

Allow

Mail Server

143

Bob’s Workstation

Allow

Mail Server

143

Cindy’s Laptop

Allow

Web Server

80

Bob’s Workstation

Allow

Web Server

80

Cindy’s Laptop

Allow

Web Server

80

VNC Server

Allow

VNC Server

5900

Alice’s Workstation

Allow

WireGuard Server B

22

Alice’s Workstation

Allow

Everything else

Everything else

Everything else

Deny

If we knew we were only going to connect Site A to Site B over WireGuard, we could enforce this list on just one of the two dedicated WireGuard servers. But in case we ever connect another site (or any other peer) to our WireGuard network, it would be best to enforce access control for Site A hosts (the Mail Server and Web Server) on WireGuard Server A, and access control for Site B hosts (the VNC Server and WireGuard Server B) on WireGuard Server B.

The site to site configuration tutorial covers the WireGuard configuration for this scenario in depth, so here again we’ll focus just on the firewall settings. The firewall we have here for WireGuard Server A looks a lot like the hub WireGuard Server in the Hub and Spoke scenario above, except that it only enforces access control for the Mail Server and Web Server (and uses LAN addresses instead of WireGuard addresses for the various hosts):

# local settings for Site A WireGuard Server
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821

# packet forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51821 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51821 -j ACCEPT

# site a firewall
PreUp = iptables -N wg0-filter
PreUp = iptables -N to-mail-server-smtp
PreUp = iptables -N to-mail-server-imap
PreUp = iptables -N to-web-server-http
PreUp = iptables -N to-web-server-admin

PreUp = iptables -I INPUT   -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -i wg0 -j wg0-filter

PreUp = iptables -A wg0-filter -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-filter -d 192.168.1.63  -p tcp --dport   25 -j to-mail-server-smtp
PreUp = iptables -A wg0-filter -d 192.168.1.63  -p tcp --dport  143 -j to-mail-server-imap
PreUp = iptables -A wg0-filter -d 192.168.1.44  -p tcp --dport   80 -j to-web-server-http
PreUp = iptables -A wg0-filter -d 192.168.1.44  -p tcp --dport 8080 -j to-web-server-admin
PreUp = iptables -A wg0-filter -j REJECT

PreUp = iptables -A to-mail-server-smtp -s 192.168.200.22 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 192.168.200.50 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 192.168.200.86 -j ACCEPT
PreUp = iptables -A to-mail-server-smtp -s 192.168.200.27 -j ACCEPT

PreUp = iptables -A to-mail-server-imap -s 192.168.200.86 -j ACCEPT
PreUp = iptables -A to-mail-server-imap -s 192.168.200.27 -j ACCEPT

PreUp = iptables -A to-web-server-http  -s 192.168.200.50 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 192.168.200.86 -j ACCEPT
PreUp = iptables -A to-web-server-http  -s 192.168.200.27 -j ACCEPT

PreUp = iptables -A to-web-server-admin -s 192.168.200.86 -j ACCEPT

PostDown = iptables -D INPUT   -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -i wg0 -j wg0-filter

PostDown = iptables -F wg0-filter
PostDown = iptables -F to-mail-server-smtp
PostDown = iptables -F to-mail-server-imap
PostDown = iptables -F to-web-server-http
PostDown = iptables -F to-web-server-admin

PostDown = iptables -X wg0-filter
PostDown = iptables -X to-mail-server-smtp
PostDown = iptables -X to-mail-server-imap
PostDown = iptables -X to-web-server-http
PostDown = iptables -X to-web-server-admin

# remote settings for Site B WireGuard Server
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIPs = 192.168.200.0/24

And similarly, the firewall for WireGuard Server B also looks a lot like the hub WireGuard Server in the Hub and Spoke scenario above, except that it only enforces access control for the VNC Server and WireGuard Server B itself (using LAN addresses instead of WireGuard addresses to match hosts):

# local settings for Site B WireGuard Server
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

# packet forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1

# wireguard ingress
PreUp = iptables -I INPUT -p udp --dport 51822 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51822 -j ACCEPT

# site b firewall
PreUp = iptables -N wg0-filter
PreUp = iptables -N to-wg-server-ssh
PreUp = iptables -N to-vnc-server

PreUp = iptables -I INPUT   -i wg0 -j wg0-filter
PreUp = iptables -I FORWARD -i wg0 -j wg0-filter

PreUp = iptables -A wg0-filter -m state --state ESTABLISHED,RELATED -j ACCEPT
PreUp = iptables -A wg0-filter -d 192.168.200.22 -p tcp --dport   22 -j to-wg-server-ssh
PreUp = iptables -A wg0-filter -d 192.168.200.50 -p tcp --dport 5900 -j to-vnc-server
PreUp = iptables -A wg0-filter -j REJECT

PreUp = iptables -A to-wg-server-ssh -s 192.168.1.12 -j ACCEPT

PreUp = iptables -A to-vnc-server    -s 192.168.1.12 -j ACCEPT

PostDown = iptables -D INPUT   -i wg0 -j wg0-filter
PostDown = iptables -D FORWARD -i wg0 -j wg0-filter

PostDown = iptables -F wg0-filter
PostDown = iptables -F to-wg-server-ssh
PostDown = iptables -F to-vnc-server

PostDown = iptables -X wg0-filter
PostDown = iptables -X to-wg-server-ssh
PostDown = iptables -X to-vnc-server

# remote settings for Site A WireGuard Server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Endpoint = 198.51.100.10:51821
AllowedIPs = 192.168.1.0/24

Note in both cases, we run the direct INPUT chain (for packets incoming through the WireGuard interface destined for the local host) in addition to the FORWARD chain (for packets incoming through the WireGuard interface destined for another host) through our wg0-filter chain. This ensures that we enforce our ACLs on traffic attempting to connect directly to our dedicated WireGuard servers, in addition to the connections that they forward (for example, to prevent Bob’s Workstation from SSHing directly into WireGuard Server A, in addition to preventing Bob’s Workstation from SSHing into any other host in Site A).

But unlike the Hub and Spoke scenario, in this scenario we’re enforcing access-control rules only for the local site on each site’s WireGuard Server — so that’s why, unlike the Hub and Spoke scenario, we don’t also direct packets going out through the WireGuard interface through our wg0-filter chain.

Outside of the iptables rules we’ve set up here for the WireGuard interface of our dedicated WireGuard severs, we would probably also want to set up some iptables rules that would enforce access control to the WireGuard Servers from within their own LAN (for example, to prevent Bob’s Workstation from SSHing into WireGuard Server B). We won’t set them up here, however, since we’d want these rules to be in effect all the time, not just when the WireGuard interface is up.

Other Tips

Chain Policies

In this guide, I’m recommending you add a final -j REJECT rule to all your WireGuard iptables chains to reject any WireGuard traffic not explicitly allowed. As a best practice, you should also set the default policy of the INPUT and FORWARD chains on all your hosts to DROP as soon as the system starts up, so as to deny network access by default, instead of grant it by default, across the board (which would render these final -j REJECT rules redundant).

However, the best way to set this varies widely among modern Linux distributions and distro versions. In the old days, you would simply run the following commands as part of your init scripts:

iptables -P INPUT DROP
iptables -P FORWARD DROP

With modern distros, however, you should check the particular distro’s documentation about how and where to set up your initial iptables configuration. Arch Linux in particular has an excellent tutorial about how to set up iptables with a simple set of baseline rules.

Making Config Changes

Whenever you make changes to your WireGuard configuration files to change your PreUp or PostDown scripts, it’s best to bring the WireGuard interface down first, make your changes, and then bring the interface back up again. Often each PreUp command is paired with PostDown command that is meant to undo or otherwise reset the effect of the PreUp command. This is particularly true with iptables commands, where in many cases each rule you add with a PreUp command you want to remove with a PostDown command.

For example, say you have a PreUp and PostDown command pair in your WireGuard config like the following:

PreUp = iptables -I INPUT -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

And without bringing down the interface, you change the port in those commands from 51820 to 51821:

PreUp = iptables -I INPUT -p udp --dport 51821 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51821 -j ACCEPT

Now if you bring down the interface, you’ll have two problems:

  1. The PostDown command will fail, since you haven’t created the port 51821 rule yet that it’s supposed to delete.

  2. The rule for port 51820 won’t be deleted (and so will remain in effect).

You can always run the old versions of PostDown commands manually to delete the old rules, but you’ll find you’ll save yourself some frustration if you remember to bring down the interface first before editing its configuration.

Iptables Errors

Here are a few error messages you might see while working on your iptables rules:

Chain already exists

iptables: Chain already exists.

You’ll see this error message if you try to create a custom itables chain that already exists. If you see this error when you try to bring a WireGuard interface up, it probably means that you have at least one PreUp = iptables -N command in your WireGuard configuration that doesn’t match a corresponding PostDown = iptables -X command exactly (or didn’t match exactly when you last brought the WireGuard interface down — see the Making Config Changes tip above).

Run sudo iptables -L to list all your iptables chains and rules. Delete any chains listed that should be created by PreUp commands in your WireGuard configuration, via the following commands (where my-chain is the name of the chain):

sudo iptables -F my-chain
sudo iptables -X my-chain

Directory not empty

iptables: Directory not empty.

You’ll see this error message if you try to delete an itables chain that still contains some rules. If you see this error when you try to bring a WireGuard interface down, it probably means that you forgot to include a PostDown = iptables -F command for a chain before the PostDown = iptables -X command for it.

Run sudo iptables -L to list all your iptables chains and rules. Delete any existing chains listed that you intended your PostDown commands to delete by running the following commands (where my-chain is the name of a chain to delete):

sudo iptables -F my-chain
sudo iptables -X my-chain
iptables: Too many links

You’ll see this error message if you try to delete a custom iptables chain that is still referenced by some other rule. Run the sudo iptables-save command to list out your full iptables rule set, and find the rules that reference the chain you’re trying to delete (if the chain you’re trying to delete is named my-chain, the rules you’re looking for will contain -j my-chain in them). The rules will begin with the -A flag, like -A INPUT -i wg0 -j my-chain — delete each offending rule by running an iptables command with the same exact rule content, except with the -A (aka --append) flag replaced by the -D (aka --delete) flag:

sudo iptables -D INPUT -i wg0 -j my-chain

Bad rule

iptables: Bad rule (does a matching rule exist in that chain?).

You’ll see this error message if you try to delete a rule that doesn’t actually exist. If you see this error when you try to bring a WireGuard interface down, it probably means you have at least one PostDown = iptables -D command in your WireGuard configuration that doesn’t match a corresponding PreUp = iptables -A (or -I) command exactly (or doesn’t match exactly the PreUp command from the last time you brought the WireGuard interface up — see the Making Config Changes tip above).

Run the sudo iptables-save command to list out your current iptables rule set, and find the exact version of the rules that you’re trying to delete. The rules will begin with the -A flag, like -A to-vnc-server -s 10.0.0.2/32 -j ACCEPT — you can delete a listed rule by running an iptables command with that exact rule content, except with the -A (aka --append) flag replaced by the -D (aka --delete) flag:

sudo iptables -D to-vnc-server -s 10.0.0.2/32 -j ACCEPT