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:
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:
-
Hub of a hub-and-spoke topology
-
Site Masquerading with a point-to-site topology
-
Site Gateway with a site-to-site topology
Hub
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
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:
-
myadmin
: allow SSH access to Host β for system administration. -
mypub
: allow public access to the WireGuard port on Host β (51822
). -
mysite
: allow access to the Site B LAN (192.168.200.0/24
) with masquerading. -
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
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