Arch Linux-based Home Router, Part III (firewalld configuration)

Configuring the firewall is of utmost importance in protecting your network!  There are many options for setting up the firewall.  The venerable iptables is now deprecated by the newer nftables framework.  Both of these directly configure the kernel's netfilter subsystem, and they have many CLI and GUI frontends.  Using iptables or nftdirectly can be quite difficult to get right, so using a frontend or some kind of abstraction is recommended (unless your goal is to become a netfilter expert). I used to use fwbuilder to graphically configure my Debian firewall which would then compile and install to iptables rules automatically.  

I decided to use firewalld because my employer's new on-premises systems use it on their Red Hat Enterprise Linux (RHEL7) appliances.  Installing it at home forced me to become more familiar with it.  Now that I have it installed, I quite like the firewall abstraction firewalld provides.

The rest of these instructions assume that firewalld.service is enabled and started.  A quick way to do that is:

# systemctl enable --now firewalld.service

Alternatively, you could use the firewall-offline-cmd instead of firewall-cmd for the next few sections, to configure the firewall while firewalld isn't running.  However, if this router is plugged up directly to the WAN, I recommend running firewalld, as it has sane defaults, and your network will be protected even if you haven't fully configured it yet.

Note, while you're configuring firewalld you may accidentally allow traffic that you do not want to allow.  You can shut down all traffic by issuing the following command:

# firewall-cmd --panic-on

To disable panic mode, and restore the permanent configuration, use the --panic-off option.

firewalld Zones

firewalld introduces the concept of "zones," which can be used to logically separate parts of your network.  firewalld comes with a set of zones by default, you can see these zones with the firewall-cmd --get-zones command:

# firewall-cmd --get-zones
block dmz drop external home internal iot public trusted work

Most of these are default zones, except for iot.  I only use the home, iot, and public zones, so that's all I will configure here.  If you wanted to have a proper DMZ you can use the dmz zone, or if you want to add specific hosts or IP addresses to your trusted zone, you could do so.

Note, when specifying commands with firewall-cmd, if you do not specify a zone it will be assumed you are operating on the default zone, which starts out as the public zone.  You can use the --get-default-zone to determine the default zone.  

Also, firewalld has the concept of runtime versus permanent configuration.  When you use firewall-cmd, it configures the runtime configuration unless you use the --permanent flag.  Note that if you use the --permanent flag, the configuration is NOT active until you reload firewalld (i.e., firewall-cmd --reload).  Alternatively, you can load the configuration only in runtime, and use the --runtime-to-permanent flag to store the current runtime configuration to disk so it will survive a reload, restart, or reboot.  

For these instructions I will only configure the permanent configuration, and finally load them into runtime with firewall-cmd --reload.  Again, this uses the --permanent flag.  Some commands can only be applied to the permanent configuration, so in my opinion it is wise to make every change permanent, then reload as necessary.  If you only apply the changes in runtime (i.e., without the --permanent flag), you run the risk of losing some of these permanent-only options if you use the flag --runtime-to-permanent.  

public Zone

To display the current configuration, use the --info-zone=public option.

# firewall-cmd  --info-zone=public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp3s0
  sources:
  services:
  ports: 
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

The main thing to pay attention to starting out is the interfaces: section.  If this section does not list only your WAN interface (in my case enp3s0), you can delete the interface listed and add the WAN interface (replace enp4s0 with the incorrect interface):

# firewall-cmd --zone=public --remove-interface=enp4s0 --permanent
# firewall-cmd --zone=public --add-interface=enp3s0 --permanent
# firewall-cmd --reload

If these commands are successful, the only output you will see is success.  Otherwise it will display an error.  You may also see some services that are open by default.  You can remove the default services using the following command:

# firewall-cmd --zone=public --remove-service=<service> --permanent
# firewall-cmd --reload

You can see which services are available by default, by using the --get-services option.  To see the services that are loaded, use --list-services.  

Next, we need to enable the masquerade options.  This will enable NAT (Network Address Translation) between the WAN and LAN:

# firewall-cmd --zone=public --add-masquerade --permanent
# firewall-cmd --reload

One final part of the public zone, the target is set as default, but could be ACCEPT, DROP, or REJECT.  The default target is similar to REJECT, except it implicitly allows ICMP traffic (ping, traceroute, etc.).  The target is the rule that applies when no other service, port, rich rule, or direct rule applies.  For the public zone, this means if a port or service is not explicitly allowed, the router will respond with connection refused.  You may want to set this to DROP, using the following command:

# firewall-cmd --zone=public --set-target=DROP --permanent
# firewall-cmd --reload

Note, setting the target can only be done to the permanent configuration.  The above example also calls firewall-cmd --reload, so runtime and permanent are in sync.  If you don't use the --permanent flag and then --reload, be careful that --runtime-to-permanent doesn't lose the desired target!

You may consider adding some port forwarding rules, so devices outside your network can access devices within your network.  For instance, I set up 61987/tcp to forward to 10.20.30.87 on 22/tcp (the standard SSH port).  First, I use the --add-port=61987/tcp, and then set a forward port rule with --add-forward-port:

# firewall-cmd --add-port=61987/tcp --add-forward-port=port=61987:proto=tcp:toport=22:toaddr=10.20.30.87 --permanent
# firewall-cmd --reload

I did the same for 32400/tcp, for Plex Media Server.  I also added the 31987/udp for WireGuard.  By this point, the public zone should look something like this:

