How to Use WireGuard With UFW

If you have a complicated firewall needs, you should probably reach for iptables (or nftables) when setting up WireGuard — see the WireGuard Access Control With Iptables article for a guide. But if you have uncomplicated needs, UFW (the Uncomplicated FireWall) will work just fine.

This article will show you how to use UFW with WireGuard for each of the primary WireGuard topologies:

Also see the notes about Ping and Troubleshooting at the end of the article.

Point to Point

Let’s take the simple two host, point-to-point WireGuard VPN (Virtual Private Network) described in the WireGuard Point to Point Configuration guide, and set up a firewall for it with UFW (replacing the iptables firewall described in the “extra” sections of the guide).

Here’s a network diagram of the scenario:

Point to Point VPN
Figure 1. Point to point scenario

On Endpoint A, which in this example is just a simple tablet computer, we’ll set up UFW to disallow all new connections to Endpoint A, except to the UDP port on which WireGuard itself is listening (51821). On Endpoint B, which in this example is running a web server on TCP port 80, we’ll set up UFW to disallow new connections except for two cases: 1) allow any connection to the UDP port on which WireGuard itself is listening (51822), and 2) allow connections tunneled through WireGuard to TCP port 80.

If you’ve already added some iptables commands to the WireGuard config on your hosts, shut down their WireGuard interfaces (sudo wg-quick down wg0), remove those commands, and start them back up again (sudo wg-quick up wg0). We won’t add anything extra to the WireGuard configuration files in this article — we’ll just use the ufw command-line tool for everything. UFW will persist our settings via files in the /etc/ufw directory, and automatically set up the firewall when the host boots up.

UFW Configuration on Endpoint A

On Endpoint A, first check the status of UFW. If you haven’t set it up yet, this is what you’ll see:

$ sudo ufw status
Status: inactive

If it’s inactive, that’s fine — we’ll set up our rules first, and then activate it.

If you’re connected to Endpoint A through SSH, your first rule should be one that continues to allow the use of that SSH connection (if you’re accessing Endpoint A physically, however, just skip this). Run the following command on Endpoint A to see the IP address of the host from which you’re SSH’d in:

$ ss -tn | grep :22
ESTAB  0        0           192.168.1.11:22       192.168.1.2:33312

192.168.1.11 is the IP address of Endpoint A on its own LAN (running SSH on TCP port 22); 192.168.1.2 is the host from which I’ve SSH’d. So run the following command to continue to allow this connection once we start up the firewall:

$ sudo ufw allow proto tcp from 192.168.1.2 to any port 22
Rules updated

If you want to allow any host on the LAN to be able to SSH into Endpoint A (not limited to just the host from which you’ve currently SSH’d), instead of 192.168.1.2 you could specify 192.168.1.0/24 (a range that includes 192.168.1.0-192.168.1.255) for the above rule.

For the second rule, we’ll allow any host to connect to Endpoint A via WireGuard (which is listening on UDP port 51821 on Endpoint A). Run the following command:

$ sudo ufw allow 51821/udp
Rules updated
Rules updated (v6)

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.1.2
51821/udp                  ALLOW       Anywhere
51821/udp (v6)             ALLOW       Anywhere (v6)

UFW is now up and running, and will prevent any connection to Endpoint A other than through WireGuard (or directly through SSH from 192.168.1.2). Additionally, UFW will prevent any new inbound connections to Endpoint A even when accessed through WireGuard — all connections through the WireGuard tunnel have to be initiated by Endpoint A (for example, if a web server was running on Endpoint A at TCP port 80, no host would be able to connect inbound to it, even through WireGuard; but Endpoint A would still be able to connect outbound through WireGuard to the web server running on Endpoint B).

UFW Configuration on Endpoint B

On Endpoint B, do the same thing. First check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Endpoint B through SSH, add a rule to allow your current SSH connection to be maintained. Run the following command on Endpoint B to see the IP address of the host from which you’ve SSH’d in:

$ ss -tn | grep :22
ESTAB  0        0           10.0.0.2:22       10.0.0.1:44123

In this case I’ve SSH’d into Endpoint B (10.0.0.2) through the WireGuard tunnel from Endpoint A (10.0.0.1). So I’d run the following command to continue to allow this connection once I start up UFW:

$ sudo ufw allow proto tcp from 10.0.0.1 to any port 22
Rules updated

Now add a second rule to allow any host to connect to Endpoint B over WireGuard (which is listening on UDP port 51822 on Endpoint B):

$ sudo ufw allow 51822/udp
Rules updated
Rules updated (v6)

And on Endpoint B only, add a third rule to allow any host to access the web server running on Endpoint B (listening on TCP port 80) over WireGuard (where the WireGuard interface name is wg0 on Endpoint B):

$ sudo ufw allow in on wg0 proto tcp to any port 80
Rules updated
Rules updated (v6)

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       10.0.0.1
51822/udp                  ALLOW       Anywhere
80/tcp on wg0              ALLOW       Anywhere
51822/udp (v6)             ALLOW       Anywhere (v6)
80/tcp (v6) on wg0         ALLOW       Anywhere (v6)

UFW is now up and running on Endpoint B, and will prevent any connection to Endpoint B other than through WireGuard (or directly through SSH from 10.0.0.1). Additionally, UFW will prevent any new inbound connections to Endpoint B through WireGuard, except to TCP port 80 (the web server running on Endpoint B).

Test It Out

You can test this out by trying to access Endpoint B’s TCP port 80 from Endpoint A:

$ curl 10.0.0.2

