Firewalld Policy-Based Access Control for WireGuard

We covered the basics of how to use WireGuard with firewalld in an article last year. This article will show you how to add firewalld policies to those basic firewalld configurations in order to apply access control to connections forwarded through firewalld.

Firewalld policies were introduced with firewalld version 0.9 (first released in the fall of 2020). Version 0.9 or newer is available today in the most recent version of many Linux distributions — consult the Distro Versions table at the end of the article to check if it’s packaged for the distros you use.

Firewalld policies control traffic forwarded between firewalld zones. When you attach one or more zones to the ingress (input) side of a policy, and one or more zones to the egress (output) side of the same policy, the policy will filter the traffic forwarded from the ingress to the egress zones.

For example, in the following diagram, the mywg2mysite policy will filter new connections forwarded from the mywg zone to the mysite zone, and the mysite2mywg policy will filter new connections forwarded from the mysite zone to the mywg zone:

Firewalld Policies
Note

Firewalld always forwards already-established connections, so if a policy allows new connections to be established in one direction, the policy settings for the reverse direction don’t matter — packets sent in the reverse direction for an already-established connection will always be allowed back to the connection’s original source.

Let’s walk through how to use firewalld policies to apply access control for the following components:

  1. Hub of a hub-and-spoke topology

  2. Site Masquerading with a point-to-site topology

  3. Site Gateway with a site-to-site topology

Hub

Hub and Spoke VPN

In the original How to Use WireGuard With Firewalld article, we covered how to set up the hub, Host C, as part of a hub-and-spoke scenario in the Firewalld Configuration on Host C section. In that section, we covered how to use firewalld’s public zone to allow public access to the hub’s WireGuard port (51823 in this example), and to set up a custom mywg zone to allow forwarding of WireGuard traffic between spokes (Endpoint A and Endpoint B).

For the custom mywg zone in the original article, we did add a bunch of rich rules and special direct rules to apply access control to the WireGuard network, such that it only allowed new connections to be initiated to the webserver on Endpoint B (10.0.0.2), and blocked everything else. With firewalld policies, however, we can greatly simplify this configuration, plus use more of firewalld’s built-in settings instead of complicated rich rules and direct rules.

So in this article, we’ll replace the mywg zone from the original article with a new, much simpler nuwg zone, and pair it with a new intrawg policy, with which we’ll apply access control.

First, on Host C, save any runtime changes you’ve made since you last saved them:

$ sudo firewall-cmd --runtime-to-permanent
success

And delete your old mywg zone if you had previously added it:

$ sudo firewall-cmd --permanent --delete-zone=mywg
success

Next, create the new nuwg zone:

$ sudo firewall-cmd --permanent --new-zone=nuwg
success

And then create the new intrawg policy, setting it to reject new connections by default:

$ sudo firewall-cmd --permanent --new-policy=intrawg
success
$ sudo firewall-cmd --permanent --policy=intrawg --set-target=REJECT
success

Then load the new zone and policy into the active runtime state:

$ sudo firewall-cmd --reload
success

Now we’ll add our access control rules, using firewalld’s rich rule syntax. In this case, we just need one rule, to grant access to Endpoint B’s webserver (10.0.0.2):

$ sudo firewall-cmd --policy=intrawg --add-rich-rule='rule family="ipv4" destination address="10.0.0.2" service name="http" accept'
success

If we wanted to limit access even further, say to allow only Endpoint A to access Endpoint B’s webserver, we could instead write the rule this way, specifying the source address of Endpoint A (10.0.0.1) as well:

$ sudo firewall-cmd --policy=intrawg --add-rich-rule='rule family="ipv4" source address="10.0.0.1" destination address="10.0.0.2" service name="http" accept'
success

Or if the webserver on Endpoint B was running on a custom port, like say TCP port 8080, we could instead write the rule this way, specifying the custom port instead of the standard HTTP service definition:

$ sudo firewall-cmd --policy=intrawg --add-rich-rule='rule family="ipv4" destination address="10.0.0.2" port port="8080" protocol="tcp" accept'
success