# firewall-cmd --info-zone=public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp3s0
  sources:
  services:
  ports:   61987/tcp 32400/tcp 31987/udp
  protocols:
  forward: no
  masquerade: yes
  forward-ports:
        port=61987:proto=tcp:toport=22:toaddr=10.20.30.87
        port=32400:proto=tcp:toport=32400:toaddr=10.20.30.87
  source-ports:
  icmp-blocks:
  rich rules:

home Zone

I've set the home zone as my primary LAN.  To display the setup of this zone, use the following command:

# firewall-cmd --info-zone=home
home (active)
  target: ACCEPT
  icmp-block-inversion: no
  interfaces: enp4s0 
  sources:
  services: dhcpv6-client dns mdns samba-client ssh
  ports:
  protocols:
  forward: no
  masquerade: yes
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

The main differences between the home zone and the public zone are the target (ACCEPT rather than default), the interfaces (enp4s0 rather than enp3s0), and the list of predefined services which are explicitly allowed.  I want to be able to SSH into my router directly from home, so the ssh service explicitly allowed.  Use the following command to enable this:

# firewall-cmd --zone=home --add-service=ssh --permanent
# firewall-cmd --reload

SSH listens on TCP port 22 by default, and enabling the ssh service this port will be allowed.  You can do the same for the other services, but it's not strictly necessary for the home zone as the target (default rule) is to allow connections if not explicitly defined.  This will allow other services (such as dhcp) work even though they're not explicitly allowed.  You can set the default target to DROP or REJECT to add an extra layer of security, but then you'd have to list all router services explicitly.

iot Zone

The final zone that I set up is the iot zone, which is not a predefined zone that ships with firewalld.  First, I create the zone:

# firewall-cmd --permanent --new-zone=iot

It is an error not to use the --permanent flag with --new-zone.  To make the zone active, you will need to add an interface (or a source) before reloading the firewalld configuration.  Do this with  --add-interface, as above.  I also added the source network for my iot zone (172.19.87.0/24).  Next I added the dnsand dhcp services, since most IoT devices will need to access these on the router.  Finally, set the masquerade option to allow hosts in the iot zone to access the Intenet

# firewall-cmd --zone=iot --add-interface=enp4s0.66 --permanent
# firewall-cmd --zone=iot --add-source=172.19.87.0/24 --permanent
# firewall-cmd --zone=iot --add-service={dhcp,dns} --permanent
# firewall-cmd --zone=iot --add-masquerade --permanent
# firewall-cmd --reload

The final step is to ensure the iot zone cannot access the home zone.  This is achievable by using a firewalld policy.  Before I learned about firewalld policies, I had used a direct rule, which requires an understanding of the underlying netfilter subsystem (think iptables or the newer nft).  Direct rules aren't usually necessary, because rich rules can cover most situations.  Direct rules require knowledge of the underlying netfilter subsystem, and its tables, chains and rules.  Thus knowledge of iptables or nftables is a requirement for direct rules.  Direct rules have been deprecated upstream by the firewalld maintainers (likely Red Hat).  So using direct rules may no longer be an option with the lates releases of firewalld.  Originally I tried to do this with rich rules, but that didn't quite work.  When I was troubleshooting after my initial draft of this article, I learned that firewalld policies are the way to go.

Setting up a firewalld policy is fairly straightforward.  One thing to note, firewalld zones and policies share the same namespace, so the policy cannot have the same name as a zone.  Sine my iot zone was already used (with a direct rule), I named the new policy iot-policy.  Policies have a concept of ingress (or input) zones, and egress (or output) zones.  The ingress and egress zones can be named, non-symbolic zones (in my case such as home and iot), or special symbolic zones (such as HOST [meaning the router running firewalld], or ANY, matching any zone).  More information can be found on the firewalld.policies(5) manual page.   I also had to set the target of the iot zone to ACCEPT, or else hosts on the IoT subnet would not have Internet access.  If you want tighter security, you can set this to DROP or REJECT.

# firewall-cmd --permanent --new-policy=iot-policy
# firewall-cmd --permanent --policy=iot-policy --add-ingress-zone=iot
# firewall-cmd --permanent --policy=iot-policy --add-egress-zone=home
# firewall-cmd --permanent --policy=iot-policy --set-target=DROP
# firewall-cmd --permanent --zone=iot --set-target=ACCEPT
# firewall-cmd --reload

The IoT zone should now look something like this:

# firewall-cmd --info-zone=iot
iot (active)
  target: ACCEPT
  icmp-block-inversion: no
  interfaces: enp4s0.66
  sources: 172.19.87.0/24
  services: dhcp dns
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

And anything on the IoT subnet should not be able to reach anything on my LAN, unless it also has an interface on the IoT subnet.

firewalld conclusion

firewalld is highly configurable, I've only just scratched the surface with its capabilities.  The permanent configuration is stored on disk in /etc/firewalld/, and the configuration is stored in XML files.  That's basically how I initially set up firewalld on the new Arch Linux OS, I simply copied over this directory from my backup of Debian, and then restarted firewalld.

NEXT STEPS

The following articles continue this series on setting up an Arch Linux-based Home Router:

  1. Arch Linux-based Home Router, Part I
  2. Arch Linux-based Home Router, Part II (systemd-networkd and sysctl/kernel)
  3. Arch Linux-based Home Router, Part III (firewalld configuration) (This article)
  4. Arch Linux-based Home Router, Part IV (dhcpd configuration)
  5. Arch Linux-based Home Router, Part V (bind)
  6. Arch Linux-based Home Router, Part VI (DDNS)