You should see the HTML of Endpoint B’s homepage printed. If you start up another web server running on Endpoint B at say TCP port 8080 (run python3 -m http.server 8080 for a temporary server serving the contents of the current directory), you won’t be able to access it from Endpoint A (or anywhere else):

$ curl 10.0.0.2:8080

You won’t see anything printed for the above (the command will “hang” — kill it by pressing the control-C key combination).

Hub and Spoke

To demonstrate UFW with a hub-and-spoke topology, we’ll use the hub-and-spoke WireGuard VPN described in the WireGuard Hub and Spoke Configuration guide. Here’s a network diagram of the scenario:

Hub and Spoke VPN
Figure 2. Hub and spoke scenario

On the “spokes” in this example scenario, Endpoint A and Endpoint B, we’ll set up UFW to disallow all new connections to the endpoints except through WireGuard — but allow full access to anything connected through WireGuard. On the “hub”, Host C, we’ll set up UFW to disallow all new connections to the hub except through WireGuard; and we’ll prevent the hub from forwarding any new connections through its WireGuard tunnels except to the web server listening at TCP port 80 on Endpoint B.

This allows us to use UFW on the hub as the central place to enforce access control throughout our WireGuard VPN. When we add more endpoints to our VPN, we can set them up with the same UFW configuration as Endpoint A or B, and disallow access to or from them via UFW configuration changes on Host C.

If you’ve already added some iptables commands to the WireGuard configuration of your hosts, shut down their WireGuard interfaces (sudo wg-quick down wg0), remove the iptables commands, and start them back up again (sudo wg-quick up wg0). We won’t add anything extra to our WireGuard config in this article — we’ll just use the ufw command-line tool for everything. UFW will persist our settings via files in the /etc/ufw directory, and automatically set up the firewall when the host boots up.

UFW Configuration on Endpoint A

On Endpoint A, first check the status of UFW. If you haven’t set it up yet, this is what you’ll see:

$ sudo ufw status
Status: inactive

If it’s inactive, that’s fine — we’ll set up our rules, and then activate it.

If you’re connected to Endpoint A through SSH, your first rule should be to continue to allow that SSH connection (if you’re accessing Endpoint A physically, you can skip this rule). Run the following command on Endpoint A to see the IP address of the host from which you’re SSH’d in:

$ ss -tn | grep :22
ESTAB  0        0           192.168.1.11:22       192.168.1.2:33312

192.168.1.11 is the IP address of Endpoint A on its own LAN (running SSH on TCP port 22); 192.168.1.2 is the host from which I’ve SSH’d. So run the following command to continue to allow this connection once we start up the firewall:

$ sudo ufw allow proto tcp from 192.168.1.2 to any port 22
Rules updated

If you want to allow any host on the LAN to be able to SSH into Endpoint A (not limited to just the host from which you’ve currently SSH’d), instead of 192.168.1.2 you could specify 192.168.1.0/24 (a range that includes 192.168.1.0-192.168.1.255) for the above rule.

For the second rule, we’ll allow any host to connect to Endpoint A via WireGuard (which is listening on UDP port 51821 on Endpoint A). Run the following command:

$ sudo ufw allow 51821/udp
Rules updated
Rules updated (v6)

Now, for the third rule, we’ll allow any kind of access from any host, as long as it goes through WireGuard (wg0 is our WireGuard interface name on Endpoint A):

$ sudo ufw allow in on wg0
Rules updated
Rules updated (v6)

