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:
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:
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:
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:
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:
-
Add a
[Peer]
entry for Dave’s Laptop (include at minimum the public key for the peer, and the lineAllowedIPs = 10.0.0.8/32
). This grants Dave’s Laptop access to connect to the Mail Server’s WireGuard interface. -
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. -
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:
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:
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:
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:
-
Add a
[Peer]
entry for Dave’s Laptop (include at minimum the public key for the peer, and the lineAllowedIPs = 10.0.0.8/32
). This grants Dave’s Laptop access to connect to the WireGuard Server’s WireGuard interface. -
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. -
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:
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:
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:
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:
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:
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:
-
The
PostDown
command will fail, since you haven’t created the port51821
rule yet that it’s supposed to delete. -
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
Too many links
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