Firewalld configuration and usage

One of the new entries to the Fedora and CentOS worlds is firewalld.

The principle behind this is an abstracted layer so that a setting in this will provide rules for ipv4 and ipv6 rather than needing to set rules for them individually, along with clear output of exactly what is permitted from where.

There have been other tools to do this as a static translator from a friendly language to the iptables syntax, an example being shorewall, but adding to this firewalld allows for dynamic configuration over dbus so that permitted daemons can change configuration (eg a bittorent client could be allowed to open the port it is listening on automatically when needed) or so that NetworkManager can assign an interface to a particular zone. In addition due to the way the rules are manipulated it prevents the issue of a mistake in /etc/sysconfig/iptables causing the iptables service to fail to parse the rules and consequently not setting up any rules at all. If the default rule was set to DROP on the INPUT chain a 'service iptables restart' could completely lock out remote access to the system in this case.

There is frequently some apparent confusion over terms like 'Default Zone' and what that actually means which this article will hopefully clear up.

With most firewall-cmd commands if a zone is not specified it'll act on the default zone of the system. In addition unless --permanent is specified a command will be runtime only and will be lost at system reboot or firewalld reload. The are some commands (creating a new custom zone for instance) that require the --permanent argument and a firewall-cmd --reload to be carried out.

Since --permanent requires a reload it's often sensible to carry out changes without it where possible and then use firewall-cmd --runtime-to-permanent to persist these. In addition if working with rules that have the potential to cause loss of system access (restricting SSH for instance) scheduling a firewall-cmd --reload 15 minutes in the future to restore the current permanent configuration might be sensible.

== What is a zone? ==

In many ways the built in zones cause more confusion than they solve...

These are:

  • block
  • dmz
  • drop
  • external
  • home
  • internal
  • public
  • trusted
  • work

On a standard EL7 install the behaviour is that the interfaces are associated with the public zone. On a Fedora system the behaviour is a special zone of FedoraWorkstation or FedoraServer being used depending on the product in place. The reason behind this is that the individual product Special Interest Groups can customize the default set up appropriate to their product. This does mean that on a default install of Fedora none of the built in zones are in use.

A list of all zones on the system can be obtained by:

firewall-cmd --get-zones

A zone can be bound to an interface or to a source IP address/network. If there is no rule that applies from a zone (whether based on interface or source address) then the default zone rules get applied. The current default can be obtained via:

firewall-cmd --get-default-zone

Although the block/reject behaviour is not seen in the output of firewall-cmd --zone=block --list-all the behaviour can be checked in the xml definition for the zone:

cat /usr/lib/firewalld/zones/block.xml
<?xml version="1.0" encoding="utf-8"?>
<zone target="%%REJECT%%">
  <short>Block</short>
  <description>Unsolicited incoming network packets are rejected. Incoming packets that are related to outgoing network connections are accepted. Outgoing network connections are allowed.</description>
</zone>

cat /usr/lib/firewalld/zones/drop.xml 
<?xml version="1.0" encoding="utf-8"?>
<zone target="DROP">
  <short>Drop</short>
  <description>Unsolicited incoming network packets are dropped. Incoming packets that are related to outgoing network connections are accepted. Outgoing network connections are allowed.</description>
</zone>

== What's the priority for applying rules between zones? ==

The mix of possible rules between direct, rich, port, service and how the zones play into these can get quite confusing.

For the priority from highest to lowest for when and where a rule applies when a packet arrives we have:

  • Direct rules
  • Source address based zone
    • log
    • deny
    • allow
  • Interface based zone
    • log
    • deny
    • allow
  • Default zone
    • log
    • deny
    • allow

Within each log/deny/allow split of a zone the priority is:

  • Rich rule
  • Port definition
  • Service definition

== Direct, rich, or standard rule? ==

The different methods to set a rule reflect the amount of fine grain control that can be delivered. A standard rule applies to all traffic that matches the port/service. A rich rule can deliver network based controls without needing a new zone or can configure logging of a traffic type. The direct rules allow direct manipulation of the underlying ip(6)tables/ebtables rulesets for use cases that a rich rule cannot manage.

As usual there is a trade off between complexity and capabilities.

A standard rule would be added like:

firewall-cmd --add-port 443/tcp # port based
or
firewall-cmd --add-service https # service based

A rich rule would be added like:

firewall-cmd --add-rich-rule "rule port port="443" protocol="tcp" accept" # port based
or
firewall-cmd --add-rich-rule "rule service name="https" accept" # service based

A direct rule would be added like:

firewall-cmd --direct --add-rule ipv4 filter INPUT 1 -m tcp -p tcp --dport 443 -j ACCEPT

