Linux firewalls are handled by netfilter, which is a kernel-level framework. For more than a decade, iptables has provided the userland abstraction layer for netfilter. iptables subjects packets to a gauntlet of rules, and if the IP/port/protocol combination of the rule matches the packet, the rule is applied causing the packet to be accepted, rejected or dropped.
Firewalld is a newer userland abstraction layer for netfilter. Unfortunately, its power and flexibility are underappreciated due to a lack of documentation describing multi-zoned configurations. This article provides examples to remedy this situation.
Firewalld Design Goals
The designers of firewalld realized that most iptables usage cases involve only a few unique IP sources, for each of which a whitelist of services is allowed and the rest are denied. To take advantage of this pattern, firewalld categorizes incoming traffic into zones defined by the source IP and/or network interface. Each zone has its own configuration to accept or deny packets based on specified criteria.
Another improvement over iptables is a simplified syntax. Firewalld makes it easier to specify services by using the name of the service rather than its port(s) and protocol(s)—for example, samba rather than UDP ports 137 and 138 and TCP ports 139 and 445. It further simplifies syntax by removing the dependence on the order of statements as was the case for iptables.
Finally, firewalld enables the interactive modification of netfilter, allowing a change in the firewall to occur independently of the permanent configuration stored in XML. Thus, the following is a temporary modification that will be overwritten by the next reload:
# firewall-cmd <some modification>
And, the following is a permanent change that persists across reboots:
# firewall-cmd --permanent <some modification> # firewall-cmd --reload
The top layer of organization in firewalld is zones. A packet is part of a zone if it matches that zone’s associated network interface or IP/mask source. Several predefined zones are available:
# firewall-cmd --get-zones block dmz drop external home internal public trusted work
An active zone is any zone that is configured with an interface and/or a source. To list active zones:
# firewall-cmd --get-active-zones public interfaces: eno1 eno2
Interfaces are the system’s names for hardware and virtual network adapters, as you can see in the above example. All active interfaces will be assigned to zones, either to the default zone or to a user-specified one. However, an interface cannot be assigned to more than one zone.
In its default configuration, firewalld pairs all interfaces with the public zone and doesn’t set up sources for any zones. As a result, public is the only active zone.
Sources are incoming IP address ranges, which also can be assigned to zones. A source (or overlapping sources) cannot be assigned to multiple zones. Doing so results in undefined behavior, as it would not be clear which rules should be applied to that source.
Since specifying a source is not required, for every packet there will be a zone with a matching interface, but there won’t necessarily be a zone with a matching source. This indicates some form of precedence with priority going to the more specific source zones, but more on that later. First, let’s inspect how the public zone is configured:
# firewall-cmd --zone=public --list-all public (default, active) interfaces: eno1 eno2 sources: services: dhcpv6-client ssh ports: masquerade: no forward-ports: icmp-blocks: rich rules: # firewall-cmd --permanent --zone=public --get-target default
Going line by line through the output:
public (default, active)indicates that the public zone is the default zone (interfaces default to it when they come up), and it is active because it has at least one interface or source associated with it.
interfaces: eno1 eno2lists the interfaces associated with the zone.
sources:lists the sources for the zone. There aren’t any now, but if there were, they would be of the form xxx.xxx.xxx.xxx/xx.
services: dhcpv6-client sshlists the services allowed through the firewall. You can get an exhaustive list of firewalld’s defined services by executing
ports:lists port destinations allowed through the firewall. This is useful if you need to allow a service that isn’t defined in firewalld.
masquerade: noindicates that IP masquerading is disabled for this zone. If enabled, this would allow IP forwarding, with your computer acting as a router.
forward-ports:lists ports that are forwarded.
icmp-blocks:a blacklist of blocked icmp traffic.
rich rules:advanced configurations, processed first in a zone.
defaultis the target of the zone, which determines the action taken on a packet that matches the zone yet isn’t explicitly handled by one of the above settings.
A Simple Single-Zoned Example
Say you just want to lock down your firewall. Simply remove the services currently allowed by the public zone and reload:
# firewall-cmd --permanent --zone=public --remove-service=dhcpv6-client # firewall-cmd --permanent --zone=public --remove-service=ssh # firewall-cmd --reload
These commands result in the following firewall:
# firewall-cmd --zone=public --list-all public (default, active) interfaces: eno1 eno2 sources: services: ports: masquerade: no forward-ports: icmp-blocks: rich rules: # firewall-cmd --permanent --zone=public --get-target default
In the spirit of keeping security as tight as possible, if a situation arises where you need to open a temporary hole in your firewall (perhaps for ssh), you can add the service to just the current session (omit
--permanent) and instruct firewalld to revert the modification after a specified amount of time:
# firewall-cmd --zone=public --add-service=ssh --timeout=5m
The timeout option takes time values in seconds (s), minutes (m) or hours (h).
When a zone processes a packet due to its source or interface, but there is no rule that explicitly handles the packet, the target of the zone determines the behavior:
ACCEPT: accept the packet.
%%REJECT%%: reject the packet, returning a reject reply.
DROP: drop the packet, returning no reply.
default: don’t do anything. The zone washes its hands of the problem, and kicks it “upstairs”.
There was a bug present in firewalld 0.3.9 (fixed in 0.3.10) for source zones with targets other than
default in which the target was applied regardless of allowed services. For example, a source zone with the target
DROP would drop all packets, even if they were whitelisted. Unfortunately, this version of firewalld was packaged for RHEL7 and its derivatives, causing it to be a fairly common bug. The examples in this article avoid situations that would manifest this behavior.
Active zones fulfill two different roles. Zones with associated interface(s) act as interface zones, and zones with associated source(s) act as source zones (a zone could fulfill both roles). Firewalld handles a packet in the following order:
- The corresponding source zone. Zero or one such zones may exist. If the source zone deals with the packet because the packet satisfies a rich rule, the service is whitelisted, or the target is not default, we end here. Otherwise, we pass the packet on.
- The corresponding interface zone. Exactly one such zone will always exist. If the interface zone deals with the packet, we end here. Otherwise, we pass the packet on.
- The firewalld default action. Accept icmp packets and reject everything else.
The take-away message is that source zones have precedence over interface zones. Therefore, the general design pattern for multi-zoned firewalld configurations is to create a privileged source zone to allow specific IP’s elevated access to system services and a restrictive interface zone to limit the access of everyone else.
A Simple Multi-Zoned Example
To demonstrate precedence, let’s swap ssh for http in the public zone and set up the default internal zone for our favorite IP address, 22.214.171.124. The following commands accomplish this task:
# firewall-cmd --permanent --zone=public --remove-service=ssh # firewall-cmd --permanent --zone=public --add-service=http # firewall-cmd --permanent --zone=internal --add-source=126.96.36.199 # firewall-cmd --reload
which results in the following configuration:
# firewall-cmd --zone=public --list-all public (default, active) interfaces: eno1 eno2 sources: services: dhcpv6-client http ports: masquerade: no forward-ports: icmp-blocks: rich rules: # firewall-cmd --permanent --zone=public --get-target default # firewall-cmd --zone=internal --list-all internal (active) interfaces: sources: 188.8.131.52 services: dhcpv6-client mdns samba-client ssh ports: masquerade: no forward-ports: icmp-blocks: rich rules: # firewall-cmd --permanent --zone=internal --get-target default
With the above configuration, if someone attempts to
ssh in from 184.108.40.206, the request would succeed because the source zone (internal) is applied first, and it allows ssh access.
If someone attempts to
ssh from somewhere else, say 220.127.116.11, there wouldn’t be a source zone, because no zones match that source. Therefore, the request would pass directly to the interface zone (public), which does not explicitly handle ssh. Since public’s target is
default, the request passes to the firewalld default action, which is to reject it.
What if 18.104.22.168 attempts http access? The source zone (internal) doesn’t allow it, but the target is
default, so the request passes to the interface zone (public), which grants access.
Now let’s suppose someone from 22.214.171.124 is trolling your website. To restrict access for that IP, simply add it to the preconfigured drop zone, aptly named because it drops all connections:
# firewall-cmd --permanent --zone=drop --add-source=126.96.36.199 # firewall-cmd --reload
The next time 188.8.131.52 attempts to access your website, firewalld will send the request first to the source zone (drop). Since the target is
DROP, the request will be denied and won’t make it to the interface zone (public) to be accepted.
A Practical Multi-Zoned Example
Suppose you are setting up a firewall for a server at your organization. You want the entire world to have http and https access, your organization (184.108.40.206/16) and workgroup (220.127.116.11/8) to have ssh access, and your workgroup to have samba access. Using zones in firewalld, you can set up this configuration in an intuitive manner.
Given the naming, it seems logical to commandeer the public zone for your world-wide purposes and the internal zone for local use. Start by replacing the dhcpv6-client and ssh services in the public zone with http and https:
# firewall-cmd --permanent --zone=public --remove-service=dhcpv6-client # firewall-cmd --permanent --zone=public --remove-service=ssh # firewall-cmd --permanent --zone=public --add-service=http # firewall-cmd --permanent --zone=public --add-service=https
Then trim mdns, samba-client and dhcpv6-client out of the internal zone (leaving only ssh) and add your organization as the source:
# firewall-cmd --permanent --zone=internal --remove-service=mdns # firewall-cmd --permanent --zone=internal --remove-service=samba-client # firewall-cmd --permanent --zone=internal --remove-service=dhcpv6-client # firewall-cmd --permanent --zone=internal --add-source=18.104.22.168/16
To accommodate your elevated workgroup samba privileges, add a rich rule:
# firewall-cmd --permanent --zone=internal --add-rich-rule='rule ↪family=ipv4 source address="22.214.171.124/8" service name="samba" ↪accept'
Finally, reload, pulling the changes into the active session:
# firewall-cmd --reload
Only a few more details remain. Attempting to
ssh in to your server from an IP outside the internal zone results in a reject message, which is the firewalld default. It is more secure to exhibit the behavior of an inactive IP and instead drop the connection. Change the public zone’s target to
DROP rather than
defaultto accomplish this:
# firewall-cmd --permanent --zone=public --set-target=DROP # firewall-cmd --reload
But wait, you no longer can ping, even from the internal zone! And icmp (the protocol ping goes over) isn’t on the list of services that firewalld can whitelist. That’s because icmp is an IP layer 3 protocol and has no concept of a port, unlike services that are tied to ports. Before setting the public zone to
DROP, pinging could pass through the firewall because both of your
default targets passed it on to the firewalld default, which allowed it. Now it’s dropped.
To restore pinging to the internal network, use a rich rule:
# firewall-cmd --permanent --zone=internal --add-rich-rule='rule ↪protocol value="icmp" accept' # firewall-cmd --reload
In summary, here’s the configuration for the two active zones:
# firewall-cmd --zone=public --list-all public (default, active) interfaces: eno1 eno2 sources: services: http https ports: masquerade: no forward-ports: icmp-blocks: rich rules: # firewall-cmd --permanent --zone=public --get-target DROP # firewall-cmd --zone=internal --list-all internal (active) interfaces: sources: 126.96.36.199/16 services: ssh ports: masquerade: no forward-ports: icmp-blocks: rich rules: rule family=ipv4 source address="188.8.131.52/8" ↪service name="samba" accept rule protocol value="icmp" accept # firewall-cmd --permanent --zone=internal --get-target default
This setup demonstrates a three-layer nested firewall. The outermost layer, public, is an interface zone and spans the entire world. The next layer, internal, is a source zone and spans your organization, which is a subset of public. Finally, a rich rule adds the innermost layer spanning your workgroup, which is a subset of internal.
The take-away message here is that when a scenario can be broken into nested layers, the broadest layer should use an interface zone, the next layer should use a source zone, and additional layers should use rich rules within the source zone.
Source : https://www.linuxjournal.com/content/understanding-firewalld-multi-zone-configurations