Start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.1.2
51821/udp                  ALLOW       Anywhere
Anywhere on wg0            ALLOW       Anywhere
51821/udp (v6)             ALLOW       Anywhere (v6)
Anywhere (v6) on wg0       ALLOW       Anywhere (v6

UFW is now up and running on Endpoint A, and will prevent any connection to Endpoint A other than through WireGuard (or directly through SSH from 192.168.1.2).

UFW Configuration on Endpoint B

On Endpoint B, do the same thing. First check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Endpoint B through SSH, add a rule to allow your current SSH connection. Run the following command on Endpoint B to see the IP address of the host from which you’ve SSH’d in:

$ ss -tn | grep :22
ESTAB  0        0           192.168.200.22:22       198.51.100.1:59193

In this case I’ve SSH’d into Endpoint B (192.168.200.22) remotely from 198.51.100.1. So I’d run the following command to continue to allow this connection once I start up UFW:

$ sudo ufw allow proto tcp from 198.51.100.1 to any port 22
Rules updated

Then add a second rule to allow any host to connect to Endpoint B over WireGuard (which is listening on UDP port 51822 on Endpoint B):

$ sudo ufw allow 51822/udp
Rules updated
Rules updated (v6)

And now add a third rule to allow any kind of access from any host over WireGuard (wg0 is our WireGuard interface name on Endpoint B):

$ sudo ufw allow in on wg0
Rules updated
Rules updated (v6)

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       198.51.100.1
51822/udp                  ALLOW       Anywhere
Anywhere on wg0            ALLOW       Anywhere
51822/udp (v6)             ALLOW       Anywhere (v6)
Anywhere (v6) on wg0       ALLOW       Anywhere (v6

UFW is now up and running on Endpoint B, and will prevent any connection to Endpoint B other than through WireGuard (or directly through SSH from 198.51.100.1).

Packet Forwarding on Host C

The original WireGuard Hub and Spoke Configuration guide, in the “Configure Routing on Host C” section, directs you to to add the following line in the WireGuard configuration on Host C:

PreUp = sysctl -w net.ipv4.ip_forward=1

This enables Host C to forward packets from Endpoint A to Endpoint B (or any other hosts). Since we’re using UFW, you can omit this line from your WireGuard configuration, and instead edit the /etc/ufw/sysctl.conf file on Host C (as root) to uncomment the following lines (ie remove the existing # at the start of the lines so they look like the following):

net/ipv4/ip_forward=1
net/ipv6/conf/default/forwarding=1
net/ipv6/conf/all/forwarding=1

UFW Configuration on Host C

On Host C, check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Host C through SSH, your first rule should be to continue to allow that SSH connection (if you’re accessing Host C physically, you can skip this rule). Run the following command on Host C to see the IP address of the host from which you’re SSH’d in:

$ ss -tn | grep :22
ESTAB  0        0           192.168.30.33:22       192.168.30.1:56818

192.168.30.33 is the IP address of Host C on its own LAN (running SSH on TCP port 22); 192.168.30.1 is the host from which I’ve SSH’d. Run the following command to continue to allow this connection once we start up the firewall:

$ sudo ufw allow proto tcp from 192.168.30.1 to any port 22
Rules updated

For the second rule, we’ll allow any host to connect to Host C via WireGuard (which is listening on UDP port 51823 on Host C) by running the following command:

$ sudo ufw allow 51823/udp
Rules updated
Rules updated (v6)

Now for our third rule, we’ll allow any host connected to Host C via WireGuard (where wg0 is the name of the WireGuard interface on Host C) to use that connection to access the web server running on Endpoint B (listening on TCP port 80 of Endpoint B):

$ sudo ufw route allow in on wg0 proto tcp to 10.0.0.2 port 80
Rules updated

Note that for UFW rules like this, which allow the local host to forward packets from one external host to another, the rule starts with the route keyword (ufw route allow …​); whereas UFW rules that apply to packets directed at the local host itself, don’t (ufw allow …​).

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.30.1
51823/udp                  ALLOW       Anywhere
51823/udp (v6)             ALLOW       Anywhere (v6)

10.0.0.2 80/tcp            ALLOW FWD   Anywhere on wg0

UFW is now up and running on Host C, and will prevent any connection to Host C other than through WireGuard (or directly through SSH from 192.168.30.1). Additionally, UFW will prevent Host C from being used to forward new connections between hosts in its WireGuard network, except new connections to TCP port 80 of the web server running on Endpoint B (10.0.0.2).

Test It Out

You can test this out by trying to access Endpoint B’s TCP port 80 from Endpoint A:

$ curl 10.0.0.2

You should see the HTML of Endpoint B’s homepage printed. If you start up another web server on Endpoint B running at TCP port 8080 (run python3 -m http.server 8080 for a temporary server serving the contents of the current directory), you won’t be able to access it from Endpoint A (or anywhere else):

$ curl 10.0.0.2:8080

You won’t see anything printed for the above command (the command will “hang” — kill it by pressing the control-C key combination).

Similarly, if you start up a web server on Endpoint A (with python3 -m http.server 8080), you won’t be able to access it from Endpoint B:

$ curl 10.0.0.1:8080

Again, you won’t see anything printed. Only if you add additional UFW rules to Host C (like ufw route allow in on wg0 proto tcp to 10.0.0.1 port 8080) would you be able to access the web server on Endpoint A.

Point to Site

For a point-to-site example, we’ll use the point-to-site WireGuard VPN described in the WireGuard Point to Site Configuration guide. We’ll set up a firewall on the remote endpoint (the “point”, Endpoint A), on the local endpoint (Endpoint B, inside the “site”), and on the local WireGuard server (Host β, the host that allows the “point” to access the “site”). This will replace the iptables firewall described in the “extra” section of the guide, and well as the routing configuration for Host β described in the guide’s “configure routing” section.

Here’s a network diagram of the scenario:

Point to Site VPN
Figure 3. Point to site scenario

On the remote endpoint (Endpoint A), we’ll set up UFW to disallow all new connections except through WireGuard, and allow full access to anything connected through WireGuard. On the local endpoint (Endpoint B), we’ll set up UFW to disallow all new connections except from the local site (Site B) to Endpoint B’s TCP port 80 (where a web server is listening).

On the local WireGuard server (Host β), we’ll set up UFW to to disallow all new connections to the server except for WireGuard connections, and disallow the forwarding of any new connections except to TCP port 80 on Endpoint B. Additionally, we’ll modify the configuration of UFW on Host β to allow the routing of packets between the WireGuard VPN and the LAN (Local Area Network) at Site B via IP masquerading (also known as SNAT, or Source Network Address Translation).

If you’ve already added some iptables commands to the WireGuard config of your hosts, shut down their WireGuard interfaces (sudo wg-quick down wg0), remove the commands, and start them back up again (sudo wg-quick up wg0). We won’t add anything extra to the WireGuard configuration files in this article — we’ll just use the ufw command-line tool for everything. UFW will persist our settings via files in the /etc/ufw directory, and automatically set up the firewall when the host boots up.

UFW Configuration on Endpoint A

On Endpoint A, first check the status of UFW. If you haven’t set it up yet, this is what you’ll see:

$ sudo ufw status
Status: inactive

If it’s inactive, that’s fine — we’ll set up our rules first, and then activate it.

If you’re connected to Endpoint A through SSH, your first rule should be one that continues to allow the use of that SSH connection (if you’re accessing Endpoint A physically, however, just skip it). Run the following command on Endpoint A to see the IP address of the host from which you’re SSH’d in:

$ ss -tn | grep :22
ESTAB  0        0           192.168.1.11:22       192.168.1.2:33312

192.168.1.11 is the IP address of Endpoint A on its own LAN (running SSH on TCP port 22); 192.168.1.2 is the host from which I’ve SSH’d. So run the following command to continue to allow this connection once we start up the firewall:

$ sudo ufw allow proto tcp from 192.168.1.2 to any port 22
Rules updated

If you want to allow any host on the Endpoint A’s LAN to be able to SSH into Endpoint A (not limited to just the host from which you’ve currently SSH’d), instead of 192.168.1.2 you could specify 192.168.1.0/24 (a range that includes 192.168.1.0-192.168.1.255) for the above rule.

For the second rule, we’ll allow any host to connect to Endpoint A via WireGuard (which is listening on UDP port 51821 on Endpoint A) by running the following command:

$ sudo ufw allow 51821/udp
Rules updated
Rules updated (v6)

And for the third rule, we’ll allow all access over WireGuard (where the WireGuard interface name is wg0 on Endpoint A):

$ sudo ufw allow in on wg0
Rules updated
Rules updated (v6)

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.1.2
51821/udp                  ALLOW       Anywhere
Anywhere on wg0            ALLOW       Anywhere
51821/udp (v6)             ALLOW       Anywhere (v6)
Anywhere (v6) on wg0       ALLOW       Anywhere (v6

UFW is now up and running on Endpoint A, and will prevent any connection to Endpoint A other than through WireGuard (or directly through SSH from 192.168.1.2).

UFW Configuration on Endpoint B

On Endpoint B, check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Endpoint B through SSH, add a rule to allow SSH access from the Site B LAN:

$ sudo ufw allow proto tcp from 192.168.200.0/24 to any port 22
Rules updated

Next, add a rule to allow any host on the LAN to connect to TCP port 80:

$ sudo ufw allow proto tcp from 192.168.200.0/24 to any port 80
Rules updated

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.200.0/24
80/tcp                     ALLOW       192.168.200.0/24

UFW is now up and running on Endpoint B, and will allow only connections from the Site B LAN to TCP port 80 (or to TCP port 22 for SSH).

Packet Forwarding and Masquerading on Host B

In the original WireGuard Point to Site Configuration guide, the “Configure Routing on Host β” section directs you to to add some lines to the WireGuard configuration of Host β to turn on IP forwarding and IP masquerading. But since we’re using UFW, you can omit these lines from your WireGuard configuration, and instead edit the configuration of UFW.

First, edit the /etc/ufw/sysctl.conf file on Host β (as root) and uncomment the following lines:

net/ipv4/ip_forward=1
net/ipv6/conf/default/forwarding=1
net/ipv6/conf/all/forwarding=1

Then edit the /etc/ufw/before.rules file (or if you’re using IPv6 addresses, the etc/ufw/before6.rules file) to add the following block after the existing COMMIT line at the end of the file:

*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT

But be sure to replace eth0 in the above with the actual network interface name that connects Host β to the Site B LAN (if it’s not in fact eth0). If you don’t know what that interface name is, run ip -brief address show on Host β to show the list of interfaces:

$ ip -brief address show
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             192.168.200.2/24
wg0              UNKNOWN        10.0.0.2/32

If you already had UFW up and running before you made these changes, restart UFW with the command sudo systemctl restart ufw.

UFW Configuration on Host B

On Host β, check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Host β through SSH, add a rule to allow SSH access from the Site B LAN:

$ sudo ufw allow proto tcp from 192.168.200.0/24 to any port 22
Rules updated

Next, add a rule to allow any host to connect to Host β via WireGuard (which is listening on UDP port 51822 on Host β):

$ sudo ufw allow 51822/udp
Rules updated
Rules updated (v6)

Then add a rule to allow any host connected to Host β via WireGuard (wg0 is the name of the WireGuard interface on Host β) to use that connection to access the web server running on Endpoint B (listening on TCP port 80 of Endpoint B):

$ sudo ufw route allow in on wg0 proto tcp to 192.168.200.22 port 80
Rules updated

Note that for UFW rules like this, which allow the local host to forward packets from one external host to another, the rule starts with the route keyword (ufw route allow …​); whereas UFW rules that apply to packets directed at the local host itself, don’t (ufw allow …​).

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.200.0/24
51822/udp                  ALLOW       Anywhere
51822/udp (v6)             ALLOW       Anywhere (v6)

192.168.200.22 80/tcp      ALLOW FWD   Anywhere on wg0

UFW is now up and running on Host β, and will prevent any connection to Host β other than through WireGuard (or directly through SSH from the Site B LAN, 192.168.200.0/24). Additionally, UFW will prevent Host β from being used to forward new connections between hosts, except through WireGuard to TCP port 80 of the web server running on Endpoint B (192.168.200.22).

Test It Out

You can test this out by trying to access Endpoint B’s TCP port 80 from Endpoint A:

$ curl 192.168.200.22

You should see the HTML of Endpoint B’s homepage printed. If you start up another web server on Endpoint B running at TCP port 8080 (run python3 -m http.server 8080 for a temporary server serving the contents of the current directory), you won’t be able to access it from Endpoint A:

$ curl 192.168.200.22:8080

You won’t see anything printed for the above command (the command will “hang” — kill it by pressing the control-C key combination).

Extra: Port Forwarding on Host B

If you want the reverse of the above example, like the scenario described by the WireGuard Point to Site With Port Forwarding guide, which allows Endpoint B (in the local site) to connect to a web server on Endpoint A (the remote “point”), you would have to edit the /etc/ufw/before.rules file on Host β to allow port forwarding (also known as Destination Network Address Translation, or DNAT).

To enable port forwarding for this example, edit the *nat block in /etc/ufw/before.rules that we added above to also include the following PREROUTING lines:

*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.0.0.1
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT

Unlike the POSTROUTING rule, which applies to any packets forwarded out the Site B LAN interface on Host β, this PREROUTING rule (the line beginning -A PREROUTING -i …​) applies specifically to just this one example case, where we want to forward packets originally destined for TCP port 80 of Host β itself on to Endpoint A (10.0.0.1). For every additional port we want to forward on Host β, we’d have to add an additional PREROUTING rule to /etc/ufw/before.rules with the specific port to forward, and destination to forward it to.

We also have to add a separate UFW rule for each forwarded port. For this specific example, add a UFW rule on Host β to allow it to forward packets to TCP port 80 on Endpoint A (10.0.0.1):

$ sudo ufw route allow proto tcp to 10.0.0.1 port 80
Rules updated

Then restart UFW and check its status:

$ sudo systemctl restart ufw
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.200.0/24
51822/udp                  ALLOW       Anywhere
51822/udp (v6)             ALLOW       Anywhere (v6)

192.168.200.22 80/tcp      ALLOW FWD   Anywhere on wg0
10.0.0.1 80/tcp            ALLOW FWD   Anywhere

Test it by trying to access TCP port 80 on Host β from Endpoint B (Host β will now forward its TCP port 80 to Endpoint A):

$ curl 192.168.200.2

You should see the HTML of Endpoint A’s homepage printed.

Site to Site

For a site-to-site example, we’ll use the site-to-site WireGuard VPN laid out in the WireGuard Site to Site Configuration guide. We’ll set up UFW firewalls on the endpoints in both the sites, as well as on the WireGuard hosts, replacing the iptables firewall described in the “extra” section of the guide.

Here’s a network diagram of the scenario:

Site to Site VPN
Figure 4. Site to site scenario

On Endpoint A we’ll set up UFW to disallow all new connections; and on Endpoint B we’ll set up UFW to allow new connections only to Endpoint B’s web server listening on TCP port 80. On the two WireGuard servers (Host α and Host β), we’ll allow any host to connect to the servers via WireGuard, and we’ll also allow the forwarding of any connections between Site A and Site B.

Site A and Site B already have been configured to route to each other through the two WireGuard servers (as described in the site-to-site configuration guide), so we won’t adjust their UFW configuration to enable NAT (Network Address Translation) as we did for Host β in the Point to Site scenario. But we will edit the WireGuard servers’ UFW config files to enable packet forwarding.

If you’ve already added some iptables commands to the WireGuard config of your hosts, shut down their WireGuard interfaces (sudo wg-quick down wg0), remove the commands, and start them back up again (sudo wg-quick up wg0). We won’t add anything extra to the WireGuard configuration files in this article — we’ll just use the ufw command-line tool for everything. UFW will persist our settings via files in the /etc/ufw directory, and automatically set up the firewall when the host boots up.

UFW Configuration on Endpoint A

On Endpoint A, first check the status of UFW. If you haven’t set it up yet, this is what you’ll see:

$ sudo ufw status
Status: inactive

Unless you’re connected to Endpoint A through SSH, the only thing you need to do on Endpoint A is just start up UFW. If you are connected to Endpoint A through SSH, however, first run the following command to continue to allow SSH access to Endpoint A from the Site A LAN (192.168.1.0/24 — a range that includes 192.168.1.0-192.168.1.255):

$ sudo ufw allow proto tcp from 192.168.1.0/24 to any port 22
Rules updated

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.1.0/24

So now UFW is up and running on Endpoint A, and will prevent any new inbound connections to Endpoint A (except through SSH directly from the Site A LAN). Endpoint A will still be able to make new outbound connections, however — like to initiate new connections to the web server running on Endpoint B.

UFW Configuration on Endpoint B

On Endpoint B, check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Endpoint B through SSH, add a rule to allow SSH access from the Site B LAN:

$ sudo ufw allow proto tcp from 192.168.200.0/24 to any port 22
Rules updated

Next, add a couple of rules to allow any host in either LAN (Site A or Site B) to connect to TCP port 80:

$ sudo ufw allow proto tcp from 192.168.1.0/24 to any port 80
Rules updated
$ sudo ufw allow proto tcp from 192.168.200.0/24 to any port 80
Rules updated

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.200.0/24
80/tcp                     ALLOW       192.168.1.0/24
80/tcp                     ALLOW       192.168.200.0/24

UFW is now up and running on Endpoint B, and will only allow connections to TCP port 80 from the Site A and B LANs (or to TCP port 22 for SSH from the Site B LAN).

Packet Forwarding

In the original WireGuard Site to Site Configuration guide, the “Configure Routing” section directs you to to add some lines to the WireGuard configuration of Host α and Host β to turn on IP forwarding. But since we’re using UFW, you can omit these lines from your WireGuard configuration, and instead edit the configuration of UFW.

On both Host α and Host β, edit the /etc/ufw/sysctl.conf file (as root) to uncomment the following lines:

net/ipv4/ip_forward=1
net/ipv6/conf/default/forwarding=1
net/ipv6/conf/all/forwarding=1

If you already have UFW up and running before you make this change, restart UFW with the command sudo systemctl restart ufw.

UFW Configuration on Host A

On Host α, check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Host α through SSH, add a rule to allow SSH access from the Site A LAN:

$ sudo ufw allow proto tcp from 192.168.1.0/24 to any port 22

Next, add a rule to allow any host to connect to Host α via WireGuard (which is listening on UDP port 51821 on Host α):

$ sudo ufw allow 51821/udp
Rules updated
Rules updated (v6)

Then add a rule to allow new connections destined for Site B (192.168.200.0/24) through the WireGuard tunnel to Host β:

$ sudo ufw route allow to 192.168.200.0/24
Rules updated

In our example scenario, the only cross-site connection we need to allow is the connection from Endpoint A to Endpoint B over HTTP. If that’s the only connection you actually need to enable, you could adjust the above rule in a number of different ways to make it more restrictive. Most restrictively, you could prevent any host in Site A other than Endpoint A from connecting to any host in Site B other than Endpoint B (and allow Endpoint A to connect only to TCP port 80 of Endpoint B):

$ sudo ufw route allow proto tcp from 192.168.1.11 to 192.168.200.22 port 80
Rules updated

Or you could allow any host in Site A to connect just to Endpoint B (but no other hosts in Site B) over HTTP (and no other ports):

$ sudo ufw route allow proto tcp to 192.168.200.22 port 80
Rules updated

Or you could allow just Endpoint A (but no other hosts from Site A) to connect to any host in Site B:

$ sudo ufw route allow from 192.168.1.11
Rules updated

Alternatively, if you wanted a reverse of this connection, for example to allow one particular host in Site B (say an Endpoint BBB with IP address 192.168.200.123) to be able to connect to one particular host in Site A (say Endpoint AAA, a mail server with IP address 192.168.1.45, listening on TCP port 25), you could allow that one specific connection to be initiated from Site B to Site A with the following rule:

$ sudo ufw route allow proto tcp from 192.168.200.123 to 192.168.1.45 port 25
Rules updated

Or you could allow any host in Site B to access Endpoint AAA:

$ sudo ufw route allow proto tcp to 192.168.1.45 port 25
Rules updated

Or you could allow Endpoint BBB to access any host in Site A:

$ sudo ufw route allow from 192.168.200.123
Rules updated

Or you could just allow any host in Site B to initiate connections to any host in Site A:

$ sudo ufw route allow from 192.168.200.0/24
Rules updated

Note that for UFW rules like this, which allow the local host to forward packets from one external host to another, the rule starts with the route keyword (ufw route allow …​); whereas UFW rules that apply to packets directed at the local host itself, don’t (ufw allow …​).

Also note that UFW automatically adds firewall rules to allow forwarding packets back from already-established connections (like if Endpoint A sends an HTTP request to Endpoint B, the HTTP response back from Endpoint B to Endpoint A would be part of an established connection, and not count as a new connection). So you only need to set up UFW rules to allow (or disallow) the initiation of new connections between one host and another.

So if the only route rule we used from the above was the first one, ufw route allow to 192.168.200.0/24, it would enable any host in Site A to initiate a new connection to any host in Site B (eg allow a browser running on a host in Site A to make an HTTP request to a web server running on a host in Site B), but it wouldn’t allow any host in Site B to initiate a new connection to any host in Site A (eg wouldn’t allow a browser running on a host in Site B to make an HTTP request to a web server running on a host in Site A).

With that one route rule from above in place, let’s start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.1.0/24
51821/udp                  ALLOW       Anywhere
51821/udp (v6)             ALLOW       Anywhere (v6)

192.168.200.0/24           ALLOW FWD   Anywhere

UFW is now up and running on Host α, and will prevent direct connections to it other than through WireGuard (or through SSH from the Site A LAN). But it will still allow new connections to be forwarded from Site A to Site B (allowing Endpoint A to browse the web server running on Endpoint B) — just not from Site B to Site A (prohibiting random connections from being initiated from hosts in Site B to hosts in Site A).

UFW Configuration on Host B

On Host β, check the status of UFW:

$ sudo ufw status
Status: inactive

If you’re connected to Host β through SSH, add a rule to allow SSH access from the Site B LAN:

$ sudo ufw allow proto tcp from 192.168.200.0/24 to any port 22
Rules updated

Next, add a rule to allow any host to connect to Host β via WireGuard (which is listening on UDP port 51822 on Host β):

$ sudo ufw allow 51822/udp
Rules updated
Rules updated (v6)

Then add a rule to allow new outgoing connections destined for Site A (192.168.1.0/24) through the WireGuard tunnel to Host α:

$ sudo ufw route allow to 192.168.1.0/24
Rules updated

And correspondingly, add a rule to allow new incoming connections to Site B (192.168.200.0/24) from the WireGuard tunnel with Host α:

$ sudo ufw route allow to 192.168.200.0/24
Rules updated

Now start up UFW and check its status:

$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       192.168.200.0/24
51822/udp                  ALLOW       Anywhere
51822/udp (v6)             ALLOW       Anywhere (v6)

192.168.1.0/24             ALLOW FWD   Anywhere
192.168.200.0/24           ALLOW FWD   Anywhere

UFW is now up and running on Host β, and will prevent direct connections to it other than through WireGuard (or through SSH from the Site B LAN). It will, however, allow any new connections to be forwarded from Site A to Site B, and vice versa.

Test It Out

You can test this out by trying to access Endpoint B’s TCP port 80 from Endpoint A:

$ curl 192.168.200.22

You should see the HTML of Endpoint B’s homepage printed. If you start up another web server on Endpoint B running at TCP port 8080 (run python3 -m http.server 8080 for a temporary server serving the contents of the current directory), you won’t be able to access it from Endpoint A:

$ curl 192.168.200.22:8080

You won’t see anything printed for the above command (the command will “hang” — kill it by pressing the control-C key combination).

Similarly, if you start up a web server on Endpoint A (with python3 -m http.server 8080), you won’t be able to access it from Endpoint B:

$ curl 192.168.1.11:8080

Again, you won’t see anything printed. Only if you add additional UFW rules 1) to Endpoint A, to allow new connections to it (like sudo ufw allow proto tcp to any port 8080), and 2) to Host α, to allow new connections to be forwarded through to Site A (like ufw route allow to 192.168.1.0/24), would you be able to access the web server running on Endpoint A.

Ping

If you try pinging a host running UFW (or any host where the connection to the host is forwarded through UFW), you may be surprised to see your pings succeed even when the UFW firewall is blocking the initiation of most other new connections. For example, with the Point to Point or Hub and Spoke scenarios above, you can run ping 10.0.0.1 on Endpoint B and see replies back from Endpoint A; or with the Site to Site scenario, you can run ping 192.168.1.11 on Endpoint B and see replies back from Endpoint A (and although you can’t ping Endpoint A from Endpoint B with the Point to Site scenario because of NAT, you could still ping Endpoint A from Host β).

This is because UFW always allows certain types of ICMP packets through the firewall it sets up, including the type of ICMP packets used by ping requests (type 8, “echo request”). So rather than using ping to check your firewall, try connecting to the specific ports that your firewall allows or disallows.

If you’re looking for a general tool to try to connect to a variety of ports, try Nmap. For example, to test if TCP port 8080 is accessible on 10.0.0.1, run this:

sudo nmap -Pn -n -sS --reason -p8080 10.0.0.1

Or to test if UDP port 51821 is accessible on 192.168.1.11, run this:

sudo nmap -Pn -n -sU --reason -p51821 192.168.1.11

UFW’s behavior of indiscriminately allowing forwarded echo-request ICMP packets is a minor information-disclosure security weakness — it could allow an attacker trying to probe your network to discover the existence of other hosts that otherwise would not be visible to the attacker. If this is a problem for your security posture, edit the /etc/ufw/before.rules file on your hosts, and comment out (ie add a # to the start of) this line:

#-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT

For hosts that are accessible over IPv6, also edit the /etc/ufw/before6.rules file, and comment out these lines:

#-A ufw6-before-forward -p icmpv6 --icmpv6-type echo-request -j ACCEPT
#-A ufw6-before-forward -p icmpv6 --icmpv6-type echo-reply -j ACCEPT

If do you want to continue to allow ping over forwarded connections for certain select hosts, replace each commented-out line with a line for each host that that you still want to be able to ping. Specify the -d (aka --destination) flag on each line with the address of the host to allow. For example, in the Hub and Spoke scenario, to allow Endpoint B (10.0.0.2) to continue to be pinged through Host C (while disallowing other hosts, like Endpoint A, from being pinged), update the /etc/ufw/before.rules file on Host C to comment out (or simply remove) the first line, and add the second line to replace it:

#-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT
-A ufw-before-forward -p icmp --icmp-type echo-request -d 10.0.0.2 -j ACCEPT

Restart UFW with the command sudo systemctl restart ufw to put your changes into effect.

Troubleshooting

Tcpdump

Tcpdump can be really helpful when troubleshooting firewall issues — you can use it to determine if packets are transiting in and out as expected from a given host. For example, with the Site to Site scenario, if you see no output when you try to connect from Endpoint A to Endpoint B (ie when running curl 192.168.200.22), try running the following command on each host on the path between Endpoint A and Endpoint B (ie Endpoint A, Host α, Host β, and Endpoint B):

$ sudo tcpdump -ni any 'tcp port 80 and host 192.168.200.22'

While those commands are running, try connecting from Endpoint A to Endpoint B again. You should see each terminal running tcpdump print a series of lines that look like the following:

22:12:16.156120 IP 192.168.1.11.36112 > 192.168.200.22.80: Flags [S], seq 767605079, win 62167, options [mss 8881,sackOK,TS val 1689421470 ecr 0,nop,wscale 6], length 0
22:12:16.157801 IP 192.168.200.22.80 > 192.168.1.11.36112: Flags [S.], seq 3979107600, ack 767605080, win 62083, options [mss 8881,sackOK,TS val 3921390190 ecr 1689421470,nop,wscale 6], length 0
22:12:16.158587 IP 192.168.1.11.36112 > 192.168.200.22.80: Flags [.], ack 1, win 972, options [nop,nop,TS val 1689421475 ecr 3921390190], length 0
22:12:16.158637 IP 192.168.1.11.36112 > 192.168.200.22.80: Flags [P.], seq 1:78, ack 1, win 972, options [nop,nop,TS val 1689421475 ecr 3921390190], length 77: HTTP: GET / HTTP/1.1
22:12:16.158755 IP 192.168.200.22.80 > 192.168.1.11.36112: Flags [.], ack 78, win 969, options [nop,nop,TS val 3921390191 ecr 1689421475], length 0

Lines with 192.168.1.11.36112 > 192.168.200.22.80 represent packets being sent to Endpoint B TCP port 80, and lines with 192.168.200.22.80 > 192.168.1.11.36112 represent packets being sent back from Endpoint B in response. If the tcpdump command on a host outputs only lines with the former, and none with the latter, it means only packets sent to Endpoint B are making it to the host — no packets on the return trip back from Endpoint B are. If the tcpdump command on a host doesn’t output anything, it means no packets are reaching the host, not even those on the first leg of the trip to Endpoint B.

Iptables

If your UFW rules are not behaving an you expect, it may be the case that you also have other iptables rules in effect, outside of the rules that UFW generates. Try running sudo iptables-save to list all of your iptables rules.

This is what you should see with an empty UFW firewall (for UFW version 0.36):

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:ufw-after-forward - [0:0]
:ufw-after-input - [0:0]
:ufw-after-logging-forward - [0:0]
:ufw-after-logging-input - [0:0]
:ufw-after-logging-output - [0:0]
:ufw-after-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-before-input - [0:0]
:ufw-before-logging-forward - [0:0]
:ufw-before-logging-input - [0:0]
:ufw-before-logging-output - [0:0]
:ufw-before-output - [0:0]
:ufw-logging-allow - [0:0]
:ufw-logging-deny - [0:0]
:ufw-not-local - [0:0]
:ufw-reject-forward - [0:0]
:ufw-reject-input - [0:0]
:ufw-reject-output - [0:0]
:ufw-skip-to-policy-forward - [0:0]
:ufw-skip-to-policy-input - [0:0]
:ufw-skip-to-policy-output - [0:0]
:ufw-track-forward - [0:0]
:ufw-track-input - [0:0]
:ufw-track-output - [0:0]
:ufw-user-forward - [0:0]
:ufw-user-input - [0:0]
:ufw-user-limit - [0:0]
:ufw-user-limit-accept - [0:0]
:ufw-user-logging-forward - [0:0]
:ufw-user-logging-input - [0:0]
:ufw-user-logging-output - [0:0]
:ufw-user-output - [0:0]
-A INPUT -j ufw-before-logging-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-after-input
-A INPUT -j ufw-after-logging-input
-A INPUT -j ufw-reject-input
-A INPUT -j ufw-track-input
-A FORWARD -j ufw-before-logging-forward
-A FORWARD -j ufw-before-forward
-A FORWARD -j ufw-after-forward
-A FORWARD -j ufw-after-logging-forward
-A FORWARD -j ufw-reject-forward
-A FORWARD -j ufw-track-forward
-A OUTPUT -j ufw-before-logging-output
-A OUTPUT -j ufw-before-output
-A OUTPUT -j ufw-after-output
-A OUTPUT -j ufw-after-logging-output
-A OUTPUT -j ufw-reject-output
-A OUTPUT -j ufw-track-output
-A ufw-after-input -p udp -m udp --dport 137 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 138 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 139 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp -m tcp --dport 445 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 67 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp -m udp --dport 68 -j ufw-skip-to-policy-input
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
-A ufw-after-logging-forward -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-after-logging-input -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-forward -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A ufw-before-forward -j ufw-user-forward
-A ufw-before-input -i lo -j ACCEPT
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP
-A ufw-before-input -p icmp -m icmp --icmp-type 3 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 12 -j ACCEPT
-A ufw-before-input -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A ufw-before-input -p udp -m udp --sport 67 --dport 68 -j ACCEPT
-A ufw-before-input -j ufw-not-local
-A ufw-before-input -d 224.0.0.251/32 -p udp -m udp --dport 5353 -j ACCEPT
-A ufw-before-input -d 239.255.255.250/32 -p udp -m udp --dport 1900 -j ACCEPT
-A ufw-before-input -j ufw-user-input
-A ufw-before-output -o lo -j ACCEPT
-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-output -j ufw-user-output
-A ufw-logging-allow -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW ALLOW] "
-A ufw-logging-deny -m conntrack --ctstate INVALID -m limit --limit 3/min --limit-burst 10 -j RETURN
-A ufw-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "
-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
-A ufw-not-local -j DROP
-A ufw-skip-to-policy-forward -j DROP
-A ufw-skip-to-policy-input -j DROP
-A ufw-skip-to-policy-output -j ACCEPT
-A ufw-track-output -p tcp -m conntrack --ctstate NEW -j ACCEPT
-A ufw-track-output -p udp -m conntrack --ctstate NEW -j ACCEPT
-A ufw-user-limit -m limit --limit 3/min -j LOG --log-prefix "[UFW LIMIT BLOCK] "
-A ufw-user-limit -j REJECT --reject-with icmp-port-unreachable
-A ufw-user-limit-accept -j ACCEPT
COMMIT

Any rules not listed above (and not beginning with -A ufw) are not from UFW (unless you’ve made changes to the /etc/ufw/before.rules file, in which case you should see those changes in the output of iptables-save too).

Once you add UFW rules, you’ll see some additional iptables rules beginning with -A ufw-user-forward or -A ufw-user-input listed just above the bottom of the output (right above the lines beginning with -A ufw-user-limit). For example, in the Point to Point scenario, once you’ve configured UFW on Endpoint B, you’ll see the following additional lines listed when you run sudo iptables-save on Endpoint B:

-A ufw-user-input -s 10.0.0.1/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A ufw-user-input -p udp -m udp --dport 51822 -j ACCEPT
-A ufw-user-input -i wg0 -p tcp -m tcp --dport 80 -j ACCEPT

As another example, with the Hub and Spoke scenario, once you’ve configured Host C, you’ll see the following additional lines when you run sudo iptables-save on Host C:

-A ufw-user-forward -d 10.0.0.2/32 -i wg0 -p tcp -m tcp --dport 80 -j ACCEPT
-A ufw-user-input -s 192.168.30.1/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A ufw-user-input -p udp -m udp --dport 51823 -j ACCEPT