Direct rules really should be the last resort if the goal can't be established with a standard or rich rule. It is inserted before any zone behaviour and not attached to any particular zone. Although the most complicated iptables arrangements would only be possible with this type of rule the 'cost' of doing so should be considered carefully.

In addition rich rules (or direct but rich is technically nicer from a firewalld perspective) are required to carry out logging of the behaviour. To log all rejections to tcp port 443 for example:

firewall-cmd --add-rich-rule "rule port port="443" protocol="tcp" log reject"

== Services versus ports ==

A common question is whether to use --add-port or --add-service in preference. The difference is essentially one of display, although as mentioned above when it comes down to precedence a port based rule will take priority over a service based rule.

firewall-cmd --list-all
FedoraWorkstation (default, active)
  interfaces: p4p1
  sources: 
  services: dhcpv6-client ipp ipp-client mdns samba-client ssh vnc-server
  ports: 1025-65535/udp 1025-65535/tcp 999/tcp
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	rule family="ipv4" source address="192.168.2.4" drop
	rule port port="443" protocol="tcp" accept
	rule family="ipv4" source address="192.168.2.2" accept

The question is in the situation involved is it more meaningful to present a readable name or a port number and protocol?

A service can present a collection of ports and protocols which can simplify control and readability. A service can also include a iptables module in its definition should one be needed, ftp_conntrack for instance.

The downside of using a service of course is the lack of transparency over which ports are being opened. If the service listed isn't very well known or is for a custom application the related port is not immediately apparent, the underlying XML file needing to be checked (or read raw iptables output which we are trying to avoid) to determine what exactly the expected behaviour is.

== Assigning a zone to an interface or source network ==

A zone can be associated with one or more interfaces or sources. This can be used to create descriptive zone names to accurately reflect the connectivity types.

If an interface is trusted and all traffic should be accepted on it (subject to the priority of direct is higher than source based zones which are higher than interface based zones) it can be associated with the trusted zone:

firewall-cmd --zone=trusted --add-interface=trusted0

If an interface should be effectively disabled then the deny zone can be used:

firewall-cmd --zone=deny --add-interface=untrusted0

An interesting side effect of the precedence behaviour means that although this says all traffic to the interface should be dropped a source based zone that allows the traffic would take precedence and prevent dropping the packets, perhaps have an IT subnet identified by source to always permit SSH for instance.

Assigning a source subnet to a zone is rather similar:

firewall-cmd --zone=work --add-source=192.168.10.0/24

It is possible to query firewalld to see if a particular source network would be bound to a particular zone:

firewall-cmd --get-zone-of-source=192.168.10.0/24
work

Removing a source or interface from a zone is simply the mirror of the above:

firewall-cmd --zone=trusted --remove-interface=trusted0
firewall-cmd --zone=work --remove-source=192.168.10.0/24

Keep in mind that since this relies on iptables strict order wins and not most specific network if overlapping subnets comes into play. Firewalld orders the zones alphabetically so if overlap is needed pay attention to the zone names to get the precedence of rules required.

== Where are the config files? ==

The behaviour in firewalld is very similar to that of systemd where configuration files provided by packages should go in /usr/lib/firewalld and local system configuration happens in /etc/firewalld which overrides anything in the system packages directory.

To see the definitions of the packaged services check the xml files in /usr/lib/firewalld/services. Remember that runtime only configuration will not be in these files.

== Using custom zones ==

If the network infrastructure divides subnets into nicely defined chunks it could be worth setting up a bunch of custom zone definitions to make it clear what traffic is allowed from where, and in a more descriptive manner than the built in names.

Creating these are rather simple:

firewall-cmd --new-zone=mgmt-lab --permananent
firewall-cmd --new-zone=db-lab --permanenent
firewall-cmd --new-zone=web-lab --permanenent
firewall-cmd --reload
firewall-cmd --zone=mgmt-lab --add-source=10.0.80.0/24
firewall-cmd --zone=db-lab --add-source=10.0.81.0/24
firewall-cmd --zone=web-lab --add-source=10.0.82.0/24
firewall-cmd --runtime-to-permanent

This would define three zones based on source networks. Rules could then be associated in such a way that web-lab could talk mysql or postgres to db-lab and that both db and web could reach ntp in the mgmt-lab for instance.

== Defining a custom service ==

This is similar to using a custom zone however it is slightly more involved as the xml files need to be edited directly.

firewall-cmd --new-service=myspecialsnowflake --permanent

Since this is a permanent change a reload will be required to load it into the runtime. Since reload would be required for changing the XML definition anyway it's best to write it out before calling reload.

cat /etc/firewalld/services/myspecialsnowflake.xml
<?xml version="1.0" encoding="utf-8"?>
<service>
</service>

