<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Firewalld on Lessons learned</title><link>https://eghuro.cz/tags/firewalld/</link><description>Recent content in Firewalld on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Fri, 16 Oct 2026 15:33:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/firewalld/index.xml" rel="self" type="application/rss+xml"/><item><title>fail2ban that never banned anyone: '/bin/sh: 1: nft: not found'</title><link>https://eghuro.cz/posts/fail2ban-nft-not-found/</link><pubDate>Fri, 16 Oct 2026 15:33:00 +0200</pubDate><guid>https://eghuro.cz/posts/fail2ban-nft-not-found/</guid><description>&lt;p&gt;fail2ban said the address was banned:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo fail2ban-client status nginx-limit-req&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; `- Banned IP list:&#9;203.0.113.10&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From that address, right after:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -s -o /dev/null -w &amp;#39;%{http_code}\n&amp;#39; https://example.com/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;302&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The reason was in &lt;code&gt;/var/log/fail2ban.log&lt;/code&gt; all along:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2026-03-14 10:21:05,205 fail2ban.utils [1293]: ERROR 7f5e8fbb0d30 -- exec: nft add table inet f2b-table&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2026-03-14 10:21:05,206 fail2ban.utils [1293]: ERROR 7f5e8fbb0d30 -- stderr: &amp;#39;/bin/sh: 1: nft: not found&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Debian, fail2ban bans through the &lt;code&gt;nft&lt;/code&gt; command. A host that runs firewalld can easily lack it. fail2ban then counts failures, lists bans, and blocks nothing, including the sshd jail.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Debian 13, fail2ban 1.1.0-8, firewalld 2.3.1 with &lt;code&gt;FirewallBackend=nftables&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;nginx as a reverse proxy in front of a few web apps, SSH forwarded from the router&lt;/li&gt;&#10;&lt;li&gt;fail2ban set up by hand, with the stock sshd jail&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Debian&amp;rsquo;s &lt;code&gt;/etc/fail2ban/jail.d/defaults-debian.conf&lt;/code&gt; picks the ban action:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[DEFAULT]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;banaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nftables&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;banaction_allports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nftables[type=allports]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="how-it-showed-up"&gt;How it showed up &lt;/h2&gt;&#10;&lt;p&gt;I was adding jails for the web logins, so I tested them from a VPS outside every whitelisted network. Testing from home proves nothing when home is in &lt;code&gt;ignoreip&lt;/code&gt;. Twenty-five quick POSTs to a login endpoint:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; i in &lt;span class="k"&gt;$(&lt;/span&gt;seq 25&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code} &amp;#39;&lt;/span&gt; -d &lt;span class="nv"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;probe https://example.com/login&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first 11 got 400 from the app, the rest 429 from nginx&amp;rsquo;s rate limit, and the &lt;code&gt;nginx-limit-req&lt;/code&gt; jail banned the VPS as it should. Then the next request got a 302, as if nothing had happened.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;sudo nft list table inet f2b-table&lt;/code&gt; answered &lt;code&gt;sudo: nft: command not found&lt;/code&gt;. The log held ten lines like the ones above. Each failed ban left two ERROR lines in a log nobody reads, while &lt;code&gt;fail2ban-client status&lt;/code&gt; still listed the bans.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-fix"&gt;The fix &lt;/h2&gt;&#10;&lt;p&gt;Install the &lt;code&gt;nft&lt;/code&gt; command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install nftables&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart fail2ban&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Leave &lt;code&gt;nftables.service&lt;/code&gt; disabled, as the package installs it. Debian&amp;rsquo;s &lt;code&gt;/etc/nftables.conf&lt;/code&gt; starts with &lt;code&gt;flush ruleset&lt;/code&gt;, so enabling the service would wipe firewalld&amp;rsquo;s rules at the next boot. fail2ban doesn&amp;rsquo;t need the service, only the binary. It keeps its bans in a table of its own, &lt;code&gt;inet f2b-table&lt;/code&gt;, next to firewalld&amp;rsquo;s &lt;code&gt;inet firewalld&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;On restart, fail2ban restores the bans it has on record, so the ones that had silently failed now take effect:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fail2ban.actions [1649343]: NOTICE [nginx-limit-req] Restore Ban 203.0.113.10&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Give it a few seconds before you check. Right after the restart, &lt;code&gt;nft list table inet f2b-table&lt;/code&gt; still said &lt;code&gt;No such file or directory&lt;/code&gt;. A moment later, it showed the set and the rule:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table inet f2b-table {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;set addr-set-nginx-limit-req {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;type ipv4_addr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;elements = { 203.0.113.10 }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;chain f2b-chain {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;type filter hook input priority filter - 1; policy accept;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;tcp dport { 80, 443 } ip saddr @addr-set-nginx-limit-req reject with icmp port-unreachable&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This time, curl on the VPS couldn&amp;rsquo;t connect at all (exit code 7). If you manage the host with Ansible or similar, add &lt;code&gt;nftables&lt;/code&gt; to the package list alongside &lt;code&gt;fail2ban&lt;/code&gt; so a rebuilt host doesn&amp;rsquo;t lose it again.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-the-binary-was-missing"&gt;Why the binary was missing &lt;/h2&gt;&#10;&lt;p&gt;firewalld talks to the kernel through the &lt;code&gt;python3-nftables&lt;/code&gt; bindings, not the &lt;code&gt;nft&lt;/code&gt; command, so it doesn&amp;rsquo;t pull in the &lt;code&gt;nftables&lt;/code&gt; package. fail2ban only recommends it as an alternative:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ apt-cache show fail2ban | grep ^Recommends&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Recommends: nftables | iptables, whois, python3-pyinotify, python3-setuptools&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;iptables&lt;/code&gt; was already installed (firewalld recommends it), so apt considered the recommendation met. The ban action still called &lt;code&gt;nft&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The same thing can happen with any ban action whose command isn&amp;rsquo;t on the host: the action fails, and the address stays on the ban list anyway.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;Banned IP list&lt;/code&gt; shows what fail2ban decided, not what the firewall does. The only proof that a ban works is a blocked connection from the banned address, and &lt;code&gt;grep ERROR /var/log/fail2ban.log&lt;/code&gt; tells you why one isn&amp;rsquo;t.&lt;/p&gt;&#10;&lt;p&gt;Get one of your own hosts to ban a machine outside its whitelist, then try to connect from that machine.&lt;/p&gt;&#10;</description></item><item><title>No DHCP for an LXC container while iptables counts the packets as ACCEPT: firewalld rejects them in its own nftables table</title><link>https://eghuro.cz/posts/lxc-dhcp-iptables-accept-firewalld-reject/</link><pubDate>Tue, 13 Oct 2026 22:56:00 +0200</pubDate><guid>https://eghuro.cz/posts/lxc-dhcp-iptables-accept-firewalld-reject/</guid><description>&lt;p&gt;A new LXC container came up with an IPv6 address and no IPv4 address. According to &lt;code&gt;iptables&lt;/code&gt;, the host allowed DHCP from the container bridge, and the counter on that rule kept growing:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo iptables -L INPUT -v -n | grep dpt:67&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0 0 ACCEPT tcp -- lxcbr0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:67&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 26 7956 ACCEPT udp -- lxcbr0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:67&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Twenty-six packets accepted, and dnsmasq, listening on that bridge, never logged a single DHCPDISCOVER.&lt;/p&gt;&#10;&lt;p&gt;With nftables, &lt;code&gt;accept&lt;/code&gt; only ends the chain it sits in. Any other base chain on the same hook still gets the packet. On this host, the next one belonged to firewalld, and firewalld rejected everything arriving on an interface that wasn&amp;rsquo;t in any of its zones.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Debian 13 host with the stock &lt;code&gt;lxc-net&lt;/code&gt; bridge: &lt;code&gt;lxcbr0&lt;/code&gt;, 10.0.3.0/24, dnsmasq at 10.0.3.1&lt;/li&gt;&#10;&lt;li&gt;firewalld 2.3.1 with &lt;code&gt;FirewallBackend=nftables&lt;/code&gt;, default zone &lt;code&gt;public&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;iptables 1.8.11, the nf_tables flavor Debian installs as &lt;code&gt;iptables&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;an unprivileged Debian 13 container running systemd-networkd with &lt;code&gt;DHCP=true&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;When lxc-net brings up the bridge, it adds its own iptables rules: ACCEPT for DNS and DHCP on &lt;code&gt;lxcbr0&lt;/code&gt;, ACCEPT for forwarding, and a MASQUERADE for the subnet. It knows nothing about firewalld.&lt;/p&gt;&#10;&#10;&lt;h2 id="following-the-packet"&gt;Following the packet &lt;/h2&gt;&#10;&lt;p&gt;The first visible symptom was the container&amp;rsquo;s bootstrap script, which spent nine minutes waiting on DNS lookups that timed out. From there, each check moved the problem one step along the path:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The container sent requests. With debug logging, systemd-networkd showed &lt;code&gt;DHCPv4 client: DISCOVER&lt;/code&gt; in the &lt;code&gt;selecting&lt;/code&gt; state.&lt;/li&gt;&#10;&lt;li&gt;The host received them. The ACCEPT counter went from 20 to 26 to 29 while I made the client retry with &lt;code&gt;networkctl reconfigure eth0&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;dnsmasq had the right socket open: &lt;code&gt;0.0.0.0%lxcbr0:67&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;dnsmasq received nothing. &lt;code&gt;strace&lt;/code&gt; showed it sitting in &lt;code&gt;poll()&lt;/code&gt;. During a retry, it read only an IPv6 router solicitation, which it answered. That is where the IPv6 address came from.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;AppArmor (no denials), reverse-path filtering (loose on the bridge), &lt;code&gt;br_netfilter&lt;/code&gt; (not loaded), leftover iptables-legacy tables (empty), and socket buffer drops (&lt;code&gt;RcvbufErrors&lt;/code&gt; 0) all came up clean. The host had no &lt;code&gt;tcpdump&lt;/code&gt;, and no &lt;code&gt;nft&lt;/code&gt; either:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo nft list ruleset&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo: nft: command not found&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That left &lt;code&gt;iptables&lt;/code&gt; as the only view of the firewall, and &lt;code&gt;iptables&lt;/code&gt; shows only the tables it created. So the packet passed an ACCEPT at the input hook and never reached the socket. Listing the running services found a second firewall:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;firewalld.service loaded active running firewalld - dynamic firewall daemon&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo firewall-cmd --get-zone-of-interface=lxcbr0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;no zone&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="two-tables-on-one-hook"&gt;Two tables on one hook &lt;/h2&gt;&#10;&lt;p&gt;After &lt;code&gt;apt install nftables&lt;/code&gt; (the binary is enough; leave &lt;code&gt;nftables.service&lt;/code&gt; disabled), you can see both chains on the input hook. Trimmed:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sudo nft list chains&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table ip filter {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;chain INPUT {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;type filter hook input priority filter; policy accept;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;table inet firewalld {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;chain filter_INPUT {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;type filter hook input priority filter + 10; policy accept;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ip filter&lt;/code&gt; holds lxc-net&amp;rsquo;s rules, written through iptables-nft. &lt;code&gt;inet firewalld&lt;/code&gt; runs right after it, and an interface that no zone claims falls through to the default zone:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chain filter_INPUT_POLICIES {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;jump filter_IN_policy_allow-host-ipv6&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;jump filter_IN_public&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;reject with icmpx admin-prohibited&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;public&lt;/code&gt; allowed SSH and the DHCPv6 client, but no DHCP server and no DNS, so the DISCOVER hit the final &lt;code&gt;reject&lt;/code&gt;. The zone chain also ends with &lt;code&gt;meta l4proto { icmp, ipv6-icmp } accept&lt;/code&gt;, which is why the router solicitation got through.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-fix"&gt;The fix &lt;/h2&gt;&#10;&lt;p&gt;Put the bridge in a firewalld zone. &lt;code&gt;trusted&lt;/code&gt; accepts everything, including forwarded traffic, which the containers need for internet access:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo firewall-cmd --zone&lt;span class="o"&gt;=&lt;/span&gt;trusted --add-interface&lt;span class="o"&gt;=&lt;/span&gt;lxcbr0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo firewall-cmd --permanent --zone&lt;span class="o"&gt;=&lt;/span&gt;trusted --add-interface&lt;span class="o"&gt;=&lt;/span&gt;lxcbr0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or with Ansible:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Trust the container bridge in firewalld&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible.posix.firewalld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;trusted&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lxcbr0&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;enabled&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The container got its lease on the client&amp;rsquo;s next retry, without restarting anything. &lt;code&gt;trusted&lt;/code&gt; also lets containers reach every port on the host. If that&amp;rsquo;s too much, give the bridge a zone of its own that allows only DHCP, DNS, and forwarding. The same applies to any interface that another tool creates, such as a VPN tunnel.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-the-counter-lied"&gt;Why the counter lied &lt;/h2&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_priority"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;nftables wiki&lt;/a&gt;&#10; says that &amp;ldquo;an accept verdict &amp;hellip; isn&amp;rsquo;t necessarily final&amp;rdquo;, while &amp;ldquo;drops take immediate effect, with no further rules or chains being evaluated.&amp;rdquo; Base chains on one hook run in priority order. An accept ends one chain, and a drop or reject in any chain wins. When two firewalls share a host, the stricter one decides, and the permissive one keeps counting packets that never arrive.&lt;/p&gt;&#10;&lt;p&gt;Most of the time you won&amp;rsquo;t see the second table. &lt;code&gt;iptables -L&lt;/code&gt; lists only what iptables-nft created, and a host running firewalld may not have the &lt;code&gt;nft&lt;/code&gt; command at all. (That missing binary &lt;a&#10; href="https://eghuro.cz/posts/fail2ban-nft-not-found"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;also leaves fail2ban unable to ban anyone&lt;/a&gt;&#10;.)&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;A counter on an ACCEPT rule only proves that the packet reached that rule. When an accepted packet still doesn&amp;rsquo;t arrive, list every base chain on the hook and find the later one that drops it.&lt;/p&gt;&#10;&lt;p&gt;If your host runs firewalld alongside anything that writes its own iptables rules, run &lt;code&gt;sudo nft list chains | grep hook&lt;/code&gt; and count the chains on &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;forward&lt;/code&gt;.&lt;/p&gt;&#10;</description></item></channel></rss>