Yocto Hardening: Firewalls, Part 2: firewalld

Find all of the Yocto hardening texts from here!

People often ask me two things. The first question is “Why did you choose to write this firewall text in two parts?”. The answer to that is I actually started writing this over a year ago, the scope of the text swelled like crazy, and in the end, to get something published I chose to write the thing in two parts to have a better focus. The second question that I get asked is “Do you often have imaginary discussions with imaginary people in your head?”. To that, the answer is yes.

Comic about imaginary conversations
I prepare for daily stand-up by mentally going through conversations where I get deservedly fired because I didn’t close three tickets the previous day.

But without further ado, let’s continue where we left off in part 1. As promised, let’s take a look at another way of setting up and configuring a firewall: firewalld.

One Step Forward, Two Steps Back: Configuring Kernel (Again)

To get the firewalld running we need a few more kernel configuration items enabled. How many? Well, I spent a few days trying to figure out the minimal possible configuration to run some simple firewalld commands, all without success. firewalld is not too helpful with its error messages, as it basically says “something is missing” without really specifying what that special something is.

Meme about Linux kernel modules
I’ve always liked Lionel’s ‘stash, but I feel like he’s looking extra fine in this pic.

After banging my head on a wall for a few days I attempted to enable every single Netfilter and nftables configuration item (because firewalld uses nftables), but no success. After some further searching, I found this blog post that contained a kernel configuration that had worked for the author of the blog. I combined that with my desperate efforts, and the resulting config actually worked! In the end, my full configuration fragment looked like this:

As you can see, my shotgun approach to configuration wasn’t 100% accurate because there were few NF and NFT configuration items I missed. Is everything here absolutely necessary? Most likely not. There actually are some warnings about unused config options for the Linux kernel version 5.15, but I’m afraid to touch the configuration at this point.

I spent some more time optimizing this and trying to make the “minimal config”, just to realize that I’m actually minimizing something that can’t be really minimized. The “minimal config” really depends on what your actual firewall configuration contains. After this realization, I decided that it’s better to have too much than too little, and found an inner peace. You can think that the fragment provides a starting point from which you can start to remove useless-sounding options if you feel like it.

While we’re still talking about the build side of things, remember to add the kernel-modules to your IMAGE_INSTALL as instructed in part 1. Or if you chose to go through the RRECOMMENDS route, remember to add all 36 modules to the dependencies.

Meme about saving disk space by removing kernel modules
To be fair, I’ve been in a situation where a few KBs of root file system content have made the difference between a success and a catastrophic failure.

The Hard Firewall – firewalld

Now that I’ve spent three chapters basically apologizing for my Linux config, it’s time to get to the actual firewall stuff. The “hard” way of setting up a firewall consists of setting up firewalld. This is in my opinion suitable if you have multiple interfaces with changing configurations, possibly edited by a human user, meaning that things change (and usually in a more complex direction).

firewalld is a firewall management tool designed for Linux distributions, providing a dynamic interface for network security (another sentence from ChatGPT, thank you AI overlords). Or to put it into more understandable words, it’s a front-end for nftables providing a D-Bus interface for configuring the firewall. The nice thing about firewalld is that it operates in zones & services, allowing different network interfaces to operate in different network zones with different available services, all of which can be edited run-time using D-Bus, creating mind-boggling, evolving, and Lovecraftian configurations. This is the nice thing, so you can imagine what the not-so-nice things are.

Now that we are somewhat aware of what we’re getting into, we can add firewalld to IMAGE_INSTALL:

IMAGE_INSTALL:append = " firewalld"

One thing worth noting is that firewalld is not part of the Poky repository, but it comes as a part of meta-openembedded. Most likely this is not a problem, because every project I’ve worked on has meta-openembedded, but it’s worth knowing nevertheless.

Now that everything is in place we can start hammering the firewalld to the desired shape. To edit the configuration we can use the firewall-cmd shipped with firewalld package. This firewall-cmd (among some other firewall-* tools) uses the D-Bus interface to command the firewalld, which in turn commands nftables, which finally sets the Netfilter in the kernel. This picture illustrates it all nicely:

Graph showing firewalld internal structure
The picture originates from here:

So, about the actual configuration process: each network interface can be assigned a zone, and the zones can enable a certain set of services. Here’s a short command reference for how to add a custom zone, add a custom service, add the new service to the new zone, and set the new zone to a network interface:

# List available zones
firewall-cmd --get-zones
# These zones are also listed in /usr/lib/firewalld/zones/
# and /etc/firewalld/zones/

# Get the default zone with the following command
# (This is usually public zone)
firewall-cmd --get-default-zone

# This'll return "no zone" if nothing is set,
# meaning that the default zone will be used
firewall-cmd --get-zone-of-interface=eth0

# Create a custom zone blocking everything
firewall-cmd --permanent --new-zone=test_zone
firewall-cmd --permanent --zone=test_zone --set-target=DROP
# If permanent option is not used, changes will be lost on reload.
# This applies to pretty much all of the commands

# Reload the firewalld to make the new zone visible
firewall-cmd --reload

#List services, and add a custom service
firewall-cmd --list-services
firewall-cmd --permanent --new-service=my-custom-service
firewall-cmd --permanent --service=my-custom-service --add-port=22/tcp
firewall-cmd --reload

# Add the service to the zone
firewall-cmd --permanent --zone=test_zone --add-service=my-custom-service

# Set zone
firewall-cmd --zone=home --change-interface=eth0
# Add --permanent flag to make the change, well, permanent

You can (and should) check between the commands how the ports appear to the outside world using nmap to get a bit better grasp on how the different commands actually work. The changes are not always as immediate and intuitive as one would hope. Good rule of thumb is that if the settings survive --reload, they’re set up properly.

How does this all work with the file-based configuration and Yocto? You could create the zone and service files for your system using the firewall-cmd, install them during build time, and change the default zone in the configuration file /etc/firewalld/firewalld.conf to have the rules active by default. This is similar to nftables approach, but is a bit easier to customize for example from a graphical user interface or scripts, and doesn’t require custom start-up scripts. An example of adding some zones and services, and a patch setting the default zone, can be found in the meta-firewall-examples repo created for this blog text.

Comparison and Closing Words

So, now we have two options for firewalling. Which one should be used? Well, I’d pick nftables, use it as long as possible, and move to firewalld if it would be required later on. This is suitable when the target is quite simple and lightweight, at least at the beginning of the lifespan of a device. On the other hand, if it’s already known from the get-go that the device is going to be a full-fledged configurable router, it’d be better to pick the firewalld right off the bat.

Meme about differences of nftables and firewalld

What about iptables and ufw then? Both have bitbake recipes and can definitely be installed, and I suppose they even work. iptables is a bit of a legacy software these days, so maybe avoid that one though. It should be possible to use iptables syntax with nftables backend if that’s what you want. ufw in Yocto uses iptables by default, but it should be possible to use nftables as the backend for ufw. So as usual with the open-source projects, everything is possible with a bit of effort. And as you can guess from the amount of weasel words in this chapter, I don’t really know these two that well, I just took a quick glance at their recipes.

Note that the strength of the firewall depends also on how protected the rulesets are. If the rules are in a plaintext file that everyone can write to, it’s safe to assume that everyone will do so at some point. So give a thought to who has the access and ability to edit the rules. Also, it may be worthwhile to consider some tampering detection for the rule files, but something like that is worth a text of its own.

In closing, I hope this helped you to set up your firewall. There are plenty of ways to set it up, this two-parter provides two options. It’s important to check with external tooling that the firewall actually works as it should and there are no unwanted surprises. Depending on the type of device you want to configure you can choose the simple way or the configurable way. I’d recommend sticking to the easy solutions if they’re possible. And finally, these are just my suggestions. I don’t claim to know anything, except that I know nothing, so you should check your system’s overall security with someone who’s an actual pro.