Ports, iptables modules and destinations are candidates for service definitions. The documentation can be seen in man 5 firewalld.service.

An example definition would be:

<?xml version="1.0" encoding="utf-8"?>
<service>
<short>My Special Snowflake</short>
<description>This is an example service covering a few different protocols.</description>
<port port="12000-13000" protocol="tcp"/>
<port port="1382" protocol="udp"/>
</service>

If protocol is not specified then either udp or tcp will be permitted. If no port is listed then any protocol in /etc/protocols can be used in the service definition such as 'ah', 'gre' and so on.

Afer the defintion is completed issue:

firewall-cmd --reload

After that it can be referenced in the --(add|remove)-service options to firewall-cmd.

== Using polkit to configure permissions ==

The following polkit actions are defined by firewalld:

  • org.fedoraproject.FirewallD1.all
  • org.fedoraproject.FirewallD1.config
  • org.fedoraproject.FirewallD1.config.info
  • org.fedoraproject.FirewallD1.direct
  • org.fedoraproject.FirewallD1.direct.info
  • org.fedoraproject.FirewallD1.info
  • org.fedoraproject.FirewallD1.policies
  • org.fedoraproject.FirewallD1.policies.info

The default policy is auth_admin_keep on most of these so multiple requests to the same action won't continually prompt for a password.

The only difference is org.fedoraproject.FirewallD1.info which allows anyone to issue a request.

This roughly maps into interfaces listed in man 5 firewalld.dbus

For desktop use it might be preferable to allow all info behaviour for a local user without credentials being requested but require admin credentials for all other activity, the default definition of admin in the polkit sense being someone in the wheel group:

cat /etc/polkit-1/rules.d/50-default.rules
polkit.addAdminRule(function(action, subject) {
    return ["unix-group:wheel"];
});

We could provide this facility as follows:

cat /etc/polkit-1/rules.d/90-firewalld-info.rules 
polkit.addRule(function(action, subject) {
  if ((action.id == "org.fedoraproject.FirewallD1.info" || 
       action.id == "org.fedoraproject.FirewallD1.config.info" || 
       action.id == "org.fedoraproject.FirewallD1.direct.info" || 
       action.id == "org.fedoraproject.FirewallD1.policies.info") && 
       subject.local && subject.active ) {
      return polkit.Result.YES;
  }
});

Then any local user who has an active session can query the local firewall state but not change it.

== Specifying the zone of a connection with NetworkManager ==

NetworkManager makes it trivial to dynamically allocate a zone to connection profile. This can be of use to differentiate inbound permissions between office, home and the local coffee place for instance.

This is as simple as placing ZONE=foobar in the appropriate ifcfg file in /etc/sysconfig/network-scripts.

Alternatively NetworkManager has nmcli to configure this from a command prompt without reloading the interface configuration:

nmcli connection modify happy-coffee-wifi connection.zone block

This will take effect over the default zone so that the connection profile uses exactly the permissions required.

== Tying it all together for Example Ltd ==

The example company will have the following requirements:

  • IT network should be able to ssh to any system
  • Server management network that has no outbound connections to any other internal network allowed.
  • Client network should access Samba, NFS4 and authentication (ldap+kerberos+http+https+dns+ntp) in server network.
  • DMZ web network that all should be able to access http and https - gets its time and authentication from server network.
  • DMZ database network that web should be able to access mysql and postgresql but no other ports - gets its time and authentication from server network.
  • The default policy on systems will be to deny, not drop, connections from any other sources than these, other than dmzweb which needs to allow any http/https.

First to define the zones. There is already the default block zone but custom zones will be used for the others.

As a side note I'm using the --permanent syntax on each of these due to a bug I encountered in carrying out runtime-to-permanent requests after certain changes.

Initial configuration on all systems:

firewall-cmd --new-zone=it --permanent
firewall-cmd --new-zone=mgmt --permanent
firewall-cmd --new-zone=user --permanent
firewall-cmd --new-zone=dmzweb --permanent
firewall-cmd --new-zone=dmzdb --permanent

firewall-cmd --zone=it --add-source=10.10.0.0/22  --permanent
firewall-cmd --zone=mgmt --add-source=10.10.4.0/22 --permanent
firewall-cmd --zone=user --add-source=10.10.8.0/22 --permanent
firewall-cmd --zone=dmzweb --add-source=10.100.0.0/22 --permanent
firewall-cmd --zone=dmzdb --add-source=10.100.4.0/22 --permanent

firewall-cmd --zone=it --add-service=ssh --permanent
firewall-cmd --set-default-zone=block
firewall-cmd --reload

Specific configuration on systems per network:

Systems in dmzweb network:

firewall-cmd --set-default-zone=dmz 
firewall-cmd --remove-service=ssh  --permanent
firewall-cmd --add-service=http  --permanent
firewall-cmd --add-service=https  --permanent
firewall-cmd --reload

Systems in dmzdb network:

firewall-cmd --set-default-zone=dmz 
firewall-cmd --remove-service=ssh  --permanent
firewall-cmd --zone=dmzweb --add-service=mysql  --permanent
firewall-cmd --zone=dmzweb --add-service=postgresql  --permanent
firewall-cmd --reload

Systems in mgmt network:

firewall-cmd --new-service=authentication --permanent
cat > /etc/firewalld/services/authentication.xml <<EOF
<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>authentication</short>
  <description>Ports required for authentication of systems to IPA</description>
  <port protocol="udp" port="123"/>
  <port protocol="udp" port="88"/>
  <port protocol="udp" port="464"/>
  <port protocol="udp" port="53"/>
  <port protocol="tcp" port="88"/>
  <port protocol="tcp" port="464"/>
  <port protocol="tcp" port="80"/>
  <port protocol="tcp" port="443"/>
  <port protocol="tcp" port="636"/>
  <port protocol="tcp" port="389"/>
  <port protocol="tcp" port="53"/>
</service>
EOF
firewall-cmd --reload
firewall-cmd --zone=user --add-service=authentication  --permanent
firewall-cmd --zone=user --add-service=nfs  --permanent
firewall-cmd --zone=user --add-service=samba  --permanent
firewall-cmd --zone=it --add-service=authentication  --permanent
firewall-cmd --zone=it --add-service=samba  --permanent
firewall-cmd --zone=it --add-service=nfs  --permanent
firewall-cmd --zone=mgmt --add-service=authentication  --permanent
firewall-cmd --zone=dmzweb --add-service=authentication  --permanent
firewall-cmd --zone=dmzdb --add-service=authentication  --permanent
firewall-cmd --reload

Skipping out the unused default zones to reduce the confusion the resulting configurations on each type of system would be like:

Standard client system:

[root@changeme ~]# firewall-cmd --list-all-zones
block (default, active)
  interfaces: eth0
  sources: 
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzdb
  interfaces: 
  sources: 10.100.4.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzweb
  interfaces: 
  sources: 10.100.0.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
it
  interfaces: 
  sources: 10.10.0.0/22
  services: ssh
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
mgmt
  interfaces: 
  sources: 10.10.4.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
user
  interfaces: 
  sources: 10.10.8.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 	

A dmzweb system:

[root@changeme ~]# firewall-cmd --list-all-zones
dmz (default, active)
  interfaces: eth0
  sources: 
  services: http https
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzdb
  interfaces: 
  sources: 10.100.4.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzweb
  interfaces: 
  sources: 10.100.0.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
it
  interfaces: 
  sources: 10.10.0.0/22
  services: ssh
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
mgmt
  interfaces: 
  sources: 10.10.4.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
user
  interfaces: 
  sources: 10.10.8.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	

A dmzdb system:

[root@changeme ~]# firewall-cmd --list-all-zones
dmz (default, active)
  interfaces: eth0
  sources: 
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzdb
  interfaces: 
  sources: 10.100.4.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzweb
  interfaces: 
  sources: 10.100.0.0/22
  services: mysql postgresql
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
it
  interfaces: 
  sources: 10.10.0.0/22
  services: ssh
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
mgmt
  interfaces: 
  sources: 10.10.4.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
		
user
  interfaces: 
  sources: 10.10.8.0/22
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 

A mgmt system:

[root@changeme ~]# firewall-cmd --list-all-zones
block (default, active)
  interfaces: eth0
  sources: 
  services: 
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmz
  interfaces: 
  sources: 
  services: ssh
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzdb
  interfaces: 
  sources: 10.100.4.0/22
  services: authentication
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
dmzweb
  interfaces: 
  sources: 10.100.0.0/22
  services: authentication
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
it
  interfaces: 
  sources: 10.10.0.0/22
  services: authentication nfs samba ssh
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
mgmt
  interfaces: 
  sources: 10.10.4.0/22
  services: authentication
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 
	
user
  interfaces: 
  sources: 10.10.8.0/22
  services: authentication nfs samba
  ports: 
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 

These list-all-zone outputs could then be provided to an auditor, for instance, to clearly explain what is permitted from where into each system given the network it sits on.

== Final word ==

I hope that's cleared up some of the misconceptions, clarified some of the poorly documented and provided some insight to the pros and cons.

There are quite clearly some rough edges but there are a few niceties that come with this.

As usual it is a matter for the right tool for the job. In many instances I would expect iptables/ip6tables to work out preferable but in others this provides a way of handling both (and maybe nftables in future?) without forgetting one by accident.

Add new comment