To finish up the policy settings, we’ll apply it to traffic forwarded within the nuwg zone itself (with both ingress and egress connected to the same zone):

$ sudo firewall-cmd --policy=intrawg --add-ingress-zone=nuwg
success
$ sudo firewall-cmd --policy=intrawg --add-egress-zone=nuwg
success

If you view the info for the intrawg policy, this is what you’ll see:

$ sudo firewall-cmd --info-policy=intrawg
intrawg (active)
  priority: -1
  target: REJECT
  ingress-zones: nuwg
  egress-zones: nuwg
  services:
  ports:
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
        rule family="ipv4" destination address="10.0.0.2" service name="http" accept

Now for the nuwg zone, simply bind the wg0 interface to it:

$ sudo firewall-cmd --zone=nuwg --add-interface=wg0
success

Unlike the complicated configuration settings for the mywg zone in the original article, this is all the configuration we need for the nuwg zone. If you view the info of the nuwg zone, this is what you’ll see:

$ sudo firewall-cmd --info-zone=nuwg
nuwg (active)
  target: default
  icmp-block-inversion: no
  interfaces: wg0
  sources:
  services:
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

If you haven’t already set up the Public Zone on Host C, as described in the original article, do that now. Then you’re ready to Test It Out!

If it’s working to allow Endpoint A to access the webserver at Endpoint B (and rejecting all other connections through the WireGuard network), save your configuration settings with the following command:

$ sudo firewall-cmd --runtime-to-permanent
success

After running that command, you’ll find the configuration for the intrawg policy at /etc/firewalld/policies/intrawg.xml:

<!-- Host C /etc/firewalld/policies/intrawg.xml -->
<?xml version="1.0" encoding="utf-8"?>
<policy target="REJECT">
  <rule family="ipv4">
    <destination address="10.0.0.2"/>
    <service name="http"/>
    <accept/>
  </rule>
  <ingress-zone name="nuwg"/>
  <egress-zone name="nuwg"/>
</policy>

And the configuration for the nuwg zone at /etc/firewalld/zones/nuwg.xml:

<!-- Host C /etc/firewalld/zones/nuwg.xml -->
<?xml version="1.0" encoding="utf-8"?>
<zone>
  <interface name="wg0"/>
</zone>

Masquerading

Point to Site VPN

In the original How to Use WireGuard With Firewalld article, we covered how to set up the site side of the connection, Host β, as part of a point-to-site with masquerading scenario in the Firewalld Configuration on Host β section. In that section, we covered how to set up four custom firewalld zones:

  1. myadmin: allow SSH access to Host β for system administration.

  2. mypub: allow public access to the WireGuard port on Host β (51822).

  3. mysite: allow access to the Site B LAN (192.168.200.0/24) with masquerading.

  4. mywg: allow access from the WireGuard network.

In this article, we’ll add a new policy to restrict access between the mywg zone and the mysite zone such that the only access allowed will be to the webserver on Endpoint B (192.168.200.22), and everything else will be blocked.

First, on Host β, save any runtime changes you’ve made since you last saved them:

$ sudo firewall-cmd --runtime-to-permanent
success

Then create a new firewalld policy for the connection between the mywg and mysite zones, and set it to reject new connections by default:

$ sudo firewall-cmd --permanent --new-policy=mywg2mysite
success
$ sudo firewall-cmd --permanent --policy=mywg2mysite --set-target=REJECT
success

And run the reload command to make those changes active:

$ sudo firewall-cmd --reload
success

Next we’ll add our access control rules, using firewalld’s rich rule syntax. In this case, we just need one rule, to grant access to Endpoint B’s webserver (192.168.200.22):

$ sudo firewall-cmd --policy=mywg2mysite --add-rich-rule='rule family="ipv4" destination address="192.168.200.22" service name="http" accept'
success

If we wanted to limit access even further, say to allow only Endpoint A to access Endpoint B’s webserver, we could instead write the rule this way, specifying the source address of Endpoint A (10.0.0.1) as well:

$ sudo firewall-cmd --policy=mywg2mysite --add-rich-rule='rule family="ipv4" source address="10.0.0.1" destination address="192.168.200.22" service name="http" accept'
success

Or if the webserver on Endpoint B was running on a custom port, like say TCP port 8080, we could instead write the rule this way, specifying the custom port instead of the standard HTTP service definition:

$ sudo firewall-cmd --policy=mywg2mysite --add-rich-rule='rule family="ipv4" destination address="192.168.200.22" port port="8080" protocol="tcp" accept'
success

To finish up the policy settings, connect the mywg and mysite zones to the new policy:

$ sudo firewall-cmd --policy=mywg2mysite --add-ingress-zone=mywg
success
$ sudo firewall-cmd --policy=mywg2mysite --add-egress-zone=mysite
success

This new policy’s settings will now apply to all new connections initiated from the mywg zone (our WireGuard network) to the mysite zone (the Site B LAN). If you view the info of the new mywg2mysite policy, this is what you’ll see:

$ sudo firewall-cmd --info-policy=mywg2mysite
mywg2mysite (active)
  priority: -1
  target: REJECT
  ingress-zones: mywg
  egress-zones: mysite
  services:
  ports:
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
        rule family="ipv4" destination address="192.168.200.22" service name="http" accept

If you haven’t already set up the Mypub Zone on Host β, as described in the original article, do that now. Then you’re ready to Test It Out!

If it’s working to allow Endpoint A to access the webserver at Endpoint B (and rejecting all other connections through the WireGuard network), save your configuration settings with the following command:

$ sudo firewall-cmd --runtime-to-permanent
success

After running that command, you’ll find the configuration for the new mywg2mysite policy at /etc/firewalld/policies/mywg2mysite.xml:

<!-- Host β /etc/firewalld/policies/mywg2mysite.xml -->
<?xml version="1.0" encoding="utf-8"?>
<policy target="REJECT">
  <rule family="ipv4">
    <destination address="192.168.200.22"/>
    <service name="http"/>
    <accept/>
  </rule>
  <ingress-zone name="mywg"/>
  <egress-zone name="mysite"/>
</policy>

Gateway

Site to Site VPN

In the original How to Use WireGuard With Firewalld article, we covered how to set up each of the WireGuard site gateways, Host α and Host β, as part of a site-to-site scenario in the Firewalld Configuration on Host α and Firewalld Configuration on Host β sections. In each of those two sections, we covered how to used firewalld’s public zone to allow public access to the gateway’s WireGuard port (51821 for Host α and 51822 for Host β), and how to use firewalld’s internal zone to allow forwarding WireGuard traffic between the two sites (Site A and Site B).

When applying access control to a site-to-site scenario, usually you will configure each site individually to restrict inbound access to the site itself, but allow full access to the other site. That’s what we’ll do here.

So in this article, we’ll add two new zones, mywg and mysite, to each WireGuard gateway, with the WireGuard interface on each gateway bound to its mywg zone, and the gateway’s own local site bound to its mysite zone. Then we’ll add two policies, mywg2mysite and mysite2mywg, to control access from the WireGuard network to the local site, and vice versa. This will be very similar to the configuration for Masquerading above, but without doing any masquerading of packets sent from the WireGuard network to the local site (and with the addition of allowing new outbound connections to be initiated from the local site to the WireGuard network).

For this example, we’ll restrict access from Site A to Site B such that we only allow new connections to be initiated from endpoints in Site A to the Site B webserver at Endpoint B (192.168.200.22), and block everything else. We’ll just cover the configuration for Host β in this article, however; the configuration for Host α will be identical, except with no rich rules for access control (therefore allowing no new connections to be initiated from Site B to Site A).

So on Host β, save any runtime changes you’ve made since you last saved them:

$ sudo firewall-cmd --runtime-to-permanent
success

Then create new mysite and mywg zones:

$ sudo firewall-cmd --permanent --new-zone=mysite
success
$ sudo firewall-cmd --permanent --new-zone=mywg
success

