Add a Last-Resort Rule with firewalld direct.xml

Direct.xml is the escape hatch you reach for only after zones, services, rich rules and policies have all failed to say what you actually mean. This walks through writing a permanent IPv4 deny rule in firewalld's direct configuration, checking its XML, loading it and verifying the result, in about ten minutes if you already know iptables syntax. This guide targets firewalld 2.1.1 and its firewalld.direct(5) format.

Direct rules are a fallback for cases that zones, services, rich rules, or policies cannot express. The interface is deprecated and scheduled for removal in a future release, so check first whether a normal firewalld rule does the job. Direct rules also require you to understand the table, chain, match options, and target you are using.

Before you start

These commands do not change the firewall. The first prints the installed version; the second and third show the logging and backend settings when the daemon is available:

firewall-cmd --version
firewall-cmd --get-log-denied
grep '^FirewallBackend=' /etc/firewalld/firewalld.conf

On the reference system, the package reports 2.1.1. firewall-cmd needs a running firewalld daemon for most queries; if it reports that firewalld is not running, start or inspect the service before applying a live change:

sudo systemctl status firewalld

Warning: keep an existing administrative session open while testing. A bad firewall rule can interrupt SSH, and an ACCEPT direct rule has a special limitation when the nftables backend is active.

1. Choose the least invasive rule first

If you are allowing a standard service or port, prefer a zone command such as --add-service or --add-port. If the match is more specific, investigate a rich rule or policy. Direct configuration earns its keep only when those interfaces cannot express the required behaviour.

The example below blocks packets from the documentation network 192.0.2.0/24 before they reach the normal filter rules. Replace that address only after confirming the source range, and understand that a DROP rule changes live traffic the moment it loads.

2. Write direct.xml with a rollback copy first

The permanent file is /etc/firewalld/direct.xml. Make a dated backup before editing; if the file does not exist yet, the backup command will simply report that there was nothing to save.

sudo cp -a /etc/firewalld/direct.xml \
  /etc/firewalld/direct.xml.before-direct-rule-$(date +%Y%m%d%H%M%S) 2>/dev/null || true
sudoedit /etc/firewalld/direct.xml

Enter this complete document. The XML declaration is optional for the parser, but keeping it makes the file's encoding explicit:

<?xml version="1.0" encoding="utf-8"?>
<direct>
  <rule ipv="ipv4" table="filter" chain="INPUT" priority="0">-s 192.0.2.0/24 -j DROP</rule>
</direct>

The ipv attribute selects iptables for IPv4, ip6tables for IPv6, or ebtables for Ethernet bridges. The table and chain attributes select where the rule belongs. The text inside the element is the corresponding iptables argument list: do not put another table or chain selection in that text.

3. Validate the XML before reloading

sudo xmllint --noout /etc/firewalld/direct.xml

No output and exit status 0 means the document is well-formed. That does not prove every iptables match is valid, so keep the source and target simple until the configuration is working.

4. Reload and confirm the rule loaded

This is the first command in the example that changes live packet filtering. Run it from a console or a connection that will remain available if the rule is broader than intended:

sudo firewall-cmd --reload
sudo firewall-cmd --direct --get-all-rules

The listing should contain a line equivalent to:

ipv4 filter INPUT 0 -s 192.0.2.0/24 -j DROP

Checkpoint: priority 0 puts the rule at the top of the direct chain. A larger priority places a rule further down. Rules with the same priority have no fixed order, so use distinct priorities when order matters.

5. Understand built-in and custom chains

A rule whose chain is a built-in chain such as INPUT is placed through firewalld's internal chain_direct arrangement, which avoids colliding with rules firewalld creates itself. A custom chain works differently: defining one does not connect it to traffic by itself. This structure creates a chain and jumps to it from INPUT:

<direct>
  <chain ipv="ipv4" table="filter" chain="MY_DENYLIST"/>
  <rule ipv="ipv4" table="filter" chain="INPUT" priority="0">-j MY_DENYLIST</rule>
  <rule ipv="ipv4" table="filter" chain="MY_DENYLIST" priority="0">-s 192.0.2.0/24 -j DROP</rule>
</direct>

The jump is the connection: without it, the custom chain can exist while no packet ever reaches its rules. A passthrough rule is more hazardous, since firewalld adds its arguments directly and does not provide the same conflict protection, so use it only when you deliberately manage that interaction.

6. Know the backend caveats

7. Undo the change

If the new rule caused trouble, restore the backup you made before editing. Replace the placeholder filename with the exact backup shown by your shell, then verify the XML and reload:

sudo cp -a /etc/firewalld/direct.xml.before-direct-rule-YYYYMMDDHHMMSS \
  /etc/firewalld/direct.xml
sudo xmllint --noout /etc/firewalld/direct.xml
sudo firewall-cmd --reload
sudo firewall-cmd --direct --get-all-rules

Do not use a guessed backup name. List the candidates first if you have lost the filename:

sudo find /etc/firewalld -maxdepth 1 -type f -name 'direct.xml.before-direct-rule-*' -printf '%f\n'

For a deliberate empty configuration, replace the file with a document containing only <direct></direct>, validate it, and reload. Keep the old file until the new runtime listing confirms the rule is gone.

Done means