And then create a policy for connections sent from the mysite zone to the mywg zone, mysite2mywg; as well as a policy for connections sent from the mywg zone to the mysite zone, mywg2mysite:

$ sudo firewall-cmd --permanent --new-policy=mysite2mywg
success
$ sudo firewall-cmd --permanent --new-policy=mywg2mysite
success

Set the default target for the mysite2mywg policy to ACCEPT, to allow all new outbound connections from the local site to the WireGuard network:

$ sudo firewall-cmd --permanent --policy=mysite2mywg --set-target=ACCEPT
success

But set the default target for the mywg2mysite policy to REJECT, to block all new inbound connections by default from the WireGuard network to the local site:

$ sudo firewall-cmd --permanent --policy=mywg2mysite --set-target=REJECT
success

Then run the reload command to make those changes active:

$ sudo firewall-cmd --reload
success

Now we’ll add our access control rules, using firewalld’s rich rule syntax. In this case for Host β, we just need one rule, to grant access to Endpoint B’s webserver (192.168.200.22). We add our access control rules to the mywg2mysite policy, which will regulate inbound connections from the WireGuard Network to the local site:

$ sudo firewall-cmd --policy=mywg2mysite --add-rich-rule='rule family="ipv4" destination address="192.168.200.22" service name="http" accept'
success

If we wanted to limit access even further, say to allow only Endpoint A to access Endpoint B’s webserver, we could instead write the rule this way, specifying the source address of Endpoint A (192.168.1.11) as well:

$ sudo firewall-cmd --policy=mywg2mysite --add-rich-rule='rule family="ipv4" source address="192.168.1.11" destination address="192.168.200.22" service name="http" accept'
success

Or if the webserver on Endpoint B was running on a custom port, like say TCP port 8080, we could instead write the rule this way, specifying the custom port instead of the standard HTTP service definition:

$ sudo firewall-cmd --policy=mywg2mysite --add-rich-rule='rule family="ipv4" destination address="192.168.200.22" port port="8080" protocol="tcp" accept'
success

To finish up the policy settings, connect the mywg and mysite zones to the mywg2mysite policy:

$ sudo firewall-cmd --policy=mywg2mysite --add-ingress-zone=mywg
success
$ sudo firewall-cmd --policy=mywg2mysite --add-egress-zone=mysite
success

And connect them in the opposite direction (local site outbound to WireGuard network) via the mysite2mywg policy:

$ sudo firewall-cmd --policy=mysite2mywg --add-ingress-zone=mysite
success
$ sudo firewall-cmd --policy=mysite2mywg --add-egress-zone=mywg
success

Now if you view the info of the new mywg2mysite policy, this is what you’ll see:

$ sudo firewall-cmd --info-policy=mywg2mysite
mywg2mysite (active)
  priority: -1
  target: REJECT
  ingress-zones: mywg
  egress-zones: mysite
  services:
  ports:
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
        rule family="ipv4" destination address="192.168.200.22" service name="http" accept

And if you view the info of the mysite2mywg policy, this is what you’ll see:

$ sudo firewall-cmd --info-policy=mysite2mywg
mysite2mywg (active)
  priority: -1
  target: ACCEPT
  ingress-zones: mysite
  egress-zones: mywg
  services:
  ports:
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

Now for the zones: Remove the wg0 interface from the internal zone if you had previously added it:

$ sudo firewall-cmd --zone=internal --remove-interface=wg0
success

And add the wg0 interface to our new mywg zone:

$ sudo firewall-cmd --zone=mywg --add-interface=wg0
success

If you view the info of the mywg zone, this is what you’ll see:

$ sudo firewall-cmd --info-zone=mywg
mywg (active)
  target: default
  icmp-block-inversion: no
  interfaces: wg0
  sources:
  services:
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

If you had previously added the eth1 interface (or whatever interface connects to the local site) to the internal zone, remove it now:

$ sudo firewall-cmd --zone=internal --remove-interface=eth1
success

Or if you had previously mapped the local site’s subnet (192.168.200.0/24 for Site B) to the internal zone, remove it:

$ sudo firewall-cmd --zone=internal --remove-source=192.168.200.0/24
success

Then bind the eth1 interface (or whatever is the name of the dedicated interface that connects to the local site) to the mysite zone:

$ sudo firewall-cmd --zone=mysite --add-interface=eth1
success

Or if the host doesn’t have a dedicated interface for the local site, bind the local site’s subnet to the mysite zone instead:

$ sudo firewall-cmd --zone=mysite --add-source=192.168.200.0/24
success

Now you view the info of the mysite zone, you should see this (if you’ve bound the eth1 interface to it):

$ sudo firewall-cmd --info-zone=mysite
mysite (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth1
  sources:
  services:
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

Or this, if you’ve bound the local site’s subnet to it:

$ sudo firewall-cmd --info-zone=mysite
mysite (active)
  target: default
  icmp-block-inversion: no
  interfaces:
  sources: 192.168.200.0/24
  services:
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

If you haven’t already set up the Public Zone on Host β, as described in the original article, do that now. Set up Host α the same way (but without any rich rules for its mywg2mysite policy). Then Test It Out!

If it’s working to allow Endpoint A to access the webserver at Endpoint B (and rejecting all other connections through the WireGuard network), save your configuration settings with the following command:

$ sudo firewall-cmd --runtime-to-permanent
success

After running that command, you’ll find the configuration for the new mysite2mywg policy at /etc/firewalld/policies/mysite2mywg.xml:

<!-- Host β /etc/firewalld/policies/mysite2mywg.xml -->
<?xml version="1.0" encoding="utf-8"?>
<policy target="ACCEPT">
  <ingress-zone name="mysite"/>
  <egress-zone name="mywg"/>
</policy>

And the configuration for the new mywg2mysite policy at /etc/firewalld/policies/mywg2mysite.xml:

<!-- Host β /etc/firewalld/policies/mywg2mysite.xml -->
<?xml version="1.0" encoding="utf-8"?>
<policy target="REJECT">
  <rule family="ipv4">
    <destination address="192.168.200.22"/>
    <service name="http"/>
    <accept/>
  </rule>
  <ingress-zone name="mywg"/>
  <egress-zone name="mysite"/>
</policy>

Plus the configuration for the mysite zone at /etc/firewalld/zones/mysite.xml:

<!-- Host β /etc/firewalld/zones/mysite.xml -->
<?xml version="1.0" encoding="utf-8"?>
<zone>
  <!-- <interface name="eth1"/> if interface bound instead of subnet -->
  <source address="192.168.200.0/24"/>
</zone>

And the configuration for the mywg zone at /etc/firewalld/zones/mywg.xml:

<!-- Host β /etc/firewalld/zones/mywg.xml -->
<?xml version="1.0" encoding="utf-8"?>
<zone>
  <interface name="wg0"/>
</zone>

Distro Versions

Following is a table of the firewalld versions packaged by some of the most popular Linux distributions as of April 2022. Version 0.9 is required to use policies:

Distribution Firewalld Version

AlmaLinux 8

0.9

Amazon Linux 2

0.4

Amazon Linux 2022

0.9

Arch

1.0

CentOS 7

0.6

CentOS 8

0.9

CentOS 9

1.0

Debian 10 (Buster)

0.6

Debian 11 (Bullseye)

0.9

Debian 12 (Bookworm)

1.1

Fedora 33

0.8

Fedora 34

0.9

Fedora 35

1.0

OpenSUSE 15.3

0.9

OpenSUSE Tumbleweed

1.1

Oracle Linux 8

0.9

RHEL 7

0.6

RHEL 8

0.9

RHEL 9

1.0

Rocky 8

0.9

Ubuntu 20.04 (Focal)

0.8

Ubuntu 20.10 (Groovy)

0.9

Ubuntu 21.04 (Hirsute)

0.9

Ubuntu 21.10 (Impish)

0.9

Ubuntu 22.04 (Jammy)

1.1

You can also run the following command to check the version of firewalld installed on a particular host:

$ sudo firewall-cmd --version
1.0.4