<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Larvitz Blog</title><link href="https://blog.hofstede.it/" rel="alternate"/><link href="https://blog.hofstede.it/feeds/all.atom.xml" rel="self"/><id>https://blog.hofstede.it/</id><updated>2026-04-16T00:00:00+02:00</updated><subtitle>FreeBSD, Linux, all things cleanly engineered</subtitle><entry><title>Running Your Own AS: Direct Hetzner Peering, a Fourth Edge, and Bringing the Home LAN into the Fabric</title><link href="https://blog.hofstede.it/running-your-own-as-direct-hetzner-peering-a-fourth-edge-and-bringing-the-home-lan-into-the-fabric/" rel="alternate"/><published>2026-04-16T00:00:00+02:00</published><updated>2026-04-16T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-04-16:/running-your-own-as-direct-hetzner-peering-a-fourth-edge-and-bringing-the-home-lan-into-the-fabric/</id><summary type="html">&lt;p&gt;Part 4 of the &lt;span class="caps"&gt;AS201379&lt;/span&gt; journey: adding a fourth FreeBSD edge router at iFog with FogIXP peering, establishing direct &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions with Hetzner, bringing the home network into the &lt;span class="caps"&gt;AS&lt;/span&gt; via an iBGP-speaking MikroTik, and a little traffic engineering to steer Deutsche Telekom traffic over&amp;nbsp;Vultr.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-04-16-as201379-part4-direct-peering-home-network.png" title="Part 4: Header image"&gt;&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#at-a-glance"&gt;At a&amp;nbsp;Glance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#architecture-four-edges-and-a-home-router"&gt;Architecture: Four Edges and a Home&amp;nbsp;Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-fourth-edge-ixbgp-at-ifog-and-fogixp"&gt;The Fourth Edge: ixbgp at iFog and FogIXP&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#why-a-fourth-edge"&gt;Why a Fourth&amp;nbsp;Edge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#network-configuration"&gt;Network&amp;nbsp;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#frr-configuration"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#pf-stateless-transit-stateful-control-plane"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;: Stateless Transit, Stateful Control&amp;nbsp;Plane&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#direct-peering-with-hetzner"&gt;Direct Peering with Hetzner&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-path-before"&gt;The Path&amp;nbsp;Before&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-path-now"&gt;The Path&amp;nbsp;Now&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-hetzner-sees"&gt;What Hetzner&amp;nbsp;Sees&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-this-buys-in-practice"&gt;What This Buys in&amp;nbsp;Practice&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#bringing-the-home-lan-into-as201379"&gt;Bringing the Home &lt;span class="caps"&gt;LAN&lt;/span&gt; into &lt;span class="caps"&gt;AS201379&lt;/span&gt;&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-gap-in-parts-1-3"&gt;The Gap in Parts&amp;nbsp;1-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#mikrotik-configuration"&gt;MikroTik&amp;nbsp;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-core-side-configuration"&gt;The Core-Side&amp;nbsp;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#traffic-engineering-steering-dtag-via-vultr"&gt;Traffic Engineering: Steering &lt;span class="caps"&gt;DTAG&lt;/span&gt; via Vultr&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-observation"&gt;The&amp;nbsp;Observation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-implementation"&gt;The&amp;nbsp;Implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-effect"&gt;The&amp;nbsp;Effect&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#hub-hygiene-one-ip-for-traceroutes"&gt;Hub Hygiene: One &lt;span class="caps"&gt;IP&lt;/span&gt; for&amp;nbsp;Traceroutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#downstream-sites-pi-addressing-for-friends-and-services"&gt;Downstream Sites: &lt;span class="caps"&gt;PI&lt;/span&gt; Addressing for Friends and&amp;nbsp;Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;Part 1&lt;/a&gt; set up a single FreeBSD &lt;span class="caps"&gt;BGP&lt;/span&gt; router with two upstream providers. &lt;a href="https://blog.hofstede.it/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/"&gt;Part 2&lt;/a&gt; added a Vultr edge with native peering and tied both routers together with iBGP. &lt;a href="https://blog.hofstede.it/running-your-own-as-joining-an-ixp-with-a-third-edge-router/"&gt;Part 3&lt;/a&gt; joined LocIX Düsseldorf with a dedicated third edge router. This is Part&amp;nbsp;4.&lt;/p&gt;
&lt;p&gt;A few months of operating a multi-PoP &lt;span class="caps"&gt;BGP&lt;/span&gt; network produces a shopping list. I wanted direct peering with networks that move real traffic, a fourth edge in a new PoP, and my own IPv6 space on the home &lt;span class="caps"&gt;LAN&lt;/span&gt; instead of &lt;span class="caps"&gt;ISP&lt;/span&gt;-assigned addressing. This article covers the changes that made that&amp;nbsp;happen.&lt;/p&gt;
&lt;p&gt;The headline, if I had to pick one, is&amp;nbsp;two &lt;code&gt;mtr&lt;/code&gt; traces. First, from a nettest jail on my home &lt;span class="caps"&gt;LAN&lt;/span&gt; to Hetzner&amp;#8217;s&amp;nbsp;network:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;root@nettest:~ # mtr -rwz hetzner.com
HOST: nettest                                Loss%   Snt   Last   Avg  Best  Wrst StDev
  1. AS201379      2a06:9801:1c:6000::1       0.0%    10    0.2   0.2   0.1   0.2   0.0
  2. AS201379      2a06:9801:1c:fff0::1       0.0%    10    3.2   3.2   2.9   3.4   0.2
  3. AS201379      ixbgp.edge.hofstede.it     0.0%    10    6.9   7.0   6.8   7.3   0.2
  4. AS???         2001:7f8:ca:1:0:2:4940:1   0.0%    10    7.6   7.6   7.3   7.9   0.2
  5. AS24940       core11.nbg1.hetzner.com    0.0%    10   10.9  10.7  10.5  10.9   0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hop 1 is the home &lt;span class="caps"&gt;LAN&lt;/span&gt; gateway - a MikroTik announcing a /64 from my /48 via iBGP. Hop 2 is the iBGP tunnel to the core router. Hop 3 is the new fourth edge&amp;nbsp;(&lt;code&gt;ixbgp&lt;/code&gt;) at the iFog datacenter in Zürich. Hop 4 is Hetzner&amp;#8217;s peering-&lt;span class="caps"&gt;LAN&lt;/span&gt; address on FogIXP in Frankfurt. &lt;strong&gt;That&amp;#8217;s a direct &lt;span class="caps"&gt;BGP&lt;/span&gt; session between &lt;span class="caps"&gt;AS201379&lt;/span&gt; and &lt;span class="caps"&gt;AS24940&lt;/span&gt;&lt;/strong&gt;. Hop 5 is Hetzner&amp;#8217;s backbone. Five hops, 10 ms, and everything in the middle is either mine or&amp;nbsp;Hetzner.&lt;/p&gt;
&lt;p&gt;The return path, from a Hetzner machine back&amp;nbsp;to &lt;code&gt;blog.hofstede.it&lt;/code&gt;, completes the&amp;nbsp;picture:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[root@krypton ~]# mtr -rwz blog.hofstede.it
HOST: krypton                              Loss%   Snt   Last   Avg  Best  Wrst StDev
  4. AS???    2001:7f8:ca:1:0:20:1379:1     0.0%    10    5.1   5.1   5.1   5.2   0.0
  5. AS201379 ifog-gw.core.hofstede.it      0.0%    10    9.2  14.3   8.9  61.1  16.4
  6. AS201379 radon.server.hofstede.it      0.0%    10    9.9  10.2   9.7  10.9   0.3
  7. AS201379 hofstede.it                   0.0%    10   10.0  10.3   9.9  11.1   0.4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hop 4 is &lt;span class="caps"&gt;AS201379&lt;/span&gt;&amp;#8217;s own peering-&lt;span class="caps"&gt;LAN&lt;/span&gt; address on FogIXP - Hetzner hands traffic for my prefix directly to my router. From there it&amp;#8217;s three hops inside &lt;span class="caps"&gt;AS201379&lt;/span&gt; to the jail serving the blog. No intermediate transit &lt;span class="caps"&gt;AS&lt;/span&gt;, no &lt;span class="caps"&gt;DE&lt;/span&gt;-&lt;span class="caps"&gt;CIX&lt;/span&gt; hop, no shared upstream - just a direct handoff at the&amp;nbsp;exchange.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on addresses:&lt;/strong&gt; &lt;span class="caps"&gt;AS201379&lt;/span&gt;&amp;#8217;s&amp;nbsp;prefix &lt;code&gt;2a06:9801:1c::/48&lt;/code&gt; and&amp;nbsp;all &lt;code&gt;hofstede.it&lt;/code&gt; hostnames are shown as-is. Public peering-&lt;span class="caps"&gt;LAN&lt;/span&gt; addresses from PeeringDB&amp;nbsp;(FogIXP&amp;#8217;s &lt;code&gt;2001:7f8:ca:1::/64&lt;/code&gt;,&amp;nbsp;LocIX&amp;#8217;s &lt;code&gt;185.1.155.0/24&lt;/code&gt; and &lt;code&gt;2a0c:b641:701::/64&lt;/code&gt;) are also shown as-is. Everything else - provider-assigned addresses, tunnel endpoints, trusted-management IPs - has been replaced with &lt;a href="https://www.rfc-editor.org/rfc/rfc5737"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 5737&lt;/a&gt; / &lt;a href="https://www.rfc-editor.org/rfc/rfc3849"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 3849&lt;/a&gt; documentation ranges. Upstream and peer &lt;span class="caps"&gt;AS&lt;/span&gt; numbers are visible in public routing&amp;nbsp;tables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="at-a-glance"&gt;At a&amp;nbsp;Glance&lt;/h2&gt;
&lt;p&gt;Part 4 adds three things to &lt;span class="caps"&gt;AS201379&lt;/span&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a fourth FreeBSD edge router at iFog Zürich&amp;nbsp;(&lt;code&gt;ixbgp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;a direct &lt;span class="caps"&gt;BGP&lt;/span&gt; peering session with Hetzner on&amp;nbsp;FogIXP&lt;/li&gt;
&lt;li&gt;a MikroTik home router speaking iBGP, bringing the home &lt;span class="caps"&gt;LAN&lt;/span&gt; into the&amp;nbsp;/48&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is lower-latency paths to Hetzner, more control over outbound policy, and stable provider-independent IPv6 at&amp;nbsp;home.&lt;/p&gt;
&lt;h2 id="architecture-four-edges-and-a-home-router"&gt;Architecture: Four Edges and a Home&amp;nbsp;Router&lt;/h2&gt;
&lt;p&gt;The topology grew in two dimensions. A fourth FreeBSD edge router&amp;nbsp;(&lt;code&gt;ixbgp&lt;/code&gt;) joins the existing three at a new PoP.&amp;nbsp;Below &lt;code&gt;hobgp&lt;/code&gt;, a new downstream site appears: the home &lt;span class="caps"&gt;LAN&lt;/span&gt;, reachable via an iBGP-speaking&amp;nbsp;MikroTik.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    ┌──────────────────────────────────────────────────┐
                    │                Default-Free Zone                 │
                    └──┬──────────┬──────────────┬──────────────┬──────┘
                       │          │              │              │
                   AS209533   AS209735       AS212895         AS34927
                   (iFog free) (Lagrange)    (route64)       (iFog paid)
                       │          │              │              │
                    GRE          GRE            GRE            eBGP over vtnet0
                       │          │              │              │      +
                       │          │              │              │  FogIXP RS1/2/3 (AS47498)
                       │          │              │              │      +
                       │          │              │              │  AS24940 (Hetzner, direct)
                  ┌────┴──────────┴──────────────┴────┐   ┌─────┴────────────────────────┐
                  │             hobgp (Core)          │   │       ixbgp (Edge - iFog)    │
                  │       FreeBSD + FRR, AS201379     │◄──┤  FreeBSD + FRR, AS201379     │
                  │       2a06:9801:1c::/48           │iBGP  FogIXP peering LAN          │
                  └─┬───────────┬─────────────┬───────┘   └──────────────────────────────┘
                    │           │             │
               iBGP GIF   iBGP GIF       iBGP 6to4
                    │           │             │
            ┌───────┴───────┐ ┌─┴──────────┐ ┌┴────────────────────────────────┐
            │ vtbgp (Vultr) │ │lobgp(LocIX)│ │ MikroTik (Home LAN)             │
            │ Native AS64515│ │Servperso   │ │ RouterOS 7.20, AS201379         │
            │               │ │+ LocIX RS  │ │ Announces 2a06:9801:1c:6000::/64│
            └───────────────┘ └────────────┘ └─────────────────────────────────┘

Downstream tunnels from hobgp (provider-independent IPv6 for services):
   :1000::/64  radon      (blog, DNS, Gemini, fedi comments)
   :2000::/64  colo       (Hetzner colo OPNsense)
   :3000::/64  mail       (mail.linuxserver.pro)
   :4000::/64  bb         (burningboard.net)
   :5000::/64  road       (road-warrior network)
   :6000::/64  home       (home LAN via MikroTik)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;hobgp&lt;/code&gt; at Hetzner Nuremberg remains the hub from Part 2 - the core router that every edge speaks iBGP to, and every downstream site tunnels&amp;nbsp;through. &lt;code&gt;ixbgp&lt;/code&gt; is the new announcement point at iFog Zürich with a direct link to FogIXP Frankfurt. The home network is a downstream site on the same tunnel pattern as radon, but runs iBGP instead of static&amp;nbsp;routing.&lt;/p&gt;
&lt;p&gt;Downstream /64s ride &lt;span class="caps"&gt;GIF&lt;/span&gt; or 6to4 tunnels rather than native transport because most hosting providers don&amp;#8217;t let customers announce foreign prefixes on their infrastructure. A tunnel gives each site a direct layer-3 path back to a router that can originate and route the&amp;nbsp;prefix.&lt;/p&gt;
&lt;h2 id="the-fourth-edge-ixbgp-at-ifog-and-fogixp"&gt;The Fourth Edge: ixbgp at iFog and&amp;nbsp;FogIXP&lt;/h2&gt;
&lt;h3 id="why-a-fourth-edge"&gt;Why a Fourth&amp;nbsp;Edge&lt;/h3&gt;
&lt;p&gt;The free BGPTunnel service I used in Parts 1-3 is a great on-ramp, but it&amp;#8217;s operated as a best-effort community service. Pairing it with a paid iFog &lt;span class="caps"&gt;BGP&lt;/span&gt; port gives me a real transit contract and something the free tunnel does not: a &lt;span class="caps"&gt;VM&lt;/span&gt; with a second &lt;span class="caps"&gt;NIC&lt;/span&gt; directly on FogIXP&amp;#8217;s peering &lt;span class="caps"&gt;LAN&lt;/span&gt;. FogIXP is a smaller &lt;span class="caps"&gt;IXP&lt;/span&gt; than LocIX but has a different participant mix - notably, a direct Hetzner presence on that&amp;nbsp;fabric.&lt;/p&gt;
&lt;p&gt;The architecture&amp;nbsp;mirrors &lt;code&gt;lobgp&lt;/code&gt; from Part 3: two physical interfaces (internet-facing and peering-&lt;span class="caps"&gt;LAN&lt;/span&gt;-facing), an iBGP &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel back&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt;, route-maps per peer. The main addition is the direct Hetzner&amp;nbsp;session.&lt;/p&gt;
&lt;h3 id="network-configuration"&gt;Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/rc.conf&lt;/code&gt; on &lt;code&gt;ixbgp&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ixbgp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kld_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;if_gif&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Internet side (iFog)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.176 netmask 255.255.255.0 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1030::1229 prefixlen 48 -rxcsum6 -tso6&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1030::1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# FogIXP peering LAN (direct L2)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;up -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet1_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:7f8:ca:1::20:1379:1 prefixlen 64 -rxcsum6 -tso6&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# iBGP tunnel to core&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.176 198.51.100.10 mtu 1480&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:fffc::2 2a06:9801:1c:fffc::1 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Uplink-to-hobgp&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myblock&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_myblock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48 -interface gif0&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;frr_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;frr_daemons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mgmtd zebra bgpd bfdd staticd&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The peering-&lt;span class="caps"&gt;LAN&lt;/span&gt;&amp;nbsp;address &lt;code&gt;2001:7f8:ca:1::20:1379:1&lt;/code&gt; encodes my &lt;span class="caps"&gt;ASN&lt;/span&gt; in the interface &lt;span class="caps"&gt;ID&lt;/span&gt;&amp;nbsp;(&lt;code&gt;20:1379&lt;/code&gt; → &lt;span class="caps"&gt;AS201379&lt;/span&gt;), which is a common convention on &lt;span class="caps"&gt;IXP&lt;/span&gt; LANs. FogIXP assigns these statically;&amp;nbsp;the &lt;code&gt;/64&lt;/code&gt; is the peering-&lt;span class="caps"&gt;LAN&lt;/span&gt; subnet shared by every&amp;nbsp;participant.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ipv6_route_myblock="2a06:9801:1c::/48 -interface gif0"&lt;/code&gt; is the same trick as&amp;nbsp;on &lt;code&gt;lobgp&lt;/code&gt;: any traffic arriving&amp;nbsp;at &lt;code&gt;ixbgp&lt;/code&gt; for the /48 gets forwarded through the iBGP tunnel&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt;. &lt;code&gt;ixbgp&lt;/code&gt; originates the route, but forwarding still happens&amp;nbsp;through &lt;code&gt;hobgp&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="frr-configuration"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;Five &lt;span class="caps"&gt;BGP&lt;/span&gt; neighbors: three FogIXP route servers, the Hetzner direct peer, the paid iFog transit, and iBGP back to the core. The excerpt below omits the full bogon and hygiene policy for brevity (see Part 1 for the&amp;nbsp;complete &lt;code&gt;PL-BOGONS&lt;/code&gt; list); only the per-peer local-preference differences matter&amp;nbsp;here.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;frr version 10.5.1
hostname ixbgp
!
ipv6 prefix-list PL-MY-NET seq 5 permit 2a06:9801:1c::/48
!
! [PL-BOGONS: same as Part 1, trimmed for brevity]
!
route-map RM-IXP-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
 set local-preference 300
exit
!
route-map RM-HETZNER-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
 set local-preference 301
exit
!
route-map RM-IFOG-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
 set local-preference 100
exit
!
route-map RM-IXP-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-HETZNER-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-IFOG-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-IBGP-OUT permit 10
exit
!
router bgp 201379
 bgp router-id 198.51.100.176
 no bgp default ipv4-unicast
 no bgp enforce-first-as
 neighbor 2001:db8:1030::1 remote-as 34927
 neighbor 2001:db8:1030::1 description Transit-iFog-Paid
 neighbor 2a06:9801:1c:fffc::1 remote-as 201379
 neighbor 2a06:9801:1c:fffc::1 description Core-Hetzner
 neighbor 2a06:9801:1c:fffc::1 update-source 2a06:9801:1c:fffc::2
 neighbor 2001:7f8:ca:1::111 remote-as 47498
 neighbor 2001:7f8:ca:1::111 description FogIXP-RS1
 neighbor 2001:7f8:ca:1::222 remote-as 47498
 neighbor 2001:7f8:ca:1::222 description FogIXP-RS2
 neighbor 2001:7f8:ca:1::333 remote-as 47498
 neighbor 2001:7f8:ca:1::333 description FogIXP-RS3
 neighbor 2001:7f8:ca:1:0:2:4940:1 remote-as 24940
 neighbor 2001:7f8:ca:1:0:2:4940:1 description Hetzner-Peer
 !
 address-family ipv6 unicast
  neighbor 2001:db8:1030::1 activate
  neighbor 2001:db8:1030::1 soft-reconfiguration inbound
  neighbor 2001:db8:1030::1 route-map RM-IFOG-IN in
  neighbor 2001:db8:1030::1 route-map RM-IFOG-OUT out
  neighbor 2a06:9801:1c:fffc::1 activate
  neighbor 2a06:9801:1c:fffc::1 next-hop-self
  neighbor 2a06:9801:1c:fffc::1 route-map RM-IBGP-OUT out
  neighbor 2001:7f8:ca:1::111 activate
  neighbor 2001:7f8:ca:1::111 soft-reconfiguration inbound
  neighbor 2001:7f8:ca:1::111 route-map RM-IXP-IN in
  neighbor 2001:7f8:ca:1::111 route-map RM-IXP-OUT out
  neighbor 2001:7f8:ca:1::222 activate
  neighbor 2001:7f8:ca:1::222 soft-reconfiguration inbound
  neighbor 2001:7f8:ca:1::222 route-map RM-IXP-IN in
  neighbor 2001:7f8:ca:1::222 route-map RM-IXP-OUT out
  neighbor 2001:7f8:ca:1::333 activate
  neighbor 2001:7f8:ca:1::333 soft-reconfiguration inbound
  neighbor 2001:7f8:ca:1::333 route-map RM-IXP-IN in
  neighbor 2001:7f8:ca:1::333 route-map RM-IXP-OUT out
  neighbor 2001:7f8:ca:1:0:2:4940:1 activate
  neighbor 2001:7f8:ca:1:0:2:4940:1 soft-reconfiguration inbound
  neighbor 2001:7f8:ca:1:0:2:4940:1 route-map RM-HETZNER-IN in
  neighbor 2001:7f8:ca:1:0:2:4940:1 route-map RM-HETZNER-OUT out
 exit-address-family
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The local-preference scheme is&amp;nbsp;deliberate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hetzner direct (&lt;span class="caps"&gt;LP&lt;/span&gt; 301)&lt;/strong&gt; - highest. A direct session with a network the size of Hetzner is preferable to&amp;nbsp;transit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FogIXP route servers (&lt;span class="caps"&gt;LP&lt;/span&gt; 300)&lt;/strong&gt; - next. Routes learned via the exchange are free to use and generally&amp;nbsp;short.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iFog paid transit (&lt;span class="caps"&gt;LP&lt;/span&gt; 100)&lt;/strong&gt; - lowest. Transit is the fallback, so it gets the lowest local&amp;nbsp;preference.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one-point gap is deliberate: if I learn a Hetzner route both directly and via the route servers, the direct session wins. That keeps path selection predictable and avoids relying on route-server propagation&amp;nbsp;behavior.&lt;/p&gt;
&lt;p&gt;FogIXP&amp;#8217;s route servers are &amp;#8220;transparent&amp;#8221; - they don&amp;#8217;t prepend their own &lt;span class="caps"&gt;AS&lt;/span&gt; onto forwarded routes, so the first &lt;span class="caps"&gt;AS&lt;/span&gt; in the path is the actual origin, not the route server itself. &lt;span class="caps"&gt;FRR&lt;/span&gt; checks for this by default, so route-server sessions usually&amp;nbsp;need &lt;code&gt;no bgp enforce-first-as&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="pf-stateless-transit-stateful-control-plane"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;: Stateless Transit, Stateful Control&amp;nbsp;Plane&lt;/h3&gt;
&lt;p&gt;The &lt;span class="caps"&gt;PF&lt;/span&gt; configuration&amp;nbsp;on &lt;code&gt;ixbgp&lt;/code&gt; follows the same pattern established in Parts 2 and 3. The tunnel encapsulation must be stateless (otherwise &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s state table interferes with asymmetric paths), while control-plane &lt;span class="caps"&gt;TCP&lt;/span&gt; sessions must be stateful (&lt;span class="caps"&gt;TCP&lt;/span&gt; needs state to track three-way handshakes). The key&amp;nbsp;bits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ext_if    = &amp;quot;vtnet0&amp;quot;
ixp_if    = &amp;quot;vtnet1&amp;quot;
hobgp_tun = &amp;quot;gif0&amp;quot;

my_network_v6 = &amp;quot;2a06:9801:1c::/48&amp;quot;
my_router_ip  = &amp;quot;2a06:9801:1c:fffc::2&amp;quot;

&lt;span class="gh"&gt;#&lt;/span&gt; Outer tunnel encapsulation: stateless
pass in  quick on $ext_if proto { 41, ipencap } from 198.51.100.10 to ($ext_if) no state
pass out quick on $ext_if proto { 41, ipencap } from ($ext_if) to 198.51.100.10 no state

&lt;span class="gh"&gt;#&lt;/span&gt; eBGP sessions: stateful
pass in quick on $ext_if proto tcp from 2001:db8:1030::1 to ($ext_if) port 179 keep state
pass in quick on $ixp_if proto tcp from 2001:7f8:ca:1::/64 to ($ixp_if) port 179 keep state

&lt;span class="gh"&gt;#&lt;/span&gt; Transit data plane: stateless (essential for DSR and inner path asymmetry)
pass in  quick inet6 from any to $my_network_v6 no state
pass out quick inet6 from any to $my_network_v6 no state
pass in  quick inet6 from $my_network_v6 to any no state
pass out quick inet6 from $my_network_v6 to any no state

&lt;span class="gh"&gt;#&lt;/span&gt; IXP best practice: don&amp;#39;t leak multicast onto the peering LAN
block out quick on $ixp_if to { 224.0.0.0/4, 255.255.255.255, ff02::/16 }
block out quick on $ixp_if proto { igmp, ospf, pim, vrrp, gre }

&lt;span class="gh"&gt;#&lt;/span&gt; Monitoring: allow-list from trusted scrapers only
pass  in quick inet6 proto tcp from { 2a06:9801:1c:2000::21, 2a06:9801:1c:2000::25 } \
    to $my_router_ip port { 9100, 9342 } keep state
block in quick proto tcp from any to any port { 9100, 9342 }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The routing-protocol block rule is &lt;span class="caps"&gt;IXP&lt;/span&gt;-specific: &lt;span class="caps"&gt;OSPF&lt;/span&gt; hellos and the like have no business leaking onto a shared peering &lt;span class="caps"&gt;LAN&lt;/span&gt;, and most IXPs explicitly require participants to block them. The multicast block rule is the same principle - anything that&amp;#8217;s not unicast peering traffic gets dropped before it hits the&amp;nbsp;fabric.&lt;/p&gt;
&lt;h2 id="direct-peering-with-hetzner"&gt;Direct Peering with&amp;nbsp;Hetzner&lt;/h2&gt;
&lt;h3 id="the-path-before"&gt;The Path&amp;nbsp;Before&lt;/h3&gt;
&lt;p&gt;Before the FogIXP session, traffic from my home network to a Hetzner server&amp;nbsp;went:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;home MikroTik → hobgp → iFog BGPTunnel → iFog&amp;#39;s upstream → DE-CIX fabric
  → AS24940 Hetzner core → target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Four transit hops after leaving my network, round-trip time in the 30-40 ms range despite Frankfurt and Nuremberg being ~180 km&amp;nbsp;apart.&lt;/p&gt;
&lt;h3 id="the-path-now"&gt;The Path&amp;nbsp;Now&lt;/h3&gt;
&lt;p&gt;Post-FogIXP:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;home MikroTik → hobgp → ixbgp (Zürich) → FogIXP fabric → Hetzner backbone → target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One exchange point, one peering session, one handoff.&amp;nbsp;The &lt;code&gt;mtr&lt;/code&gt; at the top of this article shows ~10 ms end-to-end - not because the physical distance changed, but because the routing got&amp;nbsp;shorter.&lt;/p&gt;
&lt;h3 id="what-hetzner-sees"&gt;What Hetzner&amp;nbsp;Sees&lt;/h3&gt;
&lt;p&gt;Hetzner&amp;#8217;s routers now&amp;nbsp;learn &lt;code&gt;2a06:9801:1c::/48&lt;/code&gt; directly from &lt;span class="caps"&gt;AS201379&lt;/span&gt;, with no transit &lt;span class="caps"&gt;AS&lt;/span&gt; in between. From any Hetzner-hosted machine, traffic to my prefix exits through the Hetzner-side peering session at FogIXP and lands directly&amp;nbsp;on &lt;code&gt;ixbgp&lt;/code&gt;, which forwards over iBGP&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bgp.tools&lt;/code&gt; shows the &lt;span class="caps"&gt;AS&lt;/span&gt; path diversity: the prefix is still visible via iFog free, Lagrange, route64, Vultr, and the LocIX route servers, and now via FogIXP including the Hetzner direct session. Traffic originating inside Hetzner generally prefers the direct path; everyone else picks based on their own&amp;nbsp;policy.&lt;/p&gt;
&lt;h3 id="what-this-buys-in-practice"&gt;What This Buys in&amp;nbsp;Practice&lt;/h3&gt;
&lt;p&gt;The practical benefit is not just lower latency to one &lt;span class="caps"&gt;VPS&lt;/span&gt;. Because the /48 is mine and announced by &lt;span class="caps"&gt;AS201379&lt;/span&gt;, every service behind it inherits the direct peering&amp;nbsp;automatically.&lt;/p&gt;
&lt;h2 id="bringing-the-home-lan-into-as201379"&gt;Bringing the Home &lt;span class="caps"&gt;LAN&lt;/span&gt; into &lt;span class="caps"&gt;AS201379&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="the-gap-in-parts-1-3"&gt;The Gap in Parts&amp;nbsp;1-3&lt;/h3&gt;
&lt;p&gt;Parts 1-3 built an &lt;span class="caps"&gt;AS&lt;/span&gt; that serves my public-facing infrastructure. Downstream servers&amp;nbsp;like &lt;code&gt;radon&lt;/code&gt; or the colo OPNsense get their own /64 routed natively via &lt;span class="caps"&gt;GIF&lt;/span&gt;. My home network, however, used Deutsche Telekom&amp;#8217;s provider-assigned IPv6 -&amp;nbsp;a &lt;code&gt;/56&lt;/code&gt; from &lt;span class="caps"&gt;DTAG&lt;/span&gt; that changes on every reconnect and is useless for inbound&amp;nbsp;connectivity.&lt;/p&gt;
&lt;p&gt;The fix is conceptually identical to&amp;nbsp;what &lt;code&gt;radon&lt;/code&gt; already does: allocate a /64 from my /48, tunnel it to a router at the endpoint, announce it from there. The difference is that the home router is a MikroTik, not FreeBSD, and MikroTik&amp;#8217;s tunnel implementation is 6to4&amp;nbsp;(&lt;code&gt;interface 6to4&lt;/code&gt;) rather than &lt;span class="caps"&gt;GIF&lt;/span&gt;. The iBGP session plugs the home router into the existing core&amp;nbsp;router.&lt;/p&gt;
&lt;h3 id="mikrotik-configuration"&gt;MikroTik&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;RouterOS 7.20 has a usable &lt;span class="caps"&gt;BGP&lt;/span&gt; implementation. The tunnel&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt; is a 6to4 interface (IPv6-in-IPv4, protocol 41), and iBGP runs over&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/interface 6to4
add local-address=203.0.113.86 mtu=1480 name=tun-hobgp \
    remote-address=198.51.100.10

/ipv6 address
add address=2a06:9801:1c:fff0::2/128 advertise=no interface=tun-hobgp
add address=2a06:9801:1c:6000::1 interface=lan

/ipv6 route
add dst-address=2a06:9801:1c:fff0::1/128 gateway=tun-hobgp
add blackhole dst-address=2a06:9801:1c:6000::/64

/routing bgp instance
add as=201379 name=default router-id=203.0.113.86

/routing bgp template
add as=201379 name=ibgp-hobgp

/routing bgp connection
add afi=ipv6 disabled=no instance=default \
    local.address=2a06:9801:1c:fff0::2 .role=ibgp \
    remote.address=2a06:9801:1c:fff0::1 .as=201379 \
    name=hobgp templates=ibgp-hobgp \
    output.filter-chain=bgp-out .redistribute=connected,static

/routing filter rule
add chain=bgp-out rule=&amp;quot;if (dst == 2a06:9801:1c:6000::/64) { accept }&amp;quot;
add chain=bgp-out rule=reject
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three things are worth calling&amp;nbsp;out:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The blackhole route for the /64.&lt;/strong&gt; Same idea as&amp;nbsp;the &lt;code&gt;-reject&lt;/code&gt; route&amp;nbsp;on &lt;code&gt;hobgp&lt;/code&gt; for the /48: if the MikroTik gets traffic for an address in the /64 that isn&amp;#8217;t assigned, it drops it locally rather than following the default route back out (which would cause a loop). On RouterOS this is expressed as a specific blackhole&amp;nbsp;route.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The output&amp;nbsp;filter.&lt;/strong&gt; &lt;code&gt;bgp-out&lt;/code&gt; only permits the /64 that belongs to this site. Even&amp;nbsp;though &lt;code&gt;redistribute=connected,static&lt;/code&gt; would otherwise announce every connected subnet (including &lt;span class="caps"&gt;LAN&lt;/span&gt; &lt;span class="caps"&gt;RFC&lt;/span&gt; 1918 ranges), the filter strips everything that&amp;nbsp;isn&amp;#8217;t &lt;code&gt;2a06:9801:1c:6000::/64&lt;/code&gt;. This is the MikroTik equivalent of&amp;nbsp;the &lt;code&gt;PL-MY-NET&lt;/code&gt; prefix lists on the FreeBSD&amp;nbsp;edges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;advertise=no&lt;/code&gt; on the tunnel address.&lt;/strong&gt; The tunnel link&amp;nbsp;address &lt;code&gt;2a06:9801:1c:fff0::2&lt;/code&gt; is&amp;nbsp;a &lt;code&gt;/128&lt;/code&gt; point-to-point - it should not be announced to the &lt;span class="caps"&gt;LAN&lt;/span&gt; as something hosts can &lt;span class="caps"&gt;SLAAC&lt;/span&gt;&amp;nbsp;from.&lt;/p&gt;
&lt;h3 id="the-core-side-configuration"&gt;The Core-Side&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;On &lt;code&gt;hobgp&lt;/code&gt;, the MikroTik is a downstream iBGP peer with a different routing policy than the other edges. The other three edges receive the full table&amp;nbsp;via &lt;code&gt;RM-IBGP-OUT permit 10 | match PL-MY-NET&lt;/code&gt; - they only get our prefix, since that&amp;#8217;s the only thing they need to advertise. The home router gets a default route and nothing&amp;nbsp;else:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ipv6 prefix-list PL-MIKROTIK-NET seq 5 permit 2a06:9801:1c:6000::/64
!
route-map RM-DOWNSTREAM-IN permit 10
 match ipv6 address prefix-list PL-MIKROTIK-NET
exit
route-map RM-DOWNSTREAM-IN deny 20
exit
!
route-map RM-DEFAULT-OUT deny 10
exit
!
router bgp 201379
 ...
 neighbor 2a06:9801:1c:fff0::2 remote-as 201379
 neighbor 2a06:9801:1c:fff0::2 description Downstream-MikroTik-Lab
 neighbor 2a06:9801:1c:fff0::2 update-source 2a06:9801:1c:fff0::1
 !
 address-family ipv6 unicast
  neighbor 2a06:9801:1c:fff0::2 activate
  neighbor 2a06:9801:1c:fff0::2 default-originate
  neighbor 2a06:9801:1c:fff0::2 soft-reconfiguration inbound
  neighbor 2a06:9801:1c:fff0::2 route-map RM-DOWNSTREAM-IN in
  neighbor 2a06:9801:1c:fff0::2 route-map RM-DEFAULT-OUT out
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;default-originate&lt;/code&gt; generates a&amp;nbsp;synthetic &lt;code&gt;::/0&lt;/code&gt; route and sends it to the&amp;nbsp;MikroTik. &lt;code&gt;RM-DEFAULT-OUT deny 10&lt;/code&gt; prevents any other prefix from being advertised. The inbound filter&amp;nbsp;(&lt;code&gt;RM-DOWNSTREAM-IN&lt;/code&gt;) only&amp;nbsp;accepts &lt;code&gt;2a06:9801:1c:6000::/64&lt;/code&gt; - a safety measure against the home router accidentally announcing something it shouldn&amp;#8217;t. Combined with the MikroTik&amp;#8217;s own outbound filter, this means the worst-case scenario of a misconfigured home lab is a correctly-announced /64 or nothing - never route&amp;nbsp;leakage.&lt;/p&gt;
&lt;p&gt;The result is that a &lt;span class="caps"&gt;PC&lt;/span&gt; on the home &lt;span class="caps"&gt;LAN&lt;/span&gt; now &lt;span class="caps"&gt;SLAAC&lt;/span&gt;-configures&amp;nbsp;from &lt;code&gt;2a06:9801:1c:6000::/64&lt;/code&gt;, keeps a stable globally routable address across &lt;span class="caps"&gt;ISP&lt;/span&gt; reconnects, and exits through the &lt;span class="caps"&gt;AS&lt;/span&gt; like any other downstream site. The home &lt;span class="caps"&gt;LAN&lt;/span&gt; is now just another site in &lt;span class="caps"&gt;AS201379&lt;/span&gt;, indistinguishable from radon&amp;#8217;s /64 as far as the rest of the internet is&amp;nbsp;concerned.&lt;/p&gt;
&lt;h2 id="traffic-engineering-steering-dtag-via-vultr"&gt;Traffic Engineering: Steering &lt;span class="caps"&gt;DTAG&lt;/span&gt; via&amp;nbsp;Vultr&lt;/h2&gt;
&lt;h3 id="the-observation"&gt;The&amp;nbsp;Observation&lt;/h3&gt;
&lt;p&gt;Deutsche Telekom (&lt;span class="caps"&gt;AS3320&lt;/span&gt;) is the largest German eyeball network. A large share of blog readers reach my services over &lt;span class="caps"&gt;DTAG&lt;/span&gt;&amp;#8217;s network, so the return path&amp;nbsp;matters.&lt;/p&gt;
&lt;p&gt;With four edge routers, I have four potential ways to reach &lt;span class="caps"&gt;DTAG&lt;/span&gt;. Watching the &lt;span class="caps"&gt;AS&lt;/span&gt; paths that show up&amp;nbsp;on &lt;code&gt;bgp.tools&lt;/code&gt; and&amp;nbsp;in &lt;code&gt;show ipv6 bgp&lt;/code&gt; on &lt;code&gt;hobgp&lt;/code&gt;, the Vultr edge consistently learned shorter paths to &lt;span class="caps"&gt;DTAG&lt;/span&gt; than the others - Vultr&amp;#8217;s upstream connectivity into &lt;span class="caps"&gt;DTAG&lt;/span&gt; is short and direct in ways that the transit providers&amp;nbsp;aren&amp;#8217;t.&lt;/p&gt;
&lt;h3 id="the-implementation"&gt;The&amp;nbsp;Implementation&lt;/h3&gt;
&lt;p&gt;The tool is a &lt;span class="caps"&gt;BGP&lt;/span&gt; &lt;span class="caps"&gt;AS&lt;/span&gt;-path access list combined with a route-map that matches on both the &lt;span class="caps"&gt;AS&lt;/span&gt;-path and the learning peer.&amp;nbsp;On &lt;code&gt;hobgp&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bgp as-path access-list AS-DTAG seq 5 permit _3320$

route-map RM-IBGP-IN permit 5
 match as-path AS-DTAG
 match peer 2a06:9801:1c:fffe::2
 set local-preference 260
exit
route-map RM-IBGP-IN permit 10
exit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Reading this top to&amp;nbsp;bottom:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bgp as-path access-list AS-DTAG&lt;/code&gt; matches any &lt;span class="caps"&gt;AS&lt;/span&gt; path that ends&amp;nbsp;in &lt;code&gt;3320&lt;/code&gt; (the &lt;code&gt;$&lt;/code&gt; anchors to the end of the &lt;span class="caps"&gt;AS&lt;/span&gt; path). That means &amp;#8220;routes originated by &lt;span class="caps"&gt;DTAG&lt;/span&gt;&amp;#8221; - &lt;span class="caps"&gt;DTAG&lt;/span&gt; is the terminal &lt;span class="caps"&gt;AS&lt;/span&gt;, not a&amp;nbsp;transit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;route-map RM-IBGP-IN permit 5&lt;/code&gt; has two match conditions: the path must&amp;nbsp;match &lt;code&gt;AS-DTAG&lt;/code&gt;, and the update must have come&amp;nbsp;from &lt;code&gt;2a06:9801:1c:fffe::2&lt;/code&gt; - the Vultr edge router. Both must be&amp;nbsp;true.&lt;/li&gt;
&lt;li&gt;Matching routes get local-preference 260. Higher local-pref wins in &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;#8217;s selection algorithm, so these routes are preferred over the same prefix learned via iFog, Lagrange, route64, LocIX, or FogIXP&amp;nbsp;direct.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;route-map RM-IBGP-IN permit 10&lt;/code&gt; is the catch-all - any route that didn&amp;#8217;t match clause 5 falls through and gets accepted with default&amp;nbsp;attributes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;match peer&lt;/code&gt; clause is essential. Without it, the rule would match routes to &lt;span class="caps"&gt;DTAG&lt;/span&gt; learned from &lt;em&gt;any&lt;/em&gt; iBGP peer, which isn&amp;#8217;t what I want. Vultr happens to have the short path; the other edges don&amp;#8217;t. The policy is specifically &amp;#8220;prefer Vultr&amp;#8217;s version of &lt;span class="caps"&gt;DTAG&lt;/span&gt; routes, if it has&amp;nbsp;one.&amp;#8221;&lt;/p&gt;
&lt;h3 id="the-effect"&gt;The&amp;nbsp;Effect&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;show ipv6 bgp X:X:X::/32&lt;/code&gt; for a &lt;span class="caps"&gt;DTAG&lt;/span&gt;-customer prefix shows multiple paths - from iFog, LocIX, Vultr, and so on. Before the route-map, &lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;#8217;s tiebreaker (&lt;span class="caps"&gt;AS&lt;/span&gt; path length, then &lt;span class="caps"&gt;MED&lt;/span&gt;, then &lt;span class="caps"&gt;BGP&lt;/span&gt; origin) picked whichever was shortest at that moment. After the route-map, if Vultr has a path for a &lt;span class="caps"&gt;DTAG&lt;/span&gt; destination, Vultr wins - because 260 &amp;gt; 100&amp;nbsp;(default).&lt;/p&gt;
&lt;p&gt;Inbound traffic is out of my control (that&amp;#8217;s determined by the announcing side), but outbound traffic from my servers to &lt;span class="caps"&gt;DTAG&lt;/span&gt; customers now consistently exits through Vultr Frankfurt. Latency improvement varies by destination but averages around 5 ms lower than the transit paths, and jitter drops because the path no longer changes based on transient &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;convergence.&lt;/p&gt;
&lt;p&gt;This kind of traffic engineering was technically possible before, but not especially useful - with one or two edges, there&amp;#8217;s nothing to steer between. With four, &amp;#8220;which path goes where&amp;#8221; becomes a legitimate question, and &lt;span class="caps"&gt;AS&lt;/span&gt;-path regex gives a precise&amp;nbsp;answer.&lt;/p&gt;
&lt;h2 id="hub-hygiene-one-ip-for-traceroutes"&gt;Hub Hygiene: One &lt;span class="caps"&gt;IP&lt;/span&gt; for&amp;nbsp;Traceroutes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;hobgp&lt;/code&gt; has many interfaces. When a packet transits the router, the source address used for any router-originated reply (&lt;span class="caps"&gt;ICMP&lt;/span&gt; time-exceeded from a traceroute, for instance) depends on which interface is closest in the routing table. The result was&amp;nbsp;that &lt;code&gt;hobgp&lt;/code&gt; appeared under different addresses&amp;nbsp;in &lt;code&gt;mtr&lt;/code&gt; output depending on the flow&amp;nbsp;direction.&lt;/p&gt;
&lt;p&gt;The fix is a &lt;span class="caps"&gt;PF&lt;/span&gt; &lt;code&gt;match&lt;/code&gt; rule that rewrites router-originated IPv6 traffic to a stable loopback alias from the&amp;nbsp;/48:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ifconfig_lo0_alias0=&amp;quot;inet6 2a06:9801:1c::1 prefixlen 64&amp;quot;

match out on { gre0, gre1, gre2, gif0, gif1, gif2, gif3, gif4, gif5, gif6 } \
    inet6 from 2001:db8:1c19:3ee1::1 to any nat-to 2a06:9801:1c::1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;match&lt;/code&gt; is a non-terminating &lt;span class="caps"&gt;PF&lt;/span&gt; action - it modifies packets without making a pass/block decision. Now traceroutes&amp;nbsp;through &lt;code&gt;hobgp&lt;/code&gt; always&amp;nbsp;show &lt;code&gt;2a06:9801:1c::1&lt;/code&gt; (which resolves&amp;nbsp;to &lt;code&gt;hofstede.it&lt;/code&gt;) regardless of which tunnel the forward packet took, and the Hetzner-assigned address stays off public&amp;nbsp;traceroutes.&lt;/p&gt;
&lt;h2 id="downstream-sites-pi-addressing-for-friends-and-services"&gt;Downstream Sites: &lt;span class="caps"&gt;PI&lt;/span&gt; Addressing for Friends and&amp;nbsp;Services&lt;/h2&gt;
&lt;p&gt;A side effect of having a /48 and a working &lt;span class="caps"&gt;BGP&lt;/span&gt; infrastructure is that you can offer stable, provider-independent IPv6 addressing to services you run for friends or that live in separate failure domains. The current&amp;nbsp;allocation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;loopback on hobgp&lt;/td&gt;
&lt;td&gt;router identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:1000::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;radon&lt;/td&gt;
&lt;td&gt;blog, &lt;span class="caps"&gt;DNS&lt;/span&gt;, Gemini, fediverse comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:2000::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;colo OPNsense&lt;/td&gt;
&lt;td&gt;Hetzner colo test network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:3000::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mail.linuxserver.pro&lt;/td&gt;
&lt;td&gt;mail server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:4000::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;burningboard.net&lt;/td&gt;
&lt;td&gt;Mastodon instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:5000::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;road-warrior&lt;/td&gt;
&lt;td&gt;laptop on the move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:6000::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;home &lt;span class="caps"&gt;LAN&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;residential network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a06:9801:1c:fff*::/64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;infra&lt;/td&gt;
&lt;td&gt;point-to-point tunnel link addresses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The&amp;nbsp;high &lt;code&gt;:fff*&lt;/code&gt; ranges are reserved for infrastructure and point-to-point&amp;nbsp;links: &lt;code&gt;:ffff::/64&lt;/code&gt; for the radon &lt;span class="caps"&gt;GIF&lt;/span&gt;, &lt;code&gt;:fffe::/64&lt;/code&gt; for the Vultr iBGP&amp;nbsp;link, &lt;code&gt;:fffd::/64&lt;/code&gt; for the LocIX&amp;nbsp;edge, &lt;code&gt;:fffc::/64&lt;/code&gt; for the FogIXP&amp;nbsp;edge, &lt;code&gt;:fff0::/64&lt;/code&gt; for the MikroTik. Keeping link addresses out of the customer-facing ranges means the useful space&amp;nbsp;(&lt;code&gt;:1000::&lt;/code&gt; through &lt;code&gt;:6000::&lt;/code&gt;) is contiguous and easy to reason&amp;nbsp;about.&lt;/p&gt;
&lt;p&gt;Every site runs its own services, has its own firewall, and is operationally independent. What they share is an address prefix that doesn&amp;#8217;t change if their hoster does - and that, more than anything else, is why I did all of&amp;nbsp;this.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Direct peering is worth the setup effort.&lt;/strong&gt; A peering session with a large network replaces an entire transit path with a single handoff. The operational cost is one more &lt;span class="caps"&gt;BGP&lt;/span&gt; neighbor and a handful of route-map entries; the benefit is shorter, more predictable paths for a large chunk of real&amp;nbsp;traffic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RouterOS &lt;span class="caps"&gt;BGP&lt;/span&gt; is good enough for a downstream site.&lt;/strong&gt; The MikroTik iBGP session has been stable since it was brought up. The configuration is verbose compared to &lt;span class="caps"&gt;FRR&lt;/span&gt; but not surprising - prefix-list equivalents, route-maps, the blackhole for the /64. If the home router is what you already have, &lt;span class="caps"&gt;BGP&lt;/span&gt; on it is a reasonable&amp;nbsp;answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two-condition route-maps are a precision&amp;nbsp;instrument.&lt;/strong&gt; &lt;code&gt;match as-path&lt;/code&gt; plus &lt;code&gt;match peer&lt;/code&gt; lets you say exactly what you mean: &amp;#8220;prefer this specific path for this specific destination class.&amp;#8221; It&amp;#8217;s conceptually a small jump from single-condition filters, but it unlocks meaningful traffic engineering the moment you have more than two&amp;nbsp;edges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A consistent router identity helps more than it should.&lt;/strong&gt;&amp;nbsp;Rewriting &lt;code&gt;hobgp&lt;/code&gt;&lt;span class="quo"&gt;&amp;#8216;&lt;/span&gt;s source address to the /48 loopback is five lines of &lt;span class="caps"&gt;PF&lt;/span&gt;, but having a single hostname appear for the hub in every traceroute - regardless of interface - makes debugging paths significantly easier. It also keeps provider addresses out of public&amp;nbsp;traceroutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;#8220;announce at the edge, forward at the core&amp;#8221; pattern scales.&lt;/strong&gt; Each new edge is the same recipe: two interfaces or one plus a tunnel, &lt;span class="caps"&gt;FRR&lt;/span&gt; with per-peer route-maps, &lt;span class="caps"&gt;PF&lt;/span&gt; with stateless transit rules, an iBGP tunnel&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt;. The core router does the work. Adding a fourth edge was faster than adding the second one, because the pattern was already&amp;nbsp;established.&lt;/p&gt;
&lt;div class="about-cta"&gt;
&lt;p&gt;&lt;strong&gt;Interested in peering with &lt;span class="caps"&gt;AS201379&lt;/span&gt;?&lt;/strong&gt;
More details about the network, including upstreams, IXPs, and policy, live at &lt;a href="https://hofstede.it/as201379.html"&gt;hofstede.it/as201379.html&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;d like to establish a peering session - either over a tunnel or on the shared fabric at &lt;strong&gt;LocIX Düsseldorf&lt;/strong&gt; or &lt;strong&gt;FogIXP Frankfurt&lt;/strong&gt; - send a mail to &lt;a href="mailto:peering@hofstede.it"&gt;peering@hofstede.it&lt;/a&gt;. Hobbyists, small ASNs, and experimental setups are explicitly&amp;nbsp;welcome.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Four months in, &lt;span class="caps"&gt;AS201379&lt;/span&gt; has started to feel less like a project and more like a very small &lt;span class="caps"&gt;ISP&lt;/span&gt;: a core router, multiple edges in different PoPs, direct peering, traffic engineering, and downstream sites with stable addressing. It is still, by any real metric, tiny. But the difference between &amp;#8220;tiny&amp;#8221; and &amp;#8220;zero&amp;#8221; is categorical - Hetzner now learns my prefix directly over a peering session, and a &lt;span class="caps"&gt;PC&lt;/span&gt; on my home &lt;span class="caps"&gt;LAN&lt;/span&gt; now introduces itself to the internet with an address from my own&amp;nbsp;prefix.&lt;/p&gt;
&lt;p&gt;Whether there is a Part 5 depends on what breaks, what improves, or what new opportunity shows up next. So far, every version of this network has felt complete right up until the moment it&amp;nbsp;wasn&amp;#8217;t.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fogixp.net/"&gt;FogIXP - Frankfurt Open Exchange&lt;/a&gt; - the &lt;span class="caps"&gt;IXP&lt;/span&gt; used for this&amp;nbsp;expansion&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.locix.online/"&gt;LocIX Düsseldorf&lt;/a&gt; - the &lt;span class="caps"&gt;IXP&lt;/span&gt; from Part&amp;nbsp;3&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bgp.tools/"&gt;bgp.tools&lt;/a&gt; - looking glass and &lt;span class="caps"&gt;AS&lt;/span&gt;-path&amp;nbsp;visibility&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.frrouting.org/en/latest/bgp.html"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt; Documentation: &lt;span class="caps"&gt;BGP&lt;/span&gt;&lt;/a&gt; - route-maps, &lt;span class="caps"&gt;AS&lt;/span&gt;-path&amp;nbsp;access-lists, &lt;code&gt;match peer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.mikrotik.com/docs/display/ROS/BGP"&gt;MikroTik RouterOS &lt;span class="caps"&gt;BGP&lt;/span&gt;&lt;/a&gt; - filter chains, connection&amp;nbsp;templates&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.manrs.org/"&gt;&lt;span class="caps"&gt;MANRS&lt;/span&gt; Routing Security&lt;/a&gt; - the filtering and peering-&lt;span class="caps"&gt;LAN&lt;/span&gt; hygiene principles applied on every&amp;nbsp;edge&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.peeringdb.com/"&gt;PeeringDB&lt;/a&gt; - where FogIXP and LocIX peering-&lt;span class="caps"&gt;LAN&lt;/span&gt; addresses come&amp;nbsp;from&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; is one of those protocols where you never really finish. There&amp;#8217;s always one more peer to add, one more route-map clause to tune, one more site to bring into the fabric. The satisfying part is that each addition compounds: every new edge improves paths for every existing service, and every new downstream inherits the entire backbone&amp;#8217;s routing decisions for free. Four edges and six downstream sites ago, this was one router and one &lt;span class="caps"&gt;VPS&lt;/span&gt;. The architecture was the hard part. Everything since has been variations on the same&amp;nbsp;theme.&lt;/p&gt;</content><category term="Networking"/><category term="freebsd"/><category term="bgp"/><category term="networking"/><category term="ipv6"/><category term="frr"/><category term="pf"/><category term="ibgp"/><category term="ixp"/><category term="mikrotik"/><category term="routeros"/></entry><entry><title>Automating FreeBSD Jails with cdist - Zero Dependencies Inside the Jail</title><link href="https://blog.hofstede.it/automating-freebsd-jails-with-cdist-zero-dependencies-inside-the-jail/" rel="alternate"/><published>2026-04-12T00:00:00+02:00</published><updated>2026-04-12T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-04-12:/automating-freebsd-jails-with-cdist-zero-dependencies-inside-the-jail/</id><summary type="html">&lt;p&gt;cdist is refreshingly minimal - the target only needs &lt;span class="caps"&gt;POSIX&lt;/span&gt; sh, and the control machine speaks ssh. But cdist expects one ssh endpoint per host, and FreeBSD jails are not normally their own ssh targets. Two small Python wrappers plug cdist into jexec on the host, so configuration state flows into every jail without running a single daemon, agent, or Python interpreter inside the jail&amp;nbsp;itself.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Most configuration management tools still assume they own the target. Ansible ships Python modules over the wire and runs them in place. Salt wants a minion on every host. Chef wants a client, Puppet wants an&amp;nbsp;agent.&lt;/p&gt;
&lt;p&gt;&lt;img alt="cdist - FreeBSD" src="https://blog.hofstede.it/images/2026-04-12-cdist-freebsd-jails.png" title="cdist - FreeBSD"&gt; &lt;/p&gt;
&lt;p&gt;Jails break that assumption in a satisfying way. A FreeBSD jail is supposed to be small - sometimes a single static binary,&amp;nbsp;an &lt;code&gt;rc.d&lt;/code&gt; script, and a few lines&amp;nbsp;in &lt;code&gt;rc.conf&lt;/code&gt;. Installing Python into every jail just so Ansible can run&amp;nbsp;its &lt;code&gt;setup&lt;/code&gt; module is, to borrow a phrase, the tail wagging the dog. I already wrote about a workaround for Ansible: &lt;a href="https://blog.hofstede.it/managing-freebsd-jails-with-ansible-the-jailexec-connection-plugin/"&gt;the jailexec connection plugin&lt;/a&gt;, which SSHes to the jail host and&amp;nbsp;uses &lt;code&gt;jexec&lt;/code&gt; to tunnel commands into each jail. It works, and I still reach for it when I already have an Ansible setup. But many Ansible modules assume Python on the target, so in practice some of those jails still end up growing a Python&amp;nbsp;interpreter.&lt;/p&gt;
&lt;p&gt;Then I tried cdist, and everything got&amp;nbsp;smaller.&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-cdist-actually-is"&gt;What cdist Actually&amp;nbsp;Is&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-two-hooks-that-make-this-interesting"&gt;The Two Hooks That Make This&amp;nbsp;Interesting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#jexec-sshpy"&gt;jexec-ssh.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#jexec-scppy"&gt;jexec-scp.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#actually-running-it"&gt;Actually Running It&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#a-more-practical-example-a-custom-maintenance-type"&gt;A More Practical Example: A Custom Maintenance&amp;nbsp;Type&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-this-is-the-unix-way"&gt;Why This Is The Unix&amp;nbsp;Way&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#when-to-still-reach-for-ansible"&gt;When To Still Reach For&amp;nbsp;Ansible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#getting-the-scripts"&gt;Getting The&amp;nbsp;Scripts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="what-cdist-actually-is"&gt;What cdist Actually&amp;nbsp;Is&lt;/h2&gt;
&lt;p&gt;cdist is a configuration management system written in Python that runs entirely on the control machine. On the target, it expects a&amp;nbsp;Bourne-style &lt;code&gt;sh&lt;/code&gt; environment. That is it. No agent, no client, no target-side runtime of any kind. cdist assembles a shell script locally, ships it over the wire, and watches stdout come back. The target does not know it is being managed any more than it would know it was being SSHed&amp;nbsp;into.&lt;/p&gt;
&lt;p&gt;That minimalism is the selling point. cdist&amp;#8217;s own tagline reads &amp;#8220;usable configuration management&amp;#8221;, but what I read between the lines is: the entire target-side runtime is whatever shell is already on the box. If you can ssh to it, you can cdist it. No bootstrap step, no package install, no &amp;#8220;please add our apt key first&amp;#8221;. For a FreeBSD jail that exists precisely to run one daemon and nothing else, that is&amp;nbsp;perfect.&lt;/p&gt;
&lt;p&gt;The model is also refreshingly simple conceptually. You write &lt;strong&gt;manifests&lt;/strong&gt; (declarative entry points) that invoke &lt;strong&gt;types&lt;/strong&gt; (reusable building&amp;nbsp;blocks: &lt;code&gt;__file&lt;/code&gt;, &lt;code&gt;__package_pkgng&lt;/code&gt;, &lt;code&gt;__service&lt;/code&gt;, &lt;code&gt;__line&lt;/code&gt;, and so on). Every type is itself a small directory of shell scripts -&amp;nbsp;an &lt;code&gt;explorer&lt;/code&gt; that reports current state,&amp;nbsp;a &lt;code&gt;gencode-remote&lt;/code&gt; that emits the shell commands to reach the desired state, and optional helpers. cdist runs the explorers on the target, generates a shell script locally, sends it over ssh, and runs it&amp;nbsp;with &lt;code&gt;sh -e&lt;/code&gt;. That is the whole&amp;nbsp;loop.&lt;/p&gt;
&lt;h2 id="the-two-hooks-that-make-this-interesting"&gt;The Two Hooks That Make This&amp;nbsp;Interesting&lt;/h2&gt;
&lt;p&gt;cdist&amp;#8217;s default transport is&amp;nbsp;OpenSSH: &lt;code&gt;ssh user@host sh -c '…'&lt;/code&gt; for&amp;nbsp;commands, &lt;code&gt;scp&lt;/code&gt; for files. But the transport is swappable. Two command-line flags override&amp;nbsp;it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--remote-exec PATH&lt;/code&gt; - a script that takes ssh-style options followed&amp;nbsp;by &lt;code&gt;&amp;lt;target&amp;gt; &amp;lt;command…&amp;gt;&lt;/code&gt; and runs the command on the target&amp;nbsp;somehow.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--remote-copy PATH&lt;/code&gt; - a script that takes scp-style arguments and copies files to the target&amp;nbsp;somehow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this setup, the cdist target name is not a hostname at all; it is the jail&amp;nbsp;name. &lt;code&gt;JAIL_HOST&lt;/code&gt; tells the wrappers which real &lt;span class="caps"&gt;SSH&lt;/span&gt; endpoint to&amp;nbsp;use.&lt;/p&gt;
&lt;p&gt;The &lt;span class="caps"&gt;API&lt;/span&gt; is dumb in the best way. cdist gives you the target name and a command line; the script does whatever it needs to. If you want to tunnel through a bastion, go ahead. If you want to talk to a switch over a serial console, go ahead. And if you want to &lt;span class="caps"&gt;SSH&lt;/span&gt; to a FreeBSD host and drop into a jail&amp;nbsp;with &lt;code&gt;jexec&lt;/code&gt; - which is exactly what I want - that is two small Python scripts. The only persistent requirement is host-side access&amp;nbsp;to &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;scp&lt;/code&gt;, &lt;code&gt;jexec&lt;/code&gt;,&amp;nbsp;and &lt;code&gt;jls&lt;/code&gt;; the jail itself remains untouched except for the configuration changes cdist&amp;nbsp;applies.&lt;/p&gt;
&lt;h2 id="jexec-sshpy"&gt;jexec-ssh.py&lt;/h2&gt;
&lt;p&gt;Here is the remote-exec wrapper in full. It is 60 lines, most of them&amp;nbsp;boilerplate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;shlex&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;_SSH_OPTS_WITH_VALUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-o&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-F&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-i&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-l&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-p&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-c&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-m&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-L&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_ssh_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_SSH_OPTS_WITH_VALUE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;jail_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;JAIL_HOST&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;jail_host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;error: JAIL_HOST is not set&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;jail_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;JAIL_USER&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;root&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strip_ssh_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;error: no target jail name on the command line&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;jailname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;remote_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;

    &lt;span class="n"&gt;jexec_cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jexec -u &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jail_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jailname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/bin/sh -c &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remote_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jail_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jexec_cmd&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The logic is the whole&amp;nbsp;story:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read &lt;code&gt;JAIL_HOST&lt;/code&gt; from the environment. That is the ssh endpoint for the &lt;em&gt;host&lt;/em&gt;, not the&amp;nbsp;jail.&lt;/li&gt;
&lt;li&gt;Strip the ssh-style options cdist prepends - things&amp;nbsp;like &lt;code&gt;-o User=root&lt;/code&gt; - which are meaningful to a jail&amp;#8217;s (nonexistent) direct ssh endpoint, not to ours. Real connection options belong&amp;nbsp;in &lt;code&gt;~/.ssh/config&lt;/code&gt; for the&amp;nbsp;host.&lt;/li&gt;
&lt;li&gt;The first non-option argument is the jail name. Everything after that is the command cdist wants to&amp;nbsp;execute.&lt;/li&gt;
&lt;li&gt;Wrap the command&amp;nbsp;in &lt;code&gt;jexec -u &amp;lt;user&amp;gt; &amp;lt;jail&amp;gt; /bin/sh -c '…'&lt;/code&gt;, &lt;code&gt;shlex.quote&lt;/code&gt;&lt;span class="quo"&gt;&amp;#8216;&lt;/span&gt;d so pipelines, redirections and globs survive transit through two&amp;nbsp;shells.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execvp&lt;/code&gt; into the&amp;nbsp;real &lt;code&gt;ssh&lt;/code&gt;, replacing the Python process. ssh does the heavy lifting, and its exit code flows straight back to&amp;nbsp;cdist.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The command is crossing two shell boundaries: the host-side &lt;span class="caps"&gt;SSH&lt;/span&gt; command line and the&amp;nbsp;in-jail &lt;code&gt;sh -c&lt;/code&gt;. That is why the wrapper leans so hard&amp;nbsp;on &lt;code&gt;shlex.quote()&lt;/code&gt;.&amp;nbsp;The &lt;code&gt;/bin/sh -c&lt;/code&gt; wrapper itself is not cosmetic. Without&amp;nbsp;it, &lt;code&gt;jexec&lt;/code&gt; hands the argv directly to whatever binary sits at position one, and cdist-generated constructs&amp;nbsp;like &lt;code&gt;for f in /etc/*.conf; do …; done&lt;/code&gt; die the moment a glob shows up. Running the payload through a&amp;nbsp;fresh &lt;code&gt;sh&lt;/code&gt; inside the jail restores the shell environment cdist&amp;nbsp;expects.&lt;/p&gt;
&lt;h2 id="jexec-scppy"&gt;jexec-scp.py&lt;/h2&gt;
&lt;p&gt;File transfer is slightly more involved, but only&amp;nbsp;slightly. &lt;code&gt;scp&lt;/code&gt; does not know what a jail is - but it does know what a host path is, and a jail&amp;#8217;s &amp;#8220;inside&amp;#8221; is always some path on the host&amp;nbsp;(&lt;code&gt;/zroot/jails/&amp;lt;name&amp;gt;&lt;/code&gt; is typical). We can&amp;nbsp;translate.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;_SCP_OPTS_WITH_VALUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-o&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-F&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-i&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-S&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-P&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;split_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_SCP_OPTS_WITH_VALUE&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jail_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jailname&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/usr/sbin/jls&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-j&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jailname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STDOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;:&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rpath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jail_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rpath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;jail_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;JAIL_HOST&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;jail_host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;error: JAIL_HOST is not set&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;split_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;error: scp needs a source and a destination&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;translated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jail_host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;scp&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;scp&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-r&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-p&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;translated&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Walking through&amp;nbsp;it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;For each argument, check if it looks&amp;nbsp;like &lt;code&gt;&amp;lt;jail&amp;gt;:/some/path&lt;/code&gt;. If not - local paths, host-qualified paths - leave it&amp;nbsp;alone.&lt;/li&gt;
&lt;li&gt;If it does, ssh to the host and&amp;nbsp;run &lt;code&gt;jls -j &amp;lt;jail&amp;gt; path&lt;/code&gt;. &lt;code&gt;jls&lt;/code&gt; is the canonical way to ask FreeBSD &amp;#8220;where does jail X live on disk right&amp;nbsp;now&amp;#8221;.&lt;/li&gt;
&lt;li&gt;Rewrite the argument&amp;nbsp;to &lt;code&gt;&amp;lt;JAIL_HOST&amp;gt;:&amp;lt;jailroot&amp;gt;/&amp;lt;path&amp;gt;&lt;/code&gt;, which is just a perfectly ordinary scp remote&amp;nbsp;path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execvp&lt;/code&gt; into the&amp;nbsp;system &lt;code&gt;scp&lt;/code&gt; with the rewritten&amp;nbsp;arguments.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is nothing magical&amp;nbsp;here. &lt;code&gt;scp&lt;/code&gt; was already perfectly capable of copying files&amp;nbsp;to &lt;code&gt;root@radon.example.com:/zroot/jails/testvnet/etc/motd&lt;/code&gt;. All the wrapper does is save the user the trouble of typing it and let cdist&amp;#8217;s file types behave as if the jail were a first-class ssh target. The simple implementation&amp;nbsp;asks &lt;code&gt;jls&lt;/code&gt; once per jail-qualified path; caching jail roots would be an easy optimization if you start moving lots of&amp;nbsp;files.&lt;/p&gt;
&lt;h2 id="actually-running-it"&gt;Actually Running&amp;nbsp;It&lt;/h2&gt;
&lt;p&gt;On a host with many jails, &lt;span class="caps"&gt;SSH&lt;/span&gt; connection setup can dominate runtime; enabling multiplexing on the host connection makes a dramatic difference. More on that in the repo&amp;#8217;s &lt;span class="caps"&gt;README&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;A minimal setup. First, a manifest&amp;nbsp;at &lt;code&gt;./conf/manifest/init&lt;/code&gt; - one line is enough to prove the whole pipeline works end to&amp;nbsp;end:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;__file&lt;span class="w"&gt; &lt;/span&gt;/tmp/cdist-wrapper-success&lt;span class="w"&gt; &lt;/span&gt;--state&lt;span class="w"&gt; &lt;/span&gt;present
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then point cdist at the two&amp;nbsp;wrappers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~/tmp/cdist-test ❯ export JAIL_HOST=root@radon.edelga.se
~/tmp/cdist-test ❯ cdist config -c ./conf \
    --remote-exec ./jexec-ssh.py \
    --remote-copy ./jexec-scp.py \
    testvnet lg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In this&amp;nbsp;example, &lt;code&gt;testvnet&lt;/code&gt; and &lt;code&gt;lg&lt;/code&gt; are jail names, not &lt;span class="caps"&gt;DNS&lt;/span&gt; names or &lt;span class="caps"&gt;SSH&lt;/span&gt; inventory entries. cdist is quiet on success: no output is good output. A sanity check on the host itself confirms&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[root@radon ~]# jexec testvnet ls -l /tmp/cdist-wrapper-success
-rw-------  1 root wheel 0 Apr 11 21:02 /tmp/cdist-wrapper-success
[root@radon ~]# jexec lg ls -l /tmp/cdist-wrapper-success
-rw-------  1 root wheel 0 Apr 11 21:18 /tmp/cdist-wrapper-success
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Two jails, one cdist invocation, zero daemons, zero extra interpreters, nothing mutated outside the jails themselves. Replace the one-liner manifest with real cdist types&amp;nbsp;- &lt;code&gt;__package_pkgng&lt;/code&gt;, &lt;code&gt;__file&lt;/code&gt;, &lt;code&gt;__line&lt;/code&gt;, &lt;code&gt;__service&lt;/code&gt;, &lt;code&gt;__user&lt;/code&gt; - and you get the same thing at whatever scale you&amp;nbsp;need.&lt;/p&gt;
&lt;h3 id="a-more-practical-example-a-custom-maintenance-type"&gt;A More Practical Example: A Custom Maintenance&amp;nbsp;Type&lt;/h3&gt;
&lt;p&gt;The smoke test above proves the transport works. Here is a pragmatic host-maintenance example I use in practice: a custom type that runs base and package updates inside a jail. This is more of a maintenance action than a pure convergent state type, but it is a good illustration of how little machinery cdist needs when you do want to run shell-driven lifecycle&amp;nbsp;tasks.&lt;/p&gt;
&lt;p&gt;First, create the type&amp;#8217;s directory and&amp;nbsp;its &lt;code&gt;gencode-remote&lt;/code&gt; script. This is the shell that cdist will execute inside the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;conf/type/__freebsd_system_update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c1"&gt;# conf/type/__freebsd_system_update/gencode-remote&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Checking for and installing FreeBSD base updates...&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# PAGER=cat prevents freebsd-update from dropping into an&lt;/span&gt;
&lt;span class="c1"&gt;# interactive pager and waiting for &amp;#39;q&amp;#39; on a headless target.&lt;/span&gt;
env&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PAGER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;freebsd-update&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;install

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Updating package catalogue and installing upgrades...&amp;quot;&lt;/span&gt;
pkg&lt;span class="w"&gt; &lt;/span&gt;update
pkg&lt;span class="w"&gt; &lt;/span&gt;upgrade&lt;span class="w"&gt; &lt;/span&gt;-y
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;conf/type/__freebsd_system_update/gencode-remote
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then wire it into the manifest. cdist&amp;nbsp;exposes &lt;code&gt;$__target_os&lt;/code&gt; as an explorer, so the type only fires on FreeBSD&amp;nbsp;targets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c1"&gt;# conf/manifest/init&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$__target_os&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;FreeBSD&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;__freebsd_system_update
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Info: &lt;/span&gt;&lt;span class="nv"&gt;$__target_host&lt;/span&gt;&lt;span class="s2"&gt; is not FreeBSD, skipping updates.&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Run it the same way as&amp;nbsp;before:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~/tmp/cdist-test ❯ cdist config -c ./conf \
    --remote-exec ./jexec-ssh.py \
    --remote-copy ./jexec-scp.py \
    testvnet lg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is the whole workflow for writing a custom cdist type: a directory, a shell script, and a one-line invocation in the manifest. No &lt;span class="caps"&gt;YAML&lt;/span&gt;, no plugin protocol, no class hierarchy. The type is just shell, and the wrappers make sure that shell runs inside the&amp;nbsp;jail.&lt;/p&gt;
&lt;h2 id="why-this-is-the-unix-way"&gt;Why This Is The Unix&amp;nbsp;Way&lt;/h2&gt;
&lt;p&gt;I wrote these scripts one Monday afternoon. By dinnertime I had working state management across every jail on my FreeBSD hosts, without installing anything new on a single one of them. That is not a coincidence; it is what happens when every component in the stack knows exactly one job and does it&amp;nbsp;well.&lt;/p&gt;
&lt;p&gt;Count the&amp;nbsp;actors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ssh&lt;/code&gt; - authenticated, encrypted&amp;nbsp;transport&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scp&lt;/code&gt; - file copy over&amp;nbsp;ssh&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jexec&lt;/code&gt; - process execution inside a running jail&amp;#8217;s&amp;nbsp;namespace&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jls&lt;/code&gt; - jail&amp;nbsp;inspection&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sh&lt;/code&gt; - shell&amp;nbsp;execution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdist&lt;/code&gt; - render desired state into a shell&amp;nbsp;script&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each piece is narrow, composable, and independently useful. The two Python files in this repo do not replace any of them; they only connect them. Together that is roughly 120 lines of Python orchestrating several decades of battle-tested Unix primitives. None of those tools are tightly coupled to each other. None of them share a runtime. If I delete any one file from the pipeline, the rest still do their jobs. That is the thing people mean when they talk about &amp;#8220;the Unix philosophy&amp;#8221;, and it is the thing I keep coming back to every time I look at how modern tools solve the same&amp;nbsp;problems.&lt;/p&gt;
&lt;p&gt;Compare the dependency footprint inside the&amp;nbsp;jail:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Typically required inside the jail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ansible (default)&lt;/td&gt;
&lt;td&gt;Python interpreter for many modules, plus a normal remote access path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ansible (jailexec)&lt;/td&gt;
&lt;td&gt;Python interpreter (for most modules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;salt-minion&lt;/code&gt;, Python, dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Puppet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;puppet&lt;/code&gt; agent, Ruby, dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chef&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chef-client&lt;/code&gt;, Ruby, dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cdist + these wrappers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;span class="caps"&gt;POSIX&lt;/span&gt; &lt;code&gt;sh&lt;/code&gt;, which is already in base&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The same story plays out in the implementations themselves. My &lt;a href="https://blog.hofstede.it/managing-freebsd-jails-with-ansible-the-jailexec-connection-plugin/"&gt;Ansible jailexec connection plugin&lt;/a&gt; is roughly 960 lines of Python, because Ansible&amp;#8217;s connection plugin &lt;span class="caps"&gt;API&lt;/span&gt; asks for a fair amount of scaffolding: argument translation, a file-transfer state machine, subprocess management, the whole plugin protocol. These two cdist wrappers together come to about 120 lines. That difference is not about quality; it is about how much each tool asks of the transport layer. cdist&amp;#8217;s hooks are deliberately narrow, so there is less to implement. And because cdist&amp;#8217;s types are shell, the jail does not need a Python interpreter at all, whereas the Ansible path still typically&amp;nbsp;does.&lt;/p&gt;
&lt;p&gt;The smallest running jail on my radon.edelga.se host is 61 &lt;span class="caps"&gt;MB&lt;/span&gt; on disk - a thin-jail nullfs view over a shared base, running one static binary. It still configures beautifully with this pipeline, because the pipeline asks it for nothing it was not already&amp;nbsp;providing.&lt;/p&gt;
&lt;h2 id="when-to-still-reach-for-ansible"&gt;When To Still Reach For&amp;nbsp;Ansible&lt;/h2&gt;
&lt;p&gt;This is not the universal&amp;nbsp;answer.&lt;/p&gt;
&lt;p&gt;cdist is opinionated about shell, which means that if your configuration domain is &amp;#8220;take this complicated &lt;span class="caps"&gt;JSON&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt;, reason about its responses, compute a delta, push it back&amp;#8221; - the kind of thing Ansible modules do in a page of Python each - you will end up writing that logic yourself&amp;nbsp;in &lt;code&gt;sh&lt;/code&gt;. That is fine for ten types; it is painful for a hundred. The moment you need rich remote introspection, structured data handling, or an ecosystem module that already solves a weird &lt;span class="caps"&gt;API&lt;/span&gt;, Ansible starts to earn its weight&amp;nbsp;again.&lt;/p&gt;
&lt;p&gt;If you already have a working Ansible setup across your infrastructure, the &lt;a href="https://blog.hofstede.it/managing-freebsd-jails-with-ansible-the-jailexec-connection-plugin/"&gt;jailexec connection plugin&lt;/a&gt; is the path of least resistance for adding jails to it. You keep your playbooks, your roles, your inventory. I still use it where &amp;#8220;use what&amp;#8217;s already there&amp;#8221; beats &amp;#8220;be&amp;nbsp;minimal&amp;#8221;.&lt;/p&gt;
&lt;p&gt;But for a fresh build of jails on a new host - or for the handful of single-purpose jails I run on radon - cdist plus these two wrappers is the right tool. It makes &amp;#8220;managed state on a FreeBSD jail&amp;#8221; exactly as simple as &amp;#8220;I can ssh to the host and&amp;nbsp;run &lt;code&gt;jexec&lt;/code&gt;&amp;#8221;, and it asks nothing more of the jail than FreeBSD already&amp;nbsp;provides.&lt;/p&gt;
&lt;h2 id="getting-the-scripts"&gt;Getting The&amp;nbsp;Scripts&lt;/h2&gt;
&lt;p&gt;Both scripts live on &lt;a href="https://codeberg.org/Larvitz/jexec-cdist"&gt;Codeberg as jexec-cdist&lt;/a&gt;, &lt;span class="caps"&gt;CC0&lt;/span&gt; licensed, with a &lt;span class="caps"&gt;README&lt;/span&gt; that walks through the environment variables, the ssh multiplexing tip that turns a 15-second run into an instant one,&amp;nbsp;the &lt;code&gt;doas&lt;/code&gt; pattern for running as a non-root user on the host, and the handful of edge cases (nullfs unions, non-standard jail roots) where the path rewriting needs a small&amp;nbsp;nudge.&lt;/p&gt;
&lt;p&gt;Clone, &lt;code&gt;chmod +x&lt;/code&gt;, point cdist at them,&amp;nbsp;export &lt;code&gt;JAIL_HOST&lt;/code&gt;. That is the entire onboarding. The jails stay as clean as the day you created them - and that is the thing I wanted all&amp;nbsp;along.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cdi.st/"&gt;cdist&lt;/a&gt; - the configuration management system this article is&amp;nbsp;about&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codeberg.org/Larvitz/jexec-cdist"&gt;jexec-cdist on Codeberg&lt;/a&gt; - the two wrappers in this&amp;nbsp;article&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cdi.st/manual/latest/man7/cdist-remote-exec-copy.html"&gt;cdist: writing your own remote-exec and remote-copy&lt;/a&gt; - the upstream documentation for the hooks these scripts plug&amp;nbsp;into&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/managing-freebsd-jails-with-ansible-the-jailexec-connection-plugin/"&gt;Managing FreeBSD Jails with Ansible: the jailexec connection plugin&lt;/a&gt; - the Ansible equivalent, for&amp;nbsp;comparison&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/"&gt;FreeBSD Foundationals: Jails&lt;/a&gt; - the jail fundamentals this article&amp;nbsp;assumes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jexec(8)&lt;/code&gt;, &lt;code&gt;jls(8)&lt;/code&gt; - the FreeBSD primitives doing the actual&amp;nbsp;work&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="jails"/><category term="cdist"/><category term="automation"/><category term="devops"/><category term="unix-philosophy"/><category term="jexec"/></entry><entry><title>Replacing Lenovo’s WWAN Unlock Blob with a 100-Line Bash Script</title><link href="https://blog.hofstede.it/replacing-lenovos-wwan-unlock-blob-with-a-100-line-bash-script/" rel="alternate"/><published>2026-04-11T00:00:00+02:00</published><updated>2026-04-11T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-04-11:/replacing-lenovos-wwan-unlock-blob-with-a-100-line-bash-script/</id><summary type="html">&lt;p&gt;My ThinkPad T14s shipped with an Intel &lt;span class="caps"&gt;XMM7560&lt;/span&gt; &lt;span class="caps"&gt;LTE&lt;/span&gt; modem that would not register on the network until Lenovo&amp;#8217;s proprietary &lt;span class="caps"&gt;FCC&lt;/span&gt;-unlock helper ran. I replaced it with a roughly 100-line bash script from a ModemManager merge request, and along the way learned that the &amp;#8220;unlock&amp;#8221; is just a small challenge-response handshake that is easy to explain in plain&amp;nbsp;shell.&lt;/p&gt;</summary><content type="html">&lt;p&gt;My Lenovo ThinkPad T14s Gen 4 (&lt;span class="caps"&gt;AMD&lt;/span&gt; Ryzen) shipped with an Intel &lt;span class="caps"&gt;XMM7560&lt;/span&gt; &lt;span class="caps"&gt;LTE&lt;/span&gt; Advanced Pro modem soldered to the mainboard. Useful little thing: real &lt;span class="caps"&gt;LTE&lt;/span&gt; on the go, no tethering dance, no MiFi puck in the bag. The catch: out of the box, the modem refuses to register on the network. ModemManager dutifully detects it, the &lt;span class="caps"&gt;SIM&lt;/span&gt; is recognised, but on my&amp;nbsp;machine &lt;code&gt;AT+CFUN=1&lt;/code&gt; would come&amp;nbsp;back &lt;code&gt;OK&lt;/code&gt; while the radio quietly stayed&amp;nbsp;dark.&lt;/p&gt;
&lt;p&gt;The reason is something called &lt;em&gt;&lt;span class="caps"&gt;FCC&lt;/span&gt; lock&lt;/em&gt;, and the official fix from Lenovo is a package of proprietary helpers and shared libraries. I replaced Lenovo&amp;#8217;s proprietary helper with a bash script that performs the same handshake in clear, auditable shell. Here is the why, the how, and the&amp;nbsp;script.&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-fcc-lock-actually-is"&gt;What &lt;span class="caps"&gt;FCC&lt;/span&gt; Lock Actually&amp;nbsp;Is&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-modemmanager-fcc-unlock-mechanism"&gt;The ModemManager &lt;span class="caps"&gt;FCC&lt;/span&gt; Unlock&amp;nbsp;Mechanism&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#lenovos-solution-a-proprietary-helper-package"&gt;Lenovo&amp;#8217;s Solution: A Proprietary Helper&amp;nbsp;Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-alternative-a-shell-script-that-does-the-same-thing"&gt;The Alternative: A Shell Script That Does the Same&amp;nbsp;Thing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#how-the-unlock-actually-works"&gt;How the Unlock Actually&amp;nbsp;Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#installing-it-yourself"&gt;Installing It&amp;nbsp;Yourself&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-disclaimer-about-jurisdiction"&gt;A Disclaimer About&amp;nbsp;Jurisdiction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wrapping-up"&gt;Wrapping&amp;nbsp;Up&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="what-fcc-lock-actually-is"&gt;What &lt;span class="caps"&gt;FCC&lt;/span&gt; Lock Actually&amp;nbsp;Is&lt;/h2&gt;
&lt;p&gt;If you have never run into this before, the term &amp;#8220;&lt;span class="caps"&gt;FCC&lt;/span&gt; lock&amp;#8221; sounds like &lt;span class="caps"&gt;DRM&lt;/span&gt;. It is not - or at least, not exactly. It is a regulatory compliance&amp;nbsp;mechanism.&lt;/p&gt;
&lt;p&gt;When a laptop ships with an embedded &lt;span class="caps"&gt;WWAN&lt;/span&gt; modem, the &lt;em&gt;combination&lt;/em&gt; of the laptop chassis, the antenna layout, and the modem radio is what gets certified by the regulator. In the &lt;span class="caps"&gt;US&lt;/span&gt; that regulator is the &lt;span class="caps"&gt;FCC&lt;/span&gt;, and the relevant rule is the well-known 47 &lt;span class="caps"&gt;CFR&lt;/span&gt; Part 15 / Part 22-27 framework: a radio module is certified for a specific host device, with a specific antenna gain, in a specific physical configuration. If you yank the modem out of one laptop and stick it into another, the certification no longer&amp;nbsp;applies.&lt;/p&gt;
&lt;p&gt;To enforce this, modem vendors ship the radio in a state where the &lt;span class="caps"&gt;RF&lt;/span&gt; subsystem is &lt;em&gt;administratively disabled&lt;/em&gt; until the host system performs a vendor-defined unlock handshake. The handshake is meant to be a challenge-response proof that the modem is sitting in a host the &lt;span class="caps"&gt;OEM&lt;/span&gt; has already certified together with the modem. Until it succeeds, attempts to fully enable the modem fail - upstream documentation talks about the enable step erroring out, and on my &lt;span class="caps"&gt;XMM7560&lt;/span&gt; specifically the symptom was&amp;nbsp;that &lt;code&gt;AT+CFUN=1&lt;/code&gt; came&amp;nbsp;back &lt;code&gt;OK&lt;/code&gt; while the radio never actually came up on the&amp;nbsp;network.&lt;/p&gt;
&lt;p&gt;In practice, it behaves like an &lt;span class="caps"&gt;OEM&lt;/span&gt; lock justified in regulatory terms. The same &lt;span class="caps"&gt;XMM7560&lt;/span&gt; family has shown up across multiple &lt;span class="caps"&gt;OEM&lt;/span&gt; laptops, with different vendors using different unlock material. The radio is identical across hosts; what changes is the &lt;span class="caps"&gt;OEM&lt;/span&gt;-controlled signature that gates its&amp;nbsp;use.&lt;/p&gt;
&lt;p&gt;On Windows this is invisible because the Lenovo / Dell / &lt;span class="caps"&gt;HP&lt;/span&gt; driver stack performs the handshake at boot. On Linux the responsibility falls to ModemManager, which provides the unlock mechanism and even ships some upstream helper scripts itself - but, since version 1.18.4, leaves activation to either the user or a vendor package rather than running anything by&amp;nbsp;default.&lt;/p&gt;
&lt;h2 id="the-modemmanager-fcc-unlock-mechanism"&gt;The ModemManager &lt;span class="caps"&gt;FCC&lt;/span&gt; Unlock&amp;nbsp;Mechanism&lt;/h2&gt;
&lt;p&gt;ModemManager&amp;#8217;s design here is sensible, and it is worth understanding properly because it is more nuanced than a single search path. There are &lt;em&gt;three&lt;/em&gt; directories involved, each with a distinct&amp;nbsp;role:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;${libdir}/ModemManager/fcc-unlock.d/&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;(e.g. &lt;code&gt;/usr/lib64/ModemManager/fcc-unlock.d/&lt;/code&gt; on Fedora) - this is where vendor-shipped, third-party unlock tools live. If Lenovo packages their helper for your distribution, this is where it&amp;nbsp;goes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/usr/share/ModemManager/fcc-unlock.available.d/&lt;/code&gt;&lt;/strong&gt; - this is where ModemManager itself ships a small library of unlock scripts upstream. They are present on the system but not&amp;nbsp;enabled.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/etc/ModemManager/fcc-unlock.d/&lt;/code&gt;&lt;/strong&gt; - this is where &lt;em&gt;you&lt;/em&gt;, the system administrator, opt in to one of the available scripts (or to your own) by symlinking it under the appropriate &lt;span class="caps"&gt;VID&lt;/span&gt;:&lt;span class="caps"&gt;PID&lt;/span&gt;&amp;nbsp;name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In all three locations the file is named after the modem&amp;#8217;s &lt;span class="caps"&gt;USB&lt;/span&gt; or &lt;span class="caps"&gt;PCI&lt;/span&gt; &lt;span class="caps"&gt;VID&lt;/span&gt;:&lt;span class="caps"&gt;PID&lt;/span&gt;. For my &lt;span class="caps"&gt;XMM7560&lt;/span&gt; the relevant name&amp;nbsp;is &lt;code&gt;8086:7560&lt;/code&gt; - Intel&amp;#8217;s vendor &lt;span class="caps"&gt;ID&lt;/span&gt;, the modem&amp;#8217;s product &lt;span class="caps"&gt;ID&lt;/span&gt;. ModemManager invokes the helper with the modem&amp;#8217;s DBus path and the names of its control ports as arguments, and waits for it to exit successfully. If the helper returns 0, ModemManager continues bringing the modem up; if not, the modem stays&amp;nbsp;disabled.&lt;/p&gt;
&lt;p&gt;Two things are worth noting about this layout. First, the split&amp;nbsp;between &lt;code&gt;/etc&lt;/code&gt; and the&amp;nbsp;upstream &lt;code&gt;fcc-unlock.available.d&lt;/code&gt; directory exists precisely so that activation is an explicit decision, not something that happens by default - that change landed in ModemManager 1.18.4. Second, the helper can be &lt;em&gt;any&lt;/em&gt; executable. A binary, a Python script, a shell script - ModemManager doesn&amp;#8217;t care. That is the seam I&amp;nbsp;used.&lt;/p&gt;
&lt;h2 id="lenovos-solution-a-proprietary-helper-package"&gt;Lenovo&amp;#8217;s Solution: A Proprietary Helper&amp;nbsp;Package&lt;/h2&gt;
&lt;p&gt;Lenovo&amp;#8217;s answer to this is &lt;a href="https://github.com/lenovo/lenovo-wwan-unlock"&gt;&lt;code&gt;lenovo-wwan-unlock&lt;/code&gt;&lt;/a&gt;, a GitHub-distributed package of proprietary helpers and shared libraries with setup scripts, SELinux policy and release packaging for several modules and systems. The &lt;span class="caps"&gt;README&lt;/span&gt; itself describes the project as &amp;#8220;&lt;span class="caps"&gt;FCC&lt;/span&gt; and &lt;span class="caps"&gt;DPR&lt;/span&gt; unlock for Lenovo PCs&amp;#8221;, which is worth pausing on: it is not just one unlock primitive. On supported systems Lenovo&amp;#8217;s tooling also covers &lt;span class="caps"&gt;DPR&lt;/span&gt; (Dynamic Power Reduction) / &lt;span class="caps"&gt;SAR&lt;/span&gt;-related behaviour, the mechanism by which the laptop tells the modem to back off transmit power based on proximity sensors. So a vendor helper in this space can do more than flip a single lock&amp;nbsp;bit.&lt;/p&gt;
&lt;p&gt;You install the package, it drops the right files into the right places, and it works. It is&amp;nbsp;also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Closed-source, with no way to audit what the helpers actually do to your&amp;nbsp;modem&lt;/li&gt;
&lt;li&gt;Linked against specific runtime libraries, so on slightly newer or older distributions it can break in surprising&amp;nbsp;ways&lt;/li&gt;
&lt;li&gt;The only path the vendor offers, on a free operating system, to use a piece of hardware you already paid&amp;nbsp;for&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The whole point of running Fedora on a ThinkPad is that I get to look at, modify, and trust every component of the user-space stack. Even if the proprietary helper is benign, accepting it as the permanent answer to &amp;#8220;how do I use my modem&amp;#8221; felt like the wrong default. So I went looking for an&amp;nbsp;alternative.&lt;/p&gt;
&lt;h2 id="the-alternative-a-shell-script-that-does-the-same-thing"&gt;The Alternative: A Shell Script That Does the Same&amp;nbsp;Thing&lt;/h2&gt;
&lt;p&gt;The alternative turned out to be sitting in a &lt;a href="https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/merge_requests/1141#21974e3af4aa392e9cd9a19cfadb2e992ed26538"&gt;ModemManager merge request&lt;/a&gt;, posted by Floris Stoica-Marcu. It is a single bash script, about 100 lines, &lt;span class="caps"&gt;CC0&lt;/span&gt;-licensed, that performs the entire &lt;span class="caps"&gt;XMM7560&lt;/span&gt; unlock handshake using&amp;nbsp;only &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;xxd&lt;/code&gt; and &lt;code&gt;sha256sum&lt;/code&gt;. No compiled code, no hidden state, no vendor &lt;span class="caps"&gt;SDK&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;I want to be very clear about credit: I did not write this script. Floris did. What I did was take their script, drop it&amp;nbsp;into &lt;code&gt;/opt/unlocker/8086&lt;/code&gt;, and symlink it from ModemManager&amp;#8217;s helper&amp;nbsp;directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~ ❯ ls -lah /usr/lib64/ModemManager/fcc-unlock.d/
Permissions Size User Date Modified Name
drwxr-xr-x@    - root 11 Apr 12:14  .
drwxr-xr-x@    - root 11 Apr 12:14  ..
lrwxrwxrwx@    - root 11 Apr 12:14  8086:7560 -&amp;gt; /opt/unlocker/8086
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is the entire installation.&amp;nbsp;Restart &lt;code&gt;ModemManager.service&lt;/code&gt;, plug in the &lt;span class="caps"&gt;SIM&lt;/span&gt;, and watch the&amp;nbsp;journal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Apr 11 12:15:13 neochristop ModemManager[9628]: [modem0] state changed (enabling -&amp;gt; enabled)
Apr 11 12:15:15 neochristop ModemManager[9628]: [modem0] 3GPP registration state changed (registering -&amp;gt; home)
Apr 11 12:15:15 neochristop ModemManager[9628]: [modem0] state changed (enabled -&amp;gt; registered)
Apr 11 12:15:16 neochristop ModemManager[9628]: [modem0] state changed (connecting -&amp;gt; connected)
Apr 11 12:15:16 neochristop ModemManager[9628]: [modem0] simple connect state (10/10): all done
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the interface itself, with a real public IPv6 from my&amp;nbsp;carrier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~ ❯ ifconfig wwan0
wwan0: flags=209&amp;lt;UP,POINTOPOINT,RUNNING,NOARP&amp;gt;  mtu 1500
        inet 10.127.157.102  netmask 255.255.255.0  destination 10.127.157.102
        inet6 fe80::b2ab:8cc1:6305:c055  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        inet6 2a02:3037:48d:1b55:f07e:922f:8154:e65  prefixlen 64  scopeid 0x0&amp;lt;global&amp;gt;
        RX packets 199131  bytes 251330013 (239.6 MiB)
        TX packets 52690  bytes 4544146 (4.3 MiB)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is 240 MiB of traffic over &lt;span class="caps"&gt;LTE&lt;/span&gt; through an entirely auditable user-space&amp;nbsp;path.&lt;/p&gt;
&lt;h2 id="how-the-unlock-actually-works"&gt;How the Unlock Actually&amp;nbsp;Works&lt;/h2&gt;
&lt;p&gt;Here is the part that matters for anyone who wants to understand what they are running. The Intel &lt;span class="caps"&gt;XMM7560&lt;/span&gt; unlock is a challenge/response handshake using a small set of vendor &lt;span class="caps"&gt;AT&lt;/span&gt; commands. Walking through the script in&amp;nbsp;order:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Find the &lt;span class="caps"&gt;AT&lt;/span&gt; control port.&lt;/strong&gt; ModemManager passes the modem&amp;#8217;s control port names as arguments. The modem exposes both &lt;span class="caps"&gt;MBIM&lt;/span&gt; and &lt;span class="caps"&gt;AT&lt;/span&gt; ports; the &lt;span class="caps"&gt;AT&lt;/span&gt; port is what we need. The script picks it by&amp;nbsp;reading &lt;code&gt;/sys/class/wwan/&amp;lt;port&amp;gt;/type&lt;/code&gt; (Linux 5.14+) or by name pattern (older&amp;nbsp;kernels):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;PORT&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;AT&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/sys/class/wwan/&lt;/span&gt;&lt;span class="nv"&gt;$PORT&lt;/span&gt;&lt;span class="s2"&gt;/type&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;AT_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PORT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The chosen port is something&amp;nbsp;like &lt;code&gt;wwan0at0&lt;/code&gt;, which is then opened&amp;nbsp;as &lt;code&gt;/dev/wwan0at0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Talk &lt;span class="caps"&gt;AT&lt;/span&gt; over a file descriptor.&lt;/strong&gt; Bash can open a character device on a numbered file descriptor&amp;nbsp;with &lt;code&gt;exec&lt;/code&gt;, write a command, and read the reply. There is&amp;nbsp;no &lt;code&gt;socat&lt;/code&gt;,&amp;nbsp;no &lt;code&gt;chat&lt;/code&gt;, no helper - just bash redirection. The script wraps that in a&amp;nbsp;small &lt;code&gt;at_command&lt;/code&gt; function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;at_command&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;&amp;lt;&amp;gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DEVICE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;\r&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;answer&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;answer&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;99&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;-
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The&amp;nbsp;double &lt;code&gt;read&lt;/code&gt; is there because the modem echoes the command on the first line and replies on the second. Crude, but it works on this&amp;nbsp;modem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Request a challenge.&lt;/strong&gt; The &lt;span class="caps"&gt;XMM7560&lt;/span&gt; implements a vendor&amp;nbsp;command, &lt;code&gt;AT+GTFCCLOCKGEN&lt;/code&gt;, that returns a fresh 32-bit pseudo-random nonce. This is the &amp;#8220;challenge&amp;#8221; half of the&amp;nbsp;handshake:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;RAW_CHALLENGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;at_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;at+gtfcclockgen&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;CHALLENGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$RAW_CHALLENGE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0x[0-9a-fA-F]\+&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{print $1}&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;4. Compute the response.&lt;/strong&gt; This is the only &amp;#8220;magic&amp;#8221; step, and it is not really magic at all. The response is a &lt;span class="caps"&gt;SHA&lt;/span&gt;-256 hash of the challenge concatenated with a fixed per-vendor secret, with some byte-order shuffling on each end. The Lenovo per-vendor secret in this script is the&amp;nbsp;constant &lt;code&gt;bb23be7f&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;VENDOR_ID_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bb23be7f&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;HEX_CHALLENGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%08x&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CHALLENGE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REVERSE_HEX_CHALLENGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;reverseWithLittleEndian&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HEX_CHALLENGE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;COMBINED_CHALLENGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REVERSE_HEX_CHALLENGE&lt;/span&gt;&lt;span class="si"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;VENDOR_ID_HASH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;RESPONSE_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$COMBINED_CHALLENGE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xxd&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sha256sum&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TRUNCATED_RESPONSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%.8s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESPONSE_HASH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REVERSED_RESPONSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;reverseWithLittleEndian&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TRUNCATED_RESPONSE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;RESPONSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%d&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0x&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REVERSED_RESPONSE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In plain&amp;nbsp;English:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take the challenge as a 32-bit value, write it as 8 hex chars in little-endian byte&amp;nbsp;order.&lt;/li&gt;
&lt;li&gt;Append the Lenovo vendor secret&amp;nbsp;(&lt;code&gt;bb23be7f&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Decode the resulting hex string into raw bytes&amp;nbsp;(&lt;code&gt;xxd -r -p&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Compute &lt;span class="caps"&gt;SHA&lt;/span&gt;-256 over those&amp;nbsp;bytes.&lt;/li&gt;
&lt;li&gt;Take the first 4 bytes of the digest, swap to little-endian, and convert to a decimal&amp;nbsp;integer.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That decimal integer is the&amp;nbsp;response.&lt;/p&gt;
&lt;p&gt;This is the entire secret embedded in Lenovo&amp;#8217;s helper: a four-byte constant and the byte-order conventions around it. There is no hardware-bound key, no &lt;span class="caps"&gt;TPM&lt;/span&gt;-sealed secret, no per-laptop derivation - just a baked-in constant that someone reverse-engineered out of the Lenovo helper years ago. The &lt;span class="caps"&gt;XMM7560&lt;/span&gt; hardware does not actually verify &lt;em&gt;that&lt;/em&gt; host certified it; it verifies that whoever is talking to it knows the&amp;nbsp;constant.&lt;/p&gt;
&lt;p&gt;That is why I am comfortable running this script. I am not defeating any hardware-bound secret here; I am reproducing the same host-side response the proprietary helper would generate, in shell instead of machine&amp;nbsp;code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Send the response and unlock the radio.&lt;/strong&gt; Three more &lt;span class="caps"&gt;AT&lt;/span&gt;&amp;nbsp;commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;at_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;at+gtfcclockver=&lt;/span&gt;&lt;span class="nv"&gt;$RESPONSE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# verify the response&lt;/span&gt;
at_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;at+gtfcclockmodeunlock&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# actually flip the lock bit&lt;/span&gt;
at_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;at+cfun=1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="c1"&gt;# bring the radio fully online&lt;/span&gt;
at_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;at+gtfcclockstate&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="c1"&gt;# read the current lock state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the&amp;nbsp;final &lt;code&gt;at+gtfcclockstate&lt;/code&gt; reports &lt;code&gt;OK&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt;, the modem is unlocked and the script exits 0. ModemManager picks up from there, runs its normal enable sequence, and the modem registers on the network. If the response is wrong, the modem responds&amp;nbsp;with &lt;code&gt;ERROR&lt;/code&gt; and the script retries up to nine times with a 0.5 second&amp;nbsp;backoff.&lt;/p&gt;
&lt;p&gt;That is the entire handshake. About 100 lines of bash, no compiled dependencies, every step&amp;nbsp;inspectable.&lt;/p&gt;
&lt;h2 id="installing-it-yourself"&gt;Installing It&amp;nbsp;Yourself&lt;/h2&gt;
&lt;p&gt;If you have the same modem - check&amp;nbsp;with &lt;code&gt;lspci | grep XMM&lt;/code&gt; for &lt;code&gt;XMM7560 LTE Advanced Pro&lt;/code&gt; - the steps to swap the proprietary helper for this script are short. One caveat first about paths. On Fedora, Lenovo&amp;#8217;s package and some third-party tooling may&amp;nbsp;use &lt;code&gt;/usr/lib64/ModemManager/fcc-unlock.d/&lt;/code&gt;, but the upstream user-facing path for manually enabled scripts&amp;nbsp;is &lt;code&gt;/etc/ModemManager/fcc-unlock.d/&lt;/code&gt;. The example below uses the upstream user&amp;nbsp;path.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;/opt/unlocker
sudo&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0755&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8086&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/unlocker/8086
sudo&lt;span class="w"&gt; &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/opt/unlocker/8086&lt;span class="w"&gt; &lt;/span&gt;/etc/ModemManager/fcc-unlock.d/8086:7560
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;ModemManager.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then&amp;nbsp;watch &lt;code&gt;journalctl -u ModemManager -f&lt;/code&gt; while you bring the connection up in NetworkManager. You should see the modem walk&amp;nbsp;through &lt;code&gt;enabling -&amp;gt; enabled -&amp;gt; registered -&amp;gt; connecting -&amp;gt; connected&lt;/code&gt; and end up with an interface&amp;nbsp;like &lt;code&gt;wwan0&lt;/code&gt; carrying real&amp;nbsp;traffic.&lt;/p&gt;
&lt;p&gt;If you want to see exactly what the script does on your machine,&amp;nbsp;set &lt;code&gt;FCC_UNLOCK_DEBUG_LOG=1&lt;/code&gt; in the environment ModemManager runs in, and the script will append every step (including the &lt;span class="caps"&gt;AT&lt;/span&gt; exchanges)&amp;nbsp;to &lt;code&gt;/var/log/mm-xmm7560-fcc.log&lt;/code&gt;. That is a great way to convince yourself the script is doing nothing but the handshake described&amp;nbsp;above.&lt;/p&gt;
&lt;p&gt;If you have a &lt;em&gt;different&lt;/em&gt; modem - a Sierra Wireless &lt;span class="caps"&gt;EM7565&lt;/span&gt;, a Quectel &lt;span class="caps"&gt;EM160&lt;/span&gt;, a different Intel chip - the same overall pattern (drop a helper&amp;nbsp;into &lt;code&gt;fcc-unlock.d&lt;/code&gt; named after the &lt;span class="caps"&gt;VID&lt;/span&gt;:&lt;span class="caps"&gt;PID&lt;/span&gt;) applies, but the &lt;span class="caps"&gt;AT&lt;/span&gt; command set and the secret are different. Don&amp;#8217;t blindly use this script on hardware it wasn&amp;#8217;t written&amp;nbsp;for.&lt;/p&gt;
&lt;h2 id="a-disclaimer-about-jurisdiction"&gt;A Disclaimer About&amp;nbsp;Jurisdiction&lt;/h2&gt;
&lt;p&gt;I have to write this part carefully, because the legal picture here is genuinely fuzzy, varies by country, and I am not a&amp;nbsp;lawyer.&lt;/p&gt;
&lt;p&gt;The &lt;span class="caps"&gt;FCC&lt;/span&gt; unlock mechanism exists because regulators in many jurisdictions require radio equipment to be certified as part of a specific host device. In the &lt;span class="caps"&gt;US&lt;/span&gt; that is the &lt;span class="caps"&gt;FCC&lt;/span&gt;&amp;#8217;s modular and limited modular approval rules. In the &lt;span class="caps"&gt;EU&lt;/span&gt; the &lt;span class="caps"&gt;RED&lt;/span&gt; directive (2014/53/&lt;span class="caps"&gt;EU&lt;/span&gt;) imposes broadly similar obligations on the manufacturer. Other countries have their own equivalents. The &lt;span class="caps"&gt;OEM&lt;/span&gt; - in this case Lenovo - is the entity that holds the certification, and the unlock procedure exists so that the &lt;span class="caps"&gt;OEM&lt;/span&gt; can attest to the modem that &amp;#8220;yes, I am still the host you were certified&amp;nbsp;with.&amp;#8221;&lt;/p&gt;
&lt;p&gt;When you replace the &lt;span class="caps"&gt;OEM&lt;/span&gt;&amp;#8217;s unlock helper with your own, you are bypassing that attestation. The modem does not become illegal hardware, and the radio characteristics do not change one bit - the script flips exactly the same lock bit the proprietary helper would have - but you may, depending on where you live and how strictly the rule is read, be operating a radio in a configuration the regulator did not formally bless. In some jurisdictions that is a non-issue for an end user modifying their own equipment for personal use. In others it might not&amp;nbsp;be.&lt;/p&gt;
&lt;p&gt;To be specific about the risks I am personally aware&amp;nbsp;of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;United States (&lt;span class="caps"&gt;FCC&lt;/span&gt;):&lt;/strong&gt; &lt;span class="caps"&gt;US&lt;/span&gt; equipment authorization rules are not especially sympathetic to modified certified equipment, even though the direct compliance burden usually falls on manufacturers and grantees rather than on end&amp;nbsp;users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;European Union (&lt;span class="caps"&gt;RED&lt;/span&gt;):&lt;/strong&gt; the user is generally not the addressee of the directive, but a modification that departs from the certified configuration creates an obvious compliance grey area around the original &lt;span class="caps"&gt;CE&lt;/span&gt;-marked&amp;nbsp;configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;United Kingdom, Switzerland, Canada, Australia:&lt;/strong&gt; broadly similar to the &lt;span class="caps"&gt;EU&lt;/span&gt;&amp;nbsp;model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anywhere else:&lt;/strong&gt; check your own regulator before you assume you are&amp;nbsp;fine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I am running this on my own laptop, on a &lt;span class="caps"&gt;SIM&lt;/span&gt; I personally own, on bands and with output power that the modem itself enforces in firmware. I consider the risk to me, in my jurisdiction, acceptable. I cannot make that call for you. &lt;strong&gt;If you are not comfortable with the regulatory ambiguity, keep using Lenovo&amp;#8217;s helper package, or do not use the modem at&amp;nbsp;all.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I will also point out the obvious: this changes nothing about the radio&amp;#8217;s actual emissions. The modem firmware still respects the band tables, the antenna gain limits, and the transmit power caps that Intel certified. You cannot &amp;#8220;unlock more bands&amp;#8221; or &amp;#8220;transmit at higher power&amp;#8221; through this handshake. You are flipping the bit that says &amp;#8220;I am allowed to talk to the network at all,&amp;#8221; nothing&amp;nbsp;else.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping&amp;nbsp;Up&lt;/h2&gt;
&lt;p&gt;This was not a heroic hack. I did not reverse-engineer the modem, and I did not write the script. Floris Stoica-Marcu did the real work years ago in a ModemManager merge request. What I did was notice that Lenovo&amp;#8217;s proprietary helper was not the only option, install the auditable alternative, and write down what it actually does. That still feels worth documenting, because free software does not stop mattering just because the proprietary piece is&amp;nbsp;small.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/merge_requests/1141#21974e3af4aa392e9cd9a19cfadb2e992ed26538"&gt;ModemManager merge request !1141 - the bash unlocker&lt;/a&gt; - the script and the discussion around&amp;nbsp;it&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lenovo/lenovo-wwan-unlock/"&gt;lenovo-wwan-unlock&lt;/a&gt; - Lenovo&amp;#8217;s official proprietary helper package, for&amp;nbsp;comparison&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modemmanager.org/docs/modemmanager/fcc-unlock/"&gt;ModemManager &lt;span class="caps"&gt;FCC&lt;/span&gt; Unlock Tools documentation&lt;/a&gt; - upstream description of the helper directory&amp;nbsp;mechanism&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32014L0053"&gt;Directive 2014/53/&lt;span class="caps"&gt;EU&lt;/span&gt; (&lt;span class="caps"&gt;RED&lt;/span&gt;)&lt;/a&gt; - &lt;span class="caps"&gt;EU&lt;/span&gt; radio equipment&amp;nbsp;directive&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="linux"/><category term="fedora"/><category term="hardware"/><category term="modemmanager"/><category term="lte"/><category term="wwan"/><category term="foss"/><category term="thinkpad"/></entry><entry><title>Podman on FreeBSD: OCI Containers Without systemd</title><link href="https://blog.hofstede.it/podman-on-freebsd-oci-containers-without-systemd/" rel="alternate"/><published>2026-04-06T00:00:00+02:00</published><updated>2026-04-06T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-04-06:/podman-on-freebsd-oci-containers-without-systemd/</id><summary type="html">&lt;p&gt;Podman runs on FreeBSD too - but without systemd, the workflow is different. This follow-up to my Linux Podman deep dive covers how to run both native FreeBSD and Linux &lt;span class="caps"&gt;OCI&lt;/span&gt; containers on FreeBSD, how container lifecycle management works without Quadlets, and how Podman complements Jails rather than replacing&amp;nbsp;them.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;My &lt;a href="https://blog.hofstede.it/podman-in-production-quadlets-secrets-auto-updates-and-docker-compatibility/"&gt;previous article&lt;/a&gt; covered Podman in depth on Linux - Quadlets, systemd integration, secrets management, auto-updates. That article was explicitly a Linux story. But Podman also runs on FreeBSD, and the experience is different enough to deserve its own&amp;nbsp;treatment.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Podman FreeBSD" src="https://blog.hofstede.it/images/2026-04-06-podman-freebsd.png" title="Podman FreeBSD"&gt; &lt;/p&gt;
&lt;p&gt;FreeBSD&amp;#8217;s Podman support has been maturing steadily. The FreeBSD port is current and actively maintained, with a dedicated port maintainer who participates in upstream Podman development. That said, the port is still labeled experimental and intended for evaluation and testing - so treat it as a powerful option with some rough edges rather than a drop-in Linux-equivalent experience. The absence of systemd changes the operational model fundamentally. There are no Quadlets, no systemd timers, no journald integration. What you get instead is Podman&amp;#8217;s core container runtime integrated with FreeBSD&amp;#8217;s own service management, and the ability to run both native FreeBSD containers and Linux containers side by&amp;nbsp;side.&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#installing-podman-on-freebsd"&gt;Installing Podman on FreeBSD&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#prerequisites"&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#enabling-the-service"&gt;Enabling the&amp;nbsp;Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#verification"&gt;Verification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#native-freebsd-oci-containers"&gt;Native FreeBSD &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#linux-containers-via-the-linuxulator"&gt;Linux Containers via the Linuxulator&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#prerequisites_1"&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#running-linux-images"&gt;Running Linux&amp;nbsp;Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#limitations"&gt;Limitations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#container-lifecycle-without-systemd"&gt;Container Lifecycle Without systemd&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#podmans-built-in-restart-policy"&gt;Podman&amp;#8217;s Built-in Restart&amp;nbsp;Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rcd-service-scripts"&gt;rc.d Service&amp;nbsp;Scripts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-you-lose-without-quadlets"&gt;What You Lose Without&amp;nbsp;Quadlets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#logging"&gt;Logging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#image-updates"&gt;Image&amp;nbsp;Updates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#podman-and-jails-complementary-not-competing"&gt;Podman and Jails: Complementary, Not&amp;nbsp;Competing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#practical-tips"&gt;Practical Tips&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#networking"&gt;Networking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#storage"&gt;Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#secrets"&gt;Secrets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#when-to-use-what"&gt;When to Use&amp;nbsp;What&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wrapping-up"&gt;Wrapping&amp;nbsp;Up&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="installing-podman-on-freebsd"&gt;Installing Podman on&amp;nbsp;FreeBSD&lt;/h2&gt;
&lt;p&gt;Podman is available in the FreeBSD ports tree and as a binary&amp;nbsp;package:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;podman
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;#8217;s also&amp;nbsp;a &lt;code&gt;podman-suite&lt;/code&gt; meta package that pulls in additional tooling (Buildah, Skopeo) if you need a more complete container&amp;nbsp;workflow.&lt;/p&gt;
&lt;p&gt;This pulls in the Podman binary, its dependencies, and the necessary &lt;span class="caps"&gt;OCI&lt;/span&gt; runtime components. On FreeBSD, Podman relies on FreeBSD-specific runtime support, typically&amp;nbsp;involving &lt;code&gt;ocijail&lt;/code&gt; - a FreeBSD-native &lt;span class="caps"&gt;OCI&lt;/span&gt; runtime that leverages jails under the hood to provide the container isolation boundary, rather&amp;nbsp;than &lt;code&gt;runc&lt;/code&gt; or &lt;code&gt;crun&lt;/code&gt; from the Linux world. Container data lives&amp;nbsp;under &lt;code&gt;/var/db/containers/&lt;/code&gt; by default&amp;nbsp;(not &lt;code&gt;/var/lib/containers/&lt;/code&gt; as on&amp;nbsp;Linux).&lt;/p&gt;
&lt;h3 id="prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before Podman will work, you need two things beyond the package itself: fdescfs and &lt;span class="caps"&gt;PF&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;fdescfs&lt;/strong&gt; - Podman&amp;#8217;s container monitor&amp;nbsp;(&lt;code&gt;conmon&lt;/code&gt;)&amp;nbsp;needs &lt;code&gt;fdescfs(5)&lt;/code&gt; mounted&amp;nbsp;on &lt;code&gt;/dev/fd&lt;/code&gt; to properly support container restart policies. If it&amp;#8217;s not already&amp;nbsp;mounted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mount&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;fdescfs&lt;span class="w"&gt; &lt;/span&gt;fdesc&lt;span class="w"&gt; &lt;/span&gt;/dev/fd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Make it permanent&amp;nbsp;in &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;fdesc   /dev/fd         fdescfs         rw      0       0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; firewall&lt;/strong&gt; - Container networking relies on &lt;span class="caps"&gt;NAT&lt;/span&gt; to route container traffic out to the host&amp;#8217;s network. This requires &lt;span class="caps"&gt;PF&lt;/span&gt;. The Podman package ships a sample&amp;nbsp;configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/containers/pf.conf.sample&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/pf.conf&lt;/code&gt; and set&amp;nbsp;the &lt;code&gt;v4egress_if&lt;/code&gt; and &lt;code&gt;v6egress_if&lt;/code&gt; variables to your network interface(s), then enable &lt;span class="caps"&gt;PF&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES
service&lt;span class="w"&gt; &lt;/span&gt;pf&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To support port redirections from the host to services inside containers&amp;nbsp;(e.g., &lt;code&gt;podman run -p 8080:80&lt;/code&gt;), you also need&amp;nbsp;the &lt;code&gt;pf&lt;/code&gt; kernel module loaded and local filtering&amp;nbsp;enabled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pf_load=&amp;quot;YES&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/boot/loader.conf
kldload&lt;span class="w"&gt; &lt;/span&gt;pf
sysctl&lt;span class="w"&gt; &lt;/span&gt;net.pf.filter_local&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;net.pf.filter_local=1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/sysctl.conf.local
service&lt;span class="w"&gt; &lt;/span&gt;pf&lt;span class="w"&gt; &lt;/span&gt;restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The sample &lt;span class="caps"&gt;PF&lt;/span&gt; configuration includes the&amp;nbsp;necessary &lt;code&gt;nat-anchor "cni-rdr/*"&lt;/code&gt; rule for redirect support. If you&amp;#8217;re integrating into an existing &lt;span class="caps"&gt;PF&lt;/span&gt; ruleset rather than using the sample, make sure that anchor is&amp;nbsp;present.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt; - If your system uses &lt;span class="caps"&gt;ZFS&lt;/span&gt; (and it should), create a dedicated dataset for&amp;nbsp;Podman:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;mountpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/db/containers&lt;span class="w"&gt; &lt;/span&gt;zroot/containers
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If &lt;span class="caps"&gt;ZFS&lt;/span&gt; is not available, change the storage driver&amp;nbsp;to &lt;code&gt;vfs&lt;/code&gt; in &lt;code&gt;/usr/local/etc/containers/storage.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-I&lt;span class="w"&gt; &lt;/span&gt;.bak&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/driver = &amp;quot;zfs&amp;quot;/driver = &amp;quot;vfs&amp;quot;/&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/containers/storage.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="enabling-the-service"&gt;Enabling the&amp;nbsp;Service&lt;/h3&gt;
&lt;p&gt;The FreeBSD ports package includes rc.d integration for Podman and its &lt;span class="caps"&gt;API&lt;/span&gt;/socket&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;podman_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES
service&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;API&lt;/span&gt;/socket service is particularly useful when you need Docker-compatible socket access or &lt;span class="caps"&gt;API&lt;/span&gt;-driven tooling (like Traefik&amp;#8217;s Docker provider or &lt;span class="caps"&gt;CI&lt;/span&gt; runners). The daemonless-vs-service story on FreeBSD is less clean-cut than the usual Linux Podman model - the FreeBSD port exposes distinct service knobs&amp;nbsp;like &lt;code&gt;podman_enable&lt;/code&gt; and &lt;code&gt;podman_service_enable&lt;/code&gt;, and the exact role of the service in local &lt;span class="caps"&gt;CLI&lt;/span&gt; operations depends on your configuration. When in doubt, enable&amp;nbsp;it.&lt;/p&gt;
&lt;h3 id="verification"&gt;Verification&lt;/h3&gt;
&lt;p&gt;With everything in place, verify with the Podman hello-world&amp;nbsp;image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;quay.io/dougrabson/hello
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you see the Podman mascot, you&amp;#8217;re&amp;nbsp;good.&lt;/p&gt;
&lt;h2 id="native-freebsd-oci-containers"&gt;Native FreeBSD &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Containers&lt;/h2&gt;
&lt;p&gt;This is the part that surprises people: Podman on FreeBSD can run containers built from native FreeBSD base images. These aren&amp;#8217;t Linux containers running through a compatibility layer - they&amp;#8217;re actual FreeBSD userland processes running in jail-based&amp;nbsp;isolation.&lt;/p&gt;
&lt;p&gt;The FreeBSD community maintains &lt;span class="caps"&gt;OCI&lt;/span&gt; images specifically for this&amp;nbsp;purpose:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;-it&lt;span class="w"&gt; &lt;/span&gt;docker.io/freebsd/freebsd-runtime:15.0&lt;span class="w"&gt; &lt;/span&gt;freebsd-version&lt;span class="w"&gt; &lt;/span&gt;-u
&lt;span class="m"&gt;15&lt;/span&gt;.0-RELEASE
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Inside that container, you&amp;#8217;re in FreeBSD userland&amp;nbsp;- &lt;code&gt;freebsd-version&lt;/code&gt; reports a real FreeBSD release, and standard FreeBSD tooling works as you&amp;#8217;d expect from the image contents. You can build and run FreeBSD software natively, with &lt;span class="caps"&gt;OCI&lt;/span&gt; image packaging and&amp;nbsp;distribution.&lt;/p&gt;
&lt;p&gt;Available base images&amp;nbsp;include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;freebsd/freebsd-runtime&lt;/code&gt;&lt;/strong&gt; - minimal FreeBSD userland, suitable for running pre-built&amp;nbsp;binaries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;freebsd/freebsd&lt;/code&gt;&lt;/strong&gt; - fuller base with development tools, suitable for building software inside the&amp;nbsp;container&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can build your own FreeBSD &lt;span class="caps"&gt;OCI&lt;/span&gt; images with a&amp;nbsp;standard &lt;code&gt;Containerfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;docker.io/freebsd/freebsd-runtime:15.0&lt;/span&gt;

&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;nginx
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nginx.conf&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/nginx/nginx.conf
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;80&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nginx&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-g&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;daemon off;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Build and run it like any &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;my-freebsd-nginx&lt;span class="w"&gt; &lt;/span&gt;.
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;:80&lt;span class="w"&gt; &lt;/span&gt;my-freebsd-nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is native performance - no emulation, no translation layer. The container process runs as a FreeBSD process inside a jail, with &lt;span class="caps"&gt;OCI&lt;/span&gt;-standard image packaging on&amp;nbsp;top.&lt;/p&gt;
&lt;h2 id="linux-containers-via-the-linuxulator"&gt;Linux Containers via the&amp;nbsp;Linuxulator&lt;/h2&gt;
&lt;p&gt;FreeBSD&amp;#8217;s Linux binary compatibility layer (the Linuxulator) extends to Podman.&amp;nbsp;With &lt;code&gt;linux64.ko&lt;/code&gt; loaded, Podman can run Linux &lt;span class="caps"&gt;OCI&lt;/span&gt; images - the same images you&amp;#8217;d pull from Docker Hub for any Linux-based&amp;nbsp;deployment.&lt;/p&gt;
&lt;h3 id="prerequisites_1"&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Load the Linux kernel module on the&amp;nbsp;host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Persistent across reboots&lt;/span&gt;
sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;linux_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;linux64_load=&amp;quot;YES&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/boot/loader.conf

&lt;span class="c1"&gt;# Load immediately&lt;/span&gt;
kldload&lt;span class="w"&gt; &lt;/span&gt;linux64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you&amp;#8217;ve followed my &lt;a href="https://blog.hofstede.it/running-a-factorio-headless-server-on-freebsd-with-the-linuxulator/"&gt;Factorio on FreeBSD&lt;/a&gt; article, this is the same Linuxulator setup - just applied to &lt;span class="caps"&gt;OCI&lt;/span&gt; containers instead of standalone&amp;nbsp;binaries.&lt;/p&gt;
&lt;h3 id="running-linux-images"&gt;Running Linux&amp;nbsp;Images&lt;/h3&gt;
&lt;p&gt;With the Linuxulator loaded, pulling and running Linux images works as you&amp;#8217;d&amp;nbsp;expect:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;--os&lt;span class="o"&gt;=&lt;/span&gt;linux&lt;span class="w"&gt; &lt;/span&gt;docker.io/library/alpine&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/etc/os-release&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-1
&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Alpine Linux&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--os=linux&lt;/code&gt; flag tells Podman to select the Linux variant of a multi-platform image. Inside the container, the process sees a Linux environment - glibc calls work, Linux-specific syscalls are translated by the&amp;nbsp;Linuxulator.&lt;/p&gt;
&lt;p&gt;This means many Linux-oriented &lt;span class="caps"&gt;OCI&lt;/span&gt; images can run on FreeBSD without modification, especially userland-heavy workloads that don&amp;#8217;t depend on Linux-specific kernel facilities. Your existing application server images, many database images, and standard web tooling are good&amp;nbsp;candidates.&lt;/p&gt;
&lt;h3 id="limitations"&gt;Limitations&lt;/h3&gt;
&lt;p&gt;One important practical difference from Linux is that current FreeBSD Podman deployments are typically run as root. Rootless Podman is not part of the FreeBSD workflow today, so the threat model is different from Linux setups where rootless operation is a major security&amp;nbsp;feature.&lt;/p&gt;
&lt;p&gt;The Linuxulator itself is remarkably complete, but it&amp;#8217;s not&amp;nbsp;perfect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Syscall coverage&lt;/strong&gt;: The Linuxulator implements a large subset of Linux syscalls, but some less common ones may be missing. Most mainstream software works fine; highly Linux-specific tools (those that depend on cgroups v2 internals, eBPF, or&amp;nbsp;kernel-specific &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt; interfaces) may&amp;nbsp;not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Native FreeBSD containers have zero translation overhead. Linux containers go through the Linuxulator&amp;#8217;s syscall translation, which adds some overhead - negligible for I/O-bound workloads, potentially noticeable for syscall-heavy&amp;nbsp;applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel features&lt;/strong&gt;: Linux containers on FreeBSD don&amp;#8217;t have access to Linux-specific kernel features like cgroups (FreeBSD uses its own resource limiting&amp;nbsp;via &lt;code&gt;rctl&lt;/code&gt;), seccomp, or kernel namespaces. Isolation is provided by FreeBSD jails&amp;nbsp;instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="container-lifecycle-without-systemd"&gt;Container Lifecycle Without&amp;nbsp;systemd&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s where the operational model diverges most from Linux. On Linux, Quadlets turn containers into systemd services with dependency management, restart policies, logging integration, and boot startup - all through the init system. FreeBSD doesn&amp;#8217;t have systemd, so none of that&amp;nbsp;exists.&lt;/p&gt;
&lt;p&gt;Instead, you have two&amp;nbsp;approaches.&lt;/p&gt;
&lt;h3 id="podmans-built-in-restart-policy"&gt;Podman&amp;#8217;s Built-in Restart&amp;nbsp;Policy&lt;/h3&gt;
&lt;p&gt;The simplest method.&amp;nbsp;Podman&amp;#8217;s &lt;code&gt;--restart&lt;/code&gt; flag works on FreeBSD just as it does on&amp;nbsp;Linux:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;my-app&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--restart&lt;span class="w"&gt; &lt;/span&gt;always&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;:80&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;docker.io/library/nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;podman_enable=YES&lt;/code&gt; in &lt;code&gt;/etc/rc.conf&lt;/code&gt;, the Podman service starts at boot, and containers&amp;nbsp;with &lt;code&gt;--restart=always&lt;/code&gt; are automatically restarted. This gives you basic lifecycle management without writing any service&amp;nbsp;scripts.&lt;/p&gt;
&lt;p&gt;The restart policies are the same as&amp;nbsp;Docker&amp;#8217;s:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Never restart (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always restart, including at boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on-failure[:max]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restart on non-zero exit, optional retry limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unless-stopped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;always&lt;/code&gt;, but not if manually stopped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For simple deployments, this is often sufficient. The container starts at boot, restarts on crash, and Podman handles the&amp;nbsp;lifecycle.&lt;/p&gt;
&lt;h3 id="rcd-service-scripts"&gt;rc.d Service&amp;nbsp;Scripts&lt;/h3&gt;
&lt;p&gt;For more control - dependency ordering, custom health checks, integration with FreeBSD&amp;#8217;s service framework - write an rc.d script. This is FreeBSD&amp;#8217;s native equivalent of a systemd unit&amp;nbsp;file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="c1"&gt;# PROVIDE: myapp&lt;/span&gt;
&lt;span class="c1"&gt;# REQUIRE: DAEMON podman&lt;/span&gt;
&lt;span class="c1"&gt;# KEYWORD: shutdown&lt;/span&gt;

.&lt;span class="w"&gt; &lt;/span&gt;/etc/rc.subr

&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myapp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rcvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp_enable

load_rc_config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;

:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_enable&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_image&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;docker.io/library/nginx:latest&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_name&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myapp&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;start_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_start&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;stop_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_stop&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;status_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_status&amp;quot;&lt;/span&gt;

myapp_start&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Starting &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Clean up any orphaned container from a dirty shutdown&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--restart&lt;span class="w"&gt; &lt;/span&gt;on-failure:5&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;:80&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_image&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

myapp_stop&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Stopping &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

myapp_status&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;inspect&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{{.State.Status}}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;myapp_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not running.&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_rc_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Save this&amp;nbsp;as &lt;code&gt;/usr/local/etc/rc.d/myapp&lt;/code&gt;, make it executable, and enable&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/rc.d/myapp
sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;myapp_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES
service&lt;span class="w"&gt; &lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;# REQUIRE: DAEMON podman&lt;/code&gt; line ensures the Podman service is running before your container starts. You can chain dependencies between container services the same&amp;nbsp;way:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# In your database service script&lt;/span&gt;
&lt;span class="c1"&gt;# PROVIDE: myapp_db&lt;/span&gt;
&lt;span class="c1"&gt;# REQUIRE: DAEMON podman&lt;/span&gt;

&lt;span class="c1"&gt;# In your application service script&lt;/span&gt;
&lt;span class="c1"&gt;# PROVIDE: myapp_web&lt;/span&gt;
&lt;span class="c1"&gt;# REQUIRE: myapp_db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives you explicit startup ordering through&amp;nbsp;FreeBSD&amp;#8217;s &lt;code&gt;rcorder&lt;/code&gt; system - the same mechanism that orders every other service on the&amp;nbsp;system.&lt;/p&gt;
&lt;h3 id="what-you-lose-without-quadlets"&gt;What You Lose Without&amp;nbsp;Quadlets&lt;/h3&gt;
&lt;p&gt;Let&amp;#8217;s be direct about the trade-offs. Compared to the Quadlet workflow on&amp;nbsp;Linux:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Linux (Quadlets)&lt;/th&gt;
&lt;th&gt;FreeBSD (rc.d / restart policy)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Declarative container definition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.container&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;&lt;code&gt;podman run&lt;/code&gt; flags in scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency management&lt;/td&gt;
&lt;td&gt;systemd &lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rcorder &lt;code&gt;REQUIRE:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log integration&lt;/td&gt;
&lt;td&gt;journald&amp;nbsp;via &lt;code&gt;journalctl -u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stdout/stderr to files, or syslog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-updates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;podman auto-update&lt;/code&gt; with systemd timer&lt;/td&gt;
&lt;td&gt;Custom scripting (pull + recreate via cron)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health checks driving restarts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HealthOnFailure=stop&lt;/code&gt; + systemd restart&lt;/td&gt;
&lt;td&gt;Custom scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource limits&lt;/td&gt;
&lt;td&gt;systemd cgroup directives&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rctl&lt;/code&gt; rules on the jail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets injection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Secret=&lt;/code&gt; directive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;podman secret&lt;/code&gt; &lt;span class="caps"&gt;CLI&lt;/span&gt; (works the same)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Quadlet workflow is genuinely more ergonomic for complex multi-container deployments. FreeBSD&amp;#8217;s approach requires more manual wiring. But it&amp;#8217;s the same tools FreeBSD administrators already use for everything else on the system - rc.d,&amp;nbsp;cron, &lt;code&gt;rctl&lt;/code&gt; - so the operational patterns are familiar even if they&amp;#8217;re less&amp;nbsp;integrated.&lt;/p&gt;
&lt;h3 id="logging"&gt;Logging&lt;/h3&gt;
&lt;p&gt;Without journald, container logs go to Podman&amp;#8217;s log driver. By default, Podman stores logs per container under its local container storage. You access them the usual&amp;nbsp;way:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;my-app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For centralized logging, configure the syslog log&amp;nbsp;driver:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--log-driver&lt;span class="w"&gt; &lt;/span&gt;syslog&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--log-opt&lt;span class="w"&gt; &lt;/span&gt;syslog-facility&lt;span class="o"&gt;=&lt;/span&gt;local0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;my-app&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;docker.io/library/nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This sends container output to FreeBSD&amp;#8217;s syslogd, where it integrates with your existing log infrastructure - rotation&amp;nbsp;via &lt;code&gt;newsyslog.conf&lt;/code&gt;, forwarding to a central syslog server, or whatever your setup&amp;nbsp;uses.&lt;/p&gt;
&lt;h3 id="image-updates"&gt;Image&amp;nbsp;Updates&lt;/h3&gt;
&lt;p&gt;Podman&amp;#8217;s&amp;nbsp;documented &lt;code&gt;auto-update&lt;/code&gt; workflow is systemd-centric - it&amp;#8217;s designed for containers running inside systemd units, and after pulling a newer image it restarts the systemd unit managing the container. The timer that triggers the check is only one piece; the restart/recreation mechanism itself depends on systemd. That entire chain doesn&amp;#8217;t exist on&amp;nbsp;FreeBSD.&lt;/p&gt;
&lt;p&gt;What you can do is script the image refresh and container recreation yourself. A cron job can check for newer images and log the&amp;nbsp;results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/cron.d/podman-image-refresh&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;/usr/local/sbin/podman-refresh.sh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;logger&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;podman-refresh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But unlike the Linux auto-update flow, you need to handle the restart and recreation step yourself - pulling the new image, stopping the old container, and starting a fresh one with the updated image. A simple&amp;nbsp;approach:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c1"&gt;# /usr/local/sbin/podman-refresh.sh&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;traefik&lt;span class="w"&gt; &lt;/span&gt;myapp&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;inspect&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{{.ImageName}}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;pull&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Writing manifest&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Re-create via your rc.d script or however the container is defined&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;service&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;start
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is more manual than the Linux Quadlet auto-update experience, and it requires that your rc.d scripts or startup commands can fully recreate the container from scratch. It&amp;#8217;s operational overhead worth acknowledging: on Linux, Podman and systemd handle this loop for you; on FreeBSD, you own&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="podman-and-jails-complementary-not-competing"&gt;Podman and Jails: Complementary, Not&amp;nbsp;Competing&lt;/h2&gt;
&lt;p&gt;This is the important framing. FreeBSD already has the most mature &lt;span class="caps"&gt;OS&lt;/span&gt;-level container technology in existence: &lt;a href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/"&gt;Jails&lt;/a&gt;. They&amp;#8217;ve been around since FreeBSD 4.0, they provide kernel-enforced isolation, and they&amp;#8217;re deeply integrated with the rest of the operating system. So why would you want&amp;nbsp;Podman?&lt;/p&gt;
&lt;p&gt;The answer isn&amp;#8217;t &amp;#8220;Podman is better than Jails.&amp;#8221; It&amp;#8217;s that they solve different&amp;nbsp;problems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jails&lt;/strong&gt; are FreeBSD&amp;#8217;s native isolation primitive. A jail is essentially a lightweight FreeBSD instance with its own filesystem, network stack, process space, and user database. Jails are ideal&amp;nbsp;for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long-lived services that you build and maintain&amp;nbsp;yourself&lt;/li&gt;
&lt;li&gt;Workloads that benefit from direct access to FreeBSD&amp;#8217;s kernel features (&lt;span class="caps"&gt;ZFS&lt;/span&gt;, &lt;span class="caps"&gt;PF&lt;/span&gt;, &lt;code&gt;rctl&lt;/code&gt;,&amp;nbsp;DTrace)&lt;/li&gt;
&lt;li&gt;Environments where you want full FreeBSD package management inside the isolated&amp;nbsp;environment&lt;/li&gt;
&lt;li&gt;Multi-tenant hosting where each tenant gets a complete FreeBSD&amp;nbsp;environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Podman&lt;/strong&gt; brings &lt;span class="caps"&gt;OCI&lt;/span&gt; container compatibility. It&amp;#8217;s ideal&amp;nbsp;for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Running third-party software distributed as &lt;span class="caps"&gt;OCI&lt;/span&gt;/Docker images without&amp;nbsp;repackaging&lt;/li&gt;
&lt;li&gt;Linux-only applications that have no FreeBSD port (via the&amp;nbsp;Linuxulator)&lt;/li&gt;
&lt;li&gt;Workflows that already use Dockerfiles and container&amp;nbsp;registries&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;CI&lt;/span&gt;/&lt;span class="caps"&gt;CD&lt;/span&gt; pipelines that produce &lt;span class="caps"&gt;OCI&lt;/span&gt; images as&amp;nbsp;artifacts&lt;/li&gt;
&lt;li&gt;Development environments that need to match Linux production&amp;nbsp;targets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical scenario: you run your core infrastructure - your reverse proxy, your databases, your custom applications - in Jails, managed by Bastille or your preferred jail manager, with &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots, &lt;span class="caps"&gt;VNET&lt;/span&gt; networking, and &lt;span class="caps"&gt;PF&lt;/span&gt; firewall rules. Then a project requires a specific application that&amp;#8217;s only distributed as a Docker image, or your &lt;span class="caps"&gt;CI&lt;/span&gt; pipeline produces &lt;span class="caps"&gt;OCI&lt;/span&gt; images that need to run somewhere. Instead of setting up a Linux &lt;span class="caps"&gt;VM&lt;/span&gt; or wrestling with ports and dependencies, you run that image with Podman. The Jail handles what Jails do best; Podman handles what the &lt;span class="caps"&gt;OCI&lt;/span&gt; ecosystem does&amp;nbsp;best.&lt;/p&gt;
&lt;p&gt;In theory, Podman-inside-a-Jail sounds attractive for defense in depth, but I would treat it as experimental at best rather than a recommended deployment&amp;nbsp;pattern.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a jail for container workloads&lt;/span&gt;
bastille&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;containers&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;.3-RELEASE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.252.50&lt;span class="w"&gt; &lt;/span&gt;bastille0

&lt;span class="c1"&gt;# Install Podman inside the jail&lt;/span&gt;
bastille&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;containers&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;podman

&lt;span class="c1"&gt;# Enable and start Podman inside the jail&lt;/span&gt;
bastille&lt;span class="w"&gt; &lt;/span&gt;sysrc&lt;span class="w"&gt; &lt;/span&gt;containers&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;podman_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES
bastille&lt;span class="w"&gt; &lt;/span&gt;service&lt;span class="w"&gt; &lt;/span&gt;containers&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;start

&lt;span class="c1"&gt;# Run OCI containers inside the jail&lt;/span&gt;
bastille&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;containers&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;docker.io/library/nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives you defense-in-depth: the jail provides network isolation and resource limits at the FreeBSD kernel level, while Podman handles the &lt;span class="caps"&gt;OCI&lt;/span&gt; runtime&amp;nbsp;inside.&lt;/p&gt;
&lt;h2 id="practical-tips"&gt;Practical&amp;nbsp;Tips&lt;/h2&gt;
&lt;h3 id="networking"&gt;Networking&lt;/h3&gt;
&lt;p&gt;Podman networking on FreeBSD follows the same broad model as on Linux - user-defined networks, container name resolution, and host port publishing - but the backend may differ by port version and build configuration. Current FreeBSD ports have been observed using &lt;span class="caps"&gt;CNI&lt;/span&gt; rather than netavark. Check what your install is using&amp;nbsp;with &lt;code&gt;podman info&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;info&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{{.Host.NetworkBackend}}&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Regardless of backend, the operational patterns are the&amp;nbsp;same:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a network&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;network&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;mynet

&lt;span class="c1"&gt;# Run containers on that network&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;--network&lt;span class="w"&gt; &lt;/span&gt;mynet&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;db&lt;span class="w"&gt; &lt;/span&gt;postgres:17
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;--network&lt;span class="w"&gt; &lt;/span&gt;mynet&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;app&lt;span class="w"&gt; &lt;/span&gt;myapp:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;DNS&lt;/span&gt; resolution between containers on the same network works by container&amp;nbsp;name.&lt;/p&gt;
&lt;p&gt;Remember that all of this depends on the &lt;span class="caps"&gt;PF&lt;/span&gt; setup from the installation section. Without &lt;span class="caps"&gt;PF&lt;/span&gt; doing &lt;span class="caps"&gt;NAT&lt;/span&gt;, container traffic has no route out of the container network. Port redirections&amp;nbsp;(&lt;code&gt;-p&lt;/code&gt; flag) flow through&amp;nbsp;the &lt;code&gt;cni-rdr&lt;/code&gt; &lt;span class="caps"&gt;PF&lt;/span&gt; anchors. If you&amp;#8217;re already running &lt;span class="caps"&gt;PF&lt;/span&gt; with your own ruleset (and you probably are if you&amp;#8217;ve read my &lt;a href="https://blog.hofstede.it/pf-firewall-on-freebsd-a-practical-guide/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; guide&lt;/a&gt;), integrate the Podman anchors into your existing configuration rather than replacing it with the sample&amp;nbsp;file.&lt;/p&gt;
&lt;h3 id="storage"&gt;Storage&lt;/h3&gt;
&lt;p&gt;With the &lt;span class="caps"&gt;ZFS&lt;/span&gt; dataset from the installation section in place, Podman automatically uses the &lt;span class="caps"&gt;ZFS&lt;/span&gt; storage driver. This is one area where Podman on FreeBSD genuinely outshines the Linux experience: instead of fighting with overlayfs quirks or fuse-overlayfs for rootless setups, Podman creates real &lt;span class="caps"&gt;ZFS&lt;/span&gt; datasets for container image layers. You get copy-on-write, compression, checksumming, and snapshot support for free, all backed by the same storage stack you&amp;#8217;re already using for everything else on the system. You can snapshot the entire container storage tree, roll back after a bad update, or send it to another host&amp;nbsp;with &lt;code&gt;zfs send&lt;/code&gt; - all operations that feel natural on FreeBSD and awkward on&amp;nbsp;Linux.&lt;/p&gt;
&lt;h3 id="secrets"&gt;Secrets&lt;/h3&gt;
&lt;p&gt;Podman&amp;#8217;s secret store works identically on&amp;nbsp;FreeBSD:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;db_password&lt;span class="w"&gt; &lt;/span&gt;-
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;--secret&lt;span class="w"&gt; &lt;/span&gt;db_password,type&lt;span class="o"&gt;=&lt;/span&gt;env,target&lt;span class="o"&gt;=&lt;/span&gt;DB_PASS&lt;span class="w"&gt; &lt;/span&gt;myapp:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No differences here - the secrets management story from the &lt;a href="https://blog.hofstede.it/podman-in-production-quadlets-secrets-auto-updates-and-docker-compatibility/#secrets-management"&gt;Linux article&lt;/a&gt; applies&amp;nbsp;as-is.&lt;/p&gt;
&lt;h2 id="when-to-use-what"&gt;When to Use&amp;nbsp;What&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommended approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom FreeBSD service, full &lt;span class="caps"&gt;OS&lt;/span&gt; access needed&lt;/td&gt;
&lt;td&gt;Jail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party app only available as Docker image&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux-only software with no FreeBSD port&lt;/td&gt;
&lt;td&gt;Podman + Linuxulator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenant isolation with independent networks&lt;/td&gt;
&lt;td&gt;Jail (&lt;span class="caps"&gt;VNET&lt;/span&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="caps"&gt;CI&lt;/span&gt;/&lt;span class="caps"&gt;CD&lt;/span&gt; pipeline producing &lt;span class="caps"&gt;OCI&lt;/span&gt; artifacts&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database with &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots for backup&lt;/td&gt;
&lt;td&gt;Jail (direct &lt;span class="caps"&gt;ZFS&lt;/span&gt; access)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development parity with Linux production&lt;/td&gt;
&lt;td&gt;Podman + Linux containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mix of both needs&lt;/td&gt;
&lt;td&gt;Podman inside a Jail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping&amp;nbsp;Up&lt;/h2&gt;
&lt;p&gt;Podman on FreeBSD isn&amp;#8217;t trying to replace Jails. It&amp;#8217;s filling a gap that Jails were never designed to fill: compatibility with the &lt;span class="caps"&gt;OCI&lt;/span&gt; container ecosystem. The ability&amp;nbsp;to &lt;code&gt;podman pull&lt;/code&gt; an image from Docker Hub and run it on FreeBSD - whether it&amp;#8217;s a native FreeBSD image or a Linux image through the Linuxulator - means you don&amp;#8217;t have to choose between FreeBSD&amp;#8217;s operational model and the container&amp;nbsp;ecosystem.&lt;/p&gt;
&lt;p&gt;The operational workflow is less polished than on Linux. Without systemd, you lose Quadlets and the tight init-system integration that makes Podman on Linux so compelling. But what you get in return is Podman integrated with FreeBSD&amp;#8217;s own tools: rc.d for service management, cron for&amp;nbsp;scheduling, &lt;code&gt;rctl&lt;/code&gt; for resource limits, &lt;span class="caps"&gt;PF&lt;/span&gt; for network policy, &lt;span class="caps"&gt;ZFS&lt;/span&gt; for storage. Different tools, same&amp;nbsp;result.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re running FreeBSD and you&amp;#8217;ve been eyeing the &lt;span class="caps"&gt;OCI&lt;/span&gt; ecosystem from the outside, Podman is your bridge&amp;nbsp;in.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://podman.io/docs/installation#installing-on-freebsd"&gt;Podman Installation: FreeBSD&lt;/a&gt; - official installation&amp;nbsp;guide&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/containers/podman/blob/main/docs/tutorials/podman-for-freebsd.md"&gt;Podman on FreeBSD - containers/podman&amp;nbsp;Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/u/freebsd"&gt;FreeBSD &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/linuxemu/"&gt;FreeBSD Handbook: Linux Binary&amp;nbsp;Compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nicr9/ocijail"&gt;ocijail - FreeBSD &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/podman-in-production-quadlets-secrets-auto-updates-and-docker-compatibility/"&gt;Podman in Production: Quadlets, Secrets, Auto-Updates, and Docker Compatibility&lt;/a&gt; - the Linux-focused companion&amp;nbsp;article&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/"&gt;FreeBSD Foundationals: Jails&lt;/a&gt; - deep dive into FreeBSD&amp;#8217;s native&amp;nbsp;isolation&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/running-a-factorio-headless-server-on-freebsd-with-the-linuxulator/"&gt;Running Factorio on FreeBSD with the Linuxulator&lt;/a&gt; - another Linuxulator use&amp;nbsp;case&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="podman"/><category term="containers"/><category term="oci"/><category term="jails"/><category term="linuxulator"/><category term="docker"/></entry><entry><title>Podman in Production: Quadlets, Secrets, Auto-Updates, and Docker Compatibility</title><link href="https://blog.hofstede.it/podman-in-production-quadlets-secrets-auto-updates-and-docker-compatibility/" rel="alternate"/><published>2026-04-05T00:00:00+02:00</published><updated>2026-04-05T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-04-05:/podman-in-production-quadlets-secrets-auto-updates-and-docker-compatibility/</id><summary type="html">&lt;p&gt;An opinionated production-ops guide to Podman on Linux servers - why I prefer it over Docker, how Quadlets replace Compose files, and practical patterns from real deployments including secrets management, auto-updates, and Docker&amp;nbsp;compatibility.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Podman Logo" src="https://blog.hofstede.it/images/2026-04-05-podman-deep-dive.png" title="Podman Logo"&gt; &lt;/p&gt;
&lt;p&gt;I&amp;#8217;ve been running Podman in production for years now. Every container on my infrastructure - Forgejo, Traefik, PostgreSQL, Keycloak, &lt;span class="caps"&gt;CI&lt;/span&gt; runners - is managed by Podman on &lt;span class="caps"&gt;RHEL&lt;/span&gt;. Not a single Docker daemon in sight. Regular readers know that my first instinct for isolation is &lt;a href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/"&gt;FreeBSD Jails&lt;/a&gt; - but when I&amp;#8217;m on Linux and dealing with &lt;span class="caps"&gt;OCI&lt;/span&gt; containers, Podman is the tool I reach for. It follows the Unix model more closely than Docker, using smaller composable pieces instead of a single central&amp;nbsp;daemon.&lt;/p&gt;
&lt;p&gt;This isn&amp;#8217;t a &amp;#8220;getting started&amp;#8221; tutorial. This is an opinionated production-ops perspective on Linux hosts - not a universal answer for every developer workstation or platform. It&amp;#8217;s the article I wish someone had written when I was migrating away from Docker: the architectural reasons I prefer Podman, the practical patterns that make it work in production, and the real gotchas you&amp;#8217;ll hit along the way. If you&amp;#8217;ve read my earlier &lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;Quadlets guide&lt;/a&gt; or the &lt;a href="https://blog.hofstede.it/keycloak-26-on-podman-with-quadlets-identity-management-the-systemd-way/"&gt;Keycloak deployment article&lt;/a&gt;, this ties those threads together into a bigger&amp;nbsp;picture.&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-podman-is-better-than-docker"&gt;Why Podman Is Better Than Docker&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#no-daemon-no-single-point-of-failure"&gt;No Daemon, No Single Point of&amp;nbsp;Failure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rootless-by-design-not-by-afterthought"&gt;Rootless by Design, Not by&amp;nbsp;Afterthought&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#selinux-is-a-feature-not-an-obstacle"&gt;SELinux Is a Feature, Not an&amp;nbsp;Obstacle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-oci-guarantee"&gt;The &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Guarantee&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#quadlets-containers-as-systemd-services"&gt;Quadlets: Containers as systemd Services&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-basics"&gt;The&amp;nbsp;Basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-quadlets-beat-compose"&gt;Why Quadlets Beat&amp;nbsp;Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#real-world-deployment-a-complete-stack"&gt;Real-World Deployment: A Complete Stack&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#network-topology"&gt;Network&amp;nbsp;Topology&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#database-container"&gt;Database&amp;nbsp;Container&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#application-container"&gt;Application&amp;nbsp;Container&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#reverse-proxy"&gt;Reverse&amp;nbsp;Proxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ci-runner"&gt;&lt;span class="caps"&gt;CI&lt;/span&gt;&amp;nbsp;Runner&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#secrets-management"&gt;Secrets Management&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#creating-secrets"&gt;Creating&amp;nbsp;Secrets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#using-secrets-in-quadlets"&gt;Using Secrets in&amp;nbsp;Quadlets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rotating-secrets"&gt;Rotating&amp;nbsp;Secrets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#automatic-updates"&gt;Automatic Updates&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#two-update-modes"&gt;Two Update&amp;nbsp;Modes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-word-of-caution"&gt;A Word of&amp;nbsp;Caution&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#docker-compatibility"&gt;Docker Compatibility&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-docker-socket"&gt;The Docker&amp;nbsp;Socket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#docker-compose-compatibility"&gt;Docker Compose&amp;nbsp;Compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-podman-cli-alias"&gt;The podman &lt;span class="caps"&gt;CLI&lt;/span&gt;&amp;nbsp;Alias&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#practical-tips"&gt;Practical Tips&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#networking-patterns"&gt;Networking&amp;nbsp;Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#volume-and-permission-patterns"&gt;Volume and Permission&amp;nbsp;Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#security-hardening"&gt;Security&amp;nbsp;Hardening&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#debugging"&gt;Debugging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#registry-authentication"&gt;Registry&amp;nbsp;Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#automation-with-ansible"&gt;Automation with&amp;nbsp;Ansible&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#when-podman-isnt-the-right-choice"&gt;When Podman Isn&amp;#8217;t the Right&amp;nbsp;Choice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-bigger-picture"&gt;The Bigger&amp;nbsp;Picture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="why-podman-is-better-than-docker"&gt;Why Podman Is Better Than&amp;nbsp;Docker&lt;/h2&gt;
&lt;p&gt;I&amp;#8217;m not going to pretend this is a balanced comparison. On Linux servers, I think Podman is the cleaner architecture, and the reasons are structural rather than&amp;nbsp;cosmetic.&lt;/p&gt;
&lt;h3 id="no-daemon-no-single-point-of-failure"&gt;No Daemon, No Single Point of&amp;nbsp;Failure&lt;/h3&gt;
&lt;p&gt;Docker&amp;#8217;s architecture has a trade-off I no longer think is worth accepting on Linux hosts: a privileged central daemon.&amp;nbsp;Every &lt;code&gt;docker run&lt;/code&gt;,&amp;nbsp;every &lt;code&gt;docker build&lt;/code&gt;, every container lifecycle event goes&amp;nbsp;through &lt;code&gt;dockerd&lt;/code&gt; - a process running as root with effectively unrestricted access to your&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;Docker does&amp;nbsp;offer &lt;code&gt;--live-restore&lt;/code&gt; to keep containers running during daemon restarts, but it comes with limitations around networking and interactive sessions, and it&amp;#8217;s not the default. The fundamental coupling remains: every container&amp;#8217;s control path runs through a single privileged process. That&amp;#8217;s operational risk and attack surface that adds&amp;nbsp;up.&lt;/p&gt;
&lt;p&gt;Podman uses a fork/exec model. Each container is a direct child process of whatever started it - your shell, systemd, a &lt;span class="caps"&gt;CI&lt;/span&gt; runner. There is no central daemon. Containers are independent processes managed by the kernel, the way Unix was designed to&amp;nbsp;work.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Docker:
  User → dockerd (root daemon) → containerd → runc → container
                ↑
        centralized control, shared fate by default

Podman:
  User → conmon → runc → container
  systemd → conmon → runc → container
         (no daemon, direct process tree)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I can update Podman on my server without touching running containers. Each container&amp;#8217;s lifecycle is&amp;nbsp;independent.&lt;/p&gt;
&lt;h3 id="rootless-by-design-not-by-afterthought"&gt;Rootless by Design, Not by&amp;nbsp;Afterthought&lt;/h3&gt;
&lt;p&gt;Docker bolted on rootless support years after the fact, and it shows. Running Docker rootless still requires a&amp;nbsp;separate &lt;code&gt;dockerd-rootless&lt;/code&gt; process, has compatibility limitations with storage drivers, and adds limitations and extra setup around things like privileged&amp;nbsp;ports.&lt;/p&gt;
&lt;p&gt;Podman was designed rootless from the start. User namespaces, subordinate &lt;span class="caps"&gt;UID&lt;/span&gt;/&lt;span class="caps"&gt;GID&lt;/span&gt; mapping, and unprivileged container execution are core architecture, not a compatibility layer. A regular user can run containers without any elevated&amp;nbsp;privileges:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# As a regular user, no sudo needed&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;-it&lt;span class="w"&gt; &lt;/span&gt;docker.io/library/alpine:latest&lt;span class="w"&gt; &lt;/span&gt;sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The security implications are significant. A container escape in a rootless Podman setup lands you in an unprivileged user namespace. A compromise involving Docker&amp;#8217;s privileged daemon has a much higher potential blast radius on the host than a compromise contained inside a rootless user namespace. That&amp;#8217;s not a theoretical distinction - it&amp;#8217;s a meaningful difference in your threat&amp;nbsp;model.&lt;/p&gt;
&lt;h3 id="selinux-is-a-feature-not-an-obstacle"&gt;SELinux Is a Feature, Not an&amp;nbsp;Obstacle&lt;/h3&gt;
&lt;p&gt;Docker&amp;#8217;s relationship with SELinux has historically been adversarial. Countless Docker tutorials start&amp;nbsp;with &lt;code&gt;setenforce 0&lt;/code&gt; because the daemon and SELinux don&amp;#8217;t always cooperate. Podman integrates with SELinux as a first-class security&amp;nbsp;layer.&lt;/p&gt;
&lt;p&gt;Every Podman container automatically gets an SELinux label&amp;nbsp;(&lt;code&gt;container_t&lt;/code&gt;). Volume mounts&amp;nbsp;use &lt;code&gt;:z&lt;/code&gt; and &lt;code&gt;:Z&lt;/code&gt; flags to handle context relabeling.&amp;nbsp;The &lt;code&gt;container_runtime_t&lt;/code&gt; type exists specifically for containers that need elevated access patterns (like mounting the Podman socket). None of this requires disabling SELinux or writing custom policy&amp;nbsp;modules.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re running &lt;span class="caps"&gt;RHEL&lt;/span&gt; or Fedora with SELinux enforcing (as you should be - see my &lt;a href="https://blog.hofstede.it/selinux-a-practical-guide-for-fedora-and-rhel/"&gt;SELinux guide&lt;/a&gt;), Podman just works with it. Docker sometimes fights&amp;nbsp;it.&lt;/p&gt;
&lt;h3 id="the-oci-guarantee"&gt;The &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Guarantee&lt;/h3&gt;
&lt;p&gt;Podman is fully &lt;span class="caps"&gt;OCI&lt;/span&gt;-compliant. For most real-world use cases, Docker images, registries, and Dockerfiles work with Podman without modification. You&amp;nbsp;can &lt;code&gt;podman pull&lt;/code&gt; from Docker&amp;nbsp;Hub, &lt;code&gt;podman build&lt;/code&gt; with a Dockerfile,&amp;nbsp;and &lt;code&gt;podman push&lt;/code&gt; to any registry. The images are identical because they follow the same &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;specification.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# These do the same thing&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;pull&lt;span class="w"&gt; &lt;/span&gt;nginx:latest
podman&lt;span class="w"&gt; &lt;/span&gt;pull&lt;span class="w"&gt; &lt;/span&gt;docker.io/library/nginx:latest

&lt;span class="c1"&gt;# Buildah (Podman&amp;#39;s build companion) reads Dockerfiles natively&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;Dockerfile&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The migration path from Docker to Podman is a find-and-replace&amp;nbsp;of &lt;code&gt;docker&lt;/code&gt; with &lt;code&gt;podman&lt;/code&gt; in most cases. Your images, your registries, your &lt;span class="caps"&gt;CI&lt;/span&gt; pipelines - they all&amp;nbsp;work.&lt;/p&gt;
&lt;h2 id="quadlets-containers-as-systemd-services"&gt;Quadlets: Containers as systemd&amp;nbsp;Services&lt;/h2&gt;
&lt;p&gt;If you&amp;#8217;re still&amp;nbsp;writing &lt;code&gt;podman run&lt;/code&gt; commands in shell scripts or using Compose files, you&amp;#8217;re missing the most compelling feature in Podman&amp;#8217;s ecosystem:&amp;nbsp;Quadlets.&lt;/p&gt;
&lt;p&gt;Quadlets are systemd unit files that describe containers, networks, volumes, and images. Drop them&amp;nbsp;in &lt;code&gt;/etc/containers/systemd/&lt;/code&gt; (system-wide)&amp;nbsp;or &lt;code&gt;~/.config/containers/systemd/&lt;/code&gt; (rootless), and systemd manages them like any other service. No daemon, no orchestrator, no &lt;span class="caps"&gt;YAML&lt;/span&gt; parser sitting between you and your&amp;nbsp;containers.&lt;/p&gt;
&lt;h3 id="the-basics"&gt;The&amp;nbsp;Basics&lt;/h3&gt;
&lt;p&gt;A Quadlet file looks like a systemd unit file with&amp;nbsp;a &lt;code&gt;[Container]&lt;/code&gt; section:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;my-app&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:latest&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:80&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/www:/usr/share/nginx/html:z&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Save this&amp;nbsp;as &lt;code&gt;/etc/containers/systemd/my-app.container&lt;/code&gt;,&amp;nbsp;run &lt;code&gt;systemctl daemon-reload&lt;/code&gt;, and you have a container managed by&amp;nbsp;systemd:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;my-app.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;my-app.service
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;my-app.service&lt;span class="w"&gt; &lt;/span&gt;-f
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No compose file. No daemon. Just systemd doing what it already does well: managing service&amp;nbsp;lifecycles.&lt;/p&gt;
&lt;h3 id="why-quadlets-beat-compose"&gt;Why Quadlets Beat&amp;nbsp;Compose&lt;/h3&gt;
&lt;p&gt;Docker Compose requires a separate binary, parses &lt;span class="caps"&gt;YAML&lt;/span&gt;, and maintains its own state about which containers belong to which project. It&amp;#8217;s a parallel service manager running alongside (or on top of) your actual service&amp;nbsp;manager.&lt;/p&gt;
&lt;p&gt;Quadlets eliminate that entire&amp;nbsp;layer:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service lifecycle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up/down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start/stop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot startup&lt;/td&gt;
&lt;td&gt;Requires daemon + compose plugin&lt;/td&gt;
&lt;td&gt;Native&amp;nbsp;systemd &lt;code&gt;WantedBy=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;depends_on:&lt;/code&gt; (limited)&lt;/td&gt;
&lt;td&gt;systemd &lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt; (battle-tested)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl -u&lt;/code&gt; (integrated with system logging)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource limits&lt;/td&gt;
&lt;td&gt;Compose &lt;span class="caps"&gt;YAML&lt;/span&gt; &lt;code&gt;deploy:&lt;/code&gt; section&lt;/td&gt;
&lt;td&gt;systemd cgroup directives (kernel-enforced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update mechanism&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;docker compose pull &amp;amp;&amp;amp; up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;podman auto-update&lt;/code&gt; with systemd timer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart policy&lt;/td&gt;
&lt;td&gt;Compose-level&lt;/td&gt;
&lt;td&gt;systemd&amp;#8217;s proven restart logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The dependency management alone is worth the switch. systemd&amp;#8217;s dependency graph has been solving service ordering for over a decade.&amp;nbsp;Compose&amp;#8217;s &lt;code&gt;depends_on&lt;/code&gt; is limited to basic ordering, and it can&amp;#8217;t express dependency chains involving non-container services.&amp;nbsp;systemd&amp;#8217;s &lt;code&gt;After=&lt;/code&gt; and &lt;code&gt;Requires=&lt;/code&gt; handle ordering and hard dependencies natively, and when you need actual readiness (not just &amp;#8220;the process started&amp;#8221;), Quadlets support health check&amp;nbsp;primitives:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;HealthCmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;pg_isready -U myuser&lt;/span&gt;
&lt;span class="na"&gt;HealthInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10s&lt;/span&gt;
&lt;span class="na"&gt;HealthTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;HealthRetries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;HealthStartPeriod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;
&lt;span class="na"&gt;HealthOnFailure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;HealthStartPeriod&lt;/code&gt; gives the application time to initialize before health checks&amp;nbsp;count. &lt;code&gt;HealthOnFailure=stop&lt;/code&gt; tells Podman to stop the container if it fails health checks - and&amp;nbsp;systemd&amp;#8217;s &lt;code&gt;Restart=always&lt;/code&gt; brings it back. This gets you much closer to real readiness and recovery behavior than simple startup ordering&amp;nbsp;alone.&lt;/p&gt;
&lt;h2 id="real-world-deployment-a-complete-stack"&gt;Real-World Deployment: A Complete&amp;nbsp;Stack&lt;/h2&gt;
&lt;p&gt;Let me show you what a real Podman deployment looks like. These are based on my production Quadlets, sanitized for opsec but architecturally&amp;nbsp;identical.&lt;/p&gt;
&lt;h3 id="network-topology"&gt;Network&amp;nbsp;Topology&lt;/h3&gt;
&lt;p&gt;First, the network isolation. Every multi-container deployment gets a dedicated backend&amp;nbsp;network:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/containers/systemd/forgejo-backend.network&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Network]&lt;/span&gt;
&lt;span class="na"&gt;Subnet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;172.16.0.0/24&lt;/span&gt;
&lt;span class="na"&gt;Gateway&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;172.16.0.1&lt;/span&gt;
&lt;span class="na"&gt;IPRange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;172.16.0.0/28&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;/28&lt;/code&gt; in &lt;code&gt;IPRange&lt;/code&gt; limits &lt;span class="caps"&gt;DHCP&lt;/span&gt;-assigned addresses to 14 hosts. The database and application container need two. This prevents the network from becoming a dumping ground for unrelated containers and makes the architecture&amp;nbsp;self-documenting.&lt;/p&gt;
&lt;p&gt;The frontend network (where Traefik lives) is separate. Application containers join both; databases join only the backend. The database has no route to the internet,&amp;nbsp;ever.&lt;/p&gt;
&lt;h3 id="database-container"&gt;Database&amp;nbsp;Container&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/containers/systemd/forgejo-db.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-db&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry.redhat.io/rhel10/postgresql-16:latest&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-backend.network&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;POSTGRESQL_USER=forgejo&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;POSTGRESQL_DATABASE=forgejo&lt;/span&gt;

&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo_db_password,type=env,target=POSTGRESQL_PASSWORD&lt;/span&gt;

&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/postgres:/var/lib/pgsql/data:z&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note what&amp;#8217;s &lt;em&gt;not&lt;/em&gt; here: no frontend network, no published ports, no Traefik labels. This container talks to exactly one network and serves exactly one purpose. The password comes from Podman&amp;#8217;s secret store (more on that below), not from the unit&amp;nbsp;file.&lt;/p&gt;
&lt;h3 id="application-container"&gt;Application&amp;nbsp;Container&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/containers/systemd/forgejo-server.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-server&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;codeberg.org/forgejo/forgejo:14&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="c1"&gt;# Internal network for database connection&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-backend.network&lt;/span&gt;
&lt;span class="c1"&gt;# External network with Traefik&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;frontend.network&lt;/span&gt;

&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;USER_UID=1000&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;USER_GID=1000&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__DB_TYPE=postgres&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__HOST=forgejo-db:5432&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__NAME=forgejo&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__USER=forgejo&lt;/span&gt;

&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo_db_password,type=env,target=FORGEJO__database__PASSWD&lt;/span&gt;

&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/forgejo:/data:z&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;

&lt;span class="c1"&gt;# Traefik routing labels&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.enable=true&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.docker.network=frontend&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.rule=Host(`git.example.com`)&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.entrypoints=https&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.service=forgejo-http&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.tls.certresolver=traefiktls&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.middlewares=secure-headers@file&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.services.forgejo-http.loadbalancer.server.port=3000&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.routers.forgejo-ssh.rule=HostSNI(`*`)&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.routers.forgejo-ssh.entrypoints=ssh&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.routers.forgejo-ssh.service=forgejo-ssh&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.services.forgejo-ssh.loadbalancer.server.port=22&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-db.service&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The dual network attachment is the key pattern: backend for the database, frontend for Traefik. Podman&amp;#8217;s built-in &lt;span class="caps"&gt;DNS&lt;/span&gt;&amp;nbsp;resolves &lt;code&gt;forgejo-db&lt;/code&gt; on the backend network, so the app finds its database by hostname without managing &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;addresses.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;After=&lt;/code&gt; directives ensure proper startup ordering - systemd won&amp;#8217;t start Forgejo until the database and Traefik services have started. Note&amp;nbsp;that &lt;code&gt;After=&lt;/code&gt; is ordering, not readiness: it guarantees the services launch in sequence, but not that the database is actually accepting connections yet. For that, combine with the health check primitives shown&amp;nbsp;earlier.&lt;/p&gt;
&lt;h3 id="reverse-proxy"&gt;Reverse&amp;nbsp;Proxy&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/containers/systemd/traefik.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik:latest&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="na"&gt;AddCapability&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CAP_NET_BIND_SERVICE&lt;/span&gt;

&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;frontend.network&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;80:80&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;443:443&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2222:2222&lt;/span&gt;

&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;SecurityLabelType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;container_runtime_t&lt;/span&gt;

&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/podman/podman.sock:/var/run/docker.sock:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik/traefik.yml:/etc/traefik/traefik.yml:z,ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik/config.yml:/etc/traefik/config.yml:z,ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik/letsencrypt:/letsencrypt:z&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/traefik:/var/log/traefik:z&lt;/span&gt;

&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.entrypoints=https&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.service=api@internal&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.tls=true&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.tls.certresolver=traefiktls&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.middlewares=dashboard-auth,secure-headers@file&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$...&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice &lt;code&gt;SecurityLabelType=container_runtime_t&lt;/code&gt;. Traefik needs to read the Podman socket to discover containers, and the&amp;nbsp;default &lt;code&gt;container_t&lt;/code&gt; SELinux type doesn&amp;#8217;t permit that.&amp;nbsp;The &lt;code&gt;container_runtime_t&lt;/code&gt; type grants the necessary access without disabling SELinux. This is precisely the kind of fine-grained security control that Docker makes difficult and Podman makes&amp;nbsp;natural.&lt;/p&gt;
&lt;h3 id="ci-runner"&gt;&lt;span class="caps"&gt;CI&lt;/span&gt;&amp;nbsp;Runner&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/containers/systemd/forgejo-runner.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Forgejo Runner&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-server.service network-online.target&lt;/span&gt;

&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-runner&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;code.forgejo.org/forgejo/runner:12&lt;/span&gt;

&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;

&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;Exec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-runner daemon&lt;/span&gt;

&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-backend.network&lt;/span&gt;

&lt;span class="na"&gt;SecurityLabelType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;container_runtime_t&lt;/span&gt;

&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/runner:/data:z&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/runner/config.yml:/data/config.yml:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/podman/podman.sock:/var/run/docker.sock:z&lt;/span&gt;

&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CONFIG_FILE=/data/config.yml&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DOCKER_HOST=unix:///var/run/docker.sock&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;CI&lt;/span&gt; runner is a container that spawns other containers - it needs the Podman socket. The socket is mounted&amp;nbsp;as &lt;code&gt;/var/run/docker.sock&lt;/code&gt; because the runner expects Docker&amp;#8217;s socket path. Podman&amp;#8217;s Docker-compatible &lt;span class="caps"&gt;API&lt;/span&gt; handles the rest transparently. The runner doesn&amp;#8217;t know (or care) that it&amp;#8217;s talking to Podman instead of&amp;nbsp;Docker.&lt;/p&gt;
&lt;h2 id="secrets-management"&gt;Secrets&amp;nbsp;Management&lt;/h2&gt;
&lt;p&gt;Hardcoding passwords in unit files is a non-starter. Environment variables in Quadlet files are visible in process listings, in systemd&amp;#8217;s journal, and in the unit files themselves on disk. Podman&amp;#8217;s secret store solves this&amp;nbsp;properly.&lt;/p&gt;
&lt;h3 id="creating-secrets"&gt;Creating&amp;nbsp;Secrets&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generate a strong password and store it&lt;/span&gt;
pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;forgejo_db_password&lt;span class="w"&gt; &lt;/span&gt;-

&lt;span class="c1"&gt;# Or from an existing file&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;my_api_key&lt;span class="w"&gt; &lt;/span&gt;/path/to/keyfile

&lt;span class="c1"&gt;# List stored secrets&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;ls
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;tr -d '\n'&lt;/code&gt; matters. &lt;code&gt;pwgen&lt;/code&gt; appends a trailing newline,&amp;nbsp;and &lt;code&gt;podman secret create&lt;/code&gt; stores bytes verbatim. Some applications strip trailing whitespace from passwords; others don&amp;#8217;t. A PostgreSQL init script might strip it while a &lt;span class="caps"&gt;JDBC&lt;/span&gt; driver sends it as-is, resulting in a password mismatch between two containers reading the exact same secret. Always strip the&amp;nbsp;newline.&lt;/p&gt;
&lt;h3 id="using-secrets-in-quadlets"&gt;Using Secrets in&amp;nbsp;Quadlets&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="c1"&gt;# Inject as environment variable&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo_db_password,type=env,target=FORGEJO__database__PASSWD&lt;/span&gt;

&lt;span class="c1"&gt;# Or mount as file&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;my_tls_cert,type=mount,target=/etc/ssl/certs/app.crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;type=env&lt;/code&gt; variant injects the secret as an environment variable at container creation time.&amp;nbsp;The &lt;code&gt;type=mount&lt;/code&gt; variant mounts it as a file inside the container, which is preferable for &lt;span class="caps"&gt;TLS&lt;/span&gt; certificates or configuration files that applications expect on&amp;nbsp;disk.&lt;/p&gt;
&lt;h3 id="rotating-secrets"&gt;Rotating&amp;nbsp;Secrets&lt;/h3&gt;
&lt;p&gt;Because &lt;code&gt;type=env&lt;/code&gt; secrets are injected at container creation time, a normal restart of the existing container won&amp;#8217;t refresh a secret that was injected when the container was created. You need to destroy the old container and create a fresh&amp;nbsp;one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Remove the old secret&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;forgejo_db_password

&lt;span class="c1"&gt;# Create new secret&lt;/span&gt;
pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;forgejo_db_password&lt;span class="w"&gt; &lt;/span&gt;-

&lt;span class="c1"&gt;# Stop and start (not restart) to force container recreation&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;forgejo-db.service&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;forgejo-db.service&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The stop/start cycle destroys the old container and creates a new one from the Quadlet definition, picking up the new secret value. Volumes persist across this cycle, so no data is&amp;nbsp;lost.&lt;/p&gt;
&lt;h2 id="automatic-updates"&gt;Automatic&amp;nbsp;Updates&lt;/h2&gt;
&lt;p&gt;One of Podman&amp;#8217;s most underrated features. Enable the systemd&amp;nbsp;timer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;podman-auto-update.timer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;AutoUpdate=registry&lt;/code&gt; in your Quadlet files, Podman checks daily for new image versions. When a newer image is available at the same tag, Podman pulls it and recreates the container, preserving volumes and&amp;nbsp;configuration.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# See what would be updated without doing it&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;auto-update&lt;span class="w"&gt; &lt;/span&gt;--dry-run

&lt;span class="c1"&gt;# Force an update check now&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;auto-update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="two-update-modes"&gt;Two Update&amp;nbsp;Modes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AutoUpdate=registry&lt;/code&gt;&lt;/strong&gt;: Checks the remote registry for a newer image at the same tag. Use this for images you pull from Docker Hub, Quay, or Red Hat&amp;#8217;s&amp;nbsp;registry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AutoUpdate=local&lt;/code&gt;&lt;/strong&gt;: Checks if the local image has been rebuilt. Use this for images you build locally&amp;nbsp;with &lt;code&gt;podman build&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-word-of-caution"&gt;A Word of&amp;nbsp;Caution&lt;/h3&gt;
&lt;p&gt;Auto-updates are excellent for reverse proxies, utility containers, and applications where rolling forward is safe. For databases and stateful services, be more deliberate: pin to a specific version tag and bump manually after reviewing the changelog. A PostgreSQL major version bump via auto-update at 3 &lt;span class="caps"&gt;AM&lt;/span&gt; is not how you want to discover that a migration is&amp;nbsp;required.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Safe to auto-update (stateless, easily rolled back)&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik:latest&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="c1"&gt;# Pin and update manually (stateful, needs migration planning)&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry.redhat.io/rhel10/postgresql-16:1-30&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Even with a pinned version&amp;nbsp;like &lt;code&gt;16:1-30&lt;/code&gt;, &lt;code&gt;AutoUpdate=registry&lt;/code&gt; still checks for image rebuilds at that exact tag (security patches, base image updates). The tag acts as a ceiling for how far the update can&amp;nbsp;go.&lt;/p&gt;
&lt;h2 id="docker-compatibility"&gt;Docker&amp;nbsp;Compatibility&lt;/h2&gt;
&lt;p&gt;Podman goes out of its way to be a drop-in replacement for Docker. This isn&amp;#8217;t just marketing - the compatibility is real, practical, and covers the two areas people worry about&amp;nbsp;most.&lt;/p&gt;
&lt;h3 id="the-docker-socket"&gt;The Docker&amp;nbsp;Socket&lt;/h3&gt;
&lt;p&gt;Podman provides a Docker-compatible &lt;span class="caps"&gt;API&lt;/span&gt; socket via a systemd-managed&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enable the socket (root)&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;podman.socket

&lt;span class="c1"&gt;# Or rootless&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;podman.socket
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This&amp;nbsp;creates &lt;code&gt;/run/podman/podman.sock&lt;/code&gt; (root)&amp;nbsp;or &lt;code&gt;/run/user/$UID/podman/podman.sock&lt;/code&gt; (rootless), exposing a &lt;span class="caps"&gt;REST&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt; that speaks Docker&amp;#8217;s protocol. Any tool expecting a Docker socket - Traefik, Portainer, &lt;span class="caps"&gt;CI&lt;/span&gt; runners, monitoring agents - can connect to this socket and work without&amp;nbsp;modification.&lt;/p&gt;
&lt;p&gt;In Quadlet files, mount it where the application expects Docker&amp;#8217;s&amp;nbsp;socket:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/podman/podman.sock:/var/run/docker.sock:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The application&amp;nbsp;sees &lt;code&gt;/var/run/docker.sock&lt;/code&gt;, sends Docker &lt;span class="caps"&gt;API&lt;/span&gt; calls, and Podman handles them. The application never knows the difference. This is how the Traefik and &lt;span class="caps"&gt;CI&lt;/span&gt; runner examples above work - Traefik&amp;#8217;s Docker provider discovers containers through this socket, reading their labels for routing configuration, without any Podman-specific&amp;nbsp;configuration.&lt;/p&gt;
&lt;h3 id="docker-compose-compatibility"&gt;Docker Compose&amp;nbsp;Compatibility&lt;/h3&gt;
&lt;p&gt;Podman&amp;nbsp;provides &lt;code&gt;podman compose&lt;/code&gt; as a thin wrapper around an external Compose provider, wiring that provider up to the local Podman socket. The provider can&amp;nbsp;be &lt;code&gt;docker-compose&lt;/code&gt; (the standalone Python binary), the&amp;nbsp;Go-based &lt;code&gt;docker compose&lt;/code&gt; plugin,&amp;nbsp;or &lt;code&gt;podman-compose&lt;/code&gt;. Install a supported&amp;nbsp;provider:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# podman-compose is the most common choice on Fedora/RHEL&lt;/span&gt;
dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;podman-compose
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Your&amp;nbsp;existing &lt;code&gt;docker-compose.yml&lt;/code&gt; files work&amp;nbsp;as-is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;-d
podman&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;-f
podman&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;down
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The result is standard Podman containers - you can inspect them&amp;nbsp;with &lt;code&gt;podman ps&lt;/code&gt;, check their logs&amp;nbsp;with &lt;code&gt;podman logs&lt;/code&gt;, and manage them with all the usual Podman&amp;nbsp;tooling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;That said, I don&amp;#8217;t recommend using Compose on Podman in production.&lt;/strong&gt; It works, and it&amp;#8217;s useful for quick local development or migrating an existing Docker Compose setup. But you&amp;#8217;re layering a Docker abstraction on top of Podman, bypassing the systemd integration that makes Podman compelling in the first place. Compose manages its own container lifecycle, its own dependency ordering, its own restart logic - duplicating what systemd already does&amp;nbsp;better.&lt;/p&gt;
&lt;p&gt;The migration path looks like&amp;nbsp;this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start&amp;nbsp;with &lt;code&gt;podman compose&lt;/code&gt; to verify your existing setup works on&amp;nbsp;Podman&lt;/li&gt;
&lt;li&gt;Convert each service to a Quadlet&amp;nbsp;file&lt;/li&gt;
&lt;li&gt;Delete the Compose&amp;nbsp;file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For step&amp;nbsp;2, &lt;code&gt;podman generate systemd --new --files --name my-container&lt;/code&gt; can produce systemd unit files from running containers, but this command is deprecated in Podman 5.x in favor of writing Quadlet files directly. In practice, manually converting Compose services to Quadlets is straightforward - the mapping from &lt;span class="caps"&gt;YAML&lt;/span&gt; keys to Quadlet directives is mostly one-to-one, and you end up with cleaner, more maintainable&amp;nbsp;configuration.&lt;/p&gt;
&lt;h3 id="the-podman-cli-alias"&gt;The &lt;code&gt;podman&lt;/code&gt; &lt;span class="caps"&gt;CLI&lt;/span&gt;&amp;nbsp;Alias&lt;/h3&gt;
&lt;p&gt;Many tools and scripts&amp;nbsp;hardcode &lt;code&gt;docker&lt;/code&gt; as the command. A simple alias handles&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# In /etc/profile.d/docker-compat.sh or your shell rc&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;podman
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On Fedora and &lt;span class="caps"&gt;RHEL&lt;/span&gt;,&amp;nbsp;the &lt;code&gt;podman-docker&lt;/code&gt; package does this system-wide and also creates a Docker-compatible socket&amp;nbsp;symlink:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;podman-docker
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This installs&amp;nbsp;a &lt;code&gt;docker&lt;/code&gt; wrapper that&amp;nbsp;calls &lt;code&gt;podman&lt;/code&gt;,&amp;nbsp;provides &lt;code&gt;/var/run/docker.sock&lt;/code&gt; as a symlink to the Podman socket, and makes Docker-expecting tools work without configuration changes. It even suppresses the &amp;#8220;Emulate Docker &lt;span class="caps"&gt;CLI&lt;/span&gt; using podman&amp;#8221; message&amp;nbsp;that &lt;code&gt;podman&lt;/code&gt; normally&amp;nbsp;prints.&lt;/p&gt;
&lt;h2 id="practical-tips"&gt;Practical&amp;nbsp;Tips&lt;/h2&gt;
&lt;h3 id="networking-patterns"&gt;Networking&amp;nbsp;Patterns&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Dedicated backend networks for every stack.&lt;/strong&gt; Don&amp;#8217;t share a single network across unrelated services. Each application stack (app + database) gets its own isolated backend. This is defense-in-depth: if one application is compromised, the attacker can&amp;#8217;t reach another stack&amp;#8217;s&amp;nbsp;database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Each stack gets its own backend&lt;/span&gt;
/etc/containers/systemd/forgejo-backend.network
/etc/containers/systemd/keycloak-backend.network
/etc/containers/systemd/nextcloud-backend.network
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One shared frontend network for the reverse proxy.&lt;/strong&gt; Traefik (or whatever you use) joins this network, and each application container joins it as a second network. This is the only network with a route to the&amp;nbsp;outside.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;DNS&lt;/span&gt; resolution is automatic.&lt;/strong&gt; Containers on the same Podman network resolve each other by container name. No need to manage &lt;span class="caps"&gt;IP&lt;/span&gt; addresses&amp;nbsp;or &lt;code&gt;/etc/hosts&lt;/code&gt; entries. &lt;code&gt;forgejo-db:5432&lt;/code&gt; just works from any container&amp;nbsp;on &lt;code&gt;forgejo-backend.network&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="volume-and-permission-patterns"&gt;Volume and Permission&amp;nbsp;Patterns&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Always&amp;nbsp;use &lt;code&gt;:z&lt;/code&gt; on SELinux systems.&lt;/strong&gt;&amp;nbsp;The &lt;code&gt;:z&lt;/code&gt; flag relabels the host directory&amp;nbsp;with &lt;code&gt;container_file_t&lt;/code&gt;, allowing container access. Without it, SELinux blocks the mount and you get cryptic permission&amp;nbsp;errors.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Shared volume (multiple containers can access)&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/data:/data:z&lt;/span&gt;

&lt;span class="c1"&gt;# Private volume (only this container)&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/secrets:/secrets:Z&lt;/span&gt;

&lt;span class="c1"&gt;# Read-only system files don&amp;#39;t need relabeling&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Set filesystem ACLs for container UIDs.&lt;/strong&gt; Containers often run as non-root UIDs internally. The &lt;span class="caps"&gt;RHEL&lt;/span&gt; PostgreSQL image uses &lt;span class="caps"&gt;UID&lt;/span&gt; 26, Forgejo uses &lt;span class="caps"&gt;UID&lt;/span&gt; 1000. Without filesystem-level access, the container can&amp;#8217;t write to its mounted&amp;nbsp;volumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/opt/forgejo/postgres
setfacl&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;u:26:rwx&lt;span class="w"&gt; &lt;/span&gt;/opt/forgejo/postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is separate from SELinux labeling&amp;nbsp;- &lt;code&gt;:z&lt;/code&gt; handles the &lt;span class="caps"&gt;MAC&lt;/span&gt;&amp;nbsp;context, &lt;code&gt;setfacl&lt;/code&gt; handles the &lt;span class="caps"&gt;DAC&lt;/span&gt; permissions. You need&amp;nbsp;both.&lt;/p&gt;
&lt;h3 id="security-hardening"&gt;Security&amp;nbsp;Hardening&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;NoNewPrivileges=true&lt;/code&gt;&lt;/strong&gt; on every container that doesn&amp;#8217;t explicitly need privilege escalation. This prevents setuid binaries inside the container from gaining elevated&amp;nbsp;privileges:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Drop capabilities by default.&lt;/strong&gt; Podman already drops most capabilities, but you can explicitly add only what&amp;#8217;s&amp;nbsp;needed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Only grant what the container actually needs&lt;/span&gt;
&lt;span class="na"&gt;AddCapability&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CAP_NET_BIND_SERVICE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;SecurityLabelType&lt;/code&gt; sparingly.&lt;/strong&gt; Most containers run fine with the&amp;nbsp;default &lt;code&gt;container_t&lt;/code&gt; type. Only&amp;nbsp;use &lt;code&gt;container_runtime_t&lt;/code&gt; for containers that need to manage other containers (&lt;span class="caps"&gt;CI&lt;/span&gt; runners, monitoring tools that access the Podman&amp;nbsp;socket).&lt;/p&gt;
&lt;h3 id="debugging"&gt;Debugging&lt;/h3&gt;
&lt;p&gt;When things go wrong, systemd and Podman give you more integrated diagnostic tooling on Linux&amp;nbsp;hosts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Service status with recent log lines&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service

&lt;span class="c1"&gt;# Full journal with filtering&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service&lt;span class="w"&gt; &lt;/span&gt;--since&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10 minutes ago&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Container events&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;events&lt;span class="w"&gt; &lt;/span&gt;--filter&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;forgejo-server

&lt;span class="c1"&gt;# Inspect networking&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;inspect&lt;span class="w"&gt; &lt;/span&gt;forgejo-server&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{{.NetworkSettings.Networks}}&amp;#39;&lt;/span&gt;

&lt;span class="c1"&gt;# Check systemd dependency tree&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;list-dependencies&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service

&lt;span class="c1"&gt;# See what Quadlet generated&lt;/span&gt;
/usr/libexec/podman/quadlet&lt;span class="w"&gt; &lt;/span&gt;--dryrun
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The last command is especially useful. Quadlet is a generator that produces systemd unit files from&amp;nbsp;your &lt;code&gt;.container&lt;/code&gt; files. If a Quadlet isn&amp;#8217;t behaving as&amp;nbsp;expected, &lt;code&gt;--dryrun&lt;/code&gt; shows you the actual systemd unit that was generated, so you can see exactly what systemd is working&amp;nbsp;with.&lt;/p&gt;
&lt;h3 id="registry-authentication"&gt;Registry&amp;nbsp;Authentication&lt;/h3&gt;
&lt;p&gt;For private registries (Red&amp;nbsp;Hat&amp;#8217;s &lt;code&gt;registry.redhat.io&lt;/code&gt;, corporate registries), authenticate once and Podman stores the&amp;nbsp;credentials:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Interactive login&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;registry.redhat.io

&lt;span class="c1"&gt;# Credentials are stored in&lt;/span&gt;
&lt;span class="c1"&gt;# /run/containers/0/auth.json (root)&lt;/span&gt;
&lt;span class="c1"&gt;# $XDG_RUNTIME_DIR/containers/auth.json (rootless)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For automated environments, you can provide credentials via a &lt;span class="caps"&gt;JSON&lt;/span&gt;&amp;nbsp;file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;--authfile&lt;span class="w"&gt; &lt;/span&gt;/etc/containers/auth.json&lt;span class="w"&gt; &lt;/span&gt;registry.redhat.io
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="automation-with-ansible"&gt;Automation with&amp;nbsp;Ansible&lt;/h3&gt;
&lt;p&gt;Once you&amp;#8217;re managing more than a handful of hosts, writing Quadlet files by hand stops scaling. The &lt;a href="https://galaxy.ansible.com/ui/repo/published/containers/podman/"&gt;&lt;code&gt;containers.podman&lt;/code&gt;&lt;/a&gt; Ansible Collection can manage every aspect of Podman - containers, pods, networks, volumes, secrets, registry logins - and recent versions can generate Quadlet files directly. Instead of templating unit files yourself, you declare the desired state in a playbook and the collection handles the&amp;nbsp;rest:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Deploy Forgejo database&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;containers.podman.podman_container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;forgejo-db&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;registry.redhat.io/rhel10/postgresql-16:latest&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;quadlet&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;quadlet_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/containers/systemd&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;forgejo-backend.network&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;forgejo_db_password,type=env,target=POSTGRESQL_PASSWORD&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/opt/forgejo/postgres:/var/lib/pgsql/data:z&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;POSTGRESQL_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;forgejo&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;POSTGRESQL_DATABASE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;forgejo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;state: quadlet&lt;/code&gt; parameter is the key - it tells the module to generate a Quadlet file&amp;nbsp;in &lt;code&gt;quadlet_dir&lt;/code&gt; rather than starting a container directly. This gives you Ansible&amp;#8217;s idempotency and inventory management on top of Podman&amp;#8217;s systemd integration. If you&amp;#8217;re running Podman across a fleet of &lt;span class="caps"&gt;RHEL&lt;/span&gt; hosts, this collection is the missing piece between &amp;#8220;it works on one server&amp;#8221; and &amp;#8220;it works&amp;nbsp;everywhere.&amp;#8221;&lt;/p&gt;
&lt;h2 id="when-podman-isnt-the-right-choice"&gt;When Podman Isn&amp;#8217;t the Right&amp;nbsp;Choice&lt;/h2&gt;
&lt;p&gt;I said this article was opinionated, not delusional. There are cases where Docker or Kubernetes makes more&amp;nbsp;sense:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker Desktop for local development on macOS/Windows.&lt;/strong&gt; Podman&amp;nbsp;has &lt;code&gt;podman machine&lt;/code&gt; for non-Linux platforms, and it works, but Docker Desktop&amp;#8217;s integration with macOS and Windows is more polished. If your developers are on Macs and just need to run containers locally, Docker Desktop is a fine choice. The production host should still be&amp;nbsp;Podman.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-node orchestration.&lt;/strong&gt; If you need containers spanning multiple hosts with service discovery, rolling updates, and horizontal scaling, that&amp;#8217;s Kubernetes territory. Podman is a single-host container runtime. It does that job exceptionally well, but it doesn&amp;#8217;t pretend to be an&amp;nbsp;orchestrator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem lock-in.&lt;/strong&gt; Some &lt;span class="caps"&gt;CI&lt;/span&gt;/&lt;span class="caps"&gt;CD&lt;/span&gt; platforms, development tools, and monitoring solutions have deep Docker-specific integrations that go beyond the &lt;span class="caps"&gt;API&lt;/span&gt; compatibility layer. If your entire toolchain assumes Docker and the compatibility layer doesn&amp;#8217;t fully cover your use case, forcing Podman may create more friction than it&amp;nbsp;solves.&lt;/p&gt;
&lt;p&gt;For everything else - single-host deployments, homelab infrastructure, edge computing, production services that don&amp;#8217;t need orchestration - Podman with Quadlets is the better&amp;nbsp;tool.&lt;/p&gt;
&lt;h2 id="the-bigger-picture"&gt;The Bigger&amp;nbsp;Picture&lt;/h2&gt;
&lt;p&gt;Containers are processes. systemd manages processes. Quadlets connect the two. Your containers start at boot, restart on failure, log to journald, respect cgroup limits, and integrate with SELinux - all through mechanisms that have been battle-tested in production Linux systems for over a&amp;nbsp;decade.&lt;/p&gt;
&lt;p&gt;Docker popularized containers and deserves credit for that. But the Linux ecosystem has matured - user namespaces, cgroups v2, and systemd&amp;#8217;s service management have made the central daemon architecture unnecessary for most Linux server workloads. Podman builds on that maturity rather than working around&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re starting new container infrastructure on Linux, start with Podman. If you&amp;#8217;re running Docker in production, the compatibility layer makes migration surprisingly low-friction, and the architectural improvements make it&amp;nbsp;worthwhile.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/"&gt;Podman&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"&gt;Podman Quadlet&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html"&gt;Podman Auto-Update&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-secret.1.html"&gt;Podman Secrets&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.redhat.com/en/blog/quadlet-podman"&gt;Red Hat: From Docker Compose to Podman&amp;nbsp;Quadlets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://galaxy.ansible.com/ui/repo/published/containers/podman/"&gt;containers.podman Ansible Collection&lt;/a&gt; - manage Podman and generate Quadlets via&amp;nbsp;Ansible&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;Production-Grade Container Deployment with Podman Quadlets&lt;/a&gt; - my earlier Quadlet&amp;nbsp;guide&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/keycloak-26-on-podman-with-quadlets-identity-management-the-systemd-way/"&gt;Keycloak 26 on Podman with Quadlets&lt;/a&gt; - practical Quadlet deployment&amp;nbsp;example&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/selinux-a-practical-guide-for-fedora-and-rhel/"&gt;SELinux: A Practical Guide&lt;/a&gt; - understanding SELinux in container&amp;nbsp;contexts&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="podman"/><category term="containers"/><category term="quadlet"/><category term="systemd"/><category term="linux"/><category term="rhel"/><category term="docker"/><category term="secrets"/><category term="traefik"/></entry><entry><title>Speeding Up Forgejo CI with a Custom OCI Image</title><link href="https://blog.hofstede.it/speeding-up-forgejo-ci-with-a-custom-oci-image/" rel="alternate"/><published>2026-04-01T00:00:00+02:00</published><updated>2026-04-01T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-04-01:/speeding-up-forgejo-ci-with-a-custom-oci-image/</id><summary type="html">&lt;p&gt;How I cut my blog&amp;#8217;s &lt;span class="caps"&gt;CI&lt;/span&gt;/&lt;span class="caps"&gt;CD&lt;/span&gt; build time in half by baking dependencies into a custom container image and hosting it in Forgejo&amp;#8217;s built-in &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;registry.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="The merged PR with the streamlined pipeline" src="https://blog.hofstede.it/images/2026-04-01-forgejo-ci-custom-oci-image.png" title="Forgejo Actions: the pipeline after removing the install step"&gt; &lt;/p&gt;
&lt;p&gt;Every push to this blog triggers a Forgejo Actions pipeline that builds the site with Pelican, then deploys it via rsync. If you want the full picture of how this blog&amp;#8217;s infrastructure works - Bastille jails, Caddy, the whole deployment chain - I covered that in &lt;a href="https://blog.hofstede.it/hosting-a-static-blog-on-freebsd-with-bastille-jails-and-automated-deployment/"&gt;an earlier article&lt;/a&gt;. The pipeline worked fine. But every single run started with the same&amp;nbsp;ritual: &lt;code&gt;apt-get update&lt;/code&gt;, install four system&amp;nbsp;packages, &lt;code&gt;pip install&lt;/code&gt; five Python packages. The actual build took seconds - the dependency installation took longer than the rest of the pipeline&amp;nbsp;combined.&lt;/p&gt;
&lt;p&gt;This is a solved problem in the container world. Instead of installing dependencies at runtime, bake them into the image. Here&amp;#8217;s how I did it with Forgejo&amp;#8217;s built-in container registry, keeping everything self-contained on a single Forgejo&amp;nbsp;instance.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The&amp;nbsp;Problem&lt;/h2&gt;
&lt;p&gt;My original workflow&amp;nbsp;used &lt;code&gt;python:3.11-slim&lt;/code&gt; as the &lt;span class="caps"&gt;CI&lt;/span&gt; container and installed everything from scratch on every&amp;nbsp;run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;docker&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;python:3.11-slim&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Install Runtime Dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;apt-get update&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;apt-get install -y nodejs rsync openssh-client git&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;pip install pelican pelican-sitemap markdown typogrify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every push - even a single typo fix - paid this tax. The install step alone accounted for the majority of the pipeline&amp;#8217;s wall clock&amp;nbsp;time.&lt;/p&gt;
&lt;h2 id="the-fix-a-custom-ci-image"&gt;The Fix: A Custom &lt;span class="caps"&gt;CI&lt;/span&gt;&amp;nbsp;Image&lt;/h2&gt;
&lt;p&gt;The solution is&amp;nbsp;a &lt;code&gt;Containerfile&lt;/code&gt; that does the installation once, at image build&amp;nbsp;time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;

&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;--no-install-recommends&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;nodejs&lt;span class="w"&gt; &lt;/span&gt;rsync&lt;span class="w"&gt; &lt;/span&gt;openssh-client&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/lib/apt/lists/*

&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--no-cache-dir&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pelican&lt;span class="w"&gt; &lt;/span&gt;pelican-sitemap&lt;span class="w"&gt; &lt;/span&gt;markdown&lt;span class="w"&gt; &lt;/span&gt;typogrify
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Nothing fancy. Same base image, same packages - just shifted from &amp;#8220;every &lt;span class="caps"&gt;CI&lt;/span&gt; run&amp;#8221; to &amp;#8220;once, when I build the image&amp;#8221;.&amp;nbsp;The &lt;code&gt;--no-install-recommends&lt;/code&gt; and &lt;code&gt;--no-cache-dir&lt;/code&gt; flags keep the image&amp;nbsp;lean.&lt;/p&gt;
&lt;h2 id="forgejos-built-in-oci-registry"&gt;Forgejo&amp;#8217;s Built-in &lt;span class="caps"&gt;OCI&lt;/span&gt;&amp;nbsp;Registry&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s the part that makes this particularly clean: Forgejo ships with a built-in &lt;span class="caps"&gt;OCI&lt;/span&gt;-compliant container registry. No need for Docker Hub, no need for a separate Harbor instance, no external dependency. Your &lt;span class="caps"&gt;CI&lt;/span&gt; images live right next to your&amp;nbsp;code.&lt;/p&gt;
&lt;p&gt;Build and push with&amp;nbsp;Podman:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;git.example.com/youruser/yourrepo-ci:latest&lt;span class="w"&gt; &lt;/span&gt;.
podman&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;git.example.com
podman&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;git.example.com/youruser/yourrepo-ci:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The image shows up under &lt;strong&gt;Packages&lt;/strong&gt; in your Forgejo instance. Same authentication, same access control, same backup strategy as the rest of your Forgejo&amp;nbsp;data.&lt;/p&gt;
&lt;h2 id="updating-the-pipeline"&gt;Updating the&amp;nbsp;Pipeline&lt;/h2&gt;
&lt;p&gt;With the image pushed, the workflow drops the install step entirely and pulls the pre-built image&amp;nbsp;instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;docker&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;git.example.com/youruser/yourrepo-ci:latest&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.REGISTRY_USER }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.REGISTRY_PASSWORD }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Checkout Code&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v3&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# ... rest of pipeline unchanged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;credentials&lt;/code&gt; block authenticates against the Forgejo registry. Store your registry username and an application token as repository&amp;nbsp;secrets.&lt;/p&gt;
&lt;p&gt;That&amp;#8217;s the entire change. The install step is gone. The pipeline goes straight from image pull to checkout to build to&amp;nbsp;deploy.&lt;/p&gt;
&lt;h2 id="the-result"&gt;The&amp;nbsp;Result&lt;/h2&gt;
&lt;p&gt;The dependency installation step disappeared completely. What used to be the slowest part of the pipeline is now a single container pull that Forgejo&amp;#8217;s runner likely has cached locally after the first run. The actual build-and-deploy cycle is all that&amp;nbsp;remains.&lt;/p&gt;
&lt;h2 id="when-to-rebuild"&gt;When to&amp;nbsp;Rebuild&lt;/h2&gt;
&lt;p&gt;The trade-off is that you now maintain a container image. In practice, this is minimal&amp;nbsp;overhead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Adding a Pelican plugin?&lt;/strong&gt; Rebuild and push the&amp;nbsp;image&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upgrading Python packages?&lt;/strong&gt; Rebuild and&amp;nbsp;push&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System package update?&lt;/strong&gt; Rebuild and&amp;nbsp;push&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a blog pipeline, that&amp;#8217;s maybe once a quarter.&amp;nbsp;The &lt;code&gt;ci-image/&lt;/code&gt; directory lives in the same repository as the blog, so the Containerfile is versioned alongside everything&amp;nbsp;else.&lt;/p&gt;
&lt;h2 id="takeaway"&gt;Takeaway&lt;/h2&gt;
&lt;p&gt;If your &lt;span class="caps"&gt;CI&lt;/span&gt; pipeline spends more time installing dependencies than doing actual work, build a custom image. If you&amp;#8217;re already running Forgejo, you have a container registry sitting right there - use it. Two commits, one Containerfile, and the entire install step&amp;nbsp;vanishes.&lt;/p&gt;
&lt;p&gt;This is one of several small improvements I&amp;#8217;ve been making to the blog over time. If you&amp;#8217;re running a Pelican blog yourself, you might also be interested in how I &lt;a href="https://blog.hofstede.it/adding-fediverse-comments-to-a-pelican-blog/"&gt;added Fediverse-based comments&lt;/a&gt; without any server-side&amp;nbsp;dependencies.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forgejo.org/docs/latest/user/packages/container/"&gt;Forgejo Container Registry&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forgejo.org/docs/latest/user/actions/"&gt;Forgejo Actions&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/opencontainers/distribution-spec"&gt;&lt;span class="caps"&gt;OCI&lt;/span&gt; Distribution&amp;nbsp;Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/"&gt;Podman&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Meta"/><category term="forgejo"/><category term="ci-cd"/><category term="containers"/><category term="oci"/><category term="pelican"/><category term="devops"/></entry><entry><title>Keycloak 26 on Podman with Quadlets: Identity Management the systemd Way</title><link href="https://blog.hofstede.it/keycloak-26-on-podman-with-quadlets-identity-management-the-systemd-way/" rel="alternate"/><published>2026-03-31T00:00:00+02:00</published><updated>2026-03-31T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-31:/keycloak-26-on-podman-with-quadlets-identity-management-the-systemd-way/</id><summary type="html">&lt;p&gt;Deploying Keycloak 26 as an identity provider using Podman Quadlets with network segmentation, secret management, and systemd&amp;nbsp;integration.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-03-31-keycloak-26-podman-quadlets.png" title="Keycloak on Podman: Header image"&gt;&lt;/p&gt;
&lt;p&gt;Running your own identity provider is one of those things that sounds straightforward until you&amp;#8217;re three hours into debugging &lt;span class="caps"&gt;OIDC&lt;/span&gt; token flows at 2 &lt;span class="caps"&gt;AM&lt;/span&gt;. Keycloak has become the de facto open-source solution for identity and access management, but deploying it properly - with a real database backend, network isolation, and no credentials in plaintext - still takes some deliberate&amp;nbsp;architecture.&lt;/p&gt;
&lt;p&gt;This guide walks through deploying the &lt;a href="https://access.redhat.com/products/red-hat-build-of-keycloak"&gt;Red Hat Build of Keycloak&lt;/a&gt; (&lt;span class="caps"&gt;RHBK&lt;/span&gt;) 26 on Podman using Quadlets: systemd-native unit files that give you declarative container management without a daemon or a compose file. If you&amp;#8217;ve read my earlier &lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;Podman Quadlets guide&lt;/a&gt;, this follows the same architectural pattern - isolated backend network for the database, dual-attached application container, secrets managed through Podman&amp;#8217;s secret&amp;nbsp;store.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note on support&lt;/strong&gt;: Red Hat&amp;#8217;s official documentation for the &lt;span class="caps"&gt;RHBK&lt;/span&gt; container image targets OpenShift as the supported deployment platform. Running &lt;span class="caps"&gt;RHBK&lt;/span&gt; on plain Podman is technically sound and works well, but it is not a Red Hat-supported configuration. If you need a fully supported deployment path, OpenShift is where Red Hat stands behind it. This guide is for those of us who want the &lt;span class="caps"&gt;RHBK&lt;/span&gt; image&amp;#8217;s quality, security errata, and build lifecycle on a single-host Podman setup - and are comfortable with that&amp;nbsp;trade-off.&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;p&gt;The deployment consists of three&amp;nbsp;components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL 16&lt;/strong&gt; - Keycloak&amp;#8217;s database backend, isolated on a private&amp;nbsp;network&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keycloak 26&lt;/strong&gt; - The identity provider, connected to both backend and frontend&amp;nbsp;networks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A dedicated backend network&lt;/strong&gt; - Ensuring the database is never reachable from the&amp;nbsp;outside&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    Internet
                       |
               Reverse Proxy (443)
                       |
          +------------+------------+
          |     frontend network    |
          +------------+------------+
                       |
                Keycloak Container
                   (Port 8080)
                       |
          +------------+------------+
          | keycloak-backend.network|
          |   (172.16.0.0/24)       |
          +------------+------------+
                       |
              PostgreSQL Container
                  (Port 5432)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Keycloak sits on both networks: it talks to PostgreSQL over the isolated backend, and your reverse proxy reaches it on the frontend. The database has no route to the outside&amp;nbsp;world.&lt;/p&gt;
&lt;p&gt;This guide assumes you already have a reverse proxy (Traefik, Caddy, nginx) handling &lt;span class="caps"&gt;TLS&lt;/span&gt; termination on your frontend network. If you need that piece, my &lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;earlier Quadlet article&lt;/a&gt; covers Traefik in&amp;nbsp;detail.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;You&amp;#8217;ll&amp;nbsp;need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt; 10, Fedora 43+, or any system with Podman 5.x+&lt;/strong&gt; and Quadlet&amp;nbsp;support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An active Red Hat subscription&lt;/strong&gt; for pulling&amp;nbsp;from &lt;code&gt;registry.redhat.io&lt;/code&gt; (this guide uses the Red Hat Build of&amp;nbsp;Keycloak)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A reverse proxy&lt;/strong&gt; already handling &lt;span class="caps"&gt;TLS&lt;/span&gt; on your frontend&amp;nbsp;network&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root access&lt;/strong&gt; for system-wide Quadlet deployment (or adapt for rootless&amp;nbsp;with &lt;code&gt;systemctl --user&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All Quadlet files go&amp;nbsp;into &lt;code&gt;/etc/containers/systemd/&lt;/code&gt; for system-wide&amp;nbsp;deployments.&lt;/p&gt;
&lt;h2 id="step-1-create-the-secrets"&gt;Step 1: Create the&amp;nbsp;Secrets&lt;/h2&gt;
&lt;p&gt;Never put passwords in unit files. Podman&amp;#8217;s secret store keeps credentials encrypted and out of process&amp;nbsp;listings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generate and store the database password&lt;/span&gt;
pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;keycloak_db_password&lt;span class="w"&gt; &lt;/span&gt;-

&lt;span class="c1"&gt;# Generate and store the Keycloak admin password&lt;/span&gt;
pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;keycloak_admin_password&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;tr -d '\n'&lt;/code&gt; is&amp;nbsp;important. &lt;code&gt;pwgen&lt;/code&gt; appends a trailing newline,&amp;nbsp;and &lt;code&gt;podman secret create&lt;/code&gt; stores the raw bytes verbatim. PostgreSQL&amp;#8217;s init script strips the newline when setting the password, but Keycloak&amp;#8217;s &lt;span class="caps"&gt;JDBC&lt;/span&gt; driver sends it as-is - resulting in a password mismatch that produces a confusing &amp;#8220;authentication failed&amp;#8221; error despite both containers reading the exact same&amp;nbsp;secret.&lt;/p&gt;
&lt;p&gt;Write down the admin password somewhere safe - you&amp;#8217;ll need it for first login. After initial setup, you can create additional admin accounts through the Keycloak &lt;span class="caps"&gt;UI&lt;/span&gt; and remove the bootstrap&amp;nbsp;admin.&lt;/p&gt;
&lt;h2 id="step-2-backend-network"&gt;Step 2: Backend&amp;nbsp;Network&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;/etc/containers/systemd/keycloak-backend.network&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Network]&lt;/span&gt;
&lt;span class="na"&gt;NetworkName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-backend&lt;/span&gt;
&lt;span class="na"&gt;Subnet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;172.16.0.0/24&lt;/span&gt;
&lt;span class="na"&gt;Gateway&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;172.16.0.1&lt;/span&gt;
&lt;span class="na"&gt;IPRange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;172.16.0.0/28&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A dedicated subnet with an explicit &lt;span class="caps"&gt;IP&lt;/span&gt; range.&amp;nbsp;The &lt;code&gt;/28&lt;/code&gt; range&amp;nbsp;in &lt;code&gt;IPRange&lt;/code&gt; limits &lt;span class="caps"&gt;DHCP&lt;/span&gt; assignments to 14 addresses - more than enough for a database and application container, and it prevents the network from being used as a dumping ground for unrelated&amp;nbsp;containers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not just use the default bridge?&lt;/strong&gt; Network segmentation is the point. The database should only be reachable by containers that explicitly join this network. Defense-in-depth starts at the network&amp;nbsp;layer.&lt;/p&gt;
&lt;h2 id="step-3-database-container"&gt;Step 3: Database&amp;nbsp;Container&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;/etc/containers/systemd/keycloak-db.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Keycloak PostgreSQL database&lt;/span&gt;

&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-db&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry.redhat.io/rhel10/postgresql-16:latest&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="c1"&gt;# Isolated on backend network only - no frontend access&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-backend.network&lt;/span&gt;

&lt;span class="c1"&gt;# PostgreSQL configuration&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;POSTGRESQL_USER=keycloak&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;POSTGRESQL_DATABASE=keycloak&lt;/span&gt;

&lt;span class="c1"&gt;# Password injected at runtime from Podman secret store&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak_db_password,type=env,target=POSTGRESQL_PASSWORD&lt;/span&gt;

&lt;span class="c1"&gt;# Persistent storage with SELinux relabeling&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/keycloak/postgres:/var/lib/pgsql/data:z&lt;/span&gt;

&lt;span class="c1"&gt;# Health check - verify PostgreSQL is accepting connections&lt;/span&gt;
&lt;span class="na"&gt;HealthCmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/libexec/check-container&lt;/span&gt;
&lt;span class="na"&gt;HealthInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;
&lt;span class="na"&gt;HealthTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;HealthRetries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;HealthStartPeriod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Key&amp;nbsp;points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;registry.redhat.io/rhel10/postgresql-16&lt;/code&gt;&lt;/strong&gt;: The &lt;span class="caps"&gt;RHEL&lt;/span&gt;-based PostgreSQL image follows Red Hat&amp;#8217;s support lifecycle and receives security errata. If you don&amp;#8217;t have a Red Hat&amp;nbsp;subscription, &lt;code&gt;docker.io/postgres:16-alpine&lt;/code&gt; is a solid alternative - just adjust the environment variables&amp;nbsp;(&lt;code&gt;POSTGRES_USER&lt;/code&gt;, &lt;code&gt;POSTGRES_DB&lt;/code&gt;, &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;), the internal data path&amp;nbsp;(&lt;code&gt;/var/lib/postgresql/data&lt;/code&gt; instead&amp;nbsp;of &lt;code&gt;/var/lib/pgsql/data&lt;/code&gt;), and the health check&amp;nbsp;(&lt;code&gt;pg_isready -U keycloak&lt;/code&gt; instead&amp;nbsp;of &lt;code&gt;/usr/libexec/check-container&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single network attachment&lt;/strong&gt;: The database lives exclusively on the backend network. It has no route to the frontend and cannot be reached from outside the backend&amp;nbsp;subnet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:z&lt;/code&gt; volume flag&lt;/strong&gt;: Tells Podman to relabel the SELinux context for shared container access. Don&amp;#8217;t skip this on SELinux-enforcing&amp;nbsp;systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health check&lt;/strong&gt;: The &lt;span class="caps"&gt;RHEL&lt;/span&gt; PostgreSQL image ships&amp;nbsp;with &lt;code&gt;/usr/libexec/check-container&lt;/code&gt;. For the upstream image,&amp;nbsp;use &lt;code&gt;pg_isready -U keycloak&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create the data directory and set permissions for the container&amp;#8217;s PostgreSQL user (&lt;span class="caps"&gt;UID&lt;/span&gt;&amp;nbsp;26):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/postgres
setfacl&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;u:26:rwx&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;RHEL&lt;/span&gt; PostgreSQL container runs as &lt;span class="caps"&gt;UID&lt;/span&gt; 26&amp;nbsp;(&lt;code&gt;postgres&lt;/code&gt;). Without this &lt;span class="caps"&gt;ACL&lt;/span&gt;, the container can&amp;#8217;t initialize the data directory and will fail with permission errors.&amp;nbsp;The &lt;code&gt;:z&lt;/code&gt; SELinux flag handles labeling, but filesystem-level ownership is a separate concern&amp;nbsp;- &lt;code&gt;setfacl&lt;/code&gt; grants access without changing the directory&amp;#8217;s&amp;nbsp;ownership.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Password rotation&lt;/strong&gt;: Red Hat&amp;#8217;s PostgreSQL container resets the database password to match&amp;nbsp;the &lt;code&gt;POSTGRESQL_PASSWORD&lt;/code&gt; environment variable on each startup, so rotating the password is straightforward: update the Podman secret and recreate the container. Since Podman&amp;nbsp;injects &lt;code&gt;type=env&lt;/code&gt; secrets at container creation time, a&amp;nbsp;simple &lt;code&gt;systemctl restart&lt;/code&gt; isn&amp;#8217;t enough - you need a fresh container creation to pick up the new secret&amp;nbsp;value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;keycloak_db_password
pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;keycloak_db_password&lt;span class="w"&gt; &lt;/span&gt;-
systemctl&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;keycloak-db.service&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;keycloak-db.service&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="step-4-keycloak-application-container"&gt;Step 4: Keycloak Application&amp;nbsp;Container&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;/etc/containers/systemd/keycloak-server.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Red Hat Build of Keycloak&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-db.service&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.service&lt;/span&gt;
&lt;span class="na"&gt;Requires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-db.service&lt;/span&gt;

&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-server&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry.redhat.io/rhbk/keycloak-rhel9:26.4-12&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="c1"&gt;# Dual network: backend for database, frontend for reverse proxy&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak-backend.network&lt;/span&gt;
&lt;span class="c1"&gt;# Must match the network your reverse proxy (e.g. Traefik) is on&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;frontend.network&lt;/span&gt;

&lt;span class="c1"&gt;# Database configuration&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_DB=postgres&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_DB_URL=jdbc:postgresql://keycloak-db:5432/keycloak&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_DB_USERNAME=keycloak&lt;/span&gt;

&lt;span class="c1"&gt;# Proxy configuration - Keycloak runs behind a reverse proxy&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_PROXY_HEADERS=xforwarded&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_HTTP_ENABLED=true&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_HOSTNAME_STRICT=false&lt;/span&gt;

&lt;span class="c1"&gt;# Hostname - set to your actual domain&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_HOSTNAME=keycloak.example.com&lt;/span&gt;

&lt;span class="c1"&gt;# Enable health and metrics endpoints&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_HEALTH_ENABLED=true&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_METRICS_ENABLED=true&lt;/span&gt;

&lt;span class="c1"&gt;# Bootstrap admin user (used for first login only)&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;KC_BOOTSTRAP_ADMIN_USERNAME=admin&lt;/span&gt;

&lt;span class="c1"&gt;# Secrets - database and admin passwords&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak_db_password,type=env,target=KC_DB_PASSWORD&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;keycloak_admin_password,type=env,target=KC_BOOTSTRAP_ADMIN_PASSWORD&lt;/span&gt;

&lt;span class="c1"&gt;# Persistent storage for themes and providers&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/keycloak/providers:/opt/keycloak/providers:z&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/keycloak/themes:/opt/keycloak/themes:z&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;

&lt;span class="c1"&gt;# Run Keycloak in production mode&lt;/span&gt;
&lt;span class="na"&gt;Exec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;

&lt;span class="c1"&gt;# Traefik labels for routing&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.enable=true&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.docker.network=frontend&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.rhbk.rule=Host(`keycloak.example.com`)&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.rhbk.entrypoints=https&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.rhbk.service=rhbk-svc&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.rhbk.tls.certresolver=traefiktls&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.rhbk.middlewares=secure-headers@file&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.services.rhbk-svc.loadbalancer.server.port=8080&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;#8217;s a lot happening here, so let&amp;#8217;s break it&amp;nbsp;down:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Network configuration&lt;/strong&gt;: The container joins&amp;nbsp;both &lt;code&gt;keycloak-backend.network&lt;/code&gt; (to reach PostgreSQL by&amp;nbsp;hostname &lt;code&gt;keycloak-db&lt;/code&gt;) and the frontend network where your reverse proxy lives.&amp;nbsp;Replace &lt;code&gt;frontend.network&lt;/code&gt; with whatever network your Traefik (or other reverse proxy) container is on - the name must match, or Traefik won&amp;#8217;t discover the container.&amp;nbsp;The &lt;code&gt;traefik.docker.network&lt;/code&gt; label must also&amp;nbsp;match.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database connection&lt;/strong&gt;: Keycloak&amp;nbsp;resolves &lt;code&gt;keycloak-db&lt;/code&gt; via Podman&amp;#8217;s built-in &lt;span class="caps"&gt;DNS&lt;/span&gt; on the backend network. The &lt;span class="caps"&gt;JDBC&lt;/span&gt; &lt;span class="caps"&gt;URL&lt;/span&gt; points directly to the container name - no &lt;span class="caps"&gt;IP&lt;/span&gt; addresses to&amp;nbsp;manage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proxy settings&lt;/strong&gt;: &lt;code&gt;KC_PROXY_HEADERS=xforwarded&lt;/code&gt; tells Keycloak to&amp;nbsp;trust &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers from your reverse proxy for correct &lt;span class="caps"&gt;URL&lt;/span&gt; generation, redirect URIs, and &lt;span class="caps"&gt;HTTPS&lt;/span&gt;&amp;nbsp;detection. &lt;code&gt;KC_HTTP_ENABLED=true&lt;/code&gt; allows plain &lt;span class="caps"&gt;HTTP&lt;/span&gt; on port 8080 since &lt;span class="caps"&gt;TLS&lt;/span&gt; terminates at the&amp;nbsp;proxy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;KC_HOSTNAME_STRICT=false&lt;/code&gt;&lt;/strong&gt;: This is set&amp;nbsp;to &lt;code&gt;false&lt;/code&gt; here to simplify initial setup, but the Keycloak documentation&amp;nbsp;recommends &lt;code&gt;true&lt;/code&gt; for production.&amp;nbsp;With &lt;code&gt;hostname-strict&lt;/code&gt; disabled, Keycloak will respond on any hostname forwarded by the proxy, which can create security issues if your reverse proxy doesn&amp;#8217;t overwrite&amp;nbsp;the &lt;code&gt;Host&lt;/code&gt; header. Once you&amp;#8217;ve verified that your reverse proxy correctly&amp;nbsp;sets &lt;code&gt;Host&lt;/code&gt; and &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers to your intended domain, switch this&amp;nbsp;to &lt;code&gt;true&lt;/code&gt;. If you&amp;#8217;re using Traefik with&amp;nbsp;the &lt;code&gt;Host()&lt;/code&gt; rule shown above, Traefik already filters by hostname, so&amp;nbsp;setting &lt;code&gt;KC_HOSTNAME_STRICT=true&lt;/code&gt; from the start is&amp;nbsp;safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;start&lt;/code&gt; without &lt;code&gt;--optimized&lt;/code&gt;?&lt;/strong&gt; Keycloak 26 has a build phase and a runtime phase. The upstream community image ships pre-built, but the &lt;span class="caps"&gt;RHBK&lt;/span&gt; image does not - it needs to run the build step on first startup.&amp;nbsp;Using &lt;code&gt;start&lt;/code&gt; (without &lt;code&gt;--optimized&lt;/code&gt;) lets Keycloak handle both phases automatically. This adds a few seconds to each startup, which is negligible for a service that starts once and runs continuously. If startup time matters to you (e.g., in a scaling scenario), you can build a custom image&amp;nbsp;with &lt;code&gt;kc.sh build&lt;/code&gt; baked in (see Step 5) and then switch&amp;nbsp;to &lt;code&gt;start --optimized&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bootstrap admin&lt;/strong&gt;: &lt;code&gt;KC_BOOTSTRAP_ADMIN_USERNAME&lt;/code&gt; and &lt;code&gt;KC_BOOTSTRAP_ADMIN_PASSWORD&lt;/code&gt; create an admin account on first startup. This account is meant for initial setup only. After logging in, create a proper admin user in the Keycloak &lt;span class="caps"&gt;UI&lt;/span&gt; and consider removing the bootstrap&amp;nbsp;credentials.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No health check in this Quadlet&lt;/strong&gt;: The &lt;span class="caps"&gt;RHBK&lt;/span&gt; image is based on &lt;span class="caps"&gt;UBI&lt;/span&gt; 9 Micro, which doesn&amp;#8217;t&amp;nbsp;include &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, or any other &lt;span class="caps"&gt;HTTP&lt;/span&gt; client.&amp;nbsp;A &lt;code&gt;HealthCmd&lt;/code&gt; that relies&amp;nbsp;on &lt;code&gt;curl&lt;/code&gt; will always fail, marking the container as unhealthy - and Traefik ignores unhealthy containers by default. You can define a basic readiness check using Bash&amp;#8217;s&amp;nbsp;built-in &lt;code&gt;/dev/tcp&lt;/code&gt; to probe the management&amp;nbsp;port:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;HealthCmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;bash -c &amp;#39;echo &amp;gt; /dev/tcp/localhost/9000&amp;#39;&lt;/span&gt;
&lt;span class="na"&gt;HealthInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;
&lt;span class="na"&gt;HealthTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;HealthRetries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;HealthStartPeriod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;60s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This confirms the management port is accepting connections, but it&amp;#8217;s a &lt;span class="caps"&gt;TCP&lt;/span&gt; check, not a full &lt;span class="caps"&gt;HTTP&lt;/span&gt; readiness probe. It won&amp;#8217;t catch cases where Keycloak&amp;#8217;s &lt;span class="caps"&gt;JVM&lt;/span&gt; is up but the application hasn&amp;#8217;t finished initializing. Use it as a basic liveness signal, not as a gate for traffic routing. We omit it from the main Quadlet above to keep things simple and avoid Traefik interaction issues, but add it if you want systemd-level health visibility&amp;nbsp;via &lt;code&gt;systemctl status&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traefik labels&lt;/strong&gt;: The labels configure Traefik&amp;#8217;s dynamic&amp;nbsp;routing. &lt;code&gt;traefik.docker.network&lt;/code&gt; must point to the shared frontend network so Traefik routes traffic over the right interface. The service label explicitly names the load balancer backend and maps it to port 8080. If you&amp;#8217;re using a different reverse proxy, replace these labels with your proxy&amp;#8217;s configuration (see the reverse proxy section&amp;nbsp;below).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Systemd dependencies&lt;/strong&gt;: &lt;code&gt;Requires=keycloak-db.service&lt;/code&gt; ensures PostgreSQL is running before Keycloak attempts to connect - if the database service fails, Keycloak stops&amp;nbsp;too. &lt;code&gt;After=traefik.service&lt;/code&gt; ensures the reverse proxy is up before Keycloak starts, so Traefik can discover it&amp;nbsp;immediately.&lt;/p&gt;
&lt;p&gt;Create the data directories and set permissions. The &lt;span class="caps"&gt;RHBK&lt;/span&gt; container runs as &lt;span class="caps"&gt;UID&lt;/span&gt; 1000, so the bind-mounted directories need to be accessible to that user - just as we set up &lt;span class="caps"&gt;UID&lt;/span&gt; 26 for&amp;nbsp;PostgreSQL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/&lt;span class="o"&gt;{&lt;/span&gt;providers,themes&lt;span class="o"&gt;}&lt;/span&gt;
setfacl&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;u:1000:rwx&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/providers&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/themes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="step-5-build-the-optimized-image-optional"&gt;Step 5: Build the Optimized Image&amp;nbsp;(Optional)&lt;/h2&gt;
&lt;p&gt;If you need custom providers, custom themes baked into the image, or non-default database drivers, build a custom&amp;nbsp;image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;registry.redhat.io/rhbk/keycloak-rhel9:26.4-12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="c"&gt;# Set build-time configuration&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;KC_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;KC_HEALTH_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;KC_METRICS_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Add custom providers if needed&lt;/span&gt;
&lt;span class="c"&gt;# COPY my-provider.jar /opt/keycloak/providers/&lt;/span&gt;

&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/bin/kc.sh&lt;span class="w"&gt; &lt;/span&gt;build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;registry.redhat.io/rhbk/keycloak-rhel9:26.4-12&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--from&lt;span class="o"&gt;=&lt;/span&gt;builder&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Build with&amp;nbsp;Podman:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;localhost/keycloak-custom:26.4&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then update the Quadlet to&amp;nbsp;use &lt;code&gt;Image=localhost/keycloak-custom:26.4&lt;/code&gt;,&amp;nbsp;switch &lt;code&gt;AutoUpdate=registry&lt;/code&gt; to &lt;code&gt;AutoUpdate=local&lt;/code&gt; (Podman&amp;nbsp;uses &lt;code&gt;registry&lt;/code&gt; for remote refs&amp;nbsp;and &lt;code&gt;local&lt;/code&gt; for locally built images), and&amp;nbsp;change &lt;code&gt;Exec=start&lt;/code&gt; to &lt;code&gt;Exec=start --optimized&lt;/code&gt; since the build step is already baked into the&amp;nbsp;image.&lt;/p&gt;
&lt;p&gt;For most deployments, the default &lt;span class="caps"&gt;RHBK&lt;/span&gt; image&amp;nbsp;with &lt;code&gt;start&lt;/code&gt; is sufficient. Use a custom image&amp;nbsp;plus &lt;code&gt;start --optimized&lt;/code&gt; when you need faster restarts or want providers and themes baked in rather than&amp;nbsp;bind-mounted.&lt;/p&gt;
&lt;h2 id="step-6-deployment"&gt;Step 6:&amp;nbsp;Deployment&lt;/h2&gt;
&lt;p&gt;Reload systemd to pick up the new Quadlet&amp;nbsp;files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;daemon-reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Start the&amp;nbsp;stack:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;keycloak-db.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify everything is&amp;nbsp;running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check container status&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;keycloak-db.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service

&lt;span class="c1"&gt;# Watch Keycloak logs for successful startup&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service&lt;span class="w"&gt; &lt;/span&gt;-f
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see Keycloak log its startup sequence, database migration (on first run), and&amp;nbsp;eventually:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Keycloak 26.4.10.redhat-00001 on JVM (powered by Quarkus 3.27.2.redhat-00001) started in 32.000s.
Listening on: http://0.0.0.0:8080. Management interface listening on http://0.0.0.0:9000.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="step-7-reverse-proxy-notes"&gt;Step 7: Reverse Proxy&amp;nbsp;Notes&lt;/h2&gt;
&lt;p&gt;The Quadlet above includes Traefik labels that handle routing configuration declaratively - no separate Traefik config files needed. If you&amp;#8217;re using Traefik, the Quadlet is&amp;nbsp;self-contained.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re using a different reverse proxy, remove the Traefik labels and configure routing manually. Keycloak listens on port 8080 for &lt;span class="caps"&gt;HTTP&lt;/span&gt;. Here are two common&amp;nbsp;alternatives:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caddy&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;Caddyfile&lt;/code&gt; snippet):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;keycloak.example.com {
    reverse_proxy keycloak-server:8080
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;nginx&lt;/strong&gt; (server&amp;nbsp;block):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;keycloak.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://keycloak-server:8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Real-IP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_buffer_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_buffers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;256k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_busy_buffers_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;256k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The larger proxy buffers for nginx may be necessary - Keycloak&amp;#8217;s &lt;span class="caps"&gt;OIDC&lt;/span&gt; responses can include headers that exceed nginx&amp;#8217;s conservative defaults. Without sufficient buffer space, you&amp;#8217;ll&amp;nbsp;see &lt;code&gt;502 Bad Gateway&lt;/code&gt; errors during authentication flows. The sizes above are generous; adjust downward if your setup works without&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;Both examples above assume the reverse proxy is containerized on the same frontend network, so Podman&amp;#8217;s &lt;span class="caps"&gt;DNS&lt;/span&gt;&amp;nbsp;resolves &lt;code&gt;keycloak-server&lt;/code&gt;. If your reverse proxy runs directly on the host, you&amp;#8217;ll need to publish Keycloak&amp;#8217;s port&amp;nbsp;(&lt;code&gt;PublishPort=8080:8080&lt;/code&gt; in the Quadlet) and&amp;nbsp;use &lt;code&gt;proxy_pass http://127.0.0.1:8080&lt;/code&gt; instead.&lt;/p&gt;
&lt;h2 id="step-8-automatic-updates"&gt;Step 8: Automatic&amp;nbsp;Updates&lt;/h2&gt;
&lt;p&gt;Enable Podman&amp;#8217;s auto-update&amp;nbsp;timer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;podman-auto-update.timer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;AutoUpdate=registry&lt;/code&gt; in both Quadlet files, Podman checks daily for new images and recreates containers when updates are available. Volumes persist across recreations, so your data is&amp;nbsp;safe.&lt;/p&gt;
&lt;p&gt;For Keycloak specifically, be deliberate about version updates. Pin to a specific tag&amp;nbsp;like &lt;code&gt;26.4-12&lt;/code&gt; (not &lt;code&gt;latest&lt;/code&gt;) in the Quadlet and bump the version manually after reviewing the changelog. Keycloak upgrades can include database migrations that you want to run intentionally, not at 3 &lt;span class="caps"&gt;AM&lt;/span&gt; via an&amp;nbsp;auto-update.&lt;/p&gt;
&lt;h2 id="post-deployment-first-login"&gt;Post-Deployment: First&amp;nbsp;Login&lt;/h2&gt;
&lt;p&gt;Navigate&amp;nbsp;to &lt;code&gt;https://keycloak.example.com&lt;/code&gt; and log in with the bootstrap admin credentials. From&amp;nbsp;here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a new realm&lt;/strong&gt; for your applications (don&amp;#8217;t use&amp;nbsp;the &lt;code&gt;master&lt;/code&gt; realm for end&amp;nbsp;users)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a permanent admin user&lt;/strong&gt; within the master&amp;nbsp;realm&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up your first client&lt;/strong&gt; - this is the &lt;span class="caps"&gt;OIDC&lt;/span&gt;/&lt;span class="caps"&gt;SAML&lt;/span&gt; application&amp;nbsp;registration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure identity providers&lt;/strong&gt; if you want social login or federation with existing &lt;span class="caps"&gt;LDAP&lt;/span&gt;/&lt;span class="caps"&gt;AD&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you have a proper admin account, clean up the bootstrap&amp;nbsp;user:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Delete the bootstrap admin&lt;/strong&gt; in the Keycloak &lt;span class="caps"&gt;UI&lt;/span&gt;: navigate to&amp;nbsp;the &lt;code&gt;master&lt;/code&gt; realm, go to Users, find&amp;nbsp;the &lt;code&gt;admin&lt;/code&gt; account, and delete&amp;nbsp;it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove the environment variables&lt;/strong&gt; from the Quadlet: delete&amp;nbsp;the &lt;code&gt;KC_BOOTSTRAP_ADMIN_USERNAME&lt;/code&gt; line, and&amp;nbsp;the &lt;code&gt;Secret=keycloak_admin_password&lt;/code&gt; line&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restart&lt;/strong&gt; to&amp;nbsp;apply:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;daemon-reload
systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Removing the environment variables alone doesn&amp;#8217;t delete the user from the database - it only stops Keycloak from trying to create it on boot. Both steps are&amp;nbsp;necessary.&lt;/p&gt;
&lt;h2 id="monitoring"&gt;Monitoring&lt;/h2&gt;
&lt;p&gt;Keycloak 26 exposes Prometheus-compatible metrics on the management&amp;nbsp;port:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# From the host (or any container on the same network)&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;http://keycloak-server:9000/metrics
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Point your Prometheus instance at this endpoint for dashboards covering authentication rates, token issuance, active sessions, and &lt;span class="caps"&gt;JVM&lt;/span&gt;&amp;nbsp;health.&lt;/p&gt;
&lt;p&gt;Health endpoints are equally useful for&amp;nbsp;alerting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Liveness - is the process alive?&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;http://keycloak-server:9000/health/live

&lt;span class="c1"&gt;# Readiness - is it ready to handle requests?&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;http://keycloak-server:9000/health/ready
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="operational-notes"&gt;Operational&amp;nbsp;Notes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Backups&lt;/strong&gt;: The PostgreSQL data lives&amp;nbsp;at &lt;code&gt;/opt/keycloak/postgres&lt;/code&gt;.&amp;nbsp;Use &lt;code&gt;pg_dump&lt;/code&gt; inside the container for logical&amp;nbsp;backups:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;keycloak-db&lt;span class="w"&gt; &lt;/span&gt;pg_dump&lt;span class="w"&gt; &lt;/span&gt;-U&lt;span class="w"&gt; &lt;/span&gt;keycloak&lt;span class="w"&gt; &lt;/span&gt;keycloak&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;keycloak-backup.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Log management&lt;/strong&gt;: All container output flows to journald.&amp;nbsp;Standard &lt;code&gt;journalctl&lt;/code&gt; filtering&amp;nbsp;applies:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Keycloak errors only&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;keycloak-server.service&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;err

&lt;span class="c1"&gt;# Database logs since last boot&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;keycloak-db.service&lt;span class="w"&gt; &lt;/span&gt;-b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Scaling considerations&lt;/strong&gt;: This single-host setup handles a surprising number of users. Keycloak&amp;#8217;s session cache is local by default. If you eventually need clustering, Keycloak supports Infinispan-based distributed caches - but that&amp;#8217;s a different&amp;nbsp;article.&lt;/p&gt;
&lt;h2 id="a-note-on-the-upstream-keycloak-image"&gt;A Note on the Upstream Keycloak&amp;nbsp;Image&lt;/h2&gt;
&lt;p&gt;The upstream community project publishes images&amp;nbsp;at &lt;code&gt;quay.io/keycloak/keycloak&lt;/code&gt;. If you want to use the upstream image&amp;nbsp;instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;quay.io/keycloak/keycloak:26.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The configuration is identical - same environment variables, same startup flags. One difference: the upstream image ships pre-built, so you can&amp;nbsp;use &lt;code&gt;start --optimized&lt;/code&gt; directly without a custom build&amp;nbsp;step.&lt;/p&gt;
&lt;p&gt;The upstream images are community-maintained and don&amp;#8217;t carry Red Hat&amp;#8217;s security errata or support lifecycle. This guide uses &lt;span class="caps"&gt;RHBK&lt;/span&gt; for its build quality and predictable errata cadence, though as noted above, running &lt;span class="caps"&gt;RHBK&lt;/span&gt; on Podman (rather than OpenShift) is itself not a Red Hat-supported configuration. Choose whichever image fits your operational&amp;nbsp;model.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.keycloak.org/guides#server"&gt;Keycloak 26 Server&amp;nbsp;Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.keycloak.org/server/containers"&gt;Keycloak Container Image&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"&gt;Podman Quadlet&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://access.redhat.com/documentation/en-us/red_hat_build_of_keycloak/26.0"&gt;Red Hat Build of Keycloak&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;Production-Grade Container Deployment with Podman Quadlets&lt;/a&gt; - my earlier deep-dive into Quadlet-based&amp;nbsp;deployments&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="linux"/><category term="podman"/><category term="containers"/><category term="quadlet"/><category term="systemd"/><category term="keycloak"/><category term="identity"/><category term="rhel"/></entry><entry><title>My Multi-Stage Backup Strategy: ZFS, Proxmox, and Paranoia</title><link href="https://blog.hofstede.it/my-multi-stage-backup-strategy-zfs-proxmox-and-paranoia/" rel="alternate"/><published>2026-03-28T00:00:00+01:00</published><updated>2026-03-28T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-28:/my-multi-stage-backup-strategy-zfs-proxmox-and-paranoia/</id><summary type="html">&lt;p&gt;Backups are the thing everyone knows they should do and nobody does well enough. Here&amp;#8217;s my multi-stage strategy for keeping about a dozen servers safe: &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots with sanoid, off-site replication with syncoid to rsync.net, Proxmox Backup Server with an S3 backend for VMs, and a creative Podman trick for backing up &lt;span class="caps"&gt;RHEL&lt;/span&gt; hosts that don&amp;#8217;t have proxmox-backup-client. Plus a dead man&amp;#8217;s switch, because the only thing worse than no backups is backups that silently stopped working three months&amp;nbsp;ago.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-03-28-multi-stage-backup-strategy.png" title="Multi-Stage Backup Strategy: Header image"&gt;&lt;/p&gt;
&lt;p&gt;Everybody has a backup strategy. Some people&amp;#8217;s backup strategy is &amp;#8220;I should really set up backups.&amp;#8221; Others involve a cronjob that worked once in 2019 and has been silently failing ever since. A select few involve a &lt;span class="caps"&gt;USB&lt;/span&gt; drive in a desk drawer labeled &amp;#8220;&lt;span class="caps"&gt;IMPORTANT&lt;/span&gt;&amp;#8221; in permanent&amp;nbsp;marker.&lt;/p&gt;
&lt;p&gt;Mine layers &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots, off-site replication, a Proxmox Backup Server that&amp;#8217;s really just a proxy in front of cheap object storage, a Podman container doing something that would make a packaging engineer cry, and a dead man&amp;#8217;s switch that pages me if any of it stops working. It&amp;#8217;s not elegant in every layer, but it covers roughly a dozen servers across FreeBSD and Linux, and it lets me sleep at&amp;nbsp;night.&lt;/p&gt;
&lt;p&gt;The classic &lt;strong&gt;3-2-1 backup rule&lt;/strong&gt; says: three copies of your data, on two different media types, with one copy off-site. What I&amp;#8217;m about to describe exceeds it in some areas and only meets it in others - but the principle is sound, and if you take nothing else from this article, take&amp;nbsp;3-2-1.&lt;/p&gt;
&lt;p&gt;Before diving into tools, it helps to name the threats each layer&amp;nbsp;addresses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accidental deletion or bad deploys&lt;/strong&gt; → local &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots (instant&amp;nbsp;rollback)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disk or pool failure&lt;/strong&gt; → off-site replication to&amp;nbsp;rsync.net&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total host loss&lt;/strong&gt; → remote backups via &lt;span class="caps"&gt;PBS&lt;/span&gt; + S3 and&amp;nbsp;syncoid&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Silent backup failure&lt;/strong&gt; → dead man&amp;#8217;s switch&amp;nbsp;monitoring&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Corrupt or incomplete backups&lt;/strong&gt; → quarterly restore&amp;nbsp;tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every layer below maps to at least one of these. If you&amp;#8217;re designing your own strategy, start from your threat model and work outward - the tools are interchangeable, the thinking&amp;nbsp;isn&amp;#8217;t.&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#layer-1-zfs-snapshots-with-sanoid-freebsd"&gt;Layer 1: &lt;span class="caps"&gt;ZFS&lt;/span&gt; Snapshots with Sanoid&amp;nbsp;(FreeBSD)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#layer-2-off-site-replication-with-syncoid"&gt;Layer 2: Off-Site Replication with Syncoid&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-ssh-configuration"&gt;The &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#layer-3-proxmox-backup-server-for-virtual-machines"&gt;Layer 3: Proxmox Backup Server for Virtual&amp;nbsp;Machines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#layer-4-linux-servers"&gt;Layer 4: Linux Servers&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#option-a-proxmox-backup-client"&gt;Option A: Proxmox Backup&amp;nbsp;Client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#option-b-plain-rsync"&gt;Option B: Plain&amp;nbsp;rsync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#option-c-the-podman-trick-when-packages-dont-exist"&gt;Option C: The Podman Trick (When Packages Don&amp;#8217;t&amp;nbsp;Exist)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-glue-dead-mans-switch-monitoring"&gt;The Glue: Dead Man&amp;#8217;s Switch&amp;nbsp;Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#quarterly-restore-tests"&gt;Quarterly Restore Tests&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#how-restores-actually-work"&gt;How Restores Actually&amp;nbsp;Work&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#putting-it-all-together"&gt;Putting It All&amp;nbsp;Together&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-this-costs"&gt;What This&amp;nbsp;Costs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="layer-1-zfs-snapshots-with-sanoid-freebsd"&gt;Layer 1: &lt;span class="caps"&gt;ZFS&lt;/span&gt; Snapshots with Sanoid&amp;nbsp;(FreeBSD)&lt;/h2&gt;
&lt;p&gt;All my FreeBSD servers run &lt;span class="caps"&gt;ZFS&lt;/span&gt;. If you&amp;#8217;re on FreeBSD and not using &lt;span class="caps"&gt;ZFS&lt;/span&gt;, I have questions. (If you want answers about &lt;span class="caps"&gt;ZFS&lt;/span&gt; first, I wrote &lt;a href="https://blog.hofstede.it/freebsd-foundationals-zfs-the-last-filesystem-youll-ever-need/"&gt;an entire article about it&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots are the first line of defense. They&amp;#8217;re instant, they&amp;#8217;re effectively free at creation time (copy-on-write means a snapshot consumes zero additional space until data changes), and they&amp;#8217;ve saved me from &amp;#8220;oops&amp;#8221; moments more times than I care to admit. But snapshots alone aren&amp;#8217;t backups - they live on the same pool as your data. If the disk dies, the snapshots die with&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;&lt;a href="https://github.com/jimsalterjrs/sanoid"&gt;sanoid&lt;/a&gt;&lt;/strong&gt;, a policy-driven snapshot management tool. On FreeBSD, it&amp;#8217;s&amp;nbsp;a &lt;code&gt;pkg install sanoid&lt;/code&gt; away. You define retention policies per dataset, point sanoid at them, and it handles creation and pruning&amp;nbsp;automatically.&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s a representative configuration from one of my&amp;nbsp;servers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# /usr/local/etc/sanoid/sanoid.conf&lt;/span&gt;

&lt;span class="k"&gt;[template_production]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autosnap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autoprune&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Retention Policy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;hourly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;daily&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;14&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;weekly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;monthly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;yearly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;[template_database]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autosnap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autoprune&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Retention Policy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;hourly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;daily&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;weekly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;monthly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;yearly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Apply to datasets&lt;/span&gt;
&lt;span class="k"&gt;[zroot]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;use_template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;process_children_only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;

&lt;span class="k"&gt;[zroot/postgres_data]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;use_template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;process_children_only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;

&lt;span class="c1"&gt;# Exclude datasets that don&amp;#39;t need snapshots&lt;/span&gt;
&lt;span class="k"&gt;[zroot/tmp]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autosnap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;

&lt;span class="k"&gt;[zroot/bastille/logs]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autosnap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;

&lt;span class="k"&gt;[zroot/bastille/jails/nginx/cache]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autosnap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The philosophy is straightforward: production data gets 14 daily snapshots and 4 weekly ones. That&amp;#8217;s about six weeks of rollback capability. Database datasets get a shorter window (3 days) because database snapshots without coordinated dumps are crash-consistent at best - useful, but not a replacement for proper application-level backups. Temporary directories and caches get nothing, because&amp;nbsp;snapshotting &lt;code&gt;/tmp&lt;/code&gt; is a waste of everyone&amp;#8217;s&amp;nbsp;time.&lt;/p&gt;
&lt;p&gt;Sanoid runs via&amp;nbsp;cron:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sanoid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This pattern is identical across all ~10 of my FreeBSD servers. Same templates, same retention policies, same cron entry. The only thing that changes is which datasets exist and which exclusions apply. Consistency is a feature - when you&amp;#8217;re managing multiple hosts, you don&amp;#8217;t want to remember that &lt;em&gt;this&lt;/em&gt; server has different retention because you were feeling creative at 2 &lt;span class="caps"&gt;AM&lt;/span&gt; six months&amp;nbsp;ago.&lt;/p&gt;
&lt;h2 id="layer-2-off-site-replication-with-syncoid"&gt;Layer 2: Off-Site Replication with&amp;nbsp;Syncoid&lt;/h2&gt;
&lt;p&gt;Local snapshots protect against accidental deletion and bad updates. They do absolutely nothing against disk failure, ransomware, or the catastrophic scenario where the entire server goes away. For that, you need off-site&amp;nbsp;copies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Syncoid&lt;/strong&gt; (part of the sanoid suite) handles this. It&amp;nbsp;uses &lt;code&gt;zfs send/recv&lt;/code&gt; over &lt;span class="caps"&gt;SSH&lt;/span&gt; to replicate datasets to a remote &lt;span class="caps"&gt;ZFS&lt;/span&gt; pool. The first run sends a full copy; subsequent runs send only incremental differences. For a server with hundreds of gigabytes of data, the nightly incremental transfer is typically measured in&amp;nbsp;megabytes.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://rsync.net"&gt;rsync.net&lt;/a&gt; as my off-site target. They offer &lt;span class="caps"&gt;ZFS&lt;/span&gt;-capable storage specifically designed for this use case - you get a remote &lt;span class="caps"&gt;ZFS&lt;/span&gt; pool that&amp;nbsp;accepts &lt;code&gt;zfs recv&lt;/code&gt; over &lt;span class="caps"&gt;SSH&lt;/span&gt;. It&amp;#8217;s not the cheapest option - but &amp;#8220;cheap&amp;#8221; and &amp;#8220;I trust this with my only off-site copy of everything&amp;#8221; are different&amp;nbsp;categories.&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s the backup script that runs&amp;nbsp;nightly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration&lt;/span&gt;
&lt;span class="nv"&gt;SOURCE_POOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;zroot&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;backup@ab1234c.rsync.net:data1/apollo/zroot&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;MONITOR_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://uptime.example.com/api/push/xxxxxxxxxx?status=up&amp;amp;msg=OK&amp;amp;ping=&amp;quot;&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Starting off-site backup to rsync.net...&amp;quot;&lt;/span&gt;

/usr/local/bin/syncoid&lt;span class="w"&gt; &lt;/span&gt;--no-sync-snap&lt;span class="w"&gt; &lt;/span&gt;--no-privilege-elevation&lt;span class="w"&gt; &lt;/span&gt;--recursive&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--sendoptions&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--exclude&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;zroot/tmp&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--exclude&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;zroot/bastille/logs&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--exclude&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;zroot/bastille/jails/nginx/cache&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--recvoptions&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;u o canmount=off o mountpoint=none&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE_POOL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Backup successful. Triggering monitor...&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/usr/local/bin/curl&lt;span class="w"&gt; &lt;/span&gt;-fsS&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MONITOR_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ERROR: Backup FAILED. Monitor NOT triggered.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A few things worth&amp;nbsp;noting:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--sendoptions="w"&lt;/code&gt;&lt;/strong&gt; is critical.&amp;nbsp;The &lt;code&gt;w&lt;/code&gt; flag means &amp;#8220;raw send&amp;#8221; - encrypted datasets are sent in their encrypted form. Without&amp;nbsp;this, &lt;code&gt;zfs send&lt;/code&gt; would transmit the decrypted data stream, and the datasets would arrive unencrypted at rest on the remote end. With raw send, your encrypted datasets remain encrypted at rest on the backup target. rsync.net never needs your encryption keys, and if someone compromises the backup storage, they get&amp;nbsp;ciphertext.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--no-privilege-elevation&lt;/code&gt;&lt;/strong&gt; means syncoid won&amp;#8217;t try to&amp;nbsp;use &lt;code&gt;sudo&lt;/code&gt; on the remote end. This works because the remote user&amp;nbsp;(&lt;code&gt;backup&lt;/code&gt; in this example) has been granted only the specific &lt;span class="caps"&gt;ZFS&lt;/span&gt; permissions needed to receive snapshots - nothing&amp;nbsp;more.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--recvoptions="u o canmount=off o mountpoint=none"&lt;/code&gt;&lt;/strong&gt; tells the receiving end not to mount the datasets. This is a backup target, not a live system. You don&amp;#8217;t&amp;nbsp;want &lt;code&gt;zfs recv&lt;/code&gt; trying to&amp;nbsp;mount &lt;code&gt;zroot&lt;/code&gt; on the remote&amp;nbsp;machine.&lt;/p&gt;
&lt;p&gt;The same excludes from the sanoid config reappear here. Yes, the duplication is intentional - each layer should be independently correct, so if you change one without the other, neither silently breaks. There&amp;#8217;s no point in replicating temp files and cache directories across the&amp;nbsp;internet.&lt;/p&gt;
&lt;p&gt;The cron schedule runs this at 4 &lt;span class="caps"&gt;AM&lt;/span&gt;, after sanoid has already created the nightly snapshots at&amp;nbsp;midnight:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sanoid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;daily_backup&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;backup_last&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Retention of old snapshots on the rsync.net side is handled by a separate sanoid cron job running &lt;em&gt;on&lt;/em&gt; the rsync.net host&amp;nbsp;itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sanoid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;prune&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;snapshots&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This keeps the remote snapshot history tidy without me having to manage it from every source&amp;nbsp;server.&lt;/p&gt;
&lt;h3 id="the-ssh-configuration"&gt;The &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;The backup user connects via a dedicated &lt;span class="caps"&gt;SSH&lt;/span&gt; key with limited&amp;nbsp;permissions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# ~/.ssh/config (on the source server)
Host rsync_backup
    HostName ab1234c.rsync.net
    User backup
    Port 22
    IdentityFile /root/.ssh/id_ed25519_backup
    Compression yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;backup&lt;/code&gt; user on rsync.net has only the &lt;span class="caps"&gt;ZFS&lt;/span&gt; permissions required for receiving snapshots. If someone compromises a source server and steals the &lt;span class="caps"&gt;SSH&lt;/span&gt; key, the worst they can do is send snapshots to the backup pool and list existing ones. They can&amp;#8217;t delete snapshots, destroy datasets, or access anything outside the designated backup path. This is the ransomware angle: even if an attacker has full root on a source server, they cannot destroy or encrypt the off-site&amp;nbsp;backups.&lt;/p&gt;
&lt;h2 id="layer-3-proxmox-backup-server-for-virtual-machines"&gt;Layer 3: Proxmox Backup Server for Virtual&amp;nbsp;Machines&lt;/h2&gt;
&lt;p&gt;Not everything is FreeBSD. I run two Proxmox &lt;span class="caps"&gt;VE&lt;/span&gt; nodes that serve as lab environments for testing things quickly, plus a handful of utility VMs. These need a different backup&amp;nbsp;strategy.&lt;/p&gt;
&lt;p&gt;My approach: a &lt;strong&gt;Proxmox Backup Server (&lt;span class="caps"&gt;PBS&lt;/span&gt;)&lt;/strong&gt; instance running on a minimal &lt;span class="caps"&gt;VM&lt;/span&gt; (1 vCPU, 1 &lt;span class="caps"&gt;GB&lt;/span&gt; &lt;span class="caps"&gt;RAM&lt;/span&gt;) at a cheap cloud provider, with &lt;strong&gt;Backblaze B2&lt;/strong&gt; as the storage&amp;nbsp;backend.&lt;/p&gt;
&lt;p&gt;The beauty of this setup is that &lt;span class="caps"&gt;PBS&lt;/span&gt; supports S3-compatible object storage as a datastore. The &lt;span class="caps"&gt;PBS&lt;/span&gt; &lt;span class="caps"&gt;VM&lt;/span&gt; itself is tiny and stores almost nothing locally - it&amp;#8217;s essentially a proxy that handles deduplication, encryption, and the Proxmox backup protocol, then shoves the actual data into B2. You get Proxmox&amp;#8217;s excellent incremental backup and deduplication without paying for a beefy &lt;span class="caps"&gt;VM&lt;/span&gt; with terabytes of local&amp;nbsp;storage.&lt;/p&gt;
&lt;p&gt;On the Proxmox &lt;span class="caps"&gt;VE&lt;/span&gt; side, this is completely transparent. You add the &lt;span class="caps"&gt;PBS&lt;/span&gt; as a storage target in the Proxmox web &lt;span class="caps"&gt;UI&lt;/span&gt;, configure a backup schedule, and the VMs back up nightly through the standard Proxmox backup mechanism. The &lt;span class="caps"&gt;PVE&lt;/span&gt; nodes don&amp;#8217;t know or care that B2 is the actual storage behind it - they just see a &lt;span class="caps"&gt;PBS&lt;/span&gt;&amp;nbsp;endpoint.&lt;/p&gt;
&lt;p&gt;The economics work out nicely: a tiny &lt;span class="caps"&gt;VPS&lt;/span&gt; to run &lt;span class="caps"&gt;PBS&lt;/span&gt; costs a few euros per month, and B2 storage is $6/&lt;span class="caps"&gt;TB&lt;/span&gt;/month with free egress via the Cloudflare Bandwidth Alliance. For a lab environment with a few hundred gigabytes of &lt;span class="caps"&gt;VM&lt;/span&gt; data, the total cost is&amp;nbsp;negligible.&lt;/p&gt;
&lt;h2 id="layer-4-linux-servers"&gt;Layer 4: Linux&amp;nbsp;Servers&lt;/h2&gt;
&lt;p&gt;Linux servers are the messy middle child of this strategy. There&amp;#8217;s no single approach, because the servers themselves vary too&amp;nbsp;much.&lt;/p&gt;
&lt;h3 id="option-a-proxmox-backup-client"&gt;Option A: Proxmox Backup&amp;nbsp;Client&lt;/h3&gt;
&lt;p&gt;For Linux hosts where it&amp;#8217;s&amp;nbsp;available, &lt;code&gt;proxmox-backup-client&lt;/code&gt; is the cleanest option. It supports file-level and image-level backups to &lt;span class="caps"&gt;PBS&lt;/span&gt; with deduplication and encryption. Install the client, point it at your &lt;span class="caps"&gt;PBS&lt;/span&gt; instance, schedule a cron job, done. These backups land in the same &lt;span class="caps"&gt;PBS&lt;/span&gt; + B2 infrastructure described&amp;nbsp;above.&lt;/p&gt;
&lt;h3 id="option-b-plain-rsync"&gt;Option B: Plain&amp;nbsp;rsync&lt;/h3&gt;
&lt;p&gt;Not every Linux server needs the full &lt;span class="caps"&gt;PBS&lt;/span&gt; treatment. Some are simple workhorses where the important data is just files - configuration, application data, maybe some database exports. For these, rsync over &lt;span class="caps"&gt;SSH&lt;/span&gt; to rsync.net does the&amp;nbsp;job.&lt;/p&gt;
&lt;p&gt;The key is what happens &lt;em&gt;before&lt;/em&gt; rsync runs. You can&amp;#8217;t just rsync a live PostgreSQL data directory and expect a consistent backup. The same goes for MySQL, Redis &lt;span class="caps"&gt;AOF&lt;/span&gt; files, or anything else that keeps state in memory. So the backup script follows a pattern: dump first, then&amp;nbsp;sync.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-euo&lt;span class="w"&gt; &lt;/span&gt;pipefail

&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/backups/pre-rsync&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;RSYNC_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;backup@ab1234c.rsync.net:data1/hostname/&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/root/.ssh/id_ed25519_backup&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;MONITOR_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://uptime.example.com/api/push/xxxxxxxxxx?status=up&amp;amp;msg=OK&amp;amp;ping=&amp;quot;&lt;/span&gt;

mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Dump databases before syncing&lt;/span&gt;
pg_dump&lt;span class="w"&gt; &lt;/span&gt;-U&lt;span class="w"&gt; &lt;/span&gt;postgres&lt;span class="w"&gt; &lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/myapp.sql&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# Or for MySQL:&lt;/span&gt;
&lt;span class="c1"&gt;# mysqldump --single-transaction --all-databases &amp;gt; &amp;quot;${BACKUP_DIR}/all-databases.sql&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Export container volumes (Podman or Docker)&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;volume&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;myapp-data&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/myapp-data.tar&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Sync everything to rsync.net&lt;/span&gt;
rsync&lt;span class="w"&gt; &lt;/span&gt;-avz&lt;span class="w"&gt; &lt;/span&gt;--delete&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh -i &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/etc/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RSYNC_TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/etc/&amp;quot;&lt;/span&gt;

rsync&lt;span class="w"&gt; &lt;/span&gt;-avz&lt;span class="w"&gt; &lt;/span&gt;--delete&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh -i &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/var/backups/pre-rsync/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RSYNC_TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/dumps/&amp;quot;&lt;/span&gt;

rsync&lt;span class="w"&gt; &lt;/span&gt;-avz&lt;span class="w"&gt; &lt;/span&gt;--delete&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh -i &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/opt/app-data/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RSYNC_TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/app-data/&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# If we got here, everything succeeded (set -e exits on any failure)&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;-fsS&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MONITOR_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The database dump ensures you get a consistent, point-in-time export rather than a half-written data&amp;nbsp;directory. &lt;code&gt;podman volume export&lt;/code&gt; gives you a clean tarball of container volumes - useful when your application runs in containers and the actual state lives in named volumes rather than bind&amp;nbsp;mounts.&lt;/p&gt;
&lt;p&gt;For containers specifically, don&amp;#8217;t forget that the compose file or quadlet unit and any environment files are just as important as the volume data. Losing your database is bad; losing your database &lt;em&gt;and&lt;/em&gt; the configuration that tells you how it was deployed is&amp;nbsp;worse.&lt;/p&gt;
&lt;p&gt;The rsync.net destination is just a directory on a &lt;span class="caps"&gt;ZFS&lt;/span&gt; filesystem, so you get the benefit of rsync.net&amp;#8217;s own &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots on the remote side. Even&amp;nbsp;if &lt;code&gt;--delete&lt;/code&gt; removes a file during sync, the previous version survives in a snapshot. It&amp;#8217;s belt and suspenders, and it costs nothing&amp;nbsp;extra.&lt;/p&gt;
&lt;p&gt;Boring? Yes. Reliable? Also yes. Sometimes the best backup tool is the one that&amp;#8217;s been working since&amp;nbsp;1996.&lt;/p&gt;
&lt;h3 id="option-c-the-podman-trick-when-packages-dont-exist"&gt;Option C: The Podman Trick (When Packages Don&amp;#8217;t&amp;nbsp;Exist)&lt;/h3&gt;
&lt;p&gt;And then there&amp;#8217;s the creative&amp;nbsp;part.&lt;/p&gt;
&lt;p&gt;I have a &lt;span class="caps"&gt;RHEL&lt;/span&gt; 9 server running Forgejo (a self-hosted Git forge) in Podman containers. This is a production system with SELinux in enforcing mode. I wanted to back it up to my existing &lt;span class="caps"&gt;PBS&lt;/span&gt; infrastructure, but here&amp;#8217;s the&amp;nbsp;problem: &lt;code&gt;proxmox-backup-client&lt;/code&gt; doesn&amp;#8217;t ship as an &lt;span class="caps"&gt;RPM&lt;/span&gt; for &lt;span class="caps"&gt;RHEL&lt;/span&gt;. There&amp;#8217;s no package in &lt;span class="caps"&gt;EPEL&lt;/span&gt;, no Copr repo, nothing in the Proxmox repositories for Red Hat-based&amp;nbsp;systems.&lt;/p&gt;
&lt;p&gt;The solution?&amp;nbsp;Run &lt;code&gt;proxmox-backup-client&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; a Podman container, and pass the entire root filesystem in&amp;nbsp;read-only:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;pbs-backup-job&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--security-opt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;type:spc_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--hostname&lt;span class="w"&gt; &lt;/span&gt;hermes&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--entrypoint&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/proxmox-backup-client&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--secret&lt;span class="w"&gt; &lt;/span&gt;pbs_repo_password,type&lt;span class="o"&gt;=&lt;/span&gt;env,target&lt;span class="o"&gt;=&lt;/span&gt;PBS_PASSWORD&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--secret&lt;span class="w"&gt; &lt;/span&gt;pbs_key_passphrase,type&lt;span class="o"&gt;=&lt;/span&gt;env,target&lt;span class="o"&gt;=&lt;/span&gt;PBS_ENCRYPTION_PASSWORD&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;/:/mnt/root:ro&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;/root/backup.key:/etc/proxmox/backup.key:ro,Z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;docker.io/ayufan/proxmox-backup-server:v3.4.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;backup&lt;span class="w"&gt; &lt;/span&gt;root.pxar:/mnt/root&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--repository&lt;span class="w"&gt; &lt;/span&gt;backup@pbs@backup-vps.example.com:data&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--keyfile&lt;span class="w"&gt; &lt;/span&gt;/etc/proxmox/backup.key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let&amp;#8217;s break down why this actually works&amp;nbsp;well:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--security-opt label=type:spc_t&lt;/code&gt;&lt;/strong&gt; is the key ingredient on a SELinux system.&amp;nbsp;The &lt;code&gt;spc_t&lt;/code&gt; (super privileged container) type allows the container to read host files that would normally be blocked by SELinux labels. Without this, the container would get a wall of &amp;#8220;permission denied&amp;#8221; errors trying to read the host filesystem, even though it&amp;#8217;s mounted read-only. If you&amp;#8217;re running SELinux in enforcing mode (and &lt;a href="https://blog.hofstede.it/selinux-a-practical-guide-for-fedora-and-rhel/"&gt;you should be&lt;/a&gt;), this flag is&amp;nbsp;non-negotiable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-v /:/mnt/root:ro&lt;/code&gt;&lt;/strong&gt; mounts the entire host root filesystem into the container as read-only. The container can see everything but modify nothing. The backup client reads files&amp;nbsp;from &lt;code&gt;/mnt/root&lt;/code&gt; and creates&amp;nbsp;a &lt;code&gt;root.pxar&lt;/code&gt; archive (Proxmox&amp;#8217;s archive format) that gets sent to &lt;span class="caps"&gt;PBS&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Podman secrets&lt;/strong&gt; handle the credentials. The &lt;span class="caps"&gt;PBS&lt;/span&gt; repository password and encryption key passphrase are stored as Podman secrets and injected as environment variables - they never appear in the command line, process list, or container inspect&amp;nbsp;output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--hostname hermes&lt;/code&gt;&lt;/strong&gt; sets the hostname inside the container to match the actual host. &lt;span class="caps"&gt;PBS&lt;/span&gt; uses the hostname to identify backup sources, so this ensures the backups show up correctly in the &lt;span class="caps"&gt;PBS&lt;/span&gt; &lt;span class="caps"&gt;UI&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;The result: a full file-level backup of a &lt;span class="caps"&gt;RHEL&lt;/span&gt; server to Proxmox Backup Server, with encryption, deduplication, and incremental transfers - all without installing a single package on the host. It runs nightly via a systemd timer, and it has been&amp;nbsp;rock-solid.&lt;/p&gt;
&lt;p&gt;This is absolutely a workaround, not a vendor-supported path. But it&amp;#8217;s contained, repeatable, and has proven stable in practice. Sometimes the ugliest solutions are the most reliable&amp;nbsp;ones.&lt;/p&gt;
&lt;h2 id="the-glue-dead-mans-switch-monitoring"&gt;The Glue: Dead Man&amp;#8217;s Switch&amp;nbsp;Monitoring&lt;/h2&gt;
&lt;p&gt;The most dangerous failure mode for backups isn&amp;#8217;t failure. It&amp;#8217;s &lt;em&gt;silent&lt;/em&gt; failure. The backup that stopped working three months ago and nobody noticed until the restore attempt. By the time you need it, you discover that your last good backup is from February and it&amp;#8217;s now&amp;nbsp;April.&lt;/p&gt;
&lt;p&gt;This is why every backup script ends with a monitoring ping. I run an &lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime Kuma&lt;/a&gt; instance that monitors these pings via &lt;strong&gt;push-based&lt;/strong&gt; checks (sometimes called dead man&amp;#8217;s switches or heartbeat monitors). The logic is inverted from normal monitoring: instead of checking whether a service is &lt;em&gt;up&lt;/em&gt;, it checks whether a backup has &lt;em&gt;reported in&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Each backup job pings a unique &lt;span class="caps"&gt;URL&lt;/span&gt; on&amp;nbsp;success:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-fsS&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://uptime.example.com/api/push/xxxxxxxxxx?status=up&amp;amp;msg=OK&amp;amp;ping=&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the backup fails, the curl never fires. If the backup script doesn&amp;#8217;t run at all (cron misconfigured, server down, disk full, whatever), the curl never fires. Uptime Kuma is configured with a 28-hour timeout (24 hours for the daily schedule + 4 hours of tolerance). If 28 hours pass without a ping, I get an&amp;nbsp;alert.&lt;/p&gt;
&lt;p&gt;This catches every failure mode:
- Backup script crashes → no ping → alert
- Cron job stops running → no ping → alert
- Server goes offline → no ping → alert
- &lt;span class="caps"&gt;SSH&lt;/span&gt; key expires → backup fails → no ping → alert
- Remote target is full → backup fails → no ping →&amp;nbsp;alert&lt;/p&gt;
&lt;p&gt;And yes - who monitors the monitor? Uptime Kuma itself is monitored by an external service (a simple &lt;span class="caps"&gt;HTTP&lt;/span&gt; check), so if it goes down, I know about that&amp;nbsp;too.&lt;/p&gt;
&lt;p&gt;The only thing push monitoring doesn&amp;#8217;t catch is a backup that &lt;em&gt;appears&lt;/em&gt; to succeed but produces corrupt or incomplete data. For that, you need test&amp;nbsp;restores.&lt;/p&gt;
&lt;h2 id="quarterly-restore-tests"&gt;Quarterly Restore&amp;nbsp;Tests&lt;/h2&gt;
&lt;p&gt;A mentor told me at one of my first jobs: &amp;#8220;Untested backups are just expensive hopes and dreams.&amp;#8221; That stuck with me, and I take it&amp;nbsp;seriously.&lt;/p&gt;
&lt;p&gt;Every quarter, I run full restore tests for every machine I consider production. These are scheduled in my calendar as recurring appointments, and I treat them with the same priority as any other production maintenance window. The process is straightforward: spin up a fresh &lt;span class="caps"&gt;VM&lt;/span&gt;, restore the system entirely from backups, and verify that it comes up in a functionally working state - services running, data intact, applications&amp;nbsp;responding.&lt;/p&gt;
&lt;p&gt;I record every restore session with &lt;a href="https://asciinema.org/"&gt;asciinema&lt;/a&gt; and document the results in my DokuWiki, with the terminal recording attached. This gives me three things at once: confidence that the backups actually work, regular practice with the restore procedure so I&amp;#8217;m not fumbling through it for the first time during an actual emergency, and a living library of documentation I can refer to when things go wrong at 3 &lt;span class="caps"&gt;AM&lt;/span&gt;. Under pressure is the worst possible time to be reading man pages and guessing at flags. Having a recording of yourself successfully restoring the exact same system last quarter is worth more than any&amp;nbsp;runbook.&lt;/p&gt;
&lt;p&gt;The quarterly cadence is a compromise. Monthly would be better but hard to justify the time. Yearly would be negligent. Quarterly is frequent enough that procedural drift doesn&amp;#8217;t accumulate, and any backup issue gets caught within 90 days at&amp;nbsp;most.&lt;/p&gt;
&lt;h3 id="how-restores-actually-work"&gt;How Restores Actually&amp;nbsp;Work&lt;/h3&gt;
&lt;p&gt;Since each layer uses different tools, the restore path differs per layer. A quick&amp;nbsp;overview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; local snapshots&lt;/strong&gt;&amp;nbsp;→ &lt;code&gt;zfs rollback&lt;/code&gt; to revert a dataset in place,&amp;nbsp;or &lt;code&gt;zfs clone&lt;/code&gt; to mount a snapshot as a separate dataset for selective file recovery without affecting the live&amp;nbsp;system&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Syncoid / rsync.net&lt;/strong&gt;&amp;nbsp;→ &lt;code&gt;zfs send&lt;/code&gt; from the remote pool back to the local host via &lt;span class="caps"&gt;SSH&lt;/span&gt;. Same tool, reverse direction. For a full rebuild: install the &lt;span class="caps"&gt;OS&lt;/span&gt;, create the&amp;nbsp;pool, &lt;code&gt;zfs recv&lt;/code&gt; the&amp;nbsp;datasets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;PBS&lt;/span&gt; (Proxmox Backup Server)&lt;/strong&gt; → restore via the &lt;span class="caps"&gt;PBS&lt;/span&gt; web &lt;span class="caps"&gt;UI&lt;/span&gt;&amp;nbsp;or &lt;code&gt;proxmox-backup-client restore&lt;/code&gt; on the &lt;span class="caps"&gt;CLI&lt;/span&gt;. For VMs on Proxmox &lt;span class="caps"&gt;VE&lt;/span&gt;, the restore is a one-click operation from the &lt;span class="caps"&gt;PVE&lt;/span&gt;&amp;nbsp;interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plain rsync&lt;/strong&gt; → copy the dumps and files back, re-import database dumps&amp;nbsp;with &lt;code&gt;psql&lt;/code&gt; or &lt;code&gt;mysql&lt;/code&gt;, re-create container volumes from the tarballs. The most manual path, but also the most&amp;nbsp;transparent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is exactly what I walk through during the quarterly restore tests. Each layer&amp;#8217;s restore procedure is documented and recorded, so I&amp;#8217;m not figuring it out under&amp;nbsp;pressure.&lt;/p&gt;
&lt;h2 id="putting-it-all-together"&gt;Putting It All&amp;nbsp;Together&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s the full&amp;nbsp;picture:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                     FreeBSD Servers (~10)                   │
│                                                             │
│  sanoid (local snapshots) ──► syncoid ──► rsync.net (ZFS)   │
│          daily/weekly              nightly    5 TB pool     │
│          retention                 encrypted  remote prune  │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                  Proxmox VE Nodes (2)                       │
│                                                             │
│  PVE backup schedule ──► PBS VM ──► Backblaze B2 (S3)       │
│       nightly              1C/1G    deduplicated            │
│       incremental          proxy    encrypted               │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                     Linux Servers                           │
│                                                             │
│  proxmox-backup-client ──► PBS ──► Backblaze B2             │
│  rsync over SSH ──► rsync.net                               │
│  podman + pbs-client ──► PBS  (RHEL/no native package)      │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                      Monitoring                             │
│                                                             │
│  Every backup job ──► Uptime Kuma (push / dead man switch)  │
│                        28h timeout ──► alert on silence     │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="what-this-costs"&gt;What This&amp;nbsp;Costs&lt;/h2&gt;
&lt;p&gt;One of the most common excuses for not having proper backups is cost. So let&amp;#8217;s put actual numbers on&amp;nbsp;this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scaleway &lt;span class="caps"&gt;STARDUST1&lt;/span&gt;-S (IPv6, 1 &lt;span class="caps"&gt;GB&lt;/span&gt; &lt;span class="caps"&gt;RAM&lt;/span&gt;, 1 vCPU, 10 &lt;span class="caps"&gt;GB&lt;/span&gt; disk)&lt;/td&gt;
&lt;td&gt;Runs Proxmox Backup Server&lt;/td&gt;
&lt;td&gt;€0.10/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2 object storage&lt;/td&gt;
&lt;td&gt;Stores &lt;span class="caps"&gt;VM&lt;/span&gt; backups from &lt;span class="caps"&gt;PBS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;$6/&lt;span class="caps"&gt;TB&lt;/span&gt;/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rsync.net &lt;span class="caps"&gt;ZFS&lt;/span&gt; account (5 &lt;span class="caps"&gt;TB&lt;/span&gt;)&lt;/td&gt;
&lt;td&gt;Off-site &lt;span class="caps"&gt;ZFS&lt;/span&gt; replication target for all FreeBSD servers&lt;/td&gt;
&lt;td&gt;$60/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Scaleway instance is almost comically cheap. It&amp;#8217;s a &lt;span class="caps"&gt;STARDUST1&lt;/span&gt;-S - the smallest instance they offer - and it&amp;#8217;s more than enough for &lt;span class="caps"&gt;PBS&lt;/span&gt;, because &lt;span class="caps"&gt;PBS&lt;/span&gt; is doing deduplication and proxying to S3, not storing data locally. Ten cents a month. That&amp;#8217;s not a&amp;nbsp;typo.&lt;/p&gt;
&lt;p&gt;Backblaze B2 scales linearly with what you store. For a lab environment with a few hundred gigabytes of &lt;span class="caps"&gt;VM&lt;/span&gt; data after deduplication, you&amp;#8217;re looking at low single digits per month. Egress is free if you route through Cloudflare&amp;#8217;s Bandwidth Alliance, which matters a lot if you ever actually need to&amp;nbsp;restore.&lt;/p&gt;
&lt;p&gt;The rsync.net line item looks expensive at first glance - $60/month for 5 &lt;span class="caps"&gt;TB&lt;/span&gt;. But you&amp;#8217;re not just buying storage. You get a fully managed FreeBSD virtual host with root access, sitting on top of a tolerant raidz3 pool with &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshots. rsync.net handles all hardware maintenance, &lt;span class="caps"&gt;OS&lt;/span&gt; patches, upgrades, and provides unlimited support. You can optionally add geo-redundancy. For a managed service with that feature set and reliability, it&amp;#8217;s genuinely good value. Try pricing a comparable setup at any other provider where you get a dedicated &lt;span class="caps"&gt;ZFS&lt;/span&gt; pool that&amp;nbsp;accepts &lt;code&gt;zfs recv&lt;/code&gt; natively - the options are slim, and they aren&amp;#8217;t&amp;nbsp;cheaper.&lt;/p&gt;
&lt;p&gt;All in, the entire backup infrastructure for about a dozen servers costs roughly &lt;strong&gt;$70/month&lt;/strong&gt;. That&amp;#8217;s less than one hour of emergency consulting when you lose data you can&amp;#8217;t&amp;nbsp;recover.&lt;/p&gt;
&lt;p&gt;Is it perfect? No. There are things I&amp;#8217;d improve: better alerting on partial failures, maybe a dashboard that shows backup age across all hosts at a glance, and more automation around the restore tests. But the fundamentals are solid: every server has local snapshots for fast rollback, every server has off-site copies for disaster recovery, every backup job is monitored, restores are tested quarterly, and the whole thing runs&amp;nbsp;unattended.&lt;/p&gt;
&lt;p&gt;The most important property of a backup strategy isn&amp;#8217;t that it&amp;#8217;s clever. It&amp;#8217;s that it&amp;#8217;s boring, consistent, and actually running. Fancy architectures that you never finish implementing protect exactly zero bytes of data. A simple cron job that runs every night and phones home when it&amp;#8217;s done? That protects&amp;nbsp;everything.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jimsalterjrs/sanoid"&gt;Sanoid/Syncoid - Policy-driven snapshot&amp;nbsp;management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rsync.net/products/zfsintro.html"&gt;rsync.net - &lt;span class="caps"&gt;ZFS&lt;/span&gt;-capable off-site&amp;nbsp;storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pbs.proxmox.com/docs/"&gt;Proxmox Backup Server&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.backblaze.com/cloud-storage"&gt;Backblaze B2 - S3-compatible object&amp;nbsp;storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime Kuma - Self-hosted&amp;nbsp;monitoring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/r/ayufan/proxmox-backup-server"&gt;ayufan/proxmox-backup-server container&amp;nbsp;image&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="backup"/><category term="zfs"/><category term="freebsd"/><category term="linux"/><category term="proxmox"/><category term="sanoid"/><category term="syncoid"/><category term="rsync"/><category term="podman"/><category term="infrastructure"/></entry><entry><title>Shell Tricks That Actually Make Life Easier (And Save Your Sanity)</title><link href="https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/" rel="alternate"/><published>2026-03-26T00:00:00+01:00</published><updated>2026-03-26T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-26:/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/</id><summary type="html">&lt;p&gt;Watch someone backspace 40 characters instead of pressing &lt;span class="caps"&gt;CTRL&lt;/span&gt;+W, and you&amp;#8217;ll understand why this list exists. A collection of shell tricks-grouped by what works everywhere and what&amp;#8217;s Bash/Zsh-specific-that save keystrokes and&amp;nbsp;time.&lt;/p&gt;</summary><content type="html">&lt;p&gt;There is a distinct, visceral kind of pain in watching an otherwise brilliant engineer hold down the Backspace key for six continuous seconds to fix a typo at the beginning of a&amp;nbsp;line. &lt;/p&gt;
&lt;p&gt;We’ve all been there. We&amp;nbsp;learn &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;,&amp;nbsp;and &lt;code&gt;grep&lt;/code&gt;, and then we sort of&amp;#8230; stop. The terminal becomes a place we live in-but we rarely bother to arrange the furniture. We accept that certain tasks take forty keystrokes, completely unaware that the shell authors solved our exact frustration sometime in&amp;nbsp;1989.&lt;/p&gt;
&lt;p&gt;Here are some tricks that aren&amp;#8217;t exactly secret, but aren&amp;#8217;t always taught either. To keep the peace in our extended Unix family, I’ve split these into two camps: the universal tricks that work on almost any &lt;span class="caps"&gt;POSIX&lt;/span&gt;-ish shell&amp;nbsp;(like &lt;code&gt;sh&lt;/code&gt; on FreeBSD&amp;nbsp;or &lt;code&gt;ksh&lt;/code&gt; on OpenBSD), and the quality-of-life additions specific to interactive shells like Bash or&amp;nbsp;Zsh.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-works-almost-everywhere-club"&gt;The &amp;#8220;Works (Almost) Everywhere&amp;#8221;&amp;nbsp;Club&lt;/h2&gt;
&lt;p&gt;These tricks rely on standard terminal line disciplines, generic Bourne shell behaviors, or &lt;span class="caps"&gt;POSIX&lt;/span&gt; features. If you &lt;span class="caps"&gt;SSH&lt;/span&gt; into an embedded router from 2009, a fresh OpenBSD box, or a minimal Alpine container, these will still have your&amp;nbsp;back.&lt;/p&gt;
&lt;h3 id="the-backspace-replacements"&gt;The Backspace&amp;nbsp;Replacements&lt;/h3&gt;
&lt;p&gt;Why shuffle character-by-character when you can teleport? These are standard Emacs-style line-editing bindings (via Readline or similar), enabled by default in most modern&amp;nbsp;shells.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + W&lt;/code&gt;&lt;/strong&gt;: You&amp;#8217;re&amp;nbsp;typing &lt;code&gt;/var/log/nginx/&lt;/code&gt; but you actually&amp;nbsp;meant &lt;code&gt;/var/log/apache2/&lt;/code&gt;. You have two choices: hold down Backspace until your soul leaves your body, or&amp;nbsp;hit &lt;code&gt;CTRL + W&lt;/code&gt; to instantly delete the word before the cursor. Once you get used to this, holding Backspace feels like digging a hole with a&amp;nbsp;spoon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + U&lt;/code&gt; and &lt;code&gt;CTRL + K&lt;/code&gt;&lt;/strong&gt;: You typed out a beautifully crafted,&amp;nbsp;80-character &lt;code&gt;rsync&lt;/code&gt; command, but suddenly realize you need to check if the destination directory actually exists first. You don&amp;#8217;t want to delete it, but you don&amp;#8217;t want to run it.&amp;nbsp;Hit &lt;code&gt;CTRL + U&lt;/code&gt; to cut everything from the cursor to the beginning of the line. Check your directory, and then hit &lt;strong&gt;&lt;code&gt;CTRL + Y&lt;/code&gt;&lt;/strong&gt; to paste (&amp;#8220;yank&amp;#8221;) your masterpiece right back into the prompt.&amp;nbsp;(&lt;code&gt;CTRL + K&lt;/code&gt; does the same thing, but cuts from the cursor to the &lt;em&gt;end&lt;/em&gt; of the&amp;nbsp;line.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + A&lt;/code&gt; and &lt;code&gt;CTRL + E&lt;/code&gt;&lt;/strong&gt;: Jump instantly to the beginning&amp;nbsp;(&lt;code&gt;A&lt;/code&gt;) or end&amp;nbsp;(&lt;code&gt;E&lt;/code&gt;) of the line. Stop reaching for the Home and End keys; they are miles away from the home row&amp;nbsp;anyway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ALT + B&lt;/code&gt; and &lt;code&gt;ALT + F&lt;/code&gt;&lt;/strong&gt;: Move backward&amp;nbsp;(&lt;code&gt;B&lt;/code&gt;) or forward&amp;nbsp;(&lt;code&gt;F&lt;/code&gt;) one entire word at a time. It&amp;#8217;s the arrow key&amp;#8217;s much faster, much cooler sibling. &lt;em&gt;(Mac users: you usually have to tweak your terminal settings to use Option as Meta for this to&amp;nbsp;work).&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="the-oh-no-binary-output-fix"&gt;The &amp;#8220;Oh No, Binary Output&amp;#8221;&amp;nbsp;Fix&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;reset&lt;/code&gt; (or &lt;code&gt;stty sane&lt;/code&gt;)&lt;/strong&gt;: While strictly more of a terminal recovery tip than an interactive shell trick, it belongs here. We’ve all done it: you meant&amp;nbsp;to &lt;code&gt;cat&lt;/code&gt; a text file, but you&amp;nbsp;accidentally &lt;code&gt;cat&lt;/code&gt; a compiled binary or a compressed tarball. Suddenly, your terminal is spitting out ancient runes and Wingdings, and your prompt is completely illegible. Instead of closing the terminal window in shame,&amp;nbsp;type &lt;code&gt;reset&lt;/code&gt; (even if you can&amp;#8217;t see the letters you&amp;#8217;re typing) and hit enter. Your terminal will heal&amp;nbsp;itself.&lt;/p&gt;
&lt;h3 id="the-emergency-exits"&gt;The Emergency&amp;nbsp;Exits&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + C&lt;/code&gt;&lt;/strong&gt;: Cancel the current command immediately. Your emergency exit when a command hangs, or you realize you&amp;#8217;re tailing the wrong log&amp;nbsp;file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + D&lt;/code&gt;&lt;/strong&gt;: Sends an &lt;span class="caps"&gt;EOF&lt;/span&gt; (End of File) signal. If you&amp;#8217;re typing input to a command that expects it, this closes the stream. But if the command line is empty, it logs you out of the shell completely-be careful where you press&amp;nbsp;it.&lt;/p&gt;
&lt;h3 id="the-screen-cleaner"&gt;The Screen&amp;nbsp;Cleaner&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + L&lt;/code&gt;&lt;/strong&gt;: Your terminal is cluttered with stack traces, compiler spaghetti, and pure digital noise. Running&amp;nbsp;the &lt;code&gt;clear&lt;/code&gt; command works, but what if you&amp;#8217;re already halfway through typing a new&amp;nbsp;command? &lt;code&gt;CTRL + L&lt;/code&gt; wipes the slate clean, throwing your current prompt right up to the top without interrupting your train of&amp;nbsp;thought.&lt;/p&gt;
&lt;h3 id="the-previous-directory-ping-pong"&gt;The Previous Directory&amp;nbsp;Ping-Pong&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cd -&lt;/code&gt;&lt;/strong&gt;: The classic channel-flipper. You&amp;#8217;re deep down&amp;nbsp;in &lt;code&gt;/usr/local/etc/postfix&lt;/code&gt; and you need to check something&amp;nbsp;in &lt;code&gt;/var/log&lt;/code&gt;. You&amp;nbsp;type &lt;code&gt;cd /var/log&lt;/code&gt;, look at the logs, and now you want to go back. Instead of typing that long path again,&amp;nbsp;type &lt;code&gt;cd -&lt;/code&gt;. It switches you to your previous directory. Run it again, and you&amp;#8217;re back in logs. Perfect for toggling back and&amp;nbsp;forth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;pushd&lt;/code&gt; and &lt;code&gt;popd&lt;/code&gt;&lt;/strong&gt;:&amp;nbsp;If &lt;code&gt;cd -&lt;/code&gt; is a toggle&amp;nbsp;switch, &lt;code&gt;pushd&lt;/code&gt; is a stack. Need to juggle multiple&amp;nbsp;directories? &lt;code&gt;pushd /etc&lt;/code&gt; changes&amp;nbsp;to &lt;code&gt;/etc&lt;/code&gt; but saves your previous directory to a hidden stack. When you&amp;#8217;re done,&amp;nbsp;type &lt;code&gt;popd&lt;/code&gt; to pop it off the stack and return exactly where you left&amp;nbsp;off.&lt;/p&gt;
&lt;h3 id="the-instant-truncate"&gt;The Instant&amp;nbsp;Truncate&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;gt; file.txt&lt;/code&gt;&lt;/strong&gt;: This empties a file completely without deleting and recreating it. Why does this matter? It preserves file permissions, ownership, and doesn’t interrupt processes that already have the file open. It&amp;#8217;s much cleaner&amp;nbsp;than &lt;code&gt;echo "" &amp;gt; file.txt&lt;/code&gt; (which actually leaves a newline character)&amp;nbsp;or &lt;code&gt;rm file &amp;amp;&amp;amp; touch file&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="the-last-argument-variable"&gt;The &amp;#8220;Last Argument&amp;#8221;&amp;nbsp;Variable&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;$_&lt;/code&gt;&lt;/strong&gt;: In most&amp;nbsp;shells, &lt;code&gt;$_&lt;/code&gt; expands to the last argument of the previous command-especially useful interactively or in simple scripts when you need to operate on the same long path&amp;nbsp;twice:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/some/ridiculously/long/path/newdir&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No more re-typing paths or declaring temporary variables to enter a directory you created a second&amp;nbsp;ago.&lt;/p&gt;
&lt;h3 id="scripting-sanity-savers"&gt;Scripting Sanity&amp;nbsp;Savers&lt;/h3&gt;
&lt;p&gt;If you are writing shell scripts, put these at the top immediately after your shebang. It will save you from deploying chaos to&amp;nbsp;production.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;set -e&lt;/code&gt;&lt;/strong&gt;: Exit on error. Very useful, but notoriously weird with edge cases (especially inside conditionals&amp;nbsp;like &lt;code&gt;if&lt;/code&gt; statements, &lt;code&gt;while&lt;/code&gt; loops, and pipelines). Don&amp;#8217;t rely on it blindly as it can create false confidence. &lt;em&gt;(Pro-tip:&amp;nbsp;consider &lt;code&gt;set -euo pipefail&lt;/code&gt; for a more robust safety net, but learn its caveats&amp;nbsp;first.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;set -u&lt;/code&gt;&lt;/strong&gt;: Treats referencing an unset variable as an error. This protects you from catastrophic disasters&amp;nbsp;like &lt;code&gt;rm -rf /usr/local/${MY_TYPO_VAR}/*&lt;/code&gt; accidentally expanding&amp;nbsp;into &lt;code&gt;rm -rf /usr/local/*&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="the-bash-zsh-comfort-zone"&gt;The Bash &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt; Zsh Comfort&amp;nbsp;Zone&lt;/h2&gt;
&lt;p&gt;If you&amp;#8217;re on a Linux box or using a modern interactive shell, these are the tools that make the &lt;span class="caps"&gt;CLI&lt;/span&gt; feel less like a rusty bicycle and more like something that actually responds when you&amp;nbsp;steer.&lt;/p&gt;
&lt;h3 id="the-history-hunter"&gt;The History&amp;nbsp;Hunter&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + R&lt;/code&gt;&lt;/strong&gt;: Reverse incremental search. Stop pressing the up arrow forty times to find that&amp;nbsp;one &lt;code&gt;awk&lt;/code&gt; command you used last Tuesday.&amp;nbsp;Press &lt;code&gt;CTRL + R&lt;/code&gt;, start typing a keyword from the command, and it magically pulls it from your history.&amp;nbsp;Press &lt;code&gt;CTRL + R&lt;/code&gt; again to cycle backwards through&amp;nbsp;matches. &lt;/p&gt;
&lt;h3 id="the-oops-sudo-move"&gt;The &amp;#8220;Oops, Sudo&amp;#8221;&amp;nbsp;Move&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;!!&lt;/code&gt;&lt;/strong&gt;: This expands to the entirety of your previous command. 
Its most famous use case is the &amp;#8220;Permission denied&amp;#8221; walk of shame. You confidently&amp;nbsp;type &lt;code&gt;systemctl restart nginx&lt;/code&gt;, hit enter, and the system laughs at your lack of privileges. Instead of retyping it,&amp;nbsp;run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;!!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It&amp;#8217;s your way of telling the shell, &amp;#8220;Do what I said, but this time with&amp;nbsp;authority.&amp;#8221;&lt;/p&gt;
&lt;h3 id="the-ultimate-editor-escape-hatch"&gt;The Ultimate Editor Escape&amp;nbsp;Hatch&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + X&lt;/code&gt;,&amp;nbsp;then &lt;code&gt;CTRL + E&lt;/code&gt;&lt;/strong&gt;: You start typing a quick one-liner. Then you add a pipe. Then&amp;nbsp;an &lt;code&gt;awk&lt;/code&gt; statement. Soon, you&amp;#8217;re editing a four-line monster inside your prompt and navigation is getting difficult.&amp;nbsp;Hit &lt;code&gt;CTRL + X&lt;/code&gt; followed&amp;nbsp;by &lt;code&gt;CTRL + E&lt;/code&gt; (in Bash; in Zsh, this needs configuring). This drops your current command into your default text editor (like Vim or Nano). You can edit it with all the power of a proper editor, save, and exit. The shell then executes the command&amp;nbsp;instantly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fc&lt;/code&gt;&lt;/strong&gt;: The highly portable, traditional sibling&amp;nbsp;to &lt;code&gt;CTRL+X CTRL+E&lt;/code&gt;.&amp;nbsp;Running &lt;code&gt;fc&lt;/code&gt; opens your previous command in&amp;nbsp;your &lt;code&gt;$EDITOR&lt;/code&gt;. It works across most shells and is a fantastic hidden gem for fixing complex, multi-line commands that went&amp;nbsp;wrong.&lt;/p&gt;
&lt;h3 id="the-last-argument-interactive-edition"&gt;The Last Argument (Interactive&amp;nbsp;Edition)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ESC + .&lt;/code&gt;&lt;/strong&gt; (or &lt;strong&gt;&lt;code&gt;ALT + .&lt;/code&gt;&lt;/strong&gt;): Inserts the last argument of the previous command right at your cursor. Press it repeatedly to cycle further back through your history, dropping the exact filename or parameter you need right into your current&amp;nbsp;command.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;!$&lt;/code&gt;&lt;/strong&gt;: The non-interactive sibling&amp;nbsp;of &lt;code&gt;ESC + .&lt;/code&gt;.&amp;nbsp;Unlike &lt;code&gt;ESC + .&lt;/code&gt; (which inserts the text live at your cursor for you to review or&amp;nbsp;edit), &lt;code&gt;!$&lt;/code&gt; expands blindly at the exact moment you hit enter. 
&lt;em&gt;(Pro-Tip: For scripting or&amp;nbsp;standard &lt;code&gt;sh&lt;/code&gt;, use&amp;nbsp;the &lt;code&gt;$_&lt;/code&gt; variable mentioned earlier&amp;nbsp;instead!)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="the-renaming-trick-brace-expansion"&gt;The Renaming Trick &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt; Brace&amp;nbsp;Expansion&lt;/h3&gt;
&lt;p&gt;Brace expansion is pure magic for avoiding repetitive typing, especially when doing quick backups or&amp;nbsp;renames.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Backup Expansion&lt;/strong&gt;:
Need to edit a critical config file and want to make a quick backup&amp;nbsp;first?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp&lt;span class="w"&gt; &lt;/span&gt;pf.conf&lt;span class="o"&gt;{&lt;/span&gt;,.bak&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The shell expands this seamlessly&amp;nbsp;into &lt;code&gt;cp pf.conf pf.conf.bak&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Rename Trick&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mv&lt;span class="w"&gt; &lt;/span&gt;filename.&lt;span class="o"&gt;{&lt;/span&gt;txt,md&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This expands&amp;nbsp;to &lt;code&gt;mv filename.txt filename.md&lt;/code&gt;. Fast, elegant, and makes you look like a&amp;nbsp;wizard. &lt;/p&gt;
&lt;p&gt;Need multiple&amp;nbsp;directories? &lt;code&gt;mkdir -p project/{src,tests,docs}&lt;/code&gt; creates all three at&amp;nbsp;once.&lt;/p&gt;
&lt;h3 id="process-substitution"&gt;Process&amp;nbsp;Substitution&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;(command)&lt;/code&gt;&lt;/strong&gt;: Treats the output of a command as if it were a file. 
Say you want to diff the sorted versions of two files. Traditionally, you&amp;#8217;d sort them into temporary files, diff those, and clean up. Process substitution skips the&amp;nbsp;middleman:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;diff&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;file1.txt&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;file2.txt&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="the-ultimate-glob"&gt;The Ultimate&amp;nbsp;Glob&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;**&lt;/code&gt; (Globstar)&lt;/strong&gt;: &lt;code&gt;find&lt;/code&gt; is a great command, but sometimes it feels like overkill. If you&amp;nbsp;run &lt;code&gt;shopt -s globstar&lt;/code&gt; in Bash (it&amp;#8217;s enabled by default in&amp;nbsp;Zsh), &lt;code&gt;**&lt;/code&gt; matches files recursively in all subdirectories. 
Need to find all JavaScript files in your current directory and everything beneath&amp;nbsp;it? &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;**/*.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No &lt;code&gt;find&lt;/code&gt; command&amp;nbsp;required.&lt;/p&gt;
&lt;h3 id="backgrounding-and-disowning"&gt;Backgrounding and&amp;nbsp;Disowning&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CTRL + Z&lt;/code&gt;&lt;/strong&gt;, then &lt;strong&gt;&lt;code&gt;bg&lt;/code&gt;&lt;/strong&gt;, then &lt;strong&gt;&lt;code&gt;disown&lt;/code&gt;&lt;/strong&gt;:
You started a massive, hour-long database import task, but you forgot to run it&amp;nbsp;in &lt;code&gt;tmux&lt;/code&gt; or &lt;code&gt;screen&lt;/code&gt;. It&amp;#8217;s tying up your terminal, and if your &lt;span class="caps"&gt;SSH&lt;/span&gt; connection drops, the process dies. Panic sets&amp;nbsp;in.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hit &lt;code&gt;CTRL + Z&lt;/code&gt; to suspend (pause) the&amp;nbsp;process.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;bg&lt;/code&gt; to let it resume running in the background. Your prompt is&amp;nbsp;free!&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;disown&lt;/code&gt; to detach it from your shell entirely. You can safely close your laptop, grab a coffee, and the process will&amp;nbsp;survive.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="the-everything-logger"&gt;The&amp;nbsp;Everything-Logger&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;command |&amp;amp; tee file.log&lt;/code&gt;&lt;/strong&gt;: Standard pipes&amp;nbsp;(&lt;code&gt;|&lt;/code&gt;) only catch standard output&amp;nbsp;(&lt;code&gt;stdout&lt;/code&gt;). If a script throws an error&amp;nbsp;(&lt;code&gt;stderr&lt;/code&gt;), it skips the pipe and bleeds directly onto your screen, missing the log&amp;nbsp;file. &lt;code&gt;|&amp;amp;&lt;/code&gt; pipes &lt;em&gt;both&lt;/em&gt; stdout and stderr (it&amp;#8217;s a helpful shorthand&amp;nbsp;for &lt;code&gt;2&amp;gt;&amp;amp;1 |&lt;/code&gt;). &lt;/p&gt;
&lt;p&gt;Throw&amp;nbsp;in &lt;code&gt;tee&lt;/code&gt;, and you get to watch the output on your screen while simultaneously saving it to a log file. It’s the equivalent of watching live &lt;span class="caps"&gt;TV&lt;/span&gt; while recording it to your &lt;span class="caps"&gt;DVR&lt;/span&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The shell is a toolbox, not an obstacle course. You don&amp;#8217;t need to memorize all of these today. Pick just one trick, force it into your daily habits for a week, and then pick another. Stop letting the terminal push you around, and start rearranging the furniture. It&amp;#8217;s your house&amp;nbsp;now.&lt;/p&gt;</content><category term="Unix"/><category term="shell"/><category term="bash"/><category term="unix"/><category term="sysadmin"/><category term="freebsd"/><category term="linux"/></entry><entry><title>Dual-FIB Policy Routing on FreeBSD: Two Upstreams, One Server, Zero Confusion</title><link href="https://blog.hofstede.it/dual-fib-policy-routing-on-freebsd-two-upstreams-one-server-zero-confusion/" rel="alternate"/><published>2026-03-23T00:00:00+01:00</published><updated>2026-03-23T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-23:/dual-fib-policy-routing-on-freebsd-two-upstreams-one-server-zero-confusion/</id><summary type="html">&lt;p&gt;How to run a FreeBSD server with two completely independent internet uplinks - a physical provider and a &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel - using dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; routing tables, &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s rtable and reply-to directives, and a single bridge that carries &lt;span class="caps"&gt;NAT&lt;/span&gt;&amp;#8217;d, routed, and pure public jail traffic&amp;nbsp;simultaneously.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-03-23-freebsd-dual-fib-routing-two-upstreams.png" title="Dual-FIB Routing: Header image"&gt;&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-problem-one-server-two-gateways"&gt;The Problem: One Server, Two&amp;nbsp;Gateways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-physical-upstream-netcup-fib-0"&gt;The Physical Upstream: Netcup (&lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-logical-upstream-bgp-tunnel-fib-1"&gt;The Logical Upstream: &lt;span class="caps"&gt;BGP&lt;/span&gt; Tunnel (&lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#fib-1-building-a-complete-routing-table"&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; 1: Building a Complete Routing&amp;nbsp;Table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#jail-networking-and-the-bridge"&gt;Jail Networking and the&amp;nbsp;Bridge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#pf-where-the-routing-decision-happens"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;: Where the Routing Decision Happens&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#normalization"&gt;Normalization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#translation-nat-and-rdr"&gt;Translation (&lt;span class="caps"&gt;NAT&lt;/span&gt; and &lt;span class="caps"&gt;RDR&lt;/span&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#default-policies"&gt;Default&amp;nbsp;Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#egress-the-route-to-safety-net"&gt;Egress: The route-to Safety&amp;nbsp;Net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#egress-jail-policy-routing-via-rtable"&gt;Egress: Jail Policy Routing via&amp;nbsp;rtable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ingress-tunnel-encapsulation"&gt;Ingress: Tunnel&amp;nbsp;Encapsulation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ingress-the-reply-to-imperative"&gt;Ingress: The reply-to&amp;nbsp;Imperative&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-pure-public-vnet-jail"&gt;The Pure Public &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;Jail&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-complete-flow"&gt;The Complete Flow&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#inbound-to-a-bgp-addressed-service-ipv6"&gt;Inbound to a &lt;span class="caps"&gt;BGP&lt;/span&gt;-addressed service&amp;nbsp;(IPv6)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#outbound-from-a-pure-public-jail-ipv4"&gt;Outbound from a pure public jail&amp;nbsp;(IPv4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#natd-jail-egress-ipv4-standard-path"&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt;&amp;#8217;d jail egress (IPv4, standard&amp;nbsp;path)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#verification"&gt;Verification&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#from-the-host-testing-both-fibs"&gt;From the host: testing both&amp;nbsp;FIBs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#from-inside-the-pure-public-jail"&gt;From inside the pure public&amp;nbsp;jail&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#from-external-clients-both-paths-work"&gt;From external clients: both paths&amp;nbsp;work&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#security-considerations"&gt;Security&amp;nbsp;Considerations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;previous articles&lt;/a&gt; in this series covered the &lt;span class="caps"&gt;BGP&lt;/span&gt; router side: obtaining an &lt;span class="caps"&gt;AS&lt;/span&gt;, peering with upstream providers, and building a tunnel overlay. This article covers the other end - the downstream server that consumes that tunnel and needs to serve traffic from two completely different &lt;span class="caps"&gt;IP&lt;/span&gt; ranges through two completely different paths, simultaneously, without either one interfering with the&amp;nbsp;other.&lt;/p&gt;
&lt;p&gt;The server in question&amp;nbsp;is &lt;code&gt;radon&lt;/code&gt;, a Netcup &lt;span class="caps"&gt;VPS&lt;/span&gt; running FreeBSD with Bastille jails. It has two internet uplinks: a physical connection to Netcup&amp;#8217;s network and a &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel to my &lt;span class="caps"&gt;BGP&lt;/span&gt; router&amp;nbsp;(&lt;code&gt;hobgp&lt;/code&gt;). Jails on this server use three distinct routing paradigms - private &lt;span class="caps"&gt;NAT&lt;/span&gt;, natively routed &lt;span class="caps"&gt;BGP&lt;/span&gt; IPv6, and pure public routed &lt;span class="caps"&gt;BGP&lt;/span&gt; IPv4 - all on the same bridge&amp;nbsp;interface.&lt;/p&gt;
&lt;p&gt;The mechanism that makes this work is &lt;strong&gt;dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; policy routing&lt;/strong&gt;: two independent routing tables in the kernel, with &lt;span class="caps"&gt;PF&lt;/span&gt; deciding which table handles which traffic based on source address. It&amp;#8217;s elegant once you understand it, and surprisingly simple to configure once you&amp;#8217;ve seen it&amp;nbsp;done.&lt;/p&gt;
&lt;h2 id="the-problem-one-server-two-gateways"&gt;The Problem: One Server, Two&amp;nbsp;Gateways&lt;/h2&gt;
&lt;p&gt;A server with a single default gateway has simple routing: everything goes out the same door. But what happens when you want traffic from different source addresses to take different&amp;nbsp;paths?&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;radon&lt;/code&gt;, the physical interface&amp;nbsp;(&lt;code&gt;vtnet0&lt;/code&gt;) is assigned Netcup&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;space: &lt;code&gt;152.53.147.5&lt;/code&gt; for IPv4&amp;nbsp;and &lt;code&gt;2a0a:4cc0:c1:2f90::2&lt;/code&gt; for IPv6. The default gateway points to Netcup&amp;#8217;s router. Standard traffic - package updates, &lt;span class="caps"&gt;DNS&lt;/span&gt; queries, NATed jail traffic - flows through this&amp;nbsp;path.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;radon&lt;/code&gt; also has addresses from my own &lt;span class="caps"&gt;AS201379&lt;/span&gt;: &lt;code&gt;194.28.98.217&lt;/code&gt; (shared via loopback for port&amp;nbsp;forwarding), &lt;code&gt;194.28.98.216&lt;/code&gt; (directly assigned to a jail), and the&amp;nbsp;entire &lt;code&gt;2a06:9801:1c:1000::/64&lt;/code&gt; IPv6 subnet routed to jails. This address space is announced to the internet through my &lt;span class="caps"&gt;BGP&lt;/span&gt; router. Traffic arriving for these addresses traverses the &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel. If replies to that traffic exit through Netcup&amp;#8217;s default gateway instead of the tunnel, the provider drops them as spoofed - the source &lt;span class="caps"&gt;IP&lt;/span&gt; doesn&amp;#8217;t belong to Netcup&amp;#8217;s&amp;nbsp;network.&lt;/p&gt;
&lt;p&gt;The kernel&amp;#8217;s single routing table (&lt;span class="caps"&gt;FIB&lt;/span&gt; 0) knows one default route: Netcup. It has no concept of &amp;#8220;this source address should use a different exit.&amp;#8221; That&amp;#8217;s where dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; comes&amp;nbsp;in.&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                        ┌─────────────────────────────────────────────┐
                        │           Public Internet                   │
                        └────────┬──────────────────────┬─────────────┘
                                 │                      │
                         Netcup Network           BGP (AS201379)
                         152.53.144.0/22          via hobgp router
                                 │                      │
                                 │               ┌──────┴──────────┐
                                 │               │  hobgp (Core)   │
                                 │               │  128.140.64.181 │
                                 │               └──────┬──────────┘
                                 │                      │
                                 │              GIF tunnel (proto 41)
                                 │              IPv6-in-IPv4 encap
     ┌───────────────────────────┴──────────────────────┴─────────────────────┐
     │                        radon.edelga.se                                 │
     │                                                                        │
     │  vtnet0 ──── FIB 0 (Default)                                           │
     │  152.53.147.5/22        &amp;lt;- Standard internet, NAT, mgmt                │
     │  2a0a:4cc0:c1:2f90::2/64                                               │
     │                                                                        │
     │  gif0 ────── FIB 1 (Tunnel)                                            │
     │  194.28.98.216/29       &amp;lt;- BGP-addressed traffic                       │
     │  2a06:9801:1c:ffff::2/128                                              │
     │                                ┌─────────────────────────────────────┐ │
     │  bastille0 ════════════════════│        Jail Bridge                  │ │
     │  10.254.254.1/24               │                                     │ │
     │  2a06:9801:1c:1000::1/64       │  ┌────────┐  ┌─────────────────────┐│ │
     │                                │  │ Caddy  │  │ testvnet            ││ │
     │  lo0:                          │  │ .254.10│  │194.28.98.216        ││ │
     │  194.28.98.217/32 (shared)     │  │ (NAT)  │  │2a06:9801:1c:1000::99││ │
     │                                │  └────────┘  │   (Pure Public)     ││ │
     │                                │              └─────────────────────┘│ │
     │                                └─────────────────────────────────────┘ │
     └────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key insight: &lt;span class="caps"&gt;FIB&lt;/span&gt; 0 handles &amp;#8220;normal&amp;#8221; traffic (Netcup-addressed). &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 handles &lt;span class="caps"&gt;BGP&lt;/span&gt;-addressed traffic (&lt;span class="caps"&gt;AS201379&lt;/span&gt;). &lt;span class="caps"&gt;PF&lt;/span&gt; is the glue that assigns packets to the correct &lt;span class="caps"&gt;FIB&lt;/span&gt; based on source or destination&amp;nbsp;address.&lt;/p&gt;
&lt;h2 id="the-physical-upstream-netcup-fib-0"&gt;The Physical Upstream: Netcup (&lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;0)&lt;/h2&gt;
&lt;p&gt;The physical interface lives in the default routing table. This is the server&amp;#8217;s &amp;#8220;normal&amp;#8221; internet&amp;nbsp;connection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Primary physical interface - Netcup VPS&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 152.53.147.5 netmask 255.255.252.0 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;152.53.144.1&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a0a:4cc0:c1:2f90::2 prefixlen 64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;-lro -tso&lt;/code&gt; flags deserve a callout.&lt;/strong&gt; &lt;span class="caps"&gt;LRO&lt;/span&gt; (Large Receive Offload) and &lt;span class="caps"&gt;TSO&lt;/span&gt; (&lt;span class="caps"&gt;TCP&lt;/span&gt; Segmentation Offload) are hardware acceleration features on the virtio &lt;span class="caps"&gt;NIC&lt;/span&gt;. They work fine for traffic that terminates at the host. But when the host forwards packets - as a router does for jail traffic - &lt;strong&gt;these offloads break &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s ability to correctly &lt;span class="caps"&gt;NAT&lt;/span&gt; and checksum forwarded packets.&lt;/strong&gt; The symptom is maddening: connections from the host work, but NATed jail traffic silently fails with bad checksums. &lt;strong&gt;Disable both whenever a FreeBSD host acts as a&amp;nbsp;gateway.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;SLAAC&lt;/span&gt; is disabled globally to avoid conflicts with statically routed tunnel&amp;nbsp;addresses:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ipv6_activate_all_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="the-logical-upstream-bgp-tunnel-fib-1"&gt;The Logical Upstream: &lt;span class="caps"&gt;BGP&lt;/span&gt; Tunnel (&lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;1)&lt;/h2&gt;
&lt;p&gt;The &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel connects to the &lt;span class="caps"&gt;BGP&lt;/span&gt; router&amp;nbsp;at &lt;code&gt;128.140.64.181&lt;/code&gt;. This is where the multi-&lt;span class="caps"&gt;FIB&lt;/span&gt; magic&amp;nbsp;lives:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0 gif0&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fib 1 tunnel 152.53.147.5 128.140.64.181 tunnelfib 0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_alias0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.255.255.6 10.255.255.5&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:ffff::2 2a06:9801:1c:ffff::1 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This single line&amp;nbsp;- &lt;code&gt;fib 1 tunnel 152.53.147.5 128.140.64.181 tunnelfib 0&lt;/code&gt; - is the most important configuration directive on the entire server. It contains two directives that work in&amp;nbsp;concert:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fib 1&lt;/code&gt;&lt;/strong&gt;: The tunnel interface itself lives in routing table 1. Traffic arriving&amp;nbsp;on &lt;code&gt;gif0&lt;/code&gt; and traffic routed&amp;nbsp;out &lt;code&gt;gif0&lt;/code&gt; consults &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 for routing&amp;nbsp;decisions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tunnelfib 0&lt;/code&gt;&lt;/strong&gt;: The outer IPv4 encapsulation wrapper&amp;nbsp;(the &lt;code&gt;152.53.147.5 → 128.140.64.181&lt;/code&gt; packet) uses &lt;span class="caps"&gt;FIB&lt;/span&gt; 0 - the default routing table - to find the path to the &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;router.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why both?&amp;nbsp;Without &lt;code&gt;tunnelfib 0&lt;/code&gt;, the encapsulated IPv4 packets would consult &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 for their route&amp;nbsp;to &lt;code&gt;128.140.64.181&lt;/code&gt;. But &lt;span class="caps"&gt;FIB&lt;/span&gt; 1&amp;#8217;s default route points&amp;nbsp;at &lt;code&gt;gif0&lt;/code&gt; itself (the tunnel). That creates a recursive loop: to send a packet out the tunnel, you&amp;#8217;d need to use the&amp;nbsp;tunnel. &lt;code&gt;tunnelfib 0&lt;/code&gt; breaks the recursion by telling the outer encapsulation to use the normal Netcup&amp;nbsp;route.&lt;/p&gt;
&lt;p&gt;The IPv4 point-to-point addresses&amp;nbsp;(&lt;code&gt;10.255.255.6/5&lt;/code&gt;) are used for the tunnel&amp;#8217;s inner addressing. The IPv6 link addresses&amp;nbsp;(&lt;code&gt;2a06:9801:1c:ffff::2/1&lt;/code&gt;) come from our own /48 - they&amp;#8217;re already routable within our &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;infrastructure.&lt;/p&gt;
&lt;h2 id="fib-1-building-a-complete-routing-table"&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; 1: Building a Complete Routing&amp;nbsp;Table&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; 1 starts empty. It knows nothing about the server&amp;#8217;s local networks. Every route must be explicitly added - if &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 doesn&amp;#8217;t have a route to a destination, traffic in that routing table gets&amp;nbsp;dropped.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fib1_v6_default fib1_v6_jailnet fib1_v4_default fib1_v4_jailnet fib1_v4_host217 fib1_v4_jail216 v4_jail216&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Default Routes (How FIB 1 reaches the internet)&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1_v6_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-6 default -interface gif0 -fib 1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1_v4_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-inet default 10.255.255.5 -fib 1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Subnet Routes (How FIB 1 finds the bridge)&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1_v6_jailnet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-6 2a06:9801:1c:1000::/64 -interface bastille0 -fib 1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1_v4_jailnet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-net 10.254.254.0/24 -interface bastille0 -fib 1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Host Routes (Where FIB 1 finds specific public IPs)&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1_v4_host217&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 194.28.98.217 -interface lo0 -fib 1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1_v4_jail216&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 194.28.98.216 -interface bastille0 -fib 1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_v4_jail216&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 194.28.98.216 -interface bastille0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;# FIB 0 also needs this — explained in the Pure Public section below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each route category serves a specific&amp;nbsp;purpose:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Default routes&lt;/strong&gt; tell &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 how to reach the internet. IPv6 exits&amp;nbsp;through &lt;code&gt;gif0&lt;/code&gt; directly (the tunnel interface). IPv4&amp;nbsp;uses &lt;code&gt;10.255.255.5&lt;/code&gt; as the next-hop - that&amp;#8217;s the &lt;span class="caps"&gt;BGP&lt;/span&gt; router&amp;#8217;s end of the tunnel&amp;#8217;s inner IPv4&amp;nbsp;addressing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subnet routes&lt;/strong&gt; are necessary because &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 doesn&amp;#8217;t automatically know&amp;nbsp;that &lt;code&gt;bastille0&lt;/code&gt; has the jail network attached. Without these, return traffic arriving on the tunnel for jail addresses would try to exit via the default route (back out the tunnel) instead of being delivered locally to the&amp;nbsp;bridge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Host routes&lt;/strong&gt; handle the public&amp;nbsp;IPs. &lt;code&gt;194.28.98.217&lt;/code&gt; is&amp;nbsp;on &lt;code&gt;lo0&lt;/code&gt; (the host&amp;#8217;s shared loopback &lt;span class="caps"&gt;IP&lt;/span&gt; for port&amp;nbsp;forwarding). &lt;code&gt;194.28.98.216&lt;/code&gt; is&amp;nbsp;on &lt;code&gt;bastille0&lt;/code&gt; (directly assigned to&amp;nbsp;the &lt;code&gt;testvnet&lt;/code&gt; jail). Both FIBs need to know where these addresses live - note&amp;nbsp;that &lt;code&gt;v4_jail216&lt;/code&gt; adds the same route to &lt;span class="caps"&gt;FIB&lt;/span&gt; 0 as well, so that the default routing table can also reach the&amp;nbsp;jail.&lt;/p&gt;
&lt;h2 id="jail-networking-and-the-bridge"&gt;Jail Networking and the&amp;nbsp;Bridge&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;bastille0&lt;/code&gt; bridge is the single point where all jail routing paradigms&amp;nbsp;converge:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.254.254.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:1000::1 prefixlen 64&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three types of traffic share this&amp;nbsp;bridge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Private &lt;span class="caps"&gt;NAT&lt;/span&gt; (10.254.254.0/24)&lt;/strong&gt;: Jails like Caddy&amp;nbsp;(&lt;code&gt;10.254.254.10&lt;/code&gt;) get private IPv4 addresses, NATed to the Netcup &lt;span class="caps"&gt;IP&lt;/span&gt; when&amp;nbsp;exiting &lt;code&gt;vtnet0&lt;/code&gt;. This is standard jail networking - nothing special&amp;nbsp;here.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; IPv6 Routed (2a06:9801:1c:1000::/64)&lt;/strong&gt;: Jails also receive addresses from our &lt;span class="caps"&gt;BGP&lt;/span&gt; prefix. These are globally routable, announced by &lt;span class="caps"&gt;AS201379&lt;/span&gt;, and traffic for them arrives via the &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel. No &lt;span class="caps"&gt;NAT&lt;/span&gt; - pure end-to-end IPv6 as&amp;nbsp;designed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; IPv4 Routed (194.28.98.x)&lt;/strong&gt;:&amp;nbsp;The &lt;code&gt;testvnet&lt;/code&gt; jail&amp;nbsp;has &lt;code&gt;194.28.98.216/32&lt;/code&gt; - a real public IPv4 address, no &lt;span class="caps"&gt;NAT&lt;/span&gt;, directly routed through both&amp;nbsp;FIBs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The shared host &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;(&lt;code&gt;194.28.98.217&lt;/code&gt;)&amp;nbsp;on &lt;code&gt;lo0&lt;/code&gt; is a different pattern. It&amp;#8217;s not assigned to any jail; instead, &lt;span class="caps"&gt;PF&lt;/span&gt; uses &lt;span class="caps"&gt;RDR&lt;/span&gt; rules to forward ports from this &lt;span class="caps"&gt;IP&lt;/span&gt; to internal jails. This lets Caddy serve content from a &lt;span class="caps"&gt;BGP&lt;/span&gt;-routed IPv4 address while the jail itself only has a&amp;nbsp;private &lt;code&gt;10.254.254.10&lt;/code&gt; address.&lt;/p&gt;
&lt;h2 id="pf-where-the-routing-decision-happens"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;: Where the Routing Decision&amp;nbsp;Happens&lt;/h2&gt;
&lt;p&gt;The &lt;span class="caps"&gt;PF&lt;/span&gt; configuration is where dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; stops being a kernel feature and becomes a routing policy. Let&amp;#8217;s walk through each&amp;nbsp;layer.&lt;/p&gt;
&lt;h3 id="normalization"&gt;Normalization&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;scrub in all fragment reassemble
scrub all max-mss 1220
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;MSS&lt;/span&gt; clamp at 1220 accounts for tunnel encapsulation overhead. &lt;span class="caps"&gt;MSS&lt;/span&gt; (Maximum Segment Size) dictates the maximum size of the &lt;span class="caps"&gt;TCP&lt;/span&gt; payload. To calculate it, subtract the inner &lt;span class="caps"&gt;IP&lt;/span&gt; header and the inner &lt;span class="caps"&gt;TCP&lt;/span&gt; header from the &lt;span class="caps"&gt;MTU&lt;/span&gt;. For IPv6 traffic inside the tunnel, clamped to the safe minimum &lt;span class="caps"&gt;MTU&lt;/span&gt; of 1280 bytes, you subtract the IPv6 header (40 bytes) and the &lt;span class="caps"&gt;TCP&lt;/span&gt; header (20 bytes): 1280 - 40 - 20 = 1220. Without this, large &lt;span class="caps"&gt;TCP&lt;/span&gt; segments get silently dropped or fragmented at the tunnel, causing mysterious stalls where small requests succeed but large transfers&amp;nbsp;hang.&lt;/p&gt;
&lt;h3 id="translation-nat-and-rdr"&gt;Translation (&lt;span class="caps"&gt;NAT&lt;/span&gt; and &lt;span class="caps"&gt;RDR&lt;/span&gt;)&lt;/h3&gt;
&lt;p&gt;Translation happens before filtering. This is critical to understand - filter rules for &lt;span class="caps"&gt;RDR&lt;/span&gt; traffic must match the &lt;strong&gt;translated&lt;/strong&gt; internal &lt;span class="caps"&gt;IP&lt;/span&gt;, not the public &lt;span class="caps"&gt;IP&lt;/span&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# Outbound NAT: private jails → Netcup IP&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="cp"&gt;# Port forwarding on the Netcup IP (vtnet0)&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1965&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;gemini_v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;powerdns_v4&lt;/span&gt;

&lt;span class="cp"&gt;# Port forwarding on the BGP host IP (arrives via tunnel)&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_v4_routed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The last rule is different - traffic&amp;nbsp;to &lt;code&gt;194.28.98.217:80/443&lt;/code&gt; arrives on the tunnel interface and gets redirected to the same Caddy jail. This means Caddy serves web traffic regardless of whether it arrived via Netcup or the &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel, even though Caddy only has a private &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;address.&lt;/p&gt;
&lt;h3 id="default-policies"&gt;Default&amp;nbsp;Policies&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;block quick from &amp;lt;bruteforce&amp;gt;
block drop in log all
block drop out log all
antispoof quick for { $ext_if, bastille0 }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Default deny in both directions. Bruteforce entries get an instant drop before any evaluation. Antispoof prevents packets from entering interfaces they don&amp;#8217;t belong&amp;nbsp;to.&lt;/p&gt;
&lt;h3 id="egress-the-route-to-safety-net"&gt;Egress:&amp;nbsp;The &lt;code&gt;route-to&lt;/code&gt; Safety&amp;nbsp;Net&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pass out quick on $ext_if route-to ($tun_if $tun_gw_v4) inet from $host_v4_routed to any keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a safety net rule that catches an edge case. When local processes on the host respond from the shared &lt;span class="caps"&gt;BGP&lt;/span&gt; &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;(&lt;code&gt;194.28.98.217&lt;/code&gt;), those replies naturally try to exit&amp;nbsp;via &lt;code&gt;vtnet0&lt;/code&gt; - the host&amp;#8217;s default route in &lt;span class="caps"&gt;FIB&lt;/span&gt; 0. But Netcup would drop traffic sourced from an &lt;span class="caps"&gt;IP&lt;/span&gt; that doesn&amp;#8217;t belong to their&amp;nbsp;network.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;route-to&lt;/code&gt; directive intercepts: &amp;#8220;if you see a packet about to&amp;nbsp;exit &lt;code&gt;vtnet0&lt;/code&gt; with&amp;nbsp;source &lt;code&gt;194.28.98.217&lt;/code&gt;, redirect it to the tunnel interface instead.&amp;#8221; It overrides the routing decision at the &lt;span class="caps"&gt;PF&lt;/span&gt; level, forcing the packet&amp;nbsp;into &lt;code&gt;gif0&lt;/code&gt; toward &lt;code&gt;10.255.255.5&lt;/code&gt; (the &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;router).&lt;/p&gt;
&lt;h3 id="egress-jail-policy-routing-via-rtable"&gt;Egress: Jail Policy Routing&amp;nbsp;via &lt;code&gt;rtable&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is the core mechanism. When a jail sends traffic from a &lt;span class="caps"&gt;BGP&lt;/span&gt; address, &lt;span class="caps"&gt;PF&lt;/span&gt; must force it into &lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;1:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BGP&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;addressed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;traffic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_net_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;traffic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;testvnet_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;rtable 1&lt;/code&gt; directive is the key. When a packet&amp;nbsp;from &lt;code&gt;2a06:9801:1c:1000::/64&lt;/code&gt; or &lt;code&gt;194.28.98.216&lt;/code&gt; arrives at the bridge, &lt;span class="caps"&gt;PF&lt;/span&gt; assigns it to routing table 1. The kernel then consults &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 for the forwarding decision - and &lt;span class="caps"&gt;FIB&lt;/span&gt; 1&amp;#8217;s default route points&amp;nbsp;out &lt;code&gt;gif0&lt;/code&gt; to the &lt;span class="caps"&gt;BGP&lt;/span&gt; router. The packet gets encapsulated and sent through the&amp;nbsp;tunnel.&lt;/p&gt;
&lt;p&gt;Without &lt;code&gt;rtable 1&lt;/code&gt;, these packets would use &lt;span class="caps"&gt;FIB&lt;/span&gt; 0&amp;#8217;s default route and exit through Netcup - where they&amp;#8217;d be dropped as&amp;nbsp;spoofed.&lt;/p&gt;
&lt;p&gt;There&amp;#8217;s also a &lt;span class="caps"&gt;DNS64&lt;/span&gt; translation rule for jails needing IPv4 destinations via&amp;nbsp;IPv6:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pass in quick on bastille0 inet6 from $bgp_net_v6 to 64:ff9b::/96 af-to inet from ($ext_if) keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And a standard &lt;span class="caps"&gt;NAT&lt;/span&gt; egress rule for private&amp;nbsp;jails:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pass in quick on bastille0 from &amp;lt;jails_v4&amp;gt; to ! $jail_net keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;! $jail_net&lt;/code&gt; negation allows jails to reach the internet but prevents them from directly accessing other jails - a micro-segmentation pattern that documents and enforces the network&amp;nbsp;architecture.&lt;/p&gt;
&lt;h3 id="ingress-tunnel-encapsulation"&gt;Ingress: Tunnel&amp;nbsp;Encapsulation&lt;/h3&gt;
&lt;p&gt;The &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel uses protocol 41 (IPv6-in-IPv4). &lt;span class="caps"&gt;PF&lt;/span&gt; must allow these encapsulated packets to enter and exit the physical&amp;nbsp;interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pass in  quick on $ext_if proto { 41, ipencap } from 128.140.64.181 to ($ext_if)
pass out quick on $ext_if proto { 41, ipencap } from ($ext_if) to 128.140.64.181
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Strictly locked to the &lt;span class="caps"&gt;BGP&lt;/span&gt; router&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt; - no one else should be sending protocol 41 to this&amp;nbsp;server.&lt;/p&gt;
&lt;h3 id="ingress-the-reply-to-imperative"&gt;Ingress:&amp;nbsp;The &lt;code&gt;reply-to&lt;/code&gt; Imperative&lt;/h3&gt;
&lt;p&gt;All inbound connections from the &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel must be tagged&amp;nbsp;with &lt;code&gt;reply-to&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tunnel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ingress&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_gw_v4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;host_v4_routed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_gw_v4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;trusted_v4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;host_v4_routed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30822&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_gw_v4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;frontend_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_gw_v4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;testvnet_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tunnel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ingress&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_hub_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_net_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_hub_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;frontend_v6&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_hub_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;powerdns_v6&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_hub_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;gemini_v6&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1965&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;reply-to&lt;/code&gt; instructs &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s state machine: &amp;#8220;when you see a reply to this connection, send it back&amp;nbsp;out &lt;code&gt;$tun_if&lt;/code&gt; to the specified next-hop, regardless of what the routing table says.&amp;#8221; Without it, the kernel would consult &lt;span class="caps"&gt;FIB&lt;/span&gt; 0 for return traffic (the jail doesn&amp;#8217;t live in &lt;span class="caps"&gt;FIB&lt;/span&gt; 1), and replies would exit&amp;nbsp;through &lt;code&gt;vtnet0&lt;/code&gt; - getting dropped by Netcup as&amp;nbsp;spoofed.&lt;/p&gt;
&lt;p&gt;The IPv4 and IPv6 reply-to targets differ because they use different tunnel endpoint addresses:
- &lt;strong&gt;IPv4&lt;/strong&gt;: &lt;code&gt;$tun_gw_v4&lt;/code&gt; = &lt;code&gt;10.255.255.5&lt;/code&gt; (the inner IPv4 point-to-point address)
- &lt;strong&gt;IPv6&lt;/strong&gt;: &lt;code&gt;$bgp_hub_ip&lt;/code&gt; = &lt;code&gt;2a06:9801:1c:ffff::1&lt;/code&gt; (the inner IPv6 link&amp;nbsp;address)&lt;/p&gt;
&lt;h2 id="the-pure-public-vnet-jail"&gt;The Pure Public &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;Jail&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;testvnet&lt;/code&gt; jail is the most interesting routing case. It has a real public IPv4 address&amp;nbsp;(&lt;code&gt;194.28.98.216/32&lt;/code&gt;) and a &lt;span class="caps"&gt;BGP&lt;/span&gt; IPv6 address&amp;nbsp;(&lt;code&gt;2a06:9801:1c:1000::99&lt;/code&gt;), both assigned directly to the&amp;nbsp;jail&amp;#8217;s &lt;code&gt;vnet0&lt;/code&gt; interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Inside the jail&lt;/span&gt;
vnet0:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1008843&lt;/span&gt;&amp;lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP&amp;gt;
&lt;span class="w"&gt;        &lt;/span&gt;inet&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;194&lt;/span&gt;.28.98.216&lt;span class="w"&gt; &lt;/span&gt;netmask&lt;span class="w"&gt; &lt;/span&gt;0xffffffff&lt;span class="w"&gt; &lt;/span&gt;broadcast&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;194&lt;/span&gt;.28.98.216
&lt;span class="w"&gt;        &lt;/span&gt;inet6&lt;span class="w"&gt; &lt;/span&gt;2a06:9801:1c:1000::99&lt;span class="w"&gt; &lt;/span&gt;prefixlen&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This jail has no &lt;span class="caps"&gt;NAT&lt;/span&gt;. Outbound traffic&amp;nbsp;from &lt;code&gt;194.28.98.216&lt;/code&gt; hits the bridge, &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;matches &lt;code&gt;from $testvnet_v4 → rtable 1&lt;/code&gt;, and the packet exits via the tunnel. Inbound traffic arrives on the tunnel, &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;matches &lt;code&gt;to $testvnet_v4&lt;/code&gt; with &lt;code&gt;reply-to&lt;/code&gt;, and the packet is delivered to the jail on the bridge. Return traffic follows&amp;nbsp;the &lt;code&gt;reply-to&lt;/code&gt; state back through the&amp;nbsp;tunnel.&lt;/p&gt;
&lt;p&gt;Both FIBs need host routes for this address. &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 needs it because tunnel traffic must find the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;route_fib1_v4_jail216&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 194.28.98.216 -interface bastille0 -fib 1&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; 0 needs it because proto 41 decapsulation happens before &lt;span class="caps"&gt;PF&lt;/span&gt; routing decisions - the kernel must know where to deliver the inner&amp;nbsp;packet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;route_v4_jail216&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 194.28.98.216 -interface bastille0&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="the-complete-flow"&gt;The Complete&amp;nbsp;Flow&lt;/h2&gt;
&lt;h3 id="inbound-to-a-bgp-addressed-service-ipv6"&gt;Inbound to a &lt;span class="caps"&gt;BGP&lt;/span&gt;-addressed service&amp;nbsp;(IPv6)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; 1. Client sends packet to 2a06:9801:1c:1000::10 (Caddy jail)
 2. Packet traverses the internet → AS201379 → hobgp BGP router
 3. hobgp forwards through GIF tunnel to radon (proto 41 encapsulated)
 4. radon receives proto 41 on vtnet0, decapsulates → gif0
 5. PF match: reply-to ($tun_if $bgp_hub_ip), creates state entry
 6. Forwarded to bastille0 → Caddy jail
 7. Caddy responds, packet exits on bastille0
 8. PF state table triggers reply-to: send via gif0 to bgp_hub_ip
 9. gif0 encapsulates (proto 41) using tunnelfib 0 → vtnet0 → hobgp
10. hobgp receives, forwards to upstream → internet → client
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="outbound-from-a-pure-public-jail-ipv4"&gt;Outbound from a pure public jail&amp;nbsp;(IPv4)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;1. testvnet jail sends packet from 194.28.98.216
2. Packet arrives on bastille0
3. PF match: &amp;quot;from $testvnet_v4 → rtable 1&amp;quot;
4. Kernel routes via FIB 1 → default route → gif0
5. gif0 encapsulates using tunnelfib 0 → vtnet0 → hobgp
6. hobgp receives, forwards to internet (source: 194.28.98.216)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="natd-jail-egress-ipv4-standard-path"&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt;&amp;#8217;d jail egress (IPv4, standard&amp;nbsp;path)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;1. Caddy jail sends packet from 10.254.254.10
2. Packet arrives on bastille0
3. PF match: &amp;quot;from &amp;lt;jails_v4&amp;gt; to ! $jail_net&amp;quot; (standard egress)
4. Kernel routes via FIB 0 → default route → vtnet0
5. NAT translates source to 152.53.147.5
6. Packet exits to Netcup → internet
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;h3 id="from-the-host-testing-both-fibs"&gt;From the host: testing both&amp;nbsp;FIBs&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;setfib&lt;/code&gt; command selects which routing table a command&amp;nbsp;uses:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Via Netcup (FIB 0) - physical uplink&lt;/span&gt;
root@radon:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# setfib 0 fetch -4 -o - -q https://ifconfig.co&lt;/span&gt;
&lt;span class="m"&gt;152&lt;/span&gt;.53.147.5

root@radon:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# setfib 0 fetch -6 -o - -q https://ifconfig.co&lt;/span&gt;
2a0a:4cc0:c1:2f90::2

&lt;span class="c1"&gt;# Via BGP tunnel (FIB 1) - bound to the routed IP&lt;/span&gt;
root@radon:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# setfib 1 fetch --bind-address=194.28.98.217 -4 -o - -q https://ifconfig.co&lt;/span&gt;
&lt;span class="m"&gt;194&lt;/span&gt;.28.98.217

root@radon:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# setfib 1 fetch -6 -o - -q https://ifconfig.co&lt;/span&gt;
2a06:9801:1c:ffff::2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; 0 returns Netcup&amp;#8217;s addresses. &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 returns &lt;span class="caps"&gt;BGP&lt;/span&gt; addresses. Two completely independent paths to the internet from the same&amp;nbsp;machine.&lt;/p&gt;
&lt;h3 id="from-inside-the-pure-public-jail"&gt;From inside the pure public&amp;nbsp;jail&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;root@radon:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# bastille cmd testvnet fetch -4 -o - -q https://ifconfig.co&lt;/span&gt;
&lt;span class="m"&gt;194&lt;/span&gt;.28.98.216

root@radon:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# bastille cmd testvnet fetch -6 -o - -q https://ifconfig.co&lt;/span&gt;
2a06:9801:1c:1000::99
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The jail sees its own real public &lt;span class="caps"&gt;IP&lt;/span&gt; - not a NATed address. Both IPv4 and IPv6 traffic exits through the &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel and appears on the internet with the correct source&amp;nbsp;addresses.&lt;/p&gt;
&lt;h3 id="from-external-clients-both-paths-work"&gt;From external clients: both paths&amp;nbsp;work&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Via Netcup (default DNS, resolves to 152.53.147.5)&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;❯&lt;span class="w"&gt; &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-Iv4&lt;span class="w"&gt; &lt;/span&gt;http://radon.edelga.se
*&lt;span class="w"&gt; &lt;/span&gt;IPv4:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;152&lt;/span&gt;.53.147.5
*&lt;span class="w"&gt; &lt;/span&gt;Connected&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;radon.edelga.se&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;152&lt;/span&gt;.53.147.5&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;
&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;HTTP/1.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;308&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Permanent&lt;span class="w"&gt; &lt;/span&gt;Redirect
&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;Server:&lt;span class="w"&gt; &lt;/span&gt;Caddy

&lt;span class="c1"&gt;# Via BGP tunnel (resolves to 2a06:9801:1c:1000::10)&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;❯&lt;span class="w"&gt; &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-Iv6&lt;span class="w"&gt; &lt;/span&gt;http://blog.hofstede.it
*&lt;span class="w"&gt; &lt;/span&gt;IPv6:&lt;span class="w"&gt; &lt;/span&gt;2a06:9801:1c:1000::10
*&lt;span class="w"&gt; &lt;/span&gt;Connected&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;blog.hofstede.it&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;2a06:9801:1c:1000::10&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;
&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;HTTP/1.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;308&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Permanent&lt;span class="w"&gt; &lt;/span&gt;Redirect
&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;Server:&lt;span class="w"&gt; &lt;/span&gt;Caddy

&lt;span class="c1"&gt;# Via BGP tunnel (IPv4, routed through 194.28.98.217)&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;❯&lt;span class="w"&gt; &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-Iv4&lt;span class="w"&gt; &lt;/span&gt;http://194.28.98.217
*&lt;span class="w"&gt; &lt;/span&gt;Connected&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;194&lt;/span&gt;.28.98.217&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;194&lt;/span&gt;.28.98.217&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;
&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;HTTP/1.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;308&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Permanent&lt;span class="w"&gt; &lt;/span&gt;Redirect
&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;Server:&lt;span class="w"&gt; &lt;/span&gt;Caddy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three different entry points, all reaching the same Caddy jail - which only has&amp;nbsp;a &lt;code&gt;10.254.254.10&lt;/code&gt; private address. The same service is accessible through Netcup&amp;#8217;s network (physical) and through the &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel (logical), and Caddy doesn&amp;#8217;t know or care which path the request&amp;nbsp;took.&lt;/p&gt;
&lt;h2 id="security-considerations"&gt;Security&amp;nbsp;Considerations&lt;/h2&gt;
&lt;p&gt;Dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; setups require extra security attention because more routing paths mean more attack&amp;nbsp;surface:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;kern_securelevel=2&lt;/code&gt;&lt;/strong&gt; prevents loading kernel modules and writing to raw disk devices post-boot. Even if root is compromised, the attacker can&amp;#8217;t modify the running kernel or bypass the&amp;nbsp;firewall.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; is restricted to known IPs on a non-standard port&lt;/strong&gt; with aggressive rate limiting.&amp;nbsp;The &lt;code&gt;overload &amp;lt;bruteforce&amp;gt; flush global&lt;/code&gt; combination instantly blocks attackers and kills their existing&amp;nbsp;connections:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30822&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; via the tunnel&amp;nbsp;uses &lt;code&gt;reply-to&lt;/code&gt; to ensure the management session stays within the tunnel path - an attacker on Netcup&amp;#8217;s network can&amp;#8217;t even see the tunnel-side &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;port.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Antispoof&lt;/strong&gt; blocks packets with impossible source addresses for each interface, preventing attackers from injecting traffic that pretends to come from the internal&amp;nbsp;network.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring is bound to the internal bridge only&lt;/strong&gt;, keeping Prometheus exporters invisible from the&amp;nbsp;internet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;node_exporter_listen_address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.254.254.1:9100&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;jail_exporter_listen_address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.254.254.1:9452&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tunnelfib&lt;/code&gt; is the unsung hero.&lt;/strong&gt;&amp;nbsp;Without &lt;code&gt;tunnelfib 0&lt;/code&gt; on the &lt;span class="caps"&gt;GIF&lt;/span&gt; interface, the tunnel would try to route its own encapsulated packets through itself. The error mode is a silent hang - no error messages, no crash, just packets disappearing into recursive routing. If your &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel comes up but passes no traffic,&amp;nbsp;check &lt;code&gt;tunnelfib&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; 1 needs manual completeness.&lt;/strong&gt; Unlike &lt;span class="caps"&gt;FIB&lt;/span&gt; 0, which automatically gets interface routes when interfaces come up, &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 starts empty. If you can ping the internet from &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 but jails can&amp;#8217;t reply, you&amp;#8217;re missing a subnet route that tells &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 where the bridge is. The symptom is asymmetric: inbound works&amp;nbsp;(because &lt;code&gt;reply-to&lt;/code&gt; handles replies), but jail-initiated connections&amp;nbsp;fail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;reply-to&lt;/code&gt; and &lt;code&gt;rtable&lt;/code&gt; solve different halves of the same&amp;nbsp;problem.&lt;/strong&gt; &lt;code&gt;reply-to&lt;/code&gt; handles inbound connections: &amp;#8220;replies to tunnel traffic must exit via the&amp;nbsp;tunnel.&amp;#8221; &lt;code&gt;rtable&lt;/code&gt; handles outbound connections: &amp;#8220;traffic from &lt;span class="caps"&gt;BGP&lt;/span&gt; addresses must route via &lt;span class="caps"&gt;FIB&lt;/span&gt; 1.&amp;#8221; You need both - one without the other produces subtle&amp;nbsp;failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disable hardware offloading on forwarding hosts.&lt;/strong&gt; &lt;span class="caps"&gt;LRO&lt;/span&gt; and &lt;span class="caps"&gt;TSO&lt;/span&gt; on virtio NICs cause &lt;span class="caps"&gt;PF&lt;/span&gt; to compute incorrect checksums for forwarded packets. The failure is invisible at the host level - everything looks fine locally - but downstream receivers silently drop the corrupted packets. &lt;strong&gt;The &lt;code&gt;-lro -tso&lt;/code&gt; flags are non-negotiable&amp;nbsp;when &lt;code&gt;gateway_enable="YES"&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;MSS&lt;/span&gt; clamping is not optional with tunnels.&lt;/strong&gt; Every encapsulation layer reduces the effective &lt;span class="caps"&gt;MTU&lt;/span&gt;.&amp;nbsp;Without &lt;code&gt;scrub all max-mss 1220&lt;/code&gt;, &lt;span class="caps"&gt;TCP&lt;/span&gt; connections that transfer more than ~1220 bytes of payload per segment will stall. The insidious part: small requests (&lt;span class="caps"&gt;DNS&lt;/span&gt;, &lt;span class="caps"&gt;HTTP&lt;/span&gt; redirects) work fine, making the problem seem intermittent until someone tries to download a&amp;nbsp;file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A single bridge can carry multiple routing paradigms.&lt;/strong&gt; Private NATed traffic, natively routed IPv6, and pure public IPv4 all coexist&amp;nbsp;on &lt;code&gt;bastille0&lt;/code&gt; without any &lt;span class="caps"&gt;VLAN&lt;/span&gt; tagging or interface separation. &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s source-based rules handle the routing differentiation. This simplicity is a feature - fewer moving parts means fewer failure&amp;nbsp;modes.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; policy routing on FreeBSD is the kind of feature that sounds complex until you see it in practice. Two routing tables, a handful of &lt;span class="caps"&gt;PF&lt;/span&gt; directives&amp;nbsp;(&lt;code&gt;rtable&lt;/code&gt;, &lt;code&gt;reply-to&lt;/code&gt;, &lt;code&gt;route-to&lt;/code&gt;), and some static routes. That&amp;#8217;s the entire mechanism that lets a single server speak from two completely different address spaces through two completely different internet&amp;nbsp;paths.&lt;/p&gt;
&lt;p&gt;The configuration shown here is what runs in production. The same Caddy jail&amp;nbsp;serves &lt;code&gt;blog.hofstede.it&lt;/code&gt; via IPv6 through the &lt;span class="caps"&gt;BGP&lt;/span&gt; tunnel&amp;nbsp;and &lt;code&gt;radon.edelga.se&lt;/code&gt; via IPv4 through Netcup - simultaneously, transparently, without knowing the difference.&amp;nbsp;The &lt;code&gt;testvnet&lt;/code&gt; jail has a real public IPv4 address that&amp;#8217;s globally routable through &lt;span class="caps"&gt;AS201379&lt;/span&gt;, with no &lt;span class="caps"&gt;NAT&lt;/span&gt; anywhere in the path. And the whole thing coexists peacefully with standard NATed jail traffic on the same&amp;nbsp;bridge.&lt;/p&gt;
&lt;p&gt;Is this setup necessary for running a blog? No. But it&amp;#8217;s the same pattern that production multi-homed infrastructure uses at much larger scale. The tools are the same - FIBs, &lt;span class="caps"&gt;PF&lt;/span&gt;, tunnels - just running on a single virtual machine instead of a rack of routers. FreeBSD makes this accessible because these features are first-class kernel primitives, not bolted-on&amp;nbsp;afterthoughts.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=setfib"&gt;FreeBSD setfib(1)&lt;/a&gt; - Multiple routing table&amp;nbsp;selection&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=gif"&gt;FreeBSD gif(4)&lt;/a&gt; - Generic tunnel interface,&amp;nbsp;including &lt;code&gt;tunnelfib&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=pf.conf"&gt;FreeBSD pf.conf(5)&lt;/a&gt; - &lt;span class="caps"&gt;PF&lt;/span&gt; configuration&amp;nbsp;reference&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/firewalls/"&gt;FreeBSD Handbook: Firewalls (&lt;span class="caps"&gt;PF&lt;/span&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;Running Your Own &lt;span class="caps"&gt;AS&lt;/span&gt;: &lt;span class="caps"&gt;BGP&lt;/span&gt; on FreeBSD&lt;/a&gt; - The &lt;span class="caps"&gt;BGP&lt;/span&gt; router side of this&amp;nbsp;setup&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/"&gt;Going Multi-Homed with iBGP&lt;/a&gt; - The multi-PoP&amp;nbsp;expansion&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/pf-firewall-on-freebsd-a-practical-guide/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; Firewall on FreeBSD: A Practical Guide&lt;/a&gt; - &lt;span class="caps"&gt;PF&lt;/span&gt; foundation&amp;nbsp;concepts&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;The internet doesn&amp;#8217;t care that all of this runs on one virtual machine. Packets arrive, get routed through the correct table, hit the correct jail, and the replies leave through the correct tunnel. Two FIBs, one bridge, zero confusion. That&amp;#8217;s the beauty of policy routing done right - the complexity is in the configuration, not in the&amp;nbsp;operation.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="networking"/><category term="pf"/><category term="bgp"/><category term="jails"/><category term="tunneling"/><category term="policy-routing"/></entry><entry><title>Running Your Own AS: Joining an IXP with a Third Edge Router</title><link href="https://blog.hofstede.it/running-your-own-as-joining-an-ixp-with-a-third-edge-router/" rel="alternate"/><published>2026-03-21T00:00:00+01:00</published><updated>2026-03-21T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-21:/running-your-own-as-joining-an-ixp-with-a-third-edge-router/</id><summary type="html">&lt;p&gt;Connecting &lt;span class="caps"&gt;AS201379&lt;/span&gt; to LocIX Düsseldorf via a dedicated edge router - adding direct exchange point peering to the existing multi-homed &lt;span class="caps"&gt;BGP&lt;/span&gt; setup, completing a three-router FreeBSD infrastructure with transit, native peering, and &lt;span class="caps"&gt;IXP&lt;/span&gt;&amp;nbsp;connectivity.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;Part 1&lt;/a&gt; covered running a single &lt;span class="caps"&gt;BGP&lt;/span&gt; router with two upstream providers. &lt;a href="https://blog.hofstede.it/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/"&gt;Part 2&lt;/a&gt; added a second Point of Presence with Vultr native peering and iBGP. This is Part 3: connecting to an Internet Exchange&amp;nbsp;Point.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-03-21-joining-an-ixp-locix-bgp-freebsd.png" title="Joining an IXP: Header image"&gt;&lt;/p&gt;
&lt;p&gt;An &lt;span class="caps"&gt;IXP&lt;/span&gt; changes the game. Instead of sending traffic through transit providers and their upstreams, networks at an exchange peer directly over a shared switching fabric. The result is shorter paths, lower latency, and fewer&amp;nbsp;intermediaries.&lt;/p&gt;
&lt;p&gt;This article documents connecting my &lt;span class="caps"&gt;AS201379&lt;/span&gt; to &lt;a href="https://www.locix.online/"&gt;LocIX Düsseldorf&lt;/a&gt; - a community-run &lt;span class="caps"&gt;IXP&lt;/span&gt; with over 350 participants - using a dedicated FreeBSD edge router, and the iBGP plumbing that ties it back into the existing infrastructure. This article assumes familiarity with &lt;span class="caps"&gt;BGP&lt;/span&gt;, iBGP, and basic FreeBSD networking as covered in the previous&amp;nbsp;parts.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on addresses:&lt;/strong&gt; All provider-assigned and transit-provider &lt;span class="caps"&gt;IP&lt;/span&gt; addresses have been replaced with &lt;a href="https://www.rfc-editor.org/rfc/rfc5737"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 5737&lt;/a&gt; / &lt;a href="https://www.rfc-editor.org/rfc/rfc3849"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 3849&lt;/a&gt; documentation ranges. &lt;span class="caps"&gt;AS201379&lt;/span&gt;, its prefix 2a06:9801:1c::/48, and all &lt;span class="caps"&gt;IXP&lt;/span&gt;-facing addresses (which are publicly visible in peering databases) are shown as-is. LocIX route server addresses and the peering &lt;span class="caps"&gt;LAN&lt;/span&gt; prefix are equally&amp;nbsp;public.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-join-an-ixp"&gt;Why Join an &lt;span class="caps"&gt;IXP&lt;/span&gt;?&lt;/h2&gt;
&lt;p&gt;Transit providers create indirect paths: even if two networks are geographically close, their traffic may traverse multiple intermediary ASes. At an &lt;span class="caps"&gt;IXP&lt;/span&gt;, those intermediaries disappear - networks exchange traffic directly over a shared fabric. The practical impact is&amp;nbsp;measurable:&lt;/p&gt;
&lt;p&gt;One example is traffic between my &lt;span class="caps"&gt;AS201379&lt;/span&gt; and&amp;nbsp;meerfarbig.net:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Without IXP (via transit):
  hobgp → iFog → DE-CIX fabric → meerfarbig.net
  3+ hops, 20-30ms

With IXP (direct peering):
  hobgp → lobgp → LocIX fabric → meerfarbig.net
  2 hops, ~9ms
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Beyond latency, &lt;span class="caps"&gt;IXP&lt;/span&gt; peering provides route diversity that transit alone cannot match. Route servers at the exchange announce routes from every participant. A single &lt;span class="caps"&gt;BGP&lt;/span&gt; session to the route server gives me access to routes from hundreds of networks - routes I won&amp;#8217;t see through transit because those networks either peer-lock certain prefixes or prefer to exchange&amp;nbsp;locally.&lt;/p&gt;
&lt;h2 id="architecture-the-three-router-triangle"&gt;Architecture: The Three-Router&amp;nbsp;Triangle&lt;/h2&gt;
&lt;p&gt;My third router&amp;nbsp;(&lt;code&gt;lobgp&lt;/code&gt;) connects to LocIX through a hosting provider with direct layer-2 access to the exchange fabric. It peers with LocIX&amp;#8217;s route servers over the peering &lt;span class="caps"&gt;LAN&lt;/span&gt;, announces my /48, and sends everything it learns back&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt; via iBGP over a &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel. The resulting topology looks like&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    ┌──────────────────────────────────────────────────┐
                    │                Default-Free Zone                 │
                    └──┬─────────────┬──────────────┬──────────────────┘
                       │             │              │
                   AS209533      AS209735       AS212895
                   (iFog)        (Lagrange)     (route64)
                       │             │              │
                  GRE tunnel    GRE tunnel     GRE tunnel
                       │             │              │
                  ┌────┴─────────────┴──────────────┴────────────┐
                  │                  hobgp (Core)                │
                  │          FreeBSD + FRR, AS201379             │
                  │          2a06:9801:1c::/48                   │
                  └────────────┬────────────────┬────────────────┘
                               │                │
                          GIF tunnel        GIF tunnel
                          (iBGP)            (iBGP)
                               │                │
  ┌────────────────────────────┴──┐   ┌─────────┴─────────────────────┐
  │         vtbgp (Edge)          │   │         lobgp (Edge)          │
  │  FreeBSD + FRR, AS201379      │   │  FreeBSD + FRR, AS201379      │
  │  Native BGP → AS64515 (Vultr) │   │  LocIX Peering LAN            │
  └───────────────────────────────┘   │  → RS1/RS2 (AS202409)         │
                                      │  → Transit AS34872 (Servperso)│
                                      └───────────────────────────────┘
                                                   │
                                          LocIX Düsseldorf
                                    ┌──────────────┴──────────────┐
                                    │  350+ participants          │
                                    │  Direct L2 peering fabric   │
                                    └─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The design maintains the hub-and-spoke model from Part&amp;nbsp;2: &lt;code&gt;hobgp&lt;/code&gt; remains the forwarding hub. Traffic arriving&amp;nbsp;at &lt;code&gt;lobgp&lt;/code&gt; from the &lt;span class="caps"&gt;IXP&lt;/span&gt; is forwarded through the iBGP tunnel&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt;, which handles routing to downstream&amp;nbsp;servers. &lt;code&gt;lobgp&lt;/code&gt; announces my prefix to the exchange and passes learned routes&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt; for path&amp;nbsp;selection.&lt;/p&gt;
&lt;h3 id="getting-ixp-access"&gt;Getting &lt;span class="caps"&gt;IXP&lt;/span&gt;&amp;nbsp;Access&lt;/h3&gt;
&lt;p&gt;LocIX is a community-run exchange, which makes it accessible to smaller networks. The connection itself requires a &lt;span class="caps"&gt;VM&lt;/span&gt; with a network interface on the LocIX peering &lt;span class="caps"&gt;LAN&lt;/span&gt; - a layer-2 &lt;span class="caps"&gt;VLAN&lt;/span&gt; that reaches the exchange&amp;nbsp;fabric.&lt;/p&gt;
&lt;p&gt;Several hosting providers offer VMs with LocIX connectivity. I use &lt;a href="https://www.servperso.net/"&gt;Servperso&lt;/a&gt;, which provides VMs in Düsseldorf with a dedicated interface connected to the LocIX peering &lt;span class="caps"&gt;VLAN&lt;/span&gt;. The &lt;span class="caps"&gt;VM&lt;/span&gt; gets two network&amp;nbsp;interfaces: &lt;code&gt;vtnet1&lt;/code&gt; for regular internet connectivity (transit from Servperso&amp;#8217;s own &lt;span class="caps"&gt;AS34872&lt;/span&gt;),&amp;nbsp;and &lt;code&gt;vtnet0&lt;/code&gt; directly on the LocIX peering &lt;span class="caps"&gt;LAN&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;After signing up with LocIX and receiving the peering &lt;span class="caps"&gt;LAN&lt;/span&gt; assignment, the setup is straightforward: configuring the peering &lt;span class="caps"&gt;LAN&lt;/span&gt; addresses, peering with the route servers, and announcing my&amp;nbsp;prefix.&lt;/p&gt;
&lt;h2 id="the-ixp-edge-router-lobgp"&gt;The &lt;span class="caps"&gt;IXP&lt;/span&gt; Edge Router:&amp;nbsp;lobgp&lt;/h2&gt;
&lt;h3 id="network-configuration"&gt;Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;/etc/rc.conf&lt;/code&gt; configures two physical interfaces and a &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel back to the&amp;nbsp;core:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lobgp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kld_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;if_gif&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Internet connectivity (Servperso)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.50 netmask 255.255.255.224 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.62&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet1_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:b640::50/112 -rxcsum6 -tso6&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:b640::ffff&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# LocIX Peering LAN (direct L2)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 185.1.155.23/24 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a0c:b641:701::a5:20:1379:1 prefixlen 64 -rxcsum6 -tso6&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF tunnel to core (hobgp) - runs over IPv6&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 tunnel 2001:db8:b640::50 2001:db8:1c19::1 mtu 1460&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:fffd::2 2a06:9801:1c:fffd::1 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Uplink-to-hobgp&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Routing&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myblock&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_myblock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48 -interface gif0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;frr_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Several things are different&amp;nbsp;from &lt;code&gt;vtbgp&lt;/code&gt; in Part&amp;nbsp;2:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two physical interfaces&lt;/strong&gt; instead of&amp;nbsp;one. &lt;code&gt;vtnet1&lt;/code&gt; provides regular internet&amp;nbsp;connectivity; &lt;code&gt;vtnet0&lt;/code&gt; connects directly to the LocIX peering &lt;span class="caps"&gt;LAN&lt;/span&gt;. This separation is important - peering &lt;span class="caps"&gt;LAN&lt;/span&gt; traffic should never traverse the internet-facing&amp;nbsp;interface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel runs over IPv6&lt;/strong&gt;, not IPv4. Both Servperso and Hetzner provide native IPv6, so the encapsulation&amp;nbsp;uses &lt;code&gt;inet6 tunnel&lt;/code&gt; to wrap IPv6-in-IPv6. The tunnel &lt;span class="caps"&gt;MTU&lt;/span&gt; is set to 1460 - the standard 1500-byte Ethernet &lt;span class="caps"&gt;MTU&lt;/span&gt; minus the 40-byte outer IPv6 header added by &lt;span class="caps"&gt;GIF&lt;/span&gt;. To avoid fragmentation, &lt;span class="caps"&gt;PF&lt;/span&gt; clamps &lt;span class="caps"&gt;TCP&lt;/span&gt; &lt;span class="caps"&gt;MSS&lt;/span&gt; to 1400 bytes&amp;nbsp;on &lt;code&gt;$hobgp_tun&lt;/code&gt;, ensuring segments fit cleanly inside the tunnel after the inner IPv6 header (40 bytes) and &lt;span class="caps"&gt;TCP&lt;/span&gt; header (20 bytes) are accounted&amp;nbsp;for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The peering &lt;span class="caps"&gt;LAN&lt;/span&gt; addresses&lt;/strong&gt; are publicly&amp;nbsp;registered. &lt;code&gt;185.1.155.23&lt;/code&gt; (IPv4)&amp;nbsp;and &lt;code&gt;2a0c:b641:701::a5:20:1379:1&lt;/code&gt; (IPv6) are assigned by LocIX and visible in the PeeringDB entry. These aren&amp;#8217;t documentation-range replacements - they&amp;#8217;re the real addresses, because hiding them would be counterproductive for anyone wanting to&amp;nbsp;peer.&lt;/p&gt;
&lt;p&gt;Like &lt;code&gt;vtbgp&lt;/code&gt;, the static&amp;nbsp;route &lt;code&gt;ipv6_route_myblock&lt;/code&gt; points the entire /48 at gif0, so any traffic arriving for my prefix gets forwarded through the tunnel to my core&amp;nbsp;router &lt;code&gt;hobgp&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="frr-configuration"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;The &lt;span class="caps"&gt;FRR&lt;/span&gt; config has four &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions: two to LocIX route servers, one to Servperso for transit, and one iBGP back&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;frr version 10.5.1
frr defaults traditional
hostname lobgp
log syslog informational
service integrated-vtysh-config
!
ipv6 prefix-list PL-MY-NET seq 5 permit 2a06:9801:1c::/48
!
! [PL-BOGONS: denies reserved/invalid prefixes, permits ::/0 le 48 - see Part 1]
!
route-map RM-IXP-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
 set local-preference 200
exit
!
route-map RM-IXP-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-SERVPERSO-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
 set local-preference 100
exit
!
route-map RM-SERVPERSO-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-IBGP-OUT permit 10
exit
!
router bgp 201379
 bgp router-id 185.1.155.23
 no bgp enforce-first-as
 no bgp default ipv4-unicast
 neighbor 2a06:9801:1c:fffd::1 remote-as 201379
 neighbor 2a06:9801:1c:fffd::1 description Core-hobgp
 neighbor 2a06:9801:1c:fffd::1 update-source 2a06:9801:1c:fffd::2
 neighbor 2a0c:b641:701:0:a5:20:2409:1 remote-as 202409
 neighbor 2a0c:b641:701:0:a5:20:2409:1 description LocIX-RS1-v6
 neighbor 2a0c:b641:701:0:a5:20:2409:2 remote-as 202409
 neighbor 2a0c:b641:701:0:a5:20:2409:2 description LocIX-RS2-v6
 neighbor 2001:db8:b640::ffff remote-as 34872
 neighbor 2001:db8:b640::ffff description Transit-Servperso-v6
 !
 address-family ipv6 unicast
  neighbor 2a06:9801:1c:fffd::1 activate
  neighbor 2a06:9801:1c:fffd::1 next-hop-self
  neighbor 2a06:9801:1c:fffd::1 route-map RM-IBGP-OUT out
  neighbor 2a0c:b641:701:0:a5:20:2409:1 activate
  neighbor 2a0c:b641:701:0:a5:20:2409:1 soft-reconfiguration inbound
  neighbor 2a0c:b641:701:0:a5:20:2409:1 route-map RM-IXP-IN in
  neighbor 2a0c:b641:701:0:a5:20:2409:1 route-map RM-IXP-OUT out
  neighbor 2a0c:b641:701:0:a5:20:2409:2 activate
  neighbor 2a0c:b641:701:0:a5:20:2409:2 soft-reconfiguration inbound
  neighbor 2a0c:b641:701:0:a5:20:2409:2 route-map RM-IXP-IN in
  neighbor 2a0c:b641:701:0:a5:20:2409:2 route-map RM-IXP-OUT out
  neighbor 2001:db8:b640::ffff activate
  neighbor 2001:db8:b640::ffff soft-reconfiguration inbound
  neighbor 2001:db8:b640::ffff route-map RM-SERVPERSO-IN in
  neighbor 2001:db8:b640::ffff route-map RM-SERVPERSO-OUT out
 exit-address-family
exit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="route-servers-one-session-hundreds-of-routes"&gt;Route Servers: One Session, Hundreds of&amp;nbsp;Routes&lt;/h4&gt;
&lt;p&gt;&lt;span class="caps"&gt;IXP&lt;/span&gt; route servers are the key simplification. Instead of establishing individual &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions with every network at the exchange (which could mean hundreds of sessions), you peer with the exchange&amp;#8217;s route servers. LocIX runs two route servers (&lt;span class="caps"&gt;AS202409&lt;/span&gt;) for redundancy. Each route server collects routes from all participants and re-announces them to everyone. You can think of the route server as a fan-out mechanism: one &lt;span class="caps"&gt;BGP&lt;/span&gt; session in, hundreds of routes&amp;nbsp;out.&lt;/p&gt;
&lt;p&gt;The two route server sessions deliver approximately 1,800 unique prefixes each - routes from the ~350 networks present at LocIX. These are routes you typically wouldn&amp;#8217;t see through transit, or would see with a longer &lt;span class="caps"&gt;AS&lt;/span&gt;-path. At the exchange, they&amp;#8217;re direct: one hop across the peering&amp;nbsp;fabric.&lt;/p&gt;
&lt;h4 id="no-bgp-enforce-first-as-route-server-compatibility"&gt;&lt;code&gt;no bgp enforce-first-as&lt;/code&gt;: Route Server&amp;nbsp;Compatibility&lt;/h4&gt;
&lt;p&gt;Route servers are transparent &lt;span class="caps"&gt;BGP&lt;/span&gt; speakers. They don&amp;#8217;t insert their own &lt;span class="caps"&gt;AS&lt;/span&gt; into the &lt;span class="caps"&gt;AS&lt;/span&gt;-path of re-announced routes. When &lt;span class="caps"&gt;RS1&lt;/span&gt; (&lt;span class="caps"&gt;AS202409&lt;/span&gt;) sends you a route originally announced by, say, &lt;span class="caps"&gt;AS13335&lt;/span&gt; (Cloudflare), the &lt;span class="caps"&gt;AS&lt;/span&gt;-path still starts&amp;nbsp;with &lt;code&gt;13335&lt;/code&gt; -&amp;nbsp;not &lt;code&gt;202409 13335&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Standard &lt;span class="caps"&gt;BGP&lt;/span&gt; behavior rejects routes where the first &lt;span class="caps"&gt;AS&lt;/span&gt; in the path doesn&amp;#8217;t match the neighbor&amp;#8217;s configured remote-&lt;span class="caps"&gt;AS&lt;/span&gt;. Since the route server&amp;#8217;s &lt;span class="caps"&gt;AS&lt;/span&gt; (202409) isn&amp;#8217;t in the path, &lt;span class="caps"&gt;FRR&lt;/span&gt; would reject every route the server&amp;nbsp;sends. &lt;code&gt;no bgp enforce-first-as&lt;/code&gt; disables this check, which is required for route server&amp;nbsp;peering.&lt;/p&gt;
&lt;p&gt;This is safe in this context: route servers at established IXPs perform their own &lt;span class="caps"&gt;RPKI&lt;/span&gt; and &lt;span class="caps"&gt;IRR&lt;/span&gt; validation. In practice, you&amp;#8217;re trusting their filtering - just as you trust a transit provider&amp;#8217;s ingress filtering - only applied at a different point in the&amp;nbsp;path.&lt;/p&gt;
&lt;h4 id="local-preference-ixp-routes-win"&gt;Local Preference: &lt;span class="caps"&gt;IXP&lt;/span&gt; Routes&amp;nbsp;Win&lt;/h4&gt;
&lt;p&gt;The&amp;nbsp;route-map &lt;code&gt;RM-IXP-IN&lt;/code&gt; sets local-preference 200 on routes learned from the route servers.&amp;nbsp;On &lt;code&gt;hobgp&lt;/code&gt;, the iBGP-learned routes&amp;nbsp;from &lt;code&gt;lobgp&lt;/code&gt; carry this &lt;span class="caps"&gt;LP&lt;/span&gt; value. The preference hierarchy across the entire network&amp;nbsp;becomes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Local Preference&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LocIX route servers (via lobgp)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;IXP&lt;/span&gt; direct peering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vultr (via vtbgp)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Vultr native net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iFog BGPTunnel&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;GRE&lt;/span&gt; transit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lagrange / route64 / Servperso&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;GRE&lt;/span&gt; transit (backup)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;LocIX and Vultr share &lt;span class="caps"&gt;LP&lt;/span&gt; 200 intentionally. When a destination is reachable via both (&lt;span class="caps"&gt;LP&lt;/span&gt; tied), &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;#8217;s tiebreakers apply. The most important one here is &lt;span class="caps"&gt;AS&lt;/span&gt;-path length: a route learned directly from a peer at LocIX carries that peer&amp;#8217;s &lt;span class="caps"&gt;AS&lt;/span&gt;-path as-is, while the same route via Vultr traverses Vultr&amp;#8217;s upstream chain, adding &lt;span class="caps"&gt;AS&lt;/span&gt; hops. Rather than artificially preferring one over the other, the tie lets &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;#8217;s path-length comparison pick the objectively shorter&amp;nbsp;route.&lt;/p&gt;
&lt;p&gt;The Servperso transit&amp;nbsp;(&lt;code&gt;RM-SERVPERSO-IN&lt;/code&gt;, &lt;span class="caps"&gt;LP&lt;/span&gt; 100) serves as a fallback. If the tunnel to hobgp fails or hobgp is down, lobgp still has full internet connectivity through Servperso and can continue forwarding traffic for our&amp;nbsp;/48.&lt;/p&gt;
&lt;h4 id="next-hop-self-on-the-ibgp-session"&gt;&lt;code&gt;next-hop-self&lt;/code&gt; on the iBGP&amp;nbsp;Session&lt;/h4&gt;
&lt;p&gt;Same principle&amp;nbsp;as &lt;code&gt;vtbgp&lt;/code&gt; in Part 2. Routes learned from the LocIX route servers have next-hops on the peering &lt;span class="caps"&gt;LAN&lt;/span&gt;&amp;nbsp;(e.g., &lt;code&gt;2a0c:b641:701::a5:20:13335:1&lt;/code&gt; for&amp;nbsp;Cloudflare). &lt;code&gt;hobgp&lt;/code&gt; can&amp;#8217;t reach those addresses - they&amp;#8217;re on a &lt;span class="caps"&gt;VLAN&lt;/span&gt; in Düsseldorf that hobgp has no path&amp;nbsp;to. &lt;code&gt;next-hop-self&lt;/code&gt; rewrites all next-hops&amp;nbsp;to &lt;code&gt;2a06:9801:1c:fffd::2&lt;/code&gt; (lobgp&amp;#8217;s tunnel address), making the routes actionable for hobgp: &amp;#8220;send traffic down the iBGP tunnel to lobgp, which will forward it across the peering &lt;span class="caps"&gt;LAN&lt;/span&gt;.&amp;#8221;&lt;/p&gt;
&lt;h3 id="pf-protecting-the-ixp-edge"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;: Protecting the &lt;span class="caps"&gt;IXP&lt;/span&gt;&amp;nbsp;Edge&lt;/h3&gt;
&lt;p&gt;The peering &lt;span class="caps"&gt;LAN&lt;/span&gt; is a shared medium. Dozens of networks on the same layer-2 segment means your edge router&amp;#8217;s interfaces are exposed to &lt;span class="caps"&gt;ARP&lt;/span&gt;/&lt;span class="caps"&gt;NDP&lt;/span&gt; from every participant. The firewall must protect both the router itself and the peering&amp;nbsp;infrastructure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;hobgp_tun&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;my_router_ip&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c:fffd::2&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;gw_hobgp_v6&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1c19::1&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;rs1_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a0c:b641:701::a5:20:2409:1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;rs2_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a0c:b641:701::a5:20:2409:2&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;peer_servperso&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:b640::ffff&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;rs1_v6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;rs2_v6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;peer_servperso&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;hobgp_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;

&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bogons&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;
&lt;span class="n"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Control Plane ---&lt;/span&gt;

&lt;span class="c1"&gt;# ICMPv6 on external and IXP interfaces&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Outer tunnel: IPv6-in-IPv6 encapsulation, stateless&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;gw_hobgp_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;gw_hobgp_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# iBGP session over tunnel&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;hobgp_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;a06&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9801&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;fffd&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;hobgp_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;a06&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9801&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;fffd&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# eBGP sessions on peering LAN (route servers + Servperso transit)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# ARP/ICMP on peering LAN for neighbor discovery&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ixp_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# --- Data Plane ---&lt;/span&gt;

&lt;span class="c1"&gt;# Router&amp;#39;s own outbound&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_router_ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Transit: stateless for asymmetric routing&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Default outbound fallback&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Compared&amp;nbsp;to &lt;code&gt;vtbgp&lt;/code&gt;, the &lt;span class="caps"&gt;IXP&lt;/span&gt; edge firewall adds a few important&amp;nbsp;constraints:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; is locked to the route server addresses.&lt;/strong&gt; On the peering &lt;span class="caps"&gt;LAN&lt;/span&gt;, only the LocIX route servers and the Servperso transit peer are allowed to establish &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions. Random participants can&amp;#8217;t initiate unsolicited &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions to our&amp;nbsp;router.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ICMPv6 is permitted on the peering &lt;span class="caps"&gt;LAN&lt;/span&gt;.&lt;/strong&gt; &lt;span class="caps"&gt;NDP&lt;/span&gt; (Neighbor Discovery Protocol) must work for the router to resolve &lt;span class="caps"&gt;MAC&lt;/span&gt; addresses of peers on the shared segment. Without this, the peering &lt;span class="caps"&gt;LAN&lt;/span&gt; interface is&amp;nbsp;dead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IPv4 &lt;span class="caps"&gt;ICMP&lt;/span&gt; is explicitly allowed on the peering &lt;span class="caps"&gt;LAN&lt;/span&gt;.&lt;/strong&gt; Even though this is primarily an IPv6 setup, the peering &lt;span class="caps"&gt;LAN&lt;/span&gt; carries IPv4 &lt;span class="caps"&gt;ARP&lt;/span&gt; and &lt;span class="caps"&gt;ICMP&lt;/span&gt; for dual-stack participants. Allowing &lt;span class="caps"&gt;ICMP&lt;/span&gt; keeps diagnostics&amp;nbsp;working.&lt;/p&gt;
&lt;h2 id="core-router-changes-adding-the-second-ibgp-peer"&gt;Core Router Changes: Adding the Second iBGP&amp;nbsp;Peer&lt;/h2&gt;
&lt;p&gt;On &lt;code&gt;hobgp&lt;/code&gt;,&amp;nbsp;adding &lt;code&gt;lobgp&lt;/code&gt; means a new &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel and a new iBGP session. The additions&amp;nbsp;to &lt;code&gt;rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# GIF tunnel to lobgp (LocIX via Servperso) - IPv6-in-IPv6&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif5&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 tunnel 2001:db8:1c19::1 2001:db8:b640::50 mtu 1460&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif5_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:fffd::1 2a06:9801:1c:fffd::2 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif5_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Tunnel-to-lobgp&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the &lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;additions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;neighbor 2a06:9801:1c:fffd::2 remote-as 201379
neighbor 2a06:9801:1c:fffd::2 description Edge-Servperso-IXP
neighbor 2a06:9801:1c:fffd::2 update-source 2a06:9801:1c:fffd::1
!
address-family ipv6 unicast
 neighbor 2a06:9801:1c:fffd::2 activate
 neighbor 2a06:9801:1c:fffd::2 soft-reconfiguration inbound
 neighbor 2a06:9801:1c:fffd::2 route-map RM-IBGP-IN in
 neighbor 2a06:9801:1c:fffd::2 route-map RM-IBGP-OUT out
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;RM-IBGP-IN&lt;/code&gt; and &lt;code&gt;RM-IBGP-OUT&lt;/code&gt; route-maps are shared with&amp;nbsp;the &lt;code&gt;vtbgp&lt;/code&gt; iBGP session - no new policy needed. The core router&amp;#8217;s firewall also needs the tunnel endpoint added to its permitted peers, and the gif5 tunnel added to &lt;span class="caps"&gt;MSS&lt;/span&gt; clamping&amp;nbsp;rules.&lt;/p&gt;
&lt;p&gt;With&amp;nbsp;this, &lt;code&gt;hobgp&lt;/code&gt; now has five &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions: three eBGP upstreams (iFog, Lagrange, route64) and two iBGP peers (vtbgp for Vultr, lobgp for&amp;nbsp;LocIX).&lt;/p&gt;
&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;h3 id="bgp-sessions"&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;Sessions&lt;/h3&gt;
&lt;p&gt;First, confirm that all &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions are established.&amp;nbsp;On &lt;code&gt;lobgp&lt;/code&gt;, &lt;code&gt;vtysh -c 'show bgp ipv6 summary'&lt;/code&gt; shows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Neighbor                     V    AS   MsgRcvd   MsgSent  Up/Down   PfxRcd  Desc
2a06:9801:1c:fffd::1         4  201379    4034   1792956  2d19h10m       1  Core-hobgp
2001:db8:b640::ffff          4   34872 2512958      4034  2d19h10m  238115  Transit-Servperso
2a0c:b641:701:0:a5:20:2409:1 4  202409  123879      4035  2d19h10m    1859  LocIX-RS1-v6
2a0c:b641:701:0:a5:20:2409:2 4  202409  123882      4035  2d19h10m    1859  LocIX-RS2-v6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The route servers each contribute ~1,859 prefixes - the routes announced by other LocIX participants. Servperso provides a full &lt;span class="caps"&gt;DFZ&lt;/span&gt; view (~238K prefixes) as transit backup. The iBGP session to hobgp sends exactly 1 prefix outbound: our&amp;nbsp;/48.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;hobgp&lt;/code&gt;, the five-peer summary shows the combined&amp;nbsp;view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Neighbor             V    AS   MsgRcvd   MsgSent  Up/Down   PfxRcd  Desc
2001:db8:300::1      4  209533 5096936     33805  10:02:36  239576  Upstream-iFog-FRA
fd00:ca::1           4  209735 4777022      5636  3d21h52m  237342  Upstream-Lagrange-UK
2001:db8:400::1      4  212895 1907099      5636  3d21h52m  243233  Upstream-Route64-FRA
2a06:9801:1c:fffd::2 4  201379 2762876      5622  2d19h09m  238120  Edge-Servperso-IXP
2a06:9801:1c:fffe::2 4  201379 2396813      5640  3d21h48m  233048  Edge-Vultr-Frankfurt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The vtbgp peer&amp;nbsp;(&lt;code&gt;2a06:9801:1c:fffe::2&lt;/code&gt;) is the Vultr edge router from &lt;a href="https://blog.hofstede.it/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/"&gt;Part 2&lt;/a&gt;. The new lobgp iBGP session&amp;nbsp;(&lt;code&gt;2a06:9801:1c:fffd::2&lt;/code&gt;) delivers ~238K prefixes to hobgp - the combination of LocIX routes and Servperso&amp;#8217;s full table, merged and forwarded via iBGP. The &lt;span class="caps"&gt;IXP&lt;/span&gt; routes carry &lt;span class="caps"&gt;LP&lt;/span&gt; 200 and win path selection for destinations that are reachable via the&amp;nbsp;exchange.&lt;/p&gt;
&lt;h3 id="the-ixp-difference-real-traceroutes"&gt;The &lt;span class="caps"&gt;IXP&lt;/span&gt; Difference: Real&amp;nbsp;Traceroutes&lt;/h3&gt;
&lt;p&gt;The real test is measuring what the exchange connection actually changes. Here&amp;#8217;s a traceroute to &lt;a href="https://meerfarbig.net"&gt;meerfarbig&lt;/a&gt;, a German &lt;span class="caps"&gt;ISP&lt;/span&gt; that peers with my &lt;span class="caps"&gt;AS201379&lt;/span&gt; at&amp;nbsp;LocIX:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;HOST: hobgp                             Loss%   Snt   Last   Avg  Best  Wrst StDev
  1.|-- lobgp.edge.hofstede.it             0.0%    10    8.3   8.8   8.2  11.4   1.0
  2.|-- ae0.800.mx240.dus1.meerfarbig.net  0.0%    10    9.5  14.4   9.4  29.2   6.2
  3.|-- 2a00:f820:11::45                   0.0%    10    9.4   9.6   9.4  10.0   0.2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three hops&amp;nbsp;total. &lt;code&gt;hobgp&lt;/code&gt; sends&amp;nbsp;to &lt;code&gt;lobgp&lt;/code&gt; through the iBGP tunnel (hop&amp;nbsp;1). &lt;code&gt;lobgp&lt;/code&gt; forwards across the LocIX peering fabric to meerfarbig&amp;#8217;s edge&amp;nbsp;router &lt;code&gt;ae0.800.mx240.dus1&lt;/code&gt; in Düsseldorf (hop 2) - this is where the &lt;span class="caps"&gt;ICMP&lt;/span&gt; &lt;span class="caps"&gt;TTL&lt;/span&gt;-exceeded reply comes from, confirming the packet entered meerfarbig&amp;#8217;s network directly from the exchange. Hop 3&amp;nbsp;(&lt;code&gt;2a00:f820:11::45&lt;/code&gt;) is the final destination, one hop deeper inside meerfarbig&amp;#8217;s infrastructure. Total latency: under 10ms. Without the &lt;span class="caps"&gt;IXP&lt;/span&gt;, this traffic would traverse iFog or route64, adding hops through transit infrastructure and roughly doubling the&amp;nbsp;latency.&lt;/p&gt;
&lt;p&gt;This is direct peering in practice. No transit &lt;span class="caps"&gt;AS&lt;/span&gt; in the path. The packet crosses one switching fabric and enters the peer&amp;#8217;s network at their edge router - from there, it&amp;#8217;s their internal routing to the&amp;nbsp;destination.&lt;/p&gt;
&lt;h2 id="peering-policy"&gt;Peering&amp;nbsp;Policy&lt;/h2&gt;
&lt;p&gt;With &lt;span class="caps"&gt;IXP&lt;/span&gt; access comes a peering policy. My &lt;span class="caps"&gt;AS201379&lt;/span&gt; operates an &lt;strong&gt;open peering policy&lt;/strong&gt; - I&amp;#8217;m happy to peer with anyone at LocIX or any other exchange point, with no traffic requirements or filtering restrictions beyond basic hygiene (valid &lt;span class="caps"&gt;RPKI&lt;/span&gt;, prefix&amp;nbsp;limits).&lt;/p&gt;
&lt;p&gt;The full peering policy, contact details, and current peering information are published at &lt;a href="https://hofstede.it/as201379.html"&gt;hofstede.it/as201379.html&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="whats-next-ipv4"&gt;What&amp;#8217;s Next:&amp;nbsp;IPv4&lt;/h2&gt;
&lt;p&gt;The entire setup so far is IPv6-only. Every tunnel, every &lt;span class="caps"&gt;BGP&lt;/span&gt; session, every announced prefix is IPv6. This was deliberate: IPv6 address space is cheap and easy to obtain, making it realistic for a hobby &lt;span class="caps"&gt;AS&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;The next step is adding IPv4. A sponsoring &lt;span class="caps"&gt;LIR&lt;/span&gt; will provide me a small IPv4 prefix, which opens up the setup to the ~40% of internet traffic that still runs on IPv4. Adding IPv4 means dual-stack on the existing routers, IPv4 tunnel overlays in parallel with the IPv6 ones, and &lt;span class="caps"&gt;PF&lt;/span&gt; rules for both address families. The three-router topology stays the same; it just gains a second address&amp;nbsp;family.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Route servers are the &lt;span class="caps"&gt;IXP&lt;/span&gt;&amp;#8217;s killer feature for small networks.&lt;/strong&gt; Without them, joining an &lt;span class="caps"&gt;IXP&lt;/span&gt; would mean manually negotiating &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions with individual participants - operationally intractable for a hobby network. Two &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions give you access to every participant&amp;#8217;s routes - the exchange handles policy enforcement and&amp;nbsp;validation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;no bgp enforce-first-as&lt;/code&gt; is required for route server peering.&lt;/strong&gt; This is a common gotcha. Route servers are transparent - they don&amp;#8217;t insert their own &lt;span class="caps"&gt;AS&lt;/span&gt; into the path. Without disabling the first-&lt;span class="caps"&gt;AS&lt;/span&gt; check, &lt;span class="caps"&gt;FRR&lt;/span&gt; rejects every route the server&amp;nbsp;sends.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The peering &lt;span class="caps"&gt;LAN&lt;/span&gt; is a hostile environment.&lt;/strong&gt; Dozens of networks on the same L2 segment means your router is directly exposed. Lock &lt;span class="caps"&gt;BGP&lt;/span&gt; to known peers, allow only essential ICMPv6/&lt;span class="caps"&gt;NDP&lt;/span&gt;, and drop everything else. The peering &lt;span class="caps"&gt;LAN&lt;/span&gt; is not a trusted&amp;nbsp;network.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;IXP&lt;/span&gt; routes and transit routes complement each other.&lt;/strong&gt; The ~1,800 prefixes from LocIX&amp;#8217;s route servers are a subset of the ~240K full table. For those 1,800 destinations, the &lt;span class="caps"&gt;IXP&lt;/span&gt; path is dramatically shorter and faster. For everything else, transit providers fill in. Setting &lt;span class="caps"&gt;LP&lt;/span&gt; 200 on &lt;span class="caps"&gt;IXP&lt;/span&gt; routes ensures they always win when&amp;nbsp;available.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IPv6-in-IPv6 tunnels work well for iBGP links.&lt;/strong&gt; When both endpoints have native IPv6, there&amp;#8217;s no reason to wrap IPv6 traffic in IPv4. The &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel between lobgp and hobgp&amp;nbsp;uses &lt;code&gt;inet6 tunnel&lt;/code&gt; and avoids the IPv4 dependency&amp;nbsp;entirely.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Adding an &lt;span class="caps"&gt;IXP&lt;/span&gt; connection completes the final piece of a small but functional internet infrastructure: transit provides global reachability, native Vultr peering provides reach into Vultr-connected networks, and the &lt;span class="caps"&gt;IXP&lt;/span&gt; provides direct paths to every other participant at the exchange. Three routers, three distinct connectivity roles - each solving a problem the others&amp;nbsp;can&amp;#8217;t.&lt;/p&gt;
&lt;p&gt;The three-router topology is surprisingly manageable. Each router has a clear&amp;nbsp;role: &lt;code&gt;hobgp&lt;/code&gt; is the forwarding hub with upstream&amp;nbsp;transit, &lt;code&gt;vtbgp&lt;/code&gt; announces into Vultr&amp;#8217;s network,&amp;nbsp;and &lt;code&gt;lobgp&lt;/code&gt; peers at the exchange. iBGP ties them together, and local-preference ensures traffic takes the best available path&amp;nbsp;automatically.&lt;/p&gt;
&lt;p&gt;The practical impact is measurable in every traceroute. Destinations reachable via LocIX are one hop away instead of three or four. That&amp;#8217;s not a theoretical improvement - it&amp;#8217;s 10ms instead of 25ms, visible in every&amp;nbsp;connection.&lt;/p&gt;
&lt;p&gt;For anyone running a hobby &lt;span class="caps"&gt;AS&lt;/span&gt; and wondering whether &lt;span class="caps"&gt;IXP&lt;/span&gt; connectivity is worth the extra router: it is. The route diversity alone justifies the cost, and watching your traffic take a direct two-hop path to a network that used to be five hops away through transit is exactly the kind of thing that makes this hobby worth&amp;nbsp;pursuing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.locix.online/"&gt;LocIX - Community Internet&amp;nbsp;Exchange&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.peeringdb.com/ix/2084"&gt;PeeringDB - LocIX&amp;nbsp;Düsseldorf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.servperso.net/"&gt;Servperso - Düsseldorf Hosting with &lt;span class="caps"&gt;IXP&lt;/span&gt;&amp;nbsp;Access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hofstede.it/as201379.html"&gt;&lt;span class="caps"&gt;AS201379&lt;/span&gt; Peering&amp;nbsp;Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.frrouting.org/en/latest/bgp.html"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt; Documentation: Route Servers and&amp;nbsp;enforce-first-as&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bgp.tools/"&gt;bgp.tools - &lt;span class="caps"&gt;BGP&lt;/span&gt; Looking&amp;nbsp;Glass&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;Part 1: Running Your Own &lt;span class="caps"&gt;AS&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/"&gt;Part 2: Going&amp;nbsp;Multi-Homed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Three articles, three evolutionary steps, and the same fundamental toolset: FreeBSD, &lt;span class="caps"&gt;FRR&lt;/span&gt;, &lt;span class="caps"&gt;PF&lt;/span&gt;, and tunnels. The internet doesn&amp;#8217;t care that &lt;span class="caps"&gt;AS201379&lt;/span&gt; runs on three small virtual machines. It sees valid routes, clean filters, and responsive peering. From a single router with one upstream to a three-PoP network with direct exchange peering - the architecture scales because the protocols&amp;nbsp;do.&lt;/p&gt;</content><category term="Networking"/><category term="freebsd"/><category term="bgp"/><category term="networking"/><category term="ipv6"/><category term="frr"/><category term="pf"/><category term="ixp"/><category term="locix"/><category term="ibgp"/></entry><entry><title>Why I Prefer CentOS Stream Over Old CentOS</title><link href="https://blog.hofstede.it/why-i-prefer-centos-stream-over-old-centos/" rel="alternate"/><published>2026-03-15T00:00:00+01:00</published><updated>2026-03-15T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-15:/why-i-prefer-centos-stream-over-old-centos/</id><summary type="html">&lt;p&gt;Old CentOS rebuilt &lt;span class="caps"&gt;RHEL&lt;/span&gt; faithfully, but its downstream position meant it could only follow, never contribute back. CentOS Stream changes that. Sitting upstream of &lt;span class="caps"&gt;RHEL&lt;/span&gt; and downstream of Fedora, it combines enterprise-grade stability with a genuine feedback loop into &lt;span class="caps"&gt;RHEL&lt;/span&gt; development. After years of running it in production, I&amp;#8217;m convinced it&amp;#8217;s the better&amp;nbsp;model.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-03-15-centos-stream-better-than-old-centos.png" title="CentOS Stream"&gt;&lt;/p&gt;
&lt;p&gt;When Red Hat announced in December 2020 that CentOS Linux would shift to CentOS Stream, the community response was swift and polarized. Many users who had built infrastructure around CentOS&amp;#8217;s bug-for-bug &lt;span class="caps"&gt;RHEL&lt;/span&gt; compatibility were understandably concerned about what this change meant for their&amp;nbsp;workflows.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Disclosure: I work for Red Hat, but the opinions here are my own, based on running my personal&amp;nbsp;infrastructure.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Several years later, it&amp;#8217;s worth looking at what CentOS Stream actually delivers - and why I think it&amp;#8217;s a stronger model than what came&amp;nbsp;before.&lt;/p&gt;
&lt;h2 id="what-old-centos-actually-was"&gt;What Old CentOS Actually&amp;nbsp;Was&lt;/h2&gt;
&lt;p&gt;CentOS Linux was a community-driven rebuild of Red Hat Enterprise Linux, stripped of branding and trademarks. Every binary, every library version, every kernel patch was matched as closely as possible to &lt;span class="caps"&gt;RHEL&lt;/span&gt;, bug for bug. It was a significant engineering effort, and the project served millions of users&amp;nbsp;well.&lt;/p&gt;
&lt;p&gt;And that&amp;#8217;s exactly where its limitations&amp;nbsp;lived.&lt;/p&gt;
&lt;p&gt;But by design, old CentOS couldn&amp;#8217;t diverge from &lt;span class="caps"&gt;RHEL&lt;/span&gt;. It couldn&amp;#8217;t ship a bug fix before &lt;span class="caps"&gt;RHEL&lt;/span&gt; did, or adopt a newer library version independently, because any divergence would break the compatibility guarantee users depended on. The project waited for each &lt;span class="caps"&gt;RHEL&lt;/span&gt; release, rebuilt it, and shipped - with no mechanism to feed improvements back&amp;nbsp;upstream.&lt;/p&gt;
&lt;p&gt;That model delivered real value. But it also meant CentOS could only ever&amp;nbsp;follow.&lt;/p&gt;
&lt;h2 id="what-centos-stream-actually-is"&gt;What CentOS Stream Actually&amp;nbsp;Is&lt;/h2&gt;
&lt;p&gt;CentOS Stream sits between Fedora and &lt;span class="caps"&gt;RHEL&lt;/span&gt; in the development&amp;nbsp;pipeline:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Fedora   -&amp;gt;     CentOS Stream  -&amp;gt;   RHEL
  (upstream)      (staging)     (stable release)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Fedora is where new packages, features, and technologies land first. It moves fast, breaks things occasionally, and serves as the proving ground for the broader Red Hat ecosystem. &lt;span class="caps"&gt;RHEL&lt;/span&gt; is the polished, commercially supported product - stable, certified, and conservative by&amp;nbsp;design.&lt;/p&gt;
&lt;p&gt;CentOS Stream occupies the middle ground. It&amp;#8217;s the continuous delivery branch where the &lt;em&gt;next&lt;/em&gt; minor release of &lt;span class="caps"&gt;RHEL&lt;/span&gt; is developed. When Red Hat prepares an upcoming release like &lt;span class="caps"&gt;RHEL&lt;/span&gt; 9.9 or &lt;span class="caps"&gt;RHEL&lt;/span&gt; 10.2, the changes flow through the corresponding CentOS Stream branch first. By the time those changes reach &lt;span class="caps"&gt;RHEL&lt;/span&gt;, they&amp;#8217;ve been tested, validated, and refined in&amp;nbsp;Stream.&lt;/p&gt;
&lt;p&gt;This is fundamentally different from old CentOS, which sat &lt;em&gt;downstream&lt;/em&gt; of &lt;span class="caps"&gt;RHEL&lt;/span&gt; - receiving changes only after they&amp;#8217;d been released. Stream flips that relationship&amp;nbsp;entirely.&lt;/p&gt;
&lt;h2 id="why-this-is-better"&gt;Why This Is&amp;nbsp;Better&lt;/h2&gt;
&lt;h3 id="you-see-whats-coming"&gt;You See What&amp;#8217;s&amp;nbsp;Coming&lt;/h3&gt;
&lt;p&gt;With old CentOS, updates arrived as a fait accompli. &lt;span class="caps"&gt;RHEL&lt;/span&gt; released a point release, CentOS rebuilt it, and you got whatever Red Hat decided you should have. No preview, no preparation, no&amp;nbsp;input.&lt;/p&gt;
&lt;p&gt;CentOS Stream lets you look slightly ahead into what&amp;#8217;s coming to &lt;span class="caps"&gt;RHEL&lt;/span&gt;. The packages in Stream today are the packages that will ship in the next &lt;span class="caps"&gt;RHEL&lt;/span&gt; minor release. If you&amp;#8217;re running &lt;span class="caps"&gt;RHEL&lt;/span&gt; in production alongside Stream in development or staging, you get advance visibility into every change heading your way. That&amp;#8217;s not instability - that&amp;#8217;s&amp;nbsp;intelligence.&lt;/p&gt;
&lt;h3 id="the-stability-is-real"&gt;The Stability Is&amp;nbsp;Real&lt;/h3&gt;
&lt;p&gt;This is the part that surprises people most. CentOS Stream is not Fedora. It&amp;#8217;s not a bleeding-edge rolling release. It&amp;#8217;s not&amp;nbsp;experimental.&lt;/p&gt;
&lt;p&gt;CentOS Stream contains well-tested software. Most components entering Stream have already matured through Fedora or internal &lt;span class="caps"&gt;RHEL&lt;/span&gt; development processes. Stream reflects the current development branch for the next &lt;span class="caps"&gt;RHEL&lt;/span&gt; minor release, staged for final&amp;nbsp;validation.&lt;/p&gt;
&lt;p&gt;In my personal experience running CentOS Stream since Stream 8 was released in September 2019, the stability has been remarkably good. For the types of workloads I run, I haven&amp;#8217;t encountered stability issues attributable to Stream&amp;#8217;s position in the pipeline. Stream stages already-proven changes, leaving the comprehensive hardware certification and extended &lt;span class="caps"&gt;QA&lt;/span&gt; sweeps for the official &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;release.&lt;/p&gt;
&lt;p&gt;The &amp;#8220;development branch&amp;#8221; label scares people, but the reality doesn&amp;#8217;t match the fear. Stream isn&amp;#8217;t where experiments happen - that&amp;#8217;s Fedora&amp;#8217;s job. Stream is where already-proven changes are staged for &lt;span class="caps"&gt;RHEL&lt;/span&gt; release. The delta between Stream and the next &lt;span class="caps"&gt;RHEL&lt;/span&gt; point release is intentionally&amp;nbsp;small.&lt;/p&gt;
&lt;h3 id="a-genuine-feedback-loop"&gt;A Genuine Feedback&amp;nbsp;Loop&lt;/h3&gt;
&lt;p&gt;Because Stream feeds &lt;em&gt;into&lt;/em&gt; &lt;span class="caps"&gt;RHEL&lt;/span&gt; rather than copying &lt;em&gt;from&lt;/em&gt; it, community contributions can influence what ships in &lt;span class="caps"&gt;RHEL&lt;/span&gt;. Bug fixes can be proposed and tested in Stream before they reach the enterprise release. The distribution is an active participant in its own development rather than a downstream&amp;nbsp;consumer.&lt;/p&gt;
&lt;p&gt;Stream is still closely aligned with &lt;span class="caps"&gt;RHEL&lt;/span&gt; - the packages are &lt;span class="caps"&gt;RHEL&lt;/span&gt; packages, the kernel is the &lt;span class="caps"&gt;RHEL&lt;/span&gt; kernel, the ecosystem is the &lt;span class="caps"&gt;RHEL&lt;/span&gt; ecosystem - but there&amp;#8217;s room for the community to contribute meaningful improvements that benefit both Stream and &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;users.&lt;/p&gt;
&lt;h3 id="the-support-contract-argument"&gt;The Support Contract&amp;nbsp;Argument&lt;/h3&gt;
&lt;p&gt;The most common objection to CentOS Stream is the lack of a commercial support contract. But let&amp;#8217;s be clear: &lt;strong&gt;old CentOS didn&amp;#8217;t have a support contract either.&lt;/strong&gt; That was always the trade-off. You got &lt;span class="caps"&gt;RHEL&lt;/span&gt;-compatible binaries without paying for a subscription, but you also got them without commercial support, without certified hardware matrices, and without the ability to open a support case when something went&amp;nbsp;sideways.&lt;/p&gt;
&lt;p&gt;CentOS Stream has the same arrangement. If you need a support contract, you need &lt;span class="caps"&gt;RHEL&lt;/span&gt; - and that was true before Stream existed too. Nothing changed&amp;nbsp;here.&lt;/p&gt;
&lt;p&gt;What you &lt;em&gt;do&lt;/em&gt; get with Stream is access to the CentOS community, Red Hat&amp;#8217;s public issue trackers, and the ability to see and test changes before they hit your &lt;span class="caps"&gt;RHEL&lt;/span&gt; production systems. Arguably, that&amp;#8217;s more than old CentOS ever&amp;nbsp;offered.&lt;/p&gt;
&lt;h2 id="stream-as-a-server-platform"&gt;Stream as a Server&amp;nbsp;Platform&lt;/h2&gt;
&lt;p&gt;CentOS Stream is a valid choice for running production servers. Not a compromise, not a stopgap - a legitimate, well-engineered server operating&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;Consider what you&amp;#8217;re actually&amp;nbsp;getting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt;-grade packages&lt;/strong&gt;: The software in Stream is built from the same sources as &lt;span class="caps"&gt;RHEL&lt;/span&gt;, with the same compilation flags, the same security policies, and the same enterprise-focused&amp;nbsp;defaults.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Continuous updates&lt;/strong&gt;: Rather than waiting for the next point release, you receive a steady flow of tested updates. Many fixes and improvements appear earlier than they did on old CentOS, which had to wait for &lt;span class="caps"&gt;RHEL&lt;/span&gt; to release them &lt;em&gt;and then&lt;/em&gt; rebuild. (Embargoed CVEs may reach &lt;span class="caps"&gt;RHEL&lt;/span&gt; subscribers first, but routine fixes flow through Stream without the old rebuild&amp;nbsp;lag.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SELinux, systemd, firewalld&lt;/strong&gt;: The full enterprise Linux stack, configured and tuned the same way &lt;span class="caps"&gt;RHEL&lt;/span&gt; configures&amp;nbsp;it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container and virtualization support&lt;/strong&gt;: Podman, Buildah, and the container ecosystem work exactly as they do on &lt;span class="caps"&gt;RHEL&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~5-year lifecycle&lt;/strong&gt;: CentOS Stream tracks the Full Support phase of its corresponding &lt;span class="caps"&gt;RHEL&lt;/span&gt; release - roughly five years. Stream 8 reached end of life in May 2024, and Stream 9 is expected to follow in 2027. That&amp;#8217;s shorter than &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;#8217;s full 10-year lifecycle, but still excellent for a continuous delivery&amp;nbsp;platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CentOS Stream is not a drop-in replacement for every workload that historically used CentOS Linux. Environments that require strict vendor certification, regulatory validation, or decade-long lifecycle guarantees will still be better served by &lt;span class="caps"&gt;RHEL&lt;/span&gt; itself. But for workloads that don&amp;#8217;t need those specific assurances, Stream provides a compelling platform - and because it tracks ahead of &lt;span class="caps"&gt;RHEL&lt;/span&gt; rather than behind, you&amp;#8217;re often running slightly newer&amp;nbsp;software.&lt;/p&gt;
&lt;h2 id="the-ecosystem-effect"&gt;The Ecosystem&amp;nbsp;Effect&lt;/h2&gt;
&lt;p&gt;CentOS Stream has become genuinely useful for the broader Linux ecosystem. ISVs can test against Stream to ensure compatibility with upcoming &lt;span class="caps"&gt;RHEL&lt;/span&gt; releases. Infrastructure teams can validate automation and deployment pipelines before &lt;span class="caps"&gt;RHEL&lt;/span&gt; updates land. Community members can contribute fixes that benefit both Stream and eventually &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;users.&lt;/p&gt;
&lt;p&gt;This collaborative model means that engineering effort goes toward improving the distribution rather than reproducing binaries. Contributions flow upstream into &lt;span class="caps"&gt;RHEL&lt;/span&gt;, creating a virtuous cycle that benefits the entire&amp;nbsp;ecosystem.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The Bottom&amp;nbsp;Line&lt;/h2&gt;
&lt;p&gt;Downstream &lt;span class="caps"&gt;RHEL&lt;/span&gt; rebuilds still exist for users who want the old model, and they fill the role old CentOS occupied. But CentOS Stream offers something those rebuilds structurally cannot: an upstream role with a genuine feedback loop into &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;development.&lt;/p&gt;
&lt;p&gt;Old CentOS served its users well, but its downstream model limited it to following &lt;span class="caps"&gt;RHEL&lt;/span&gt;. CentOS Stream replaces that with a seat at the table - stable in practice, with the ability to contribute back. After years of running it in production, I&amp;#8217;m convinced it&amp;#8217;s the better&amp;nbsp;model.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.centos.org/centos-stream/"&gt;CentOS Stream&lt;/a&gt; - official project&amp;nbsp;page&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.redhat.com/en/topics/linux/what-is-centos-stream"&gt;Red Hat - CentOS Stream &lt;span class="caps"&gt;FAQ&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.centos.org/centos10/"&gt;CentOS Stream 10 Release&amp;nbsp;Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.fedoraproject.org/en-US/quick-docs/fedora-and-red-hat-enterprise-linux/"&gt;Fedora → CentOS Stream → &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;Pipeline&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="linux"/><category term="centos"/><category term="centos-stream"/><category term="rhel"/><category term="fedora"/><category term="server"/><category term="enterprise"/></entry><entry><title>Linux Firewalls: How to Actually Secure a Cloud Server (iptables, nftables, firewalld, ufw)</title><link href="https://blog.hofstede.it/linux-firewalls-how-to-actually-secure-a-cloud-server-iptables-nftables-firewalld-ufw/" rel="alternate"/><published>2026-03-14T00:00:00+01:00</published><updated>2026-03-14T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-14:/linux-firewalls-how-to-actually-secure-a-cloud-server-iptables-nftables-firewalld-ufw/</id><summary type="html">&lt;p&gt;A practical guide to the four major Linux firewall technologies - iptables, nftables, firewalld, and ufw. Covers real-world cloud server hardening with concrete examples, from locking down &lt;span class="caps"&gt;SSH&lt;/span&gt; to building zone-based configurations. Includes an honest comparison and an entirely unbiased opinion about which firewall is actually&amp;nbsp;best.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-03-14-linux-firewall-guide.png" title="Linux Firewall Technologies Guide"&gt;&lt;/p&gt;
&lt;p&gt;You&amp;#8217;ve just provisioned a fresh cloud server. It has a public IPv4 address, maybe an IPv6 range, and exactly zero protection between it and the internet&amp;#8217;s endless stream of &lt;span class="caps"&gt;SSH&lt;/span&gt; brute-force bots, port scanners, and whatever that traffic on port 5900 is. The clock is ticking - within minutes, your auth log will start filling up with connection attempts from &lt;span class="caps"&gt;IP&lt;/span&gt; addresses you&amp;#8217;ve never heard of in countries you&amp;#8217;ve never&amp;nbsp;visited.&lt;/p&gt;
&lt;p&gt;Linux gives you several ways to build a firewall between your server and this chaos. The problem isn&amp;#8217;t a lack of options - it&amp;#8217;s too many. iptables, nftables, firewalld, ufw - they all filter packets, but they approach the job differently, target different audiences, and impose different mental models. This guide walks through all four with real configurations you can actually deploy on a freshly provisioned cloud&amp;nbsp;server.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(And yes, I know - I&amp;#8217;ve spent most of this blog praising &lt;a href="https://blog.hofstede.it/pf-firewall-on-freebsd-a-practical-guide/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; on FreeBSD&lt;/a&gt; as the pinnacle of firewall design. &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s syntax is cleaner, its stateful inspection is more elegant, and&amp;nbsp;writing &lt;code&gt;pf.conf&lt;/code&gt; genuinely brings me joy. But sometimes you&amp;#8217;re handed a Linux box and told to make it safe. So here we are, slumming it on the other side of the fence. Let&amp;#8217;s make the best of&amp;nbsp;it.)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="the-landscape-how-we-got-here"&gt;The Landscape: How We Got&amp;nbsp;Here&lt;/h2&gt;
&lt;p&gt;Linux packet filtering has a history that mirrors Linux itself - organic, layered, and occasionally&amp;nbsp;contradictory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ipfwadm&lt;/strong&gt; (1994) was the first Linux firewall, basic and limited. &lt;strong&gt;ipchains&lt;/strong&gt; (1999, Linux 2.2) replaced it with chain-based filtering. &lt;strong&gt;iptables&lt;/strong&gt; (2001, Linux 2.4) brought stateful inspection and became the dominant firewall for two decades. &lt;strong&gt;nftables&lt;/strong&gt; (2014, Linux 3.13) was designed as iptables&amp;#8217; successor, with a unified framework and better performance. &lt;strong&gt;firewalld&lt;/strong&gt; and &lt;strong&gt;ufw&lt;/strong&gt; sit on top, providing higher-level&amp;nbsp;abstractions.&lt;/p&gt;
&lt;p&gt;The key thing to understand: all of these ultimately talk to &lt;strong&gt;Netfilter&lt;/strong&gt;, the packet filtering framework in the Linux kernel. iptables and nftables are different interfaces to the same underlying machinery. firewalld and ufw are frontends that generate iptables or nftables rules for you. The packets don&amp;#8217;t care which tool wrote the rules - they get filtered either&amp;nbsp;way.&lt;/p&gt;
&lt;h2 id="iptables-the-classic"&gt;iptables: The&amp;nbsp;Classic&lt;/h2&gt;
&lt;p&gt;iptables has been the standard Linux firewall since 2001. If you&amp;#8217;ve administered a Linux server in the last twenty years, you&amp;#8217;ve encountered it. Every tutorial, every Stack Overflow answer, every &amp;#8220;how to set up a &lt;span class="caps"&gt;VPS&lt;/span&gt;&amp;#8221; blog post assumes&amp;nbsp;iptables.&lt;/p&gt;
&lt;h3 id="the-mental-model"&gt;The Mental&amp;nbsp;Model&lt;/h3&gt;
&lt;p&gt;iptables organizes rules into &lt;strong&gt;tables&lt;/strong&gt; and &lt;strong&gt;chains&lt;/strong&gt;. The default table&amp;nbsp;(&lt;code&gt;filter&lt;/code&gt;) contains three built-in&amp;nbsp;chains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;INPUT&lt;/span&gt;&lt;/strong&gt;: packets destined for this&amp;nbsp;machine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;FORWARD&lt;/span&gt;&lt;/strong&gt;: packets being routed through this&amp;nbsp;machine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;OUTPUT&lt;/span&gt;&lt;/strong&gt;: packets originating from this&amp;nbsp;machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each chain has a &lt;strong&gt;policy&lt;/strong&gt; (default action) and a list of rules evaluated in order. The first matching rule&amp;nbsp;wins.&lt;/p&gt;
&lt;h3 id="securing-a-cloud-server-with-iptables"&gt;Securing a Cloud Server with&amp;nbsp;iptables&lt;/h3&gt;
&lt;p&gt;Here&amp;#8217;s a complete iptables configuration for a cloud server running a web application and &lt;span class="caps"&gt;SSH&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# Cloud server firewall - iptables&lt;/span&gt;
&lt;span class="c1"&gt;# Flush existing rules&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-F
iptables&lt;span class="w"&gt; &lt;/span&gt;-X
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-F
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-X

&lt;span class="c1"&gt;# Default policies: drop everything, allow outbound&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;DROP
iptables&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;FORWARD&lt;span class="w"&gt; &lt;/span&gt;DROP
iptables&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;OUTPUT&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

ip6tables&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;DROP
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;FORWARD&lt;span class="w"&gt; &lt;/span&gt;DROP
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;OUTPUT&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# Allow loopback&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;lo&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;lo&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# Allow established and related connections&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;ESTABLISHED,RELATED&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;ESTABLISHED,RELATED&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# Drop invalid packets&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;INVALID&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;DROP
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;INVALID&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;DROP

&lt;span class="c1"&gt;# SSH: rate-limited to slow down brute-force attempts&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;--dport&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;recent&lt;span class="w"&gt; &lt;/span&gt;--set&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;SSH
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;--dport&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;recent&lt;span class="w"&gt; &lt;/span&gt;--update&lt;span class="w"&gt; &lt;/span&gt;--seconds&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--hitcount&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;SSH&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;DROP
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;--dport&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;--dport&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;recent&lt;span class="w"&gt; &lt;/span&gt;--set&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;SSH
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;--dport&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;recent&lt;span class="w"&gt; &lt;/span&gt;--update&lt;span class="w"&gt; &lt;/span&gt;--seconds&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--hitcount&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;SSH&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;DROP
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;--dport&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# HTTP and HTTPS&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;multiport&lt;span class="w"&gt; &lt;/span&gt;--dports&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;,443&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;multiport&lt;span class="w"&gt; &lt;/span&gt;--dports&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;,443&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;conntrack&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ctstate&lt;span class="w"&gt; &lt;/span&gt;NEW&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# ICMPv4: allow ping and necessary types&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;icmp&lt;span class="w"&gt; &lt;/span&gt;--icmp-type&lt;span class="w"&gt; &lt;/span&gt;echo-request&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;icmp&lt;span class="w"&gt; &lt;/span&gt;--icmp-type&lt;span class="w"&gt; &lt;/span&gt;destination-unreachable&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;icmp&lt;span class="w"&gt; &lt;/span&gt;--icmp-type&lt;span class="w"&gt; &lt;/span&gt;time-exceeded&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# ICMPv6: essential for IPv6 to function&lt;/span&gt;
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;echo-request&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;echo-reply&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;destination-unreachable&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;packet-too-big&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;time-exceeded&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;parameter-problem&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;neighbour-solicitation&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;neighbour-advertisement&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;router-solicitation&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;ipv6-icmp&lt;span class="w"&gt; &lt;/span&gt;--icmpv6-type&lt;span class="w"&gt; &lt;/span&gt;router-advertisement&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;ACCEPT

&lt;span class="c1"&gt;# Log dropped packets (rate-limited to avoid log flooding)&lt;/span&gt;
iptables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;limit&lt;span class="w"&gt; &lt;/span&gt;--limit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;/min&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;LOG&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--log-prefix&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;iptables-dropped: &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--log-level&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;
ip6tables&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;INPUT&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;limit&lt;span class="w"&gt; &lt;/span&gt;--limit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;/min&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;LOG&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--log-prefix&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ip6tables-dropped: &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--log-level&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="making-rules-persistent"&gt;Making Rules&amp;nbsp;Persistent&lt;/h3&gt;
&lt;p&gt;iptables rules live in kernel memory - they vanish on reboot. Making them persistent depends on your&amp;nbsp;distribution:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debian/Ubuntu:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;iptables-persistent
netfilter-persistent&lt;span class="w"&gt; &lt;/span&gt;save
netfilter-persistent&lt;span class="w"&gt; &lt;/span&gt;reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt;/Fedora (if not using&amp;nbsp;firewalld):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;iptables-services
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;iptables&lt;span class="w"&gt; &lt;/span&gt;ip6tables
service&lt;span class="w"&gt; &lt;/span&gt;iptables&lt;span class="w"&gt; &lt;/span&gt;save
service&lt;span class="w"&gt; &lt;/span&gt;ip6tables&lt;span class="w"&gt; &lt;/span&gt;save
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The rules are saved&amp;nbsp;to &lt;code&gt;/etc/iptables/rules.v4&lt;/code&gt; and &lt;code&gt;/etc/iptables/rules.v6&lt;/code&gt; (Debian)&amp;nbsp;or &lt;code&gt;/etc/sysconfig/iptables&lt;/code&gt; and &lt;code&gt;/etc/sysconfig/ip6tables&lt;/code&gt; (&lt;span class="caps"&gt;RHEL&lt;/span&gt;).&lt;/p&gt;
&lt;h3 id="the-good-and-the-ugly"&gt;The Good and the&amp;nbsp;Ugly&lt;/h3&gt;
&lt;p&gt;iptables works. It&amp;#8217;s battle-tested, universally available, and documented to the point of exhaustion. But the syntax is verbose - especially with dual-stack configurations where you need to duplicate everything for IPv4 and IPv6. There&amp;#8217;s no native concept of sets (you&amp;nbsp;need &lt;code&gt;ipset&lt;/code&gt; for that), and large rulesets become linear chains that the kernel evaluates rule by rule, which doesn&amp;#8217;t scale&amp;nbsp;elegantly.&lt;/p&gt;
&lt;p&gt;Technically, iptables is considered deprecated in favor of nftables. Modern kernels include&amp;nbsp;an &lt;code&gt;iptables-nft&lt;/code&gt; compatibility layer that translates iptables commands into nftables rules behind the scenes. On many current distributions, when you&amp;nbsp;run &lt;code&gt;iptables&lt;/code&gt;, you&amp;#8217;re actually&amp;nbsp;running &lt;code&gt;iptables-nft&lt;/code&gt; without knowing it. Check&amp;nbsp;with &lt;code&gt;iptables -V&lt;/code&gt; - if it&amp;nbsp;says &lt;code&gt;nf_tables&lt;/code&gt;, you&amp;#8217;re already on the&amp;nbsp;bridge.&lt;/p&gt;
&lt;h2 id="nftables-the-modern-replacement"&gt;nftables: The Modern&amp;nbsp;Replacement&lt;/h2&gt;
&lt;p&gt;nftables is iptables&amp;#8217; designated successor, included in the Linux kernel since 3.13. It addresses iptables&amp;#8217; design limitations: a single tool handles both IPv4 and IPv6, the rule language is more expressive, and the kernel representation uses a virtual machine that evaluates rulesets more&amp;nbsp;efficiently.&lt;/p&gt;
&lt;h3 id="the-mental-model_1"&gt;The Mental&amp;nbsp;Model&lt;/h3&gt;
&lt;p&gt;nftables replaces the fixed table/chain structure with a flexible one you define yourself. You create your own tables, your own chains, and configure their hook points. This sounds more complex, but it means you can organize rules however makes sense for your&amp;nbsp;infrastructure.&lt;/p&gt;
&lt;h3 id="the-same-cloud-server-in-nftables"&gt;The Same Cloud Server, in&amp;nbsp;nftables&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;sbin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;nft&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;

&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ruleset&lt;/span&gt;

&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;firewall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tracking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SSH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;brute&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssh_meter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv4_addr&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dynamic&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ssh_meter6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6_addr&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dynamic&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Loopback&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;iif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Established&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;related&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;ct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;established&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;related&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;invalid&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;ct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;invalid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SSH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limiting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;statement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dynamically&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;adds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;single&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;ssh_meter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;saddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;over&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;ssh_meter6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ip6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;saddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;over&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;HTTPS&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMPv6&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;unreachable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;exceeded&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;ip6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nexthdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmpv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;unreachable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;too&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;big&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;exceeded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;parameter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;nd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;neighbor&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;solicit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;nd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;neighbor&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;advert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;nd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;solicit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;nd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;advert&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dropped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;packets&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;nft-dropped: &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;warn&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;forward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;forward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice&amp;nbsp;the &lt;code&gt;inet&lt;/code&gt; family - a single table handles both IPv4 and IPv6. No more duplicating every rule. The set syntax for rate limiting is built into nftables natively, no&amp;nbsp;external &lt;code&gt;ipset&lt;/code&gt; needed.&lt;/p&gt;
&lt;h3 id="loading-and-persisting"&gt;Loading and&amp;nbsp;Persisting&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Validate syntax&lt;/span&gt;
nft&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/nftables.conf

&lt;span class="c1"&gt;# Load rules&lt;/span&gt;
nft&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/nftables.conf

&lt;span class="c1"&gt;# Enable at boot&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nftables
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The entire ruleset lives&amp;nbsp;in &lt;code&gt;/etc/nftables.conf&lt;/code&gt;. One file, one syntax, both address families. Compared to iptables&amp;#8217; separate save files for v4 and v6, this is a breath of fresh&amp;nbsp;air.&lt;/p&gt;
&lt;h3 id="named-sets-ip-allowlists-and-blocklists"&gt;Named Sets: &lt;span class="caps"&gt;IP&lt;/span&gt; Allowlists and&amp;nbsp;Blocklists&lt;/h3&gt;
&lt;p&gt;One of nftables&amp;#8217; strongest features is native support for sets - named collections of addresses, ports, or interfaces that can be referenced in rules and updated at&amp;nbsp;runtime:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;firewall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Trusted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;management&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPs&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;trusted_admins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv4_addr&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;198.51.100.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;203.0.113.50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;trusted_admins6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6_addr&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;ffff&lt;/span&gt;&lt;span class="o"&gt;::/&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Blocklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;populated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dynamically&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;blocklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv4_addr&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;actors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;immediately&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;saddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;blocklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SSH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;trusted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;saddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;trusted_admins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ip6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;saddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;trusted_admins6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Manage sets at runtime without reloading the&amp;nbsp;ruleset:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add to blocklist (auto-expires after 24h due to timeout flag)&lt;/span&gt;
nft&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;element&lt;span class="w"&gt; &lt;/span&gt;inet&lt;span class="w"&gt; &lt;/span&gt;firewall&lt;span class="w"&gt; &lt;/span&gt;blocklist&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.100&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Add with custom timeout&lt;/span&gt;
nft&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;element&lt;span class="w"&gt; &lt;/span&gt;inet&lt;span class="w"&gt; &lt;/span&gt;firewall&lt;span class="w"&gt; &lt;/span&gt;blocklist&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.101&lt;span class="w"&gt; &lt;/span&gt;timeout&lt;span class="w"&gt; &lt;/span&gt;1h&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Remove from set&lt;/span&gt;
nft&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;element&lt;span class="w"&gt; &lt;/span&gt;inet&lt;span class="w"&gt; &lt;/span&gt;firewall&lt;span class="w"&gt; &lt;/span&gt;blocklist&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.100&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# List set contents&lt;/span&gt;
nft&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;inet&lt;span class="w"&gt; &lt;/span&gt;firewall&lt;span class="w"&gt; &lt;/span&gt;blocklist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="why-nftables-over-iptables"&gt;Why nftables Over&amp;nbsp;iptables&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unified IPv4/IPv6&lt;/strong&gt;: One ruleset, one syntax, one mental&amp;nbsp;model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native sets&lt;/strong&gt;: No&amp;nbsp;external &lt;code&gt;ipset&lt;/code&gt; dependency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better performance&lt;/strong&gt;: Binary rules compiled into a virtual machine, not linear chain&amp;nbsp;matching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atomic rule replacement&lt;/strong&gt;: Load an entire new ruleset atomically - no window where rules are partially&amp;nbsp;loaded&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Programmable Netlink interface&lt;/strong&gt;: nftables exposes a Netlink socket&amp;nbsp;(&lt;code&gt;NETLINK_NETFILTER&lt;/code&gt;) that lets applications manage rules directly without shelling out&amp;nbsp;to &lt;code&gt;nft&lt;/code&gt;. Libraries like Google&amp;#8217;s &lt;a href="https://github.com/google/nftables"&gt;google/nftables&lt;/a&gt; for Go make this practical - you can build fully programmable firewall tooling, including things like injecting &lt;span class="caps"&gt;BPF&lt;/span&gt; into nftables rules. ngrok&amp;#8217;s team used this approach to build &lt;a href="https://github.com/ngrok/firewall_toolkit"&gt;firewall_toolkit&lt;/a&gt;, a host-level DDoS mitigation&amp;nbsp;library&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner syntax&lt;/strong&gt;: Subjective, but ask anyone who&amp;#8217;s written a 200-line iptables&amp;nbsp;script&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The learning curve is real if you&amp;#8217;re coming from iptables, but I&amp;#8217;ve found the transition is worth the effort. Once the nftables mental model clicks, going back to iptables feels like writing assembly after learning a proper&amp;nbsp;language.&lt;/p&gt;
&lt;h2 id="firewalld-the-enterprise-linux-firewall"&gt;firewalld: The Enterprise Linux&amp;nbsp;Firewall&lt;/h2&gt;
&lt;p&gt;firewalld is the default firewall manager on Fedora, &lt;span class="caps"&gt;RHEL&lt;/span&gt;, CentOS Stream, and their derivatives - which means it&amp;#8217;s also the firewall running on the majority of production Linux servers in enterprise environments. Rather than asking you to write packet filter rules, firewalld introduces &lt;strong&gt;zones&lt;/strong&gt; - named security contexts that you assign to network&amp;nbsp;interfaces.&lt;/p&gt;
&lt;p&gt;What sets firewalld apart from the other tools in this article is its architecture. It runs as a &lt;strong&gt;system daemon&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;firewalld.service&lt;/code&gt;) with a &lt;strong&gt;D-Bus &lt;span class="caps"&gt;API&lt;/span&gt;&lt;/strong&gt;, which means any authorized application - Cockpit, NetworkManager, libvirt, Ansible, or your own tooling - can query and modify firewall rules programmatically without parsing text files or shelling out to command-line tools. It also cleanly separates &lt;strong&gt;runtime&lt;/strong&gt; state (what&amp;#8217;s active right now) from &lt;strong&gt;permanent&lt;/strong&gt; configuration (what survives a reboot), giving you a built-in safety net for testing changes on remote&amp;nbsp;systems.&lt;/p&gt;
&lt;h3 id="the-mental-model_2"&gt;The Mental&amp;nbsp;Model&lt;/h3&gt;
&lt;p&gt;Zones represent trust levels. An interface in&amp;nbsp;the &lt;code&gt;public&lt;/code&gt; zone gets restrictive rules. An interface in&amp;nbsp;the &lt;code&gt;trusted&lt;/code&gt; zone allows everything. Instead of thinking about chains and rules, you think about &amp;#8220;this interface is in this zone, and this zone allows these&amp;nbsp;services.&amp;#8221;&lt;/p&gt;
&lt;p&gt;The predefined zones, from most restrictive to most&amp;nbsp;permissive:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Default Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;drop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drop all incoming, no reply sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;block&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reject all incoming with &lt;span class="caps"&gt;ICMP&lt;/span&gt; response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;public&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only selected services allowed (default zone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;external&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Like public, with masquerading enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dmz&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Selected services, limited incoming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;work&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trust most networked machines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;home&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trust most networked machines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;internal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trust most networked machines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trusted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accept everything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="cloud-server-with-firewalld"&gt;Cloud Server with&amp;nbsp;firewalld&lt;/h3&gt;
&lt;p&gt;On &lt;span class="caps"&gt;RHEL&lt;/span&gt;/Fedora, firewalld is likely already running. Here&amp;#8217;s how to configure it for our cloud server&amp;nbsp;scenario:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check current state&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--state
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--get-active-zones

&lt;span class="c1"&gt;# Set the default zone&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--set-default-zone&lt;span class="o"&gt;=&lt;/span&gt;public

&lt;span class="c1"&gt;# Allow SSH (usually already enabled in public zone)&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Allow HTTP and HTTPS&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;http&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;https&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Reload to apply permanent changes&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--reload

&lt;span class="c1"&gt;# Verify&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--list-all
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Instead of&amp;nbsp;adding &lt;code&gt;--permanent&lt;/code&gt; to every command and then reloading, you can test rules in &lt;em&gt;runtime&lt;/em&gt; first. Runtime rules are active immediately but disappear on reboot&amp;nbsp;(or &lt;code&gt;--reload&lt;/code&gt;). If you accidentally lock yourself out, a reboot reverts the damage. Once you&amp;#8217;ve confirmed everything works, save the running state in one&amp;nbsp;shot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Test in runtime first (no --permanent flag)&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;http
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;https

&lt;span class="c1"&gt;# Everything working? Save the current runtime state as permanent&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--runtime-to-permanent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This workflow is especially useful on remote servers where a typo in a permanent rule could mean a trip to the out-of-band&amp;nbsp;console.&lt;/p&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client http https ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  rich-rules:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="service-definitions"&gt;Service&amp;nbsp;Definitions&lt;/h3&gt;
&lt;p&gt;firewalld uses &lt;span class="caps"&gt;XML&lt;/span&gt; service definitions&amp;nbsp;in &lt;code&gt;/usr/lib/firewalld/services/&lt;/code&gt;. Each service defines which ports and protocols it needs. When you add a &amp;#8220;service&amp;#8221; rather than a raw port, you&amp;#8217;re working at a higher abstraction&amp;nbsp;level:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# These are equivalent:&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;https&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;/tcp&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# But the service approach is self-documenting and multi-port aware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can create custom services for your own&amp;nbsp;applications:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a custom service definition&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--new-service&lt;span class="o"&gt;=&lt;/span&gt;myapp
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--service&lt;span class="o"&gt;=&lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;--set-description&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;My Application&amp;quot;&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--service&lt;span class="o"&gt;=&lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;--add-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;/tcp
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--service&lt;span class="o"&gt;=&lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;--add-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8443&lt;/span&gt;/tcp

&lt;span class="c1"&gt;# Use it&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;myapp&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="rich-rules-when-zones-arent-enough"&gt;Rich Rules: When Zones Aren&amp;#8217;t&amp;nbsp;Enough&lt;/h3&gt;
&lt;p&gt;For more granular control, firewalld supports &amp;#8220;rich rules&amp;#8221; - a more expressive syntax that sits between simple zone/service management and raw nftables&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow SSH only from a trusted subnet&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-rich-rule&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;    rule family=&amp;quot;ipv4&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    source address=&amp;quot;198.51.100.0/24&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    service name=&amp;quot;ssh&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    accept&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Rate-limit SSH connections&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-rich-rule&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;    rule service name=&amp;quot;ssh&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    accept&lt;/span&gt;
&lt;span class="s1"&gt;    limit value=&amp;quot;4/m&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Block a specific IP&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-rich-rule&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;    rule family=&amp;quot;ipv4&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    source address=&amp;quot;192.0.2.100&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    drop&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Log dropped packets from a specific source&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-rich-rule&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;    rule family=&amp;quot;ipv4&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    source address=&amp;quot;192.0.2.0/24&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    log prefix=&amp;quot;suspect-traffic: &amp;quot; level=&amp;quot;warning&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    drop&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--permanent

firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="multi-zone-network-segmentation"&gt;Multi-Zone Network&amp;nbsp;Segmentation&lt;/h3&gt;
&lt;p&gt;One of firewalld&amp;#8217;s most powerful enterprise features is the ability to assign different zones to different interfaces, creating proper network segmentation without touching nftables&amp;nbsp;directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Public-facing interface: restrictive&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--change-interface&lt;span class="o"&gt;=&lt;/span&gt;eth0&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;http&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;https&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Internal management network: more permissive&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;internal&lt;span class="w"&gt; &lt;/span&gt;--change-interface&lt;span class="o"&gt;=&lt;/span&gt;eth1&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;internal&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;internal&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;cockpit&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;internal&lt;span class="w"&gt; &lt;/span&gt;--add-service&lt;span class="o"&gt;=&lt;/span&gt;dns&lt;span class="w"&gt; &lt;/span&gt;--permanent

&lt;span class="c1"&gt;# Database network: only database traffic&lt;/span&gt;
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;dmz&lt;span class="w"&gt; &lt;/span&gt;--change-interface&lt;span class="o"&gt;=&lt;/span&gt;eth2&lt;span class="w"&gt; &lt;/span&gt;--permanent
firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;dmz&lt;span class="w"&gt; &lt;/span&gt;--add-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5432&lt;/span&gt;/tcp&lt;span class="w"&gt; &lt;/span&gt;--permanent

firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This maps naturally to how enterprise networks are actually designed: distinct interfaces for distinct trust levels, each with its own policy. A single nftables ruleset can do the same thing, but you&amp;#8217;d be hand-coding the interface dispatch logic that firewalld provides out of the&amp;nbsp;box.&lt;/p&gt;
&lt;h3 id="firewallds-backend-nftables"&gt;firewalld&amp;#8217;s Backend:&amp;nbsp;nftables&lt;/h3&gt;
&lt;p&gt;Since firewalld 0.6.0, the default backend is nftables. firewalld translates its zone/service model into nftables rules. You can see the generated rules&amp;nbsp;with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;nft&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;ruleset
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The output will be significantly more complex than hand-written nftables - firewalld generates a multi-table structure with dispatch chains for its zone model. This is the trade-off: you get a managed, auditable interface with a well-defined &lt;span class="caps"&gt;API&lt;/span&gt;, while the underlying nftables ruleset handles the heavy&amp;nbsp;lifting.&lt;/p&gt;
&lt;h3 id="when-firewalld-shines"&gt;When firewalld&amp;nbsp;Shines&lt;/h3&gt;
&lt;p&gt;firewalld is at its best in environments where firewall management isn&amp;#8217;t a one-person artisanal craft but a repeatable, auditable process across many&amp;nbsp;machines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cockpit integration&lt;/strong&gt;: The web-based Cockpit console (standard on &lt;span class="caps"&gt;RHEL&lt;/span&gt;) can manage firewalld zones, services, and ports through a graphical interface. For teams where not everyone is comfortable with &lt;span class="caps"&gt;CLI&lt;/span&gt; firewall management, this lowers the barrier without sacrificing&amp;nbsp;security.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ansible automation&lt;/strong&gt;:&amp;nbsp;The &lt;code&gt;ansible.posix.firewalld&lt;/code&gt; module maps directly to firewalld&amp;#8217;s zone/service model.&amp;nbsp;Declaring &lt;code&gt;service: https, zone: public, state: enabled, permanent: true&lt;/code&gt; in a playbook is about as clean as firewall configuration management&amp;nbsp;gets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D-Bus &lt;span class="caps"&gt;API&lt;/span&gt;&lt;/strong&gt;: Any application can query the current firewall state or request changes through D-Bus. libvirt uses this to dynamically open ports for &lt;span class="caps"&gt;VM&lt;/span&gt; networking. NetworkManager uses it to assign interfaces to zones when connections come up. This kind of runtime integration simply isn&amp;#8217;t possible with static ruleset&amp;nbsp;files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistent across the ecosystem&lt;/strong&gt;: Whether you&amp;#8217;re managing a single &lt;span class="caps"&gt;RHEL&lt;/span&gt; server, a fleet of Fedora CoreOS nodes, or a CentOS Stream build farm, the firewall interface is identical. The same commands, the same zones, the same service&amp;nbsp;definitions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where firewalld is less ideal is when you need complete, low-level control over rule ordering or heavily customized chain structures. Rich rules and direct passthrough rules can handle most advanced cases, but if you&amp;#8217;re building something truly bespoke, nftables gives you more direct&amp;nbsp;control.&lt;/p&gt;
&lt;h2 id="ufw-uncomplicated-firewall"&gt;ufw: Uncomplicated&amp;nbsp;Firewall&lt;/h2&gt;
&lt;p&gt;ufw does exactly what its name promises: it makes firewall configuration uncomplicated. It&amp;#8217;s the default on Ubuntu and is popular on any Debian-based system where the administrator wants a firewall without studying Netfilter&amp;nbsp;internals.&lt;/p&gt;
&lt;h3 id="the-mental-model_3"&gt;The Mental&amp;nbsp;Model&lt;/h3&gt;
&lt;p&gt;ufw has essentially no mental model beyond &amp;#8220;allow or deny things.&amp;#8221; It&amp;#8217;s a command-line interface that generates iptables (or nftables) rules from simple, human-readable commands. It trades flexibility for&amp;nbsp;approachability.&lt;/p&gt;
&lt;h3 id="cloud-server-with-ufw"&gt;Cloud Server with&amp;nbsp;ufw&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Reset to clean state&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;reset

&lt;span class="c1"&gt;# Default policies&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;default&lt;span class="w"&gt; &lt;/span&gt;deny&lt;span class="w"&gt; &lt;/span&gt;incoming
ufw&lt;span class="w"&gt; &lt;/span&gt;default&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;outgoing

&lt;span class="c1"&gt;# Allow SSH (do this BEFORE enabling ufw, or you&amp;#39;ll lock yourself out)&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;ssh

&lt;span class="c1"&gt;# Allow HTTP and HTTPS&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;http
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;https

&lt;span class="c1"&gt;# Enable the firewall&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;span class="c1"&gt;# Check status&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;verbose
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)
80/tcp (v6)                ALLOW IN    Anywhere (v6)
443/tcp (v6)               ALLOW IN    Anywhere (v6)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That&amp;#8217;s it. Six commands and your server is firewalled. ufw automatically handles both IPv4 and&amp;nbsp;IPv6.&lt;/p&gt;
&lt;h3 id="slightly-more-advanced-ufw"&gt;Slightly More Advanced&amp;nbsp;ufw&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow SSH only from a specific subnet&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;198&lt;/span&gt;.51.100.0/24&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;any&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;proto&lt;span class="w"&gt; &lt;/span&gt;tcp

&lt;span class="c1"&gt;# Allow a port range&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6000&lt;/span&gt;:6007/tcp

&lt;span class="c1"&gt;# Deny a specific IP&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;deny&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.100

&lt;span class="c1"&gt;# Rate-limit SSH (allows 6 connections per 30 seconds)&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;limit&lt;span class="w"&gt; &lt;/span&gt;ssh

&lt;span class="c1"&gt;# Delete a rule&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;http

&lt;span class="c1"&gt;# Insert a rule at a specific position&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;insert&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deny&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.0/24

&lt;span class="c1"&gt;# Allow from a specific IP to a specific port&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;198&lt;/span&gt;.51.100.22&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;any&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5432&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;proto&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;comment&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PostgreSQL admin&amp;#39;&lt;/span&gt;

&lt;span class="c1"&gt;# Show numbered rules for deletion&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;numbered
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="application-profiles"&gt;Application&amp;nbsp;Profiles&lt;/h3&gt;
&lt;p&gt;ufw supports application profiles - predefined port configurations stored&amp;nbsp;in &lt;code&gt;/etc/ufw/applications.d/&lt;/code&gt;. Many packages drop their own profile files into this directory automatically when installed&amp;nbsp;via &lt;code&gt;apt&lt;/code&gt; - install Nginx or Apache and the corresponding ufw profile appears without any extra work. This is part of why ufw feels so frictionless on Ubuntu: the ecosystem does the heavy&amp;nbsp;lifting.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# List available profiles&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;app&lt;span class="w"&gt; &lt;/span&gt;list

&lt;span class="c1"&gt;# Get profile details&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;app&lt;span class="w"&gt; &lt;/span&gt;info&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Nginx Full&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Profile: Nginx Full
Title: Web Server (Nginx, HTTP + HTTPS)
Description: Small, but very powerful and efficient web server

Ports:
  80,443/tcp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use a profile&lt;/span&gt;
ufw&lt;span class="w"&gt; &lt;/span&gt;allow&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Nginx Full&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="behind-the-scenes"&gt;Behind the&amp;nbsp;Scenes&lt;/h3&gt;
&lt;p&gt;ufw stores its rules&amp;nbsp;in &lt;code&gt;/etc/ufw/user.rules&lt;/code&gt; and &lt;code&gt;/etc/ufw/user6.rules&lt;/code&gt;. It also&amp;nbsp;has &lt;code&gt;before.rules&lt;/code&gt; and &lt;code&gt;after.rules&lt;/code&gt; files for advanced configurations that go beyond ufw&amp;#8217;s command-line interface - for example, &lt;span class="caps"&gt;NAT&lt;/span&gt; rules or custom &lt;span class="caps"&gt;FORWARD&lt;/span&gt;&amp;nbsp;chains:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/ufw/before.rules - add NAT for containers/VMs&lt;/span&gt;
*nat
:POSTROUTING&lt;span class="w"&gt; &lt;/span&gt;ACCEPT&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:0&lt;span class="o"&gt;]&lt;/span&gt;
-A&lt;span class="w"&gt; &lt;/span&gt;POSTROUTING&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.0.0.0/24&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;eth0&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="w"&gt; &lt;/span&gt;MASQUERADE
COMMIT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="when-to-use-ufw"&gt;When to Use&amp;nbsp;ufw&lt;/h3&gt;
&lt;p&gt;ufw is perfect for single-purpose servers, personal &lt;span class="caps"&gt;VPS&lt;/span&gt; instances, and situations where you just need basic allow/deny rules without ceremony. It&amp;#8217;s genuinely the fastest path from &amp;#8220;naked server&amp;#8221; to &amp;#8220;firewalled server,&amp;#8221; and there&amp;#8217;s no shame in using it. Not everything needs to be a hand-crafted artisanal nftables&amp;nbsp;configuration.&lt;/p&gt;
&lt;p&gt;Where it falls short: complex multi-zone setups, advanced &lt;span class="caps"&gt;NAT&lt;/span&gt;, detailed logging configurations, or anything where you need to understand and control the exact rule structure. ufw deliberately hides that complexity, which is a feature until it&amp;nbsp;isn&amp;#8217;t.&lt;/p&gt;
&lt;h2 id="choosing-your-firewall"&gt;Choosing Your&amp;nbsp;Firewall&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;iptables&lt;/th&gt;
&lt;th&gt;nftables&lt;/th&gt;
&lt;th&gt;firewalld&lt;/th&gt;
&lt;th&gt;ufw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IPv4/IPv6 unified&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Syntax&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verbose&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;CLI&lt;/span&gt;/&lt;span class="caps"&gt;XML&lt;/span&gt;/D-Bus&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Legacy systems&lt;/td&gt;
&lt;td&gt;New deployments&lt;/td&gt;
&lt;td&gt;Enterprise/fleet management&lt;/td&gt;
&lt;td&gt;Ubuntu/quick setups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native sets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (needs ipset)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Via zones/ipsets&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Atomic updates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime/permanent split&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Programmatic &lt;span class="caps"&gt;API&lt;/span&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Netlink&lt;/td&gt;
&lt;td&gt;D-Bus&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web &lt;span class="caps"&gt;UI&lt;/span&gt; (Cockpit)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Config management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Ansible module&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt; default&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ubuntu default&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;span class="caps"&gt;CLI&lt;/span&gt; complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;My general recommendation for new Linux&amp;nbsp;deployments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Running &lt;span class="caps"&gt;RHEL&lt;/span&gt;, Fedora, or CentOS Stream?&lt;/strong&gt; Use firewalld. It&amp;#8217;s the native choice and the integration benefits are substantial - Cockpit, Ansible, NetworkManager, and libvirt all speak firewalld natively. If you&amp;#8217;re managing more than a handful of servers, the zone/service abstraction and D-Bus &lt;span class="caps"&gt;API&lt;/span&gt; will save you significant time compared to hand-rolled nftables across every&amp;nbsp;machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running Ubuntu or Debian and just need basic rules?&lt;/strong&gt; Use ufw. It gets the job done in under a&amp;nbsp;minute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Need precise control, custom rulesets, or complex &lt;span class="caps"&gt;NAT&lt;/span&gt;?&lt;/strong&gt; Use nftables directly. It&amp;#8217;s the future of Linux packet&amp;nbsp;filtering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintaining an existing iptables setup that works?&lt;/strong&gt; Keep it.&amp;nbsp;The &lt;code&gt;iptables-nft&lt;/code&gt; compatibility layer means your rules are already running on nftables under the hood. Migrate when you have a reason, not because someone on Reddit told you&amp;nbsp;to.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="common-patterns-for-cloud-servers"&gt;Common Patterns for Cloud&amp;nbsp;Servers&lt;/h2&gt;
&lt;p&gt;Regardless of which tool you choose, certain patterns apply to every cloud server&amp;nbsp;firewall:&lt;/p&gt;
&lt;h3 id="1-default-deny-incoming"&gt;1. Default Deny&amp;nbsp;Incoming&lt;/h3&gt;
&lt;p&gt;Every firewall configuration should start with a default deny policy for incoming traffic. This is the single most important rule. If you forget to allow a service, it&amp;#8217;s unreachable (you notice immediately). If you forget to block a service with a default-allow policy, it&amp;#8217;s exposed (you might never&amp;nbsp;notice).&lt;/p&gt;
&lt;h3 id="2-always-allow-established-connections"&gt;2. Always Allow Established&amp;nbsp;Connections&lt;/h3&gt;
&lt;p&gt;Stateful tracking is essential. A rule&amp;nbsp;like &lt;code&gt;ct state established,related accept&lt;/code&gt; (nftables)&amp;nbsp;or &lt;code&gt;-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT&lt;/code&gt; (iptables) ensures that return traffic for allowed outbound connections gets through without explicit rules for every possible&amp;nbsp;response.&lt;/p&gt;
&lt;h3 id="3-rate-limit-ssh"&gt;3. Rate-Limit &lt;span class="caps"&gt;SSH&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Every public-facing &lt;span class="caps"&gt;SSH&lt;/span&gt; server gets brute-forced. Rate limiting won&amp;#8217;t stop a determined attacker, but it dramatically slows down automated scanners. Combine it with key-only authentication&amp;nbsp;(&lt;code&gt;PasswordAuthentication no&lt;/code&gt; in &lt;code&gt;sshd_config&lt;/code&gt;) for real&amp;nbsp;security.&lt;/p&gt;
&lt;h3 id="4-never-block-essential-icmpv6"&gt;4. Never Block Essential&amp;nbsp;ICMPv6&lt;/h3&gt;
&lt;p&gt;Blocking all ICMPv6 will break IPv6 connectivity. Neighbor Discovery (the IPv6 equivalent of &lt;span class="caps"&gt;ARP&lt;/span&gt;) uses ICMPv6. Path &lt;span class="caps"&gt;MTU&lt;/span&gt; Discovery uses ICMPv6. If you&amp;nbsp;block &lt;code&gt;neighbour-solicitation&lt;/code&gt; or &lt;code&gt;packet-too-big&lt;/code&gt;, your IPv6 connectivity will fail in mysterious&amp;nbsp;ways.&lt;/p&gt;
&lt;h3 id="5-log-selectively"&gt;5. Log&amp;nbsp;Selectively&lt;/h3&gt;
&lt;p&gt;Logging every dropped packet will fill your disk. Log with rate limiting, and log the things that matter - unexpected traffic on specific ports, connections from specific sources, or rules that should never trigger but&amp;nbsp;do.&lt;/p&gt;
&lt;h3 id="6-the-docker-trap"&gt;6. The Docker&amp;nbsp;Trap&lt;/h3&gt;
&lt;p&gt;This one burns almost every Linux admin at least once: &lt;strong&gt;Docker bypasses your firewall&amp;nbsp;rules.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When you&amp;nbsp;run &lt;code&gt;docker run -p 8080:80 nginx&lt;/code&gt;, Docker doesn&amp;#8217;t politely ask ufw or firewalld to open port 8080. Instead, it manipulates the&amp;nbsp;iptables &lt;code&gt;PREROUTING&lt;/code&gt; and &lt;code&gt;DOCKER&lt;/code&gt; chains directly, inserting rules that take effect &lt;em&gt;before&lt;/em&gt; your &lt;span class="caps"&gt;INPUT&lt;/span&gt; chain is evaluated. The result: that container port is exposed to the entire internet, completely ignoring your carefully&amp;nbsp;crafted &lt;code&gt;ufw default deny incoming&lt;/code&gt; policy.&lt;/p&gt;
&lt;p&gt;This happens because Docker needs to implement its port-forwarding via &lt;span class="caps"&gt;NAT&lt;/span&gt;, and it does so by writing directly to Netfilter - bypassing any frontend that manages the &lt;span class="caps"&gt;INPUT&lt;/span&gt;&amp;nbsp;chain.&lt;/p&gt;
&lt;p&gt;Mitigations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Option 1: Bind to localhost only (container accessible only from the host)&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1:8080:80&lt;span class="w"&gt; &lt;/span&gt;nginx

&lt;span class="c1"&gt;# Option 2: Disable Docker&amp;#39;s iptables manipulation entirely&lt;/span&gt;
&lt;span class="c1"&gt;# In /etc/docker/daemon.json:&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;iptables&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;false&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# WARNING: You are now responsible for all container networking rules.&lt;/span&gt;
&lt;span class="c1"&gt;# Containers will lose outbound internet access until you manually&lt;/span&gt;
&lt;span class="c1"&gt;# configure NAT masquerading for the docker0 bridge.&lt;/span&gt;

&lt;span class="c1"&gt;# Option 3: Use Docker&amp;#39;s internal network and reverse-proxy through&lt;/span&gt;
&lt;span class="c1"&gt;# a host-level web server (Nginx, Caddy) that IS behind your firewall&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The safest pattern for production: don&amp;#8217;t publish ports directly&amp;nbsp;with &lt;code&gt;-p&lt;/code&gt;. Instead, put a reverse proxy on the host that listens on the public interface (behind your firewall), and have it forward traffic to containers on Docker&amp;#8217;s internal bridge network. This way, your firewall rules actually apply to the traffic reaching your&amp;nbsp;services.&lt;/p&gt;
&lt;p&gt;Podman, notably, doesn&amp;#8217;t have this problem in rootless mode - it&amp;nbsp;uses &lt;code&gt;slirp4netns&lt;/code&gt; or &lt;code&gt;pasta&lt;/code&gt; for networking, which doesn&amp;#8217;t touch the host&amp;#8217;s iptables chains. One more reason to consider it for server&amp;nbsp;workloads.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;All four tools solve the same fundamental problem: controlling which packets get into your server and which don&amp;#8217;t. The differences are in ergonomics, abstraction level, and ecosystem integration. iptables is the weathered veteran - it works everywhere but shows its age. nftables is the well-designed successor that hasn&amp;#8217;t fully taken over yet because the old guard is entrenched. firewalld wraps everything in Red Hat&amp;#8217;s zone model, which is either a helpful abstraction or an unnecessary layer depending on your perspective. ufw strips it all down to the bare essentials, which is either refreshingly simple or frustratingly&amp;nbsp;limited.&lt;/p&gt;
&lt;p&gt;Pick the tool that matches your distribution, your team, and your complexity requirements. Then configure it, test it, and - most importantly - actually turn it on. A perfectly designed ruleset sitting in a text file isn&amp;#8217;t protecting&amp;nbsp;anything.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="corrections"&gt;Corrections&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;2026-03-14&lt;/strong&gt;: An earlier version of this article stated that nftables has no programmatic &lt;span class="caps"&gt;API&lt;/span&gt;. This is incorrect - nftables exposes a Netlink interface&amp;nbsp;(&lt;code&gt;NETLINK_NETFILTER&lt;/code&gt;) that allows applications to manage rules directly without&amp;nbsp;the &lt;code&gt;nft&lt;/code&gt; command-line tool. The comparison table and nftables section have been updated to reflect this. Thanks to &lt;a href="https://hachyderm.io/@joew"&gt;Joe Chilliams (@joew@hachyderm.io)&lt;/a&gt; for the correction and the excellent real-world&amp;nbsp;examples.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wiki.nftables.org/"&gt;nftables wiki&lt;/a&gt; - the canonical nftables&amp;nbsp;documentation&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/iptables.8.html"&gt;iptables man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netfilter.org/"&gt;Netfilter project&lt;/a&gt; - the umbrella project for Linux packet&amp;nbsp;filtering&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firewalld.org/documentation/"&gt;firewalld&amp;nbsp;documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.ubuntu.com/community/UFW"&gt;ufw community help&amp;nbsp;(Ubuntu)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/10/html/configuring_firewalls_and_packet_filters/using-and-configuring-firewalld_firewall-packet-filters"&gt;Red Hat - Using&amp;nbsp;firewalld&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.debian.org/nftables"&gt;Debian wiki -&amp;nbsp;nftables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Every Linux firewall tool on this page gets the job done. But if you really want to see packet filtering done with elegance - a single, clean configuration file, last-match-wins semantics, tables that scale to hundreds of thousands of entries without breaking a sweat, and a syntax that reads like it was designed by someone who actually enjoys writing firewall rules - well, you know where I stand. &lt;a href="https://blog.hofstede.it/pf-firewall-on-freebsd-a-practical-guide/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; on FreeBSD&lt;/a&gt; remains the gold standard. The rest of us are just approximating&amp;nbsp;greatness.&lt;/p&gt;</content><category term="Linux"/><category term="linux"/><category term="firewall"/><category term="iptables"/><category term="nftables"/><category term="firewalld"/><category term="ufw"/><category term="security"/><category term="networking"/><category term="cloud"/></entry><entry><title>FreeBSD Foundationals: ZFS - The Last Filesystem You’ll Ever Need</title><link href="https://blog.hofstede.it/freebsd-foundationals-zfs-the-last-filesystem-youll-ever-need/" rel="alternate"/><published>2026-03-13T00:00:00+01:00</published><updated>2026-03-13T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-13:/freebsd-foundationals-zfs-the-last-filesystem-youll-ever-need/</id><summary type="html">&lt;p&gt;The second in the FreeBSD Foundationals series. This one covers &lt;span class="caps"&gt;ZFS&lt;/span&gt; from philosophy to practice: why it exists, how pools and datasets work, what checksumming and self-healing actually do, how to tune recordsize, compression, and atime, how encryption works with key management, how snapshots and the hidden .zfs directory give you time travel, and how &lt;span class="caps"&gt;ZFS&lt;/span&gt; send/recv turns backup and migration into a solved problem. Includes a look at sanoid/syncoid for automated snapshot&amp;nbsp;management.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; was born at Sun Microsystems in 2004, open-sourced in 2005 as part of OpenSolaris, and has since become the default filesystem on FreeBSD. Not just the default in the installer - the default in production, the default in the Handbook, the default in the minds of people who have lost data exactly once and decided never again. (It&amp;#8217;s also available on Linux, where it works beautifully - just don&amp;#8217;t ask me how I know you can &lt;a href="https://blog.hofstede.it/rhel-on-zfs-root-an-unholy-experiment/"&gt;run &lt;span class="caps"&gt;RHEL&lt;/span&gt; on a &lt;span class="caps"&gt;ZFS&lt;/span&gt; root pool&lt;/a&gt;. That was a crime, not a&amp;nbsp;tutorial.)&lt;/p&gt;
&lt;p&gt;This is the second article in the &lt;strong&gt;FreeBSD Foundationals&lt;/strong&gt; series. The &lt;a href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/"&gt;first one covered Jails&lt;/a&gt;. We&amp;#8217;re covering &lt;span class="caps"&gt;ZFS&lt;/span&gt; now because it&amp;#8217;s the foundation everything else sits on: your jails, your databases, your mail spools, your backups. Understanding &lt;span class="caps"&gt;ZFS&lt;/span&gt; isn&amp;#8217;t optional if you&amp;#8217;re running FreeBSD&amp;nbsp;seriously.&lt;/p&gt;
&lt;h2 id="why-zfs-exists"&gt;Why &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;nbsp;Exists&lt;/h2&gt;
&lt;p&gt;Traditional filesystems trust the hardware. They write data to disk, read it back later, and assume what comes back is what was written. This assumption is wrong more often than most people realize. Disks develop bad sectors. Controllers corrupt data in transit. &lt;span class="caps"&gt;RAM&lt;/span&gt; bit-flips go undetected. &lt;span class="caps"&gt;RAID&lt;/span&gt; controllers with battery-backed caches fail in ways that silently eat data. The industry term for this is &lt;strong&gt;silent data corruption&lt;/strong&gt;, and every filesystem that doesn&amp;#8217;t checksum its data is vulnerable to&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; was designed from the ground up with one overriding principle: &lt;strong&gt;your data should never be silently wrong&lt;/strong&gt;. Every block of data and metadata is checksummed. Every read is verified against that checksum. If the checksum doesn&amp;#8217;t match, &lt;span class="caps"&gt;ZFS&lt;/span&gt; knows the data is corrupt - and if redundancy exists (mirror or raidz), it automatically repairs the damage from a good copy without anyone having to notice or&amp;nbsp;intervene.&lt;/p&gt;
&lt;p&gt;But &lt;span class="caps"&gt;ZFS&lt;/span&gt; is not just a filesystem. It&amp;#8217;s a &lt;strong&gt;filesystem, volume manager, and software &lt;span class="caps"&gt;RAID&lt;/span&gt; implementation rolled into one&lt;/strong&gt;. Traditional Unix storage stacks look like&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Traditional stack:              ZFS stack:

┌────────────────┐              ┌────────────────┐
│   Filesystem   │              │                │
│   (ext4/ufs)   │              │      ZFS       │
├────────────────┤              │                │
│ Volume Manager │              │  (filesystem + │
│   (LVM/gpart)  │              │  volume mgmt + │
├────────────────┤              │   RAID + ...   │
│  Software RAID │              │                │
│   (gmirror)    │              │                │
├────────────────┤              ├────────────────┤
│     Disks      │              │     Disks      │
└────────────────┘              └────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With a traditional stack, you need to coordinate between three or four separate layers, each with its own tools, its own failure modes, and its own ideas about how data is organized. &lt;span class="caps"&gt;ZFS&lt;/span&gt; eliminates this entire class of problems by managing everything from raw disks to individual files in a single, coherent&amp;nbsp;system.&lt;/p&gt;
&lt;h2 id="core-concepts-pools-and-datasets"&gt;Core Concepts: Pools and&amp;nbsp;Datasets&lt;/h2&gt;
&lt;h3 id="pools-the-storage-foundation"&gt;Pools: The Storage&amp;nbsp;Foundation&lt;/h3&gt;
&lt;p&gt;A &lt;span class="caps"&gt;ZFS&lt;/span&gt; &lt;strong&gt;pool&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;zpool&lt;/code&gt;) is the fundamental unit of storage. You give &lt;span class="caps"&gt;ZFS&lt;/span&gt; one or more disks (or partitions, or files - it doesn&amp;#8217;t care), and it creates a pool. All storage in &lt;span class="caps"&gt;ZFS&lt;/span&gt; comes from a pool. There are no partitions to resize, no logical volumes to extend, no separate &amp;#8220;allocate space, then format&amp;#8221;&amp;nbsp;steps.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a simple pool from a single disk&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;da0

&lt;span class="c1"&gt;# Create a mirror (two-way redundancy)&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;mirror&lt;span class="w"&gt; &lt;/span&gt;da0&lt;span class="w"&gt; &lt;/span&gt;da1

&lt;span class="c1"&gt;# Create a raidz1 pool (single-parity, like RAID-5)&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;raidz1&lt;span class="w"&gt; &lt;/span&gt;da0&lt;span class="w"&gt; &lt;/span&gt;da1&lt;span class="w"&gt; &lt;/span&gt;da2

&lt;span class="c1"&gt;# Create a raidz2 pool (double-parity, like RAID-6)&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;raidz2&lt;span class="w"&gt; &lt;/span&gt;da0&lt;span class="w"&gt; &lt;/span&gt;da1&lt;span class="w"&gt; &lt;/span&gt;da2&lt;span class="w"&gt; &lt;/span&gt;da3

&lt;span class="c1"&gt;# Check pool status&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pool topology matters and cannot be changed after creation. You choose your redundancy level when you create the pool. A mirror can tolerate one disk failure per mirror pair. Raidz1 tolerates one disk failure per vdev. Raidz2 tolerates two. Choose raidz2 for anything that matters - modern drives are massive, and resilvering (rebuilding) a &lt;span class="caps"&gt;16TB&lt;/span&gt; disk takes days of heavy I/O, which is exactly when a second aging drive is most likely to fail. The cost of one extra parity disk is nothing compared to a lost&amp;nbsp;pool.&lt;/p&gt;
&lt;p&gt;You can check the health of a pool at any&amp;nbsp;time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;zpool&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 02:31:44 with 0 errors on Sun Mar  9 03:01:44 2026
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          mirror-0  ONLINE       0     0     0
            da0     ONLINE       0     0     0
            da1     ONLINE       0     0     0

errors: No known data errors
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;CKSUM&lt;/code&gt; column is the one that matters most. A non-zero value there means &lt;span class="caps"&gt;ZFS&lt;/span&gt; detected a checksum mismatch - silent corruption that most other filesystems would have delivered to your application without a&amp;nbsp;word.&lt;/p&gt;
&lt;h3 id="scrubs-proactive-integrity-verification"&gt;Scrubs: Proactive Integrity&amp;nbsp;Verification&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; doesn&amp;#8217;t just verify data when you read it. You can (and should) run periodic &lt;strong&gt;scrubs&lt;/strong&gt; that read every block in the pool and verify its&amp;nbsp;checksum:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Start a scrub&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;scrub&lt;span class="w"&gt; &lt;/span&gt;tank

&lt;span class="c1"&gt;# Check scrub progress&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On a redundant pool (mirror or raidz), a scrub will automatically repair any corruption it finds using the redundant copy. On a non-redundant pool, a scrub will detect the corruption and report it, but can&amp;#8217;t repair it - there&amp;#8217;s no good copy to repair&amp;nbsp;from.&lt;/p&gt;
&lt;p&gt;Schedule scrubs regularly. Weekly is common for servers. &lt;span class="caps"&gt;ZFS&lt;/span&gt; automatically pauses scrubs during heavy I/O to avoid degrading application performance, so don&amp;#8217;t be afraid to schedule them during the work week if your maintenance windows are limited. FreeBSD enables periodic &lt;span class="caps"&gt;ZFS&lt;/span&gt; scrubs by default via a cron entry&amp;nbsp;in &lt;code&gt;/etc/periodic.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# In /etc/periodic.conf (or check /etc/defaults/periodic.conf)&lt;/span&gt;
&lt;span class="nv"&gt;daily_scrub_zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="datasets-flexible-storage-without-partitions"&gt;Datasets: Flexible Storage Without&amp;nbsp;Partitions&lt;/h3&gt;
&lt;p&gt;A &lt;span class="caps"&gt;ZFS&lt;/span&gt; &lt;strong&gt;dataset&lt;/strong&gt; is roughly analogous to a traditional filesystem, but without fixed size. Datasets share the pool&amp;#8217;s storage dynamically. You don&amp;#8217;t pre-allocate space to a dataset - it grows as needed, up to whatever the pool (or an optional quota)&amp;nbsp;allows.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create datasets&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/home
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/home/chris
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/jails
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Datasets are hierarchical. Properties set on a parent are inherited by children unless explicitly overridden. This inheritance model is one of &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;#8217;s most practical&amp;nbsp;features:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Set compression on the parent - all children inherit it&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zstd&lt;span class="w"&gt; &lt;/span&gt;tank

&lt;span class="c1"&gt;# Override compression for a specific child dataset&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lz4&lt;span class="w"&gt; &lt;/span&gt;tank/postgresql

&lt;span class="c1"&gt;# Check inherited vs. local properties&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;compression&lt;span class="w"&gt; &lt;/span&gt;tank/home
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;NAME        PROPERTY     VALUE           SOURCE
tank/home   compression  zstd            inherited from tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="key-dataset-properties"&gt;Key Dataset&amp;nbsp;Properties&lt;/h3&gt;
&lt;p&gt;These are the properties you&amp;#8217;ll actually care about in&amp;nbsp;practice:&lt;/p&gt;
&lt;h4 id="compression"&gt;compression&lt;/h4&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; supports transparent compression at the dataset level. Every block is compressed before being written to disk and decompressed on read. This sounds like it should be slower, but on modern CPUs the compression is so fast that the reduced I/O usually makes things &lt;em&gt;faster&lt;/em&gt;&amp;nbsp;overall.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Modern default - excellent compression ratio, very fast&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zstd&lt;span class="w"&gt; &lt;/span&gt;tank

&lt;span class="c1"&gt;# Alternative - slightly faster, slightly worse ratio&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lz4&lt;span class="w"&gt; &lt;/span&gt;tank/fast-storage

&lt;span class="c1"&gt;# Check compression effectiveness&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;compressratio&lt;span class="w"&gt; &lt;/span&gt;tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;#8217;s almost no reason to leave compression off. Even on data that doesn&amp;#8217;t compress well (encrypted files, already-compressed media), &lt;span class="caps"&gt;ZFS&lt;/span&gt; detects incompressible blocks and stores them uncompressed with negligible&amp;nbsp;overhead.&lt;/p&gt;
&lt;h4 id="recordsize"&gt;recordsize&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;recordsize&lt;/code&gt; property controls the maximum block size for a dataset. The default is &lt;span class="caps"&gt;128KB&lt;/span&gt;, which works well for general-purpose workloads. But specific workloads benefit from tuning&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# PostgreSQL: match the database page size (8KB)&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;recordsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8k&lt;span class="w"&gt; &lt;/span&gt;tank/postgresql

&lt;span class="c1"&gt;# MySQL/InnoDB: match the InnoDB page size (16KB)&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;recordsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16k&lt;span class="w"&gt; &lt;/span&gt;tank/mysql

&lt;span class="c1"&gt;# Large sequential files (media, backups, VM images): use 1MB&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;recordsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M&lt;span class="w"&gt; &lt;/span&gt;tank/backups
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The principle: match the recordsize to the I/O pattern of the application. Databases do many small, random reads and writes at their page size - a &lt;span class="caps"&gt;128KB&lt;/span&gt; recordsize means &lt;span class="caps"&gt;ZFS&lt;/span&gt; reads &lt;span class="caps"&gt;128KB&lt;/span&gt; to satisfy an &lt;span class="caps"&gt;8KB&lt;/span&gt; database page read.&amp;nbsp;Setting &lt;code&gt;recordsize=8k&lt;/code&gt; for PostgreSQL can dramatically improve random read&amp;nbsp;performance.&lt;/p&gt;
&lt;p&gt;For sequential workloads (backups, media storage, large file copies), a larger recordsize means fewer metadata operations and better compression&amp;nbsp;ratios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; recordsize only affects newly written data. Changing it on an existing dataset does not rewrite existing blocks. For databases, set the recordsize before importing&amp;nbsp;data.&lt;/p&gt;
&lt;h4 id="atime"&gt;atime&lt;/h4&gt;
&lt;p&gt;Every time a file is read, Unix traditionally updates its &amp;#8220;access time&amp;#8221;&amp;nbsp;(&lt;code&gt;atime&lt;/code&gt;) metadata. On a busy system, this means every read operation also triggers a write operation. &lt;span class="caps"&gt;ZFS&lt;/span&gt; inherits this behavior by default, but you should almost always disable&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Disable access time updates entirely&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;atime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off&lt;span class="w"&gt; &lt;/span&gt;tank

&lt;span class="c1"&gt;# Or use relative atime (only update when mtime/ctime is newer than atime)&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;relatime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Disabling &lt;code&gt;atime&lt;/code&gt; eliminates a write I/O for every read I/O. For a mail server scanning thousands of files per second, or a web server serving static assets, this is a significant performance improvement with essentially no downside. Almost nothing on a modern system depends on accurate&amp;nbsp;atime.&lt;/p&gt;
&lt;h4 id="quota-and-reservation"&gt;quota and&amp;nbsp;reservation&lt;/h4&gt;
&lt;p&gt;Quotas limit how much space a dataset can consume. Reservations guarantee a minimum amount of space. These are useful for preventing runaway datasets from starving&amp;nbsp;others:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Limit a dataset to 50GB&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;quota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50G&lt;span class="w"&gt; &lt;/span&gt;tank/home/chris

&lt;span class="c1"&gt;# Guarantee 10GB is always available for this dataset&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;reservation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10G&lt;span class="w"&gt; &lt;/span&gt;tank/postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;refquota&lt;/code&gt; and &lt;code&gt;refreservation&lt;/code&gt; are the variants that exclude snapshots from the accounting.&amp;nbsp;Usually &lt;code&gt;refquota&lt;/code&gt; is what you want for user quotas - you don&amp;#8217;t want someone&amp;#8217;s disk usage to silently increase because of snapshots they didn&amp;#8217;t&amp;nbsp;take.&lt;/p&gt;
&lt;h2 id="self-healing-how-it-actually-works"&gt;Self-Healing: How It Actually&amp;nbsp;Works&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; checksums every block of data and metadata using &lt;span class="caps"&gt;SHA&lt;/span&gt;-256 (by default; other algorithms are available). The checksum is stored separately from the data it protects - in the parent block&amp;#8217;s metadata. This means corruption of a data block can&amp;#8217;t also corrupt its own checksum, which is the fatal flaw of systems that store checksums inline (like a &lt;span class="caps"&gt;TCP&lt;/span&gt; checksum covering the &lt;span class="caps"&gt;TCP&lt;/span&gt;&amp;nbsp;header).&lt;/p&gt;
&lt;p&gt;The verification&amp;nbsp;flow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Write path:
  Application data → compress → checksum → write to disk
                                   ↓
                          store checksum in
                          parent metadata block

Read path:
  Read block from disk → verify checksum → decompress → deliver to application
       ↓                      ↓
  If checksum fails:    On redundant pool:
  &amp;quot;This block is bad&amp;quot;   → read from mirror/parity
                         → repair the bad copy
                         → deliver good data
                         → increment CKSUM error counter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This happens transparently. Your application never sees the corrupt data. It gets the correct data, repaired silently, and the error is logged. You&amp;#8217;ll see it&amp;nbsp;in &lt;code&gt;zpool status&lt;/code&gt; output as a non-zero &lt;span class="caps"&gt;CKSUM&lt;/span&gt; count, which tells you a disk is developing problems before it fails&amp;nbsp;catastrophically.&lt;/p&gt;
&lt;p&gt;On a non-redundant pool, &lt;span class="caps"&gt;ZFS&lt;/span&gt; can detect corruption but can&amp;#8217;t repair it. The read will fail with an I/O error, which is still better than silently returning corrupted data. At least you know the data is&amp;nbsp;bad.&lt;/p&gt;
&lt;h2 id="snapshots-instant-point-in-time-copies"&gt;Snapshots: Instant Point-in-Time&amp;nbsp;Copies&lt;/h2&gt;
&lt;p&gt;A &lt;span class="caps"&gt;ZFS&lt;/span&gt; snapshot is a read-only point-in-time copy of a dataset. Snapshots are essentially free to create - they take no additional space initially, because they share all their data with the live dataset. Space is only consumed as the live dataset changes and the snapshot needs to preserve the old&amp;nbsp;blocks.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a snapshot&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@before-upgrade

&lt;span class="c1"&gt;# Create a snapshot of all datasets under a hierarchy&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;tank/jails@2026-03-13

&lt;span class="c1"&gt;# List snapshots&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;snapshot

&lt;span class="c1"&gt;# See how much space a snapshot is consuming&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;name,used,refer&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="rolling-back"&gt;Rolling&amp;nbsp;Back&lt;/h3&gt;
&lt;p&gt;If something goes wrong, you can roll a dataset back to a snapshot&amp;nbsp;instantly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Roll back to the snapshot (destroys all changes since the snapshot)&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@before-upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note: &lt;code&gt;zfs rollback&lt;/code&gt; can only roll back to the most recent snapshot. If you have multiple snapshots and want to go back further, you need to destroy the intermediate snapshots first (or&amp;nbsp;use &lt;code&gt;-r&lt;/code&gt; to do it&amp;nbsp;automatically):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Roll back, destroying intermediate snapshots&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@before-upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This makes snapshots invaluable for system upgrades. Snapshot before you upgrade, upgrade, test. If the upgrade broke something, roll back. The entire operation takes seconds, not hours of restoring from&amp;nbsp;backup.&lt;/p&gt;
&lt;h3 id="the-beauty-of-zfs-accessing-snapshots-directly"&gt;The Beauty of /.zfs: Accessing Snapshots&amp;nbsp;Directly&lt;/h3&gt;
&lt;p&gt;Here&amp;#8217;s something that surprises people who are new to &lt;span class="caps"&gt;ZFS&lt;/span&gt;: every dataset has a&amp;nbsp;hidden &lt;code&gt;.zfs&lt;/code&gt; directory at its mount point. Inside it,&amp;nbsp;under &lt;code&gt;.zfs/snapshot/&lt;/code&gt;, every snapshot is accessible as a normal read-only directory&amp;nbsp;tree.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# The .zfs directory is hidden from normal ls output&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;/tank/home/chris/.zfs/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;total 0
dr-xr-xr-x  2 root  wheel  2 Mar 13 19:00 .
drwxr-xr-x  5 chris chris 12 Mar 13 19:30 ..
drwxr-xr-x  5 chris chris 12 Mar 10 08:00 snapshot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# List all snapshots as directories&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;/tank/home/chris/.zfs/snapshot/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;2026-03-10    2026-03-11    2026-03-12    2026-03-13
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Just... cd into a snapshot and find your file&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/tank/home/chris/.zfs/snapshot/2026-03-12/
ls&lt;span class="w"&gt; &lt;/span&gt;Documents/
&lt;span class="c1"&gt;# → there&amp;#39;s the file you accidentally deleted yesterday&lt;/span&gt;
cp&lt;span class="w"&gt; &lt;/span&gt;Documents/important-report.pdf&lt;span class="w"&gt; &lt;/span&gt;/tank/home/chris/Documents/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No restore tools. No backup software. No waiting. You browse the snapshot like a normal directory, find the file you need, and copy it back.&amp;nbsp;The &lt;code&gt;.zfs&lt;/code&gt; directory doesn&amp;#8217;t show up in&amp;nbsp;normal &lt;code&gt;ls&lt;/code&gt; output (it&amp;#8217;s hidden by default), but you can access it directly by name. This means regular users can recover their own accidentally deleted files without root access or administrator&amp;nbsp;intervention.&lt;/p&gt;
&lt;p&gt;This is arguably &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;#8217;s most user-facing killer feature. With automated snapshots (see the sanoid section below), users have a continuously available time machine for their&amp;nbsp;files.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.zfs&lt;/code&gt; directory can be made visible in directory listings if you&amp;nbsp;prefer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;snapdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;visible&lt;span class="w"&gt; &lt;/span&gt;tank/home
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="zfs-native-encryption"&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; Native&amp;nbsp;Encryption&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; supports native dataset-level encryption, added in OpenZFS 0.8. Encryption is per-dataset, transparent to applications, and managed entirely through &lt;span class="caps"&gt;ZFS&lt;/span&gt; commands. No separate tools, no dm-crypt, no &lt;span class="caps"&gt;GELI&lt;/span&gt; - just &lt;span class="caps"&gt;ZFS&lt;/span&gt;.&lt;/p&gt;
&lt;h3 id="creating-encrypted-datasets"&gt;Creating Encrypted&amp;nbsp;Datasets&lt;/h3&gt;
&lt;p&gt;Encryption is set at dataset creation time and cannot be added to an existing unencrypted dataset (you&amp;#8217;d need to create a new encrypted dataset and move the&amp;nbsp;data):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create an encrypted dataset with a passphrase&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;encryption&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aes-256-gcm&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keylocation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prompt&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keyformat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;passphrase&lt;span class="w"&gt; &lt;/span&gt;tank/private

&lt;span class="c1"&gt;# Create an encrypted dataset with a key file&lt;/span&gt;
dd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/random&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/root/keys/tank-private.key&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;encryption&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aes-256-gcm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keylocation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;file:///root/keys/tank-private.key&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keyformat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raw&lt;span class="w"&gt; &lt;/span&gt;tank/secure
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Child datasets inherit encryption from their parent.&amp;nbsp;If &lt;code&gt;tank/private&lt;/code&gt; is&amp;nbsp;encrypted, &lt;code&gt;tank/private/documents&lt;/code&gt; and &lt;code&gt;tank/private/photos&lt;/code&gt; are automatically encrypted with the same&amp;nbsp;key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# These inherit encryption from tank/private&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/private/documents
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;tank/private/photos
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="key-management"&gt;Key&amp;nbsp;Management&lt;/h3&gt;
&lt;p&gt;When a system boots, encrypted datasets are not automatically mounted because &lt;span class="caps"&gt;ZFS&lt;/span&gt; doesn&amp;#8217;t have the decryption key yet. You need to load the key before the dataset becomes&amp;nbsp;accessible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Load the key for a single dataset&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;load-key&lt;span class="w"&gt; &lt;/span&gt;tank/private

&lt;span class="c1"&gt;# Load keys for all encrypted datasets&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;load-key&lt;span class="w"&gt; &lt;/span&gt;-a

&lt;span class="c1"&gt;# Mount after loading key&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;mount&lt;span class="w"&gt; &lt;/span&gt;tank/private

&lt;span class="c1"&gt;# Check encryption status&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;keystatus&lt;span class="w"&gt; &lt;/span&gt;tank/private
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;NAME           PROPERTY    VALUE        SOURCE
tank/private   keystatus   available    -
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For servers that need encrypted datasets available at boot without manual intervention, you can store the key file on a separate device - for example, a &lt;span class="caps"&gt;USB&lt;/span&gt; stick that stays plugged into the server but could be removed and secured&amp;nbsp;independently:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Key file on a separate USB device mounted at boot&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keylocation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;file:///mnt/usbkey/tank-private.key&lt;span class="w"&gt; &lt;/span&gt;tank/private
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you store the key file on the boot pool itself, be aware that this provides &lt;strong&gt;no protection against physical theft&lt;/strong&gt; of the drive - the thief gets both the encrypted data and the key to decrypt it. The only scenario where a same-disk key file helps is disk disposal or &lt;span class="caps"&gt;RMA&lt;/span&gt;, where you wipe or destroy the boot partition but send the data disks out with encrypted blocks a third party can&amp;#8217;t&amp;nbsp;read.&lt;/p&gt;
&lt;p&gt;For meaningful data-at-rest protection, either keep the key on a physically separate device, or use passphrase-based keys and accept the manual unlock step at&amp;nbsp;boot.&lt;/p&gt;
&lt;h3 id="changing-keys"&gt;Changing&amp;nbsp;Keys&lt;/h3&gt;
&lt;p&gt;You can change the encryption key (or switch between passphrase and key file) without re-encrypting the&amp;nbsp;data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Change from key file to passphrase&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;change-key&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keyformat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;passphrase&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keylocation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prompt&lt;span class="w"&gt; &lt;/span&gt;tank/private

&lt;span class="c1"&gt;# Change from passphrase to key file&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;change-key&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keyformat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raw&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;keylocation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;file:///root/keys/new.key&lt;span class="w"&gt; &lt;/span&gt;tank/private
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a key-wrapping operation - the actual data encryption key doesn&amp;#8217;t change, only the wrapper protecting it. It&amp;#8217;s instantaneous regardless of dataset&amp;nbsp;size.&lt;/p&gt;
&lt;h3 id="locking-datasets"&gt;Locking&amp;nbsp;Datasets&lt;/h3&gt;
&lt;p&gt;When you&amp;#8217;re done working with encrypted data, you can unload the key, making the dataset inaccessible until the key is loaded&amp;nbsp;again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Unmount and unload the key&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;unmount&lt;span class="w"&gt; &lt;/span&gt;tank/private
zfs&lt;span class="w"&gt; &lt;/span&gt;unload-key&lt;span class="w"&gt; &lt;/span&gt;tank/private
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="zfs-sendrecv-the-killer-feature-for-backups"&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; Send/Recv: The Killer Feature for&amp;nbsp;Backups&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;zfs send&lt;/code&gt; serializes a snapshot (or the difference between two snapshots) into a stream of&amp;nbsp;bytes. &lt;code&gt;zfs recv&lt;/code&gt; takes that stream and reconstructs the dataset on the other end. This is &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;#8217;s native backup and replication mechanism, and it&amp;#8217;s remarkably&amp;nbsp;powerful.&lt;/p&gt;
&lt;h3 id="basic-sendrecv"&gt;Basic&amp;nbsp;Send/Recv&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Send a full snapshot to a file&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@2026-03-13&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/backup/mailserver-2026-03-13.zfs

&lt;span class="c1"&gt;# Receive it into a different pool&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;backup/jails/mailserver&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;/backup/mailserver-2026-03-13.zfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="incremental-sends-only-the-changes"&gt;Incremental Sends: Only the&amp;nbsp;Changes&lt;/h3&gt;
&lt;p&gt;The real power is incremental sends. After the initial full send, subsequent sends only transfer the blocks that changed between two&amp;nbsp;snapshots:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# First: full send of the initial snapshot&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@monday&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;backup/jails/mailserver

&lt;span class="c1"&gt;# Later: incremental send (only changes since Monday)&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@monday&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@tuesday&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;backup/jails/mailserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-i&lt;/code&gt; flag tells &lt;span class="caps"&gt;ZFS&lt;/span&gt; to send only the delta between the two snapshots. On a &lt;span class="caps"&gt;500GB&lt;/span&gt; dataset where &lt;span class="caps"&gt;2GB&lt;/span&gt; changed since the last snapshot, the incremental send transfers ~&lt;span class="caps"&gt;2GB&lt;/span&gt;, not &lt;span class="caps"&gt;500GB&lt;/span&gt;. This makes daily off-site backups of large datasets entirely practical, even over modest network&amp;nbsp;links.&lt;/p&gt;
&lt;h3 id="sendrecv-over-ssh"&gt;Send/Recv Over &lt;span class="caps"&gt;SSH&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;For remote backups, pipe through &lt;span class="caps"&gt;SSH&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Send to a remote backup server&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@monday&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@tuesday&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;backup-server&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;mountpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;canmount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off&lt;span class="w"&gt; &lt;/span&gt;backup/jails/mailserver

&lt;span class="c1"&gt;# For initial full replication with all properties&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@tuesday&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;backup-server&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;mountpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;canmount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off&lt;span class="w"&gt; &lt;/span&gt;backup/jails/mailserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-F&lt;/code&gt; flag on the receiving end forces a rollback to match the incoming stream, discarding any local changes on the backup target. This ensures your backup remains an exact replica of the source - but be careful not to&amp;nbsp;use &lt;code&gt;-F&lt;/code&gt; against a dataset that contains data you care about independently, as any local modifications will be&amp;nbsp;destroyed.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-o mountpoint=none -o canmount=off&lt;/code&gt; flags are equally important: without them, the backup server will try to mount the incoming datasets at their original mount points. If you&amp;#8217;re replicating a dataset&amp;nbsp;with &lt;code&gt;mountpoint=/etc&lt;/code&gt; or &lt;code&gt;mountpoint=/var/mail&lt;/code&gt;, the backup server would mount it right over its own live directories. Setting these overrides ensures received datasets are stored but never&amp;nbsp;auto-mounted.&lt;/p&gt;
&lt;h3 id="raw-sends-for-encrypted-datasets"&gt;Raw Sends for Encrypted&amp;nbsp;Datasets&lt;/h3&gt;
&lt;p&gt;When sending encrypted datasets, you can choose whether to send the data encrypted or&amp;nbsp;decrypted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Raw send: data stays encrypted. Receiver doesn&amp;#39;t need the key.&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;--raw&lt;span class="w"&gt; &lt;/span&gt;tank/private@2026-03-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;backup-server&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;backup/private

&lt;span class="c1"&gt;# Normal send: data is decrypted, then re-encrypted (or stored plain) at the receiver.&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;tank/private@2026-03-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;backup-server&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;backup/private
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Raw sends&amp;nbsp;(&lt;code&gt;--raw&lt;/code&gt;) are essential for secure off-site backups. The backup server receives and stores encrypted data without ever having the decryption key. If the backup server is compromised, the attacker gets encrypted blocks they can&amp;#8217;t read. An important practical benefit: the receiving pool doesn&amp;#8217;t need to have encryption support enabled at all - it just stores opaque blocks. This makes raw sends ideal for replicating to older backup servers or cloud storage targets that may not run a current OpenZFS&amp;nbsp;version.&lt;/p&gt;
&lt;h3 id="sanoid-and-syncoid-automated-snapshot-management"&gt;Sanoid and Syncoid: Automated Snapshot&amp;nbsp;Management&lt;/h3&gt;
&lt;p&gt;Manually creating snapshots and&amp;nbsp;running &lt;code&gt;zfs send&lt;/code&gt; works, but it doesn&amp;#8217;t scale. &lt;a href="https://github.com/jimsalterjrs/sanoid"&gt;Sanoid&lt;/a&gt; automates snapshot creation and retention, and its companion tool &lt;strong&gt;syncoid&lt;/strong&gt;&amp;nbsp;automates &lt;code&gt;zfs send/recv&lt;/code&gt; replication.&lt;/p&gt;
&lt;p&gt;Sanoid is available in FreeBSD&amp;#8217;s package&amp;nbsp;repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;sanoid
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sanoid uses a configuration file&amp;nbsp;at &lt;code&gt;/usr/local/etc/sanoid/sanoid.conf&lt;/code&gt; to define snapshot&amp;nbsp;policies:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[tank/jails]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;use_template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="k"&gt;[tank/home]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;use_template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;

&lt;span class="k"&gt;[tank/postgresql]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;use_template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="k"&gt;[template_production]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;hourly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;24&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;daily&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;monthly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;yearly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autosnap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;autoprune&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This keeps 24 hourly, 30 daily, 12 monthly, and 2 yearly snapshots - all managed automatically. Old snapshots are pruned when they exceed the retention count.&amp;nbsp;Run &lt;code&gt;sanoid --cron&lt;/code&gt; from a cron job (typically every 15 minutes), and snapshot management becomes a solved&amp;nbsp;problem.&lt;/p&gt;
&lt;p&gt;Syncoid handles the replication&amp;nbsp;side:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Replicate a dataset to a remote server&lt;/span&gt;
syncoid&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver&lt;span class="w"&gt; &lt;/span&gt;backup-server:backup/jails/mailserver

&lt;span class="c1"&gt;# Replicate recursively (all child datasets)&lt;/span&gt;
syncoid&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;tank/jails&lt;span class="w"&gt; &lt;/span&gt;backup-server:backup/jails

&lt;span class="c1"&gt;# Use raw mode for encrypted datasets&lt;/span&gt;
syncoid&lt;span class="w"&gt; &lt;/span&gt;--sendoptions&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tank/private&lt;span class="w"&gt; &lt;/span&gt;backup-server:backup/private
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Syncoid automatically determines whether a full or incremental send is needed, creates temporary snapshots for consistency, handles&amp;nbsp;the &lt;code&gt;zfs send | ssh | zfs recv&lt;/code&gt; pipeline, and cleans up after itself. A single cron entry&amp;nbsp;running &lt;code&gt;syncoid&lt;/code&gt; gives you continuous off-site replication with minimal bandwidth&amp;nbsp;usage.&lt;/p&gt;
&lt;p&gt;A typical backup cron setup (shown here as root&amp;#8217;s personal crontab&amp;nbsp;via &lt;code&gt;crontab -e&lt;/code&gt;; if you add these&amp;nbsp;to &lt;code&gt;/etc/crontab&lt;/code&gt; instead,&amp;nbsp;add &lt;code&gt;root&lt;/code&gt; as the user field after the time&amp;nbsp;specification):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Sanoid: manage local snapshots every 15 minutes
*/15 &lt;span class="gs"&gt;* *&lt;/span&gt; * &lt;span class="gs"&gt;* /usr/local/bin/sanoid --cron&lt;/span&gt;

&lt;span class="gs"&gt;# Syncoid: replicate to backup server daily at 2 AM&lt;/span&gt;
&lt;span class="gs"&gt;0 2 *&lt;/span&gt; * * /usr/local/bin/syncoid -r tank backup-server:backup/tank
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="performance-tuning-arc-and-l2arc"&gt;Performance Tuning: &lt;span class="caps"&gt;ARC&lt;/span&gt; and &lt;span class="caps"&gt;L2ARC&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; uses a sophisticated caching system called the &lt;strong&gt;&lt;span class="caps"&gt;ARC&lt;/span&gt;&lt;/strong&gt; (Adaptive Replacement Cache). The &lt;span class="caps"&gt;ARC&lt;/span&gt; lives in &lt;span class="caps"&gt;RAM&lt;/span&gt; and caches recently and frequently accessed data. Unlike traditional filesystem caches, the &lt;span class="caps"&gt;ARC&lt;/span&gt; uses an algorithm that balances recency and frequency, keeping both &amp;#8220;recently used&amp;#8221; and &amp;#8220;frequently used&amp;#8221; data&amp;nbsp;cached.&lt;/p&gt;
&lt;p&gt;On FreeBSD, &lt;span class="caps"&gt;ZFS&lt;/span&gt; is greedy by default - it will automatically use most of your system&amp;#8217;s &lt;span class="caps"&gt;RAM&lt;/span&gt; for the &lt;span class="caps"&gt;ARC&lt;/span&gt;, leaving only a small reserve for the kernel. On a dedicated file server, this is exactly what you want and no tuning is needed. But if you&amp;#8217;re running memory-hungry jails on the same machine (PostgreSQL, application runtimes), you need to &lt;strong&gt;restrict&lt;/strong&gt; the &lt;span class="caps"&gt;ARC&lt;/span&gt; so it doesn&amp;#8217;t starve your&amp;nbsp;applications:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check current ARC size&lt;/span&gt;
sysctl&lt;span class="w"&gt; &lt;/span&gt;kstat.zfs.misc.arcstats.size

&lt;span class="c1"&gt;# Restrict ARC to 8GB on a jail host (in /boot/loader.conf)&lt;/span&gt;
&lt;span class="c1"&gt;# Leave the rest for jails and applications&lt;/span&gt;
vfs.zfs.arc_max&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;8589934592&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For systems with an available &lt;span class="caps"&gt;SSD&lt;/span&gt;, &lt;strong&gt;&lt;span class="caps"&gt;L2ARC&lt;/span&gt;&lt;/strong&gt; extends the cache to &lt;span class="caps"&gt;SSD&lt;/span&gt; storage. It&amp;#8217;s a second-level cache: data evicted from the &lt;span class="caps"&gt;RAM&lt;/span&gt;-based &lt;span class="caps"&gt;ARC&lt;/span&gt; gets written to the &lt;span class="caps"&gt;L2ARC&lt;/span&gt; device, giving you a much larger effective&amp;nbsp;cache:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add an SSD as L2ARC&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;cache&lt;span class="w"&gt; &lt;/span&gt;nvd1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;L2ARC&lt;/span&gt; is most effective when your working set is larger than available &lt;span class="caps"&gt;RAM&lt;/span&gt; but fits on an &lt;span class="caps"&gt;SSD&lt;/span&gt;. For read-heavy workloads on spinning disks (databases, mail servers), an &lt;span class="caps"&gt;L2ARC&lt;/span&gt; can provide dramatic performance&amp;nbsp;improvements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; &lt;span class="caps"&gt;L2ARC&lt;/span&gt; isn&amp;#8217;t free. Every block cached in &lt;span class="caps"&gt;L2ARC&lt;/span&gt; requires an index pointer in your primary &lt;span class="caps"&gt;ARC&lt;/span&gt; (&lt;span class="caps"&gt;RAM&lt;/span&gt;). If you add a massive &lt;span class="caps"&gt;1TB&lt;/span&gt; NVMe as &lt;span class="caps"&gt;L2ARC&lt;/span&gt; to a system with limited &lt;span class="caps"&gt;RAM&lt;/span&gt;, the pointer overhead will starve the primary &lt;span class="caps"&gt;ARC&lt;/span&gt; and actually &lt;em&gt;degrade&lt;/em&gt; overall performance. Size your &lt;span class="caps"&gt;L2ARC&lt;/span&gt; proportionally to your available &lt;span class="caps"&gt;RAM&lt;/span&gt; - not to the size of the &lt;span class="caps"&gt;SSD&lt;/span&gt; you happen to have lying&amp;nbsp;around.&lt;/p&gt;
&lt;p&gt;Similarly, a &lt;strong&gt;&lt;span class="caps"&gt;SLOG&lt;/span&gt;&lt;/strong&gt; (Separate Log) device accelerates synchronous writes by providing a fast storage target for the &lt;span class="caps"&gt;ZFS&lt;/span&gt; Intent Log (&lt;span class="caps"&gt;ZIL&lt;/span&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add an SSD as SLOG&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;nvd2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is primarily useful for &lt;span class="caps"&gt;NFS&lt;/span&gt; servers, databases&amp;nbsp;with &lt;code&gt;fsync&lt;/code&gt;-heavy workloads, and iSCSI targets where synchronous write performance&amp;nbsp;matters.&lt;/p&gt;
&lt;h2 id="zfs-and-jails-better-together"&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; and Jails: Better&amp;nbsp;Together&lt;/h2&gt;
&lt;p&gt;If you read the &lt;a href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/"&gt;first article in this series&lt;/a&gt;, you know that Jails and &lt;span class="caps"&gt;ZFS&lt;/span&gt; are a natural pairing. Each jail gets its own dataset, which&amp;nbsp;means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Independent snapshots:&lt;/strong&gt; Snapshot a jail before upgrading its packages. Roll back if something breaks. No impact on other&amp;nbsp;jails.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instant cloning:&lt;/strong&gt; Want a test copy of your production mail&amp;nbsp;jail? &lt;code&gt;zfs clone&lt;/code&gt; creates one in seconds, sharing data with the original until&amp;nbsp;divergence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-jail quotas:&lt;/strong&gt; Prevent one jail from consuming all available&amp;nbsp;storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Independent compression and recordsize:&lt;/strong&gt; A jail running PostgreSQL&amp;nbsp;gets &lt;code&gt;recordsize=8k&lt;/code&gt;. A jail serving static files&amp;nbsp;gets &lt;code&gt;recordsize=1M&lt;/code&gt;. Each workload gets the optimal&amp;nbsp;setting.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Snapshot a jail before upgrade&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@pre-upgrade

&lt;span class="c1"&gt;# Clone a jail for testing&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver@pre-upgrade&lt;span class="w"&gt; &lt;/span&gt;tank/jails/mailserver-test

&lt;span class="c1"&gt;# Set per-jail storage quota&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;quota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20G&lt;span class="w"&gt; &lt;/span&gt;tank/jails/webmail

&lt;span class="c1"&gt;# Tune recordsize per workload&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;recordsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8k&lt;span class="w"&gt; &lt;/span&gt;tank/jails/database
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Cloning is particularly powerful. A clone is a writable snapshot - it starts sharing all data with the original and only consumes space as you make changes. Standing up a test environment from production data takes seconds, not&amp;nbsp;hours.&lt;/p&gt;
&lt;h2 id="common-pitfalls"&gt;Common&amp;nbsp;Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Not&amp;nbsp;setting &lt;code&gt;recordsize&lt;/code&gt; before importing data:&lt;/strong&gt;&amp;nbsp;Changing &lt;code&gt;recordsize&lt;/code&gt; only affects new writes. If you import a PostgreSQL database and &lt;em&gt;then&lt;/em&gt;&amp;nbsp;set &lt;code&gt;recordsize=8k&lt;/code&gt;, the existing data keeps the old block size. Set it&amp;nbsp;first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forgetting to scrub:&lt;/strong&gt; &lt;span class="caps"&gt;ZFS&lt;/span&gt; can only repair corruption it knows about. Without regular scrubs, corruption sits undetected until something tries to read the affected block - possibly months later when the redundant copy has also degraded. Scrub&amp;nbsp;weekly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Letting the pool get too full:&lt;/strong&gt; &lt;span class="caps"&gt;ZFS&lt;/span&gt; performance degrades as the pool fills up, particularly above 80% capacity. This is because &lt;span class="caps"&gt;ZFS&lt;/span&gt; uses a copy-on-write allocation strategy that becomes increasingly constrained as free space fragments. Monitor pool usage and expand before you hit&amp;nbsp;80%.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;dedup&lt;/code&gt; without understanding the cost:&lt;/strong&gt; &lt;span class="caps"&gt;ZFS&lt;/span&gt; supports block-level deduplication, but it requires enormous amounts of &lt;span class="caps"&gt;RAM&lt;/span&gt; for the deduplication table (roughly &lt;span class="caps"&gt;5GB&lt;/span&gt; of &lt;span class="caps"&gt;RAM&lt;/span&gt; per &lt;span class="caps"&gt;TB&lt;/span&gt; of deduplicated storage). Unless you have a specific, measured use case and the &lt;span class="caps"&gt;RAM&lt;/span&gt; to support it, don&amp;#8217;t enable dedup. Use compression instead - it&amp;#8217;s nearly&amp;nbsp;free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not using raw send for encrypted backups:&lt;/strong&gt; If&amp;nbsp;you &lt;code&gt;zfs send&lt;/code&gt; an encrypted dataset&amp;nbsp;without &lt;code&gt;--raw&lt;/code&gt;, the data is decrypted for the send and arrives unencrypted at the receiver. This might be what you want, or it might defeat the entire purpose of encrypting it. Be intentional about which mode you&amp;nbsp;use.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; replaces an entire stack of tools - filesystem, volume manager, &lt;span class="caps"&gt;RAID&lt;/span&gt;, backup - with a single system that checksums everything, heals itself, compresses transparently, encrypts natively, and replicates efficiently. It&amp;#8217;s the foundation that makes FreeBSD&amp;#8217;s jail-based architecture practical at scale: each jail gets its own dataset with its own properties, snapshots, and&amp;nbsp;quotas.&lt;/p&gt;
&lt;p&gt;The features that matter most in practice aren&amp;#8217;t the flashy ones. It&amp;#8217;s the scrub that catches a degrading disk before data loss. It&amp;#8217;s&amp;nbsp;the &lt;code&gt;.zfs&lt;/code&gt; directory that lets a user recover a deleted file without filing a ticket. It&amp;#8217;s the incremental send that replicates &lt;span class="caps"&gt;500GB&lt;/span&gt; of jail data by transferring &lt;span class="caps"&gt;2GB&lt;/span&gt; of changes. These are the things that let you sleep at&amp;nbsp;night.&lt;/p&gt;
&lt;p&gt;If you take nothing else away from this article: enable compression, schedule scrubs, automate snapshots with sanoid, and replicate off-site with syncoid. That&amp;#8217;s a complete data management strategy in four&amp;nbsp;decisions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/zfs/"&gt;FreeBSD Handbook - The Z File&amp;nbsp;System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=zpool&amp;amp;sektion=8"&gt;zpool(8) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=zfs&amp;amp;sektion=8"&gt;zfs(8) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/"&gt;OpenZFS&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jimsalterjrs/sanoid"&gt;Sanoid/Syncoid - Policy-driven snapshot&amp;nbsp;management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arstechnica.com/information-technology/2020/05/zfs-101-understanding-zfs-storage-and-performance/"&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; 101 - Understanding &lt;span class="caps"&gt;ZFS&lt;/span&gt; storage and performance (Ars&amp;nbsp;Technica)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20060428092023/http://www.opensolaris.org/os/community/zfs/docs/zfs_last.pdf"&gt;Jeff Bonwick - &lt;span class="caps"&gt;ZFS&lt;/span&gt;: The Last Word in Filesystems&lt;/a&gt; - the original Sun Microsystems&amp;nbsp;presentation&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="zfs"/><category term="filesystem"/><category term="storage"/><category term="encryption"/><category term="backup"/><category term="snapshots"/><category term="sanoid"/></entry><entry><title>FreeBSD Foundationals: Jails - From Chroot on Steroids to Full Virtual Networks</title><link href="https://blog.hofstede.it/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/" rel="alternate"/><published>2026-03-02T00:00:00+01:00</published><updated>2026-03-02T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-03-02:/freebsd-foundationals-jails-from-chroot-on-steroids-to-full-virtual-networks/</id><summary type="html">&lt;p&gt;The first in a series on FreeBSD fundamentals. This one covers Jails from the ground up: why they exist, how classic Jails differ from &lt;span class="caps"&gt;VNET&lt;/span&gt; Jails, what epair interfaces actually are, how bridges tie it all together, and what devfs rules do for your isolation story. Practical configurations&amp;nbsp;included.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;FreeBSD Jails have been around since FreeBSD 4.0, released in the year 2000. That makes them older than Linux cgroups, older than &lt;span class="caps"&gt;LXC&lt;/span&gt;, older than Docker, and older than most people&amp;#8217;s understanding of what &amp;#8220;containers&amp;#8221; even means. Yet they remain one of the most elegant and underappreciated isolation mechanisms available on any operating&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;This article is the first in a series called &lt;strong&gt;FreeBSD Foundationals&lt;/strong&gt; - covering core FreeBSD concepts that deserve more than a man page skim. We start with Jails because they&amp;#8217;re central to how FreeBSD is deployed in practice: from hosting providers to Netflix&amp;#8217;s &lt;span class="caps"&gt;CDN&lt;/span&gt; to small mail servers on rented&amp;nbsp;hardware.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;ve used Docker or &lt;span class="caps"&gt;LXC&lt;/span&gt; on Linux, some concepts will feel familiar. But Jails are not Linux containers with a different name. The design philosophy, the networking model, and the security boundaries are fundamentally different. Understanding those differences is the&amp;nbsp;point.&lt;/p&gt;
&lt;h2 id="what-problem-do-jails-solve"&gt;What Problem Do Jails&amp;nbsp;Solve?&lt;/h2&gt;
&lt;p&gt;Unix has always&amp;nbsp;had &lt;code&gt;chroot&lt;/code&gt;, which changes the apparent root directory for a process. A process inside a chroot&amp;nbsp;sees &lt;code&gt;/&lt;/code&gt; as whatever directory you pointed it at. It can&amp;#8217;t traverse above that point in the filesystem. Useful, but limited: chroot only isolates the filesystem view. A chrooted process still shares the network stack, the process table, &lt;span class="caps"&gt;IPC&lt;/span&gt;, and - crucially - the ability to escape the chroot if it has root&amp;nbsp;privileges.&lt;/p&gt;
&lt;p&gt;Jails extend this concept into a proper isolation boundary. A jailed process&amp;nbsp;gets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Its own filesystem root&lt;/strong&gt; (like chroot, but enforced at the kernel&amp;nbsp;level)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its own process space&lt;/strong&gt; - processes inside a jail can only see other processes in the same&amp;nbsp;jail&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its own hostname and network&amp;nbsp;identity&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restricted system calls&lt;/strong&gt; - no loading kernel modules, no mounting filesystems (unless explicitly allowed), no modifying the host&amp;#8217;s network&amp;nbsp;configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key insight: Jails are a kernel-enforced boundary, not a userspace trick. A root user inside a jail is &lt;em&gt;not&lt;/em&gt; equivalent to root on the host. The kernel itself refuses operations that would break the isolation. This is a fundamentally different security model from &amp;#8220;root in a Docker container&amp;#8221; on Linux, where escaping to the host has historically been a much shorter&amp;nbsp;path.&lt;/p&gt;
&lt;h2 id="classic-jails-vs-vnet-jails"&gt;Classic Jails vs &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;Jails&lt;/h2&gt;
&lt;p&gt;There are two distinct approaches to jail networking, and understanding the difference matters for every design decision that&amp;nbsp;follows.&lt;/p&gt;
&lt;h3 id="classic-ip-based-jails"&gt;Classic (&lt;span class="caps"&gt;IP&lt;/span&gt;-Based)&amp;nbsp;Jails&lt;/h3&gt;
&lt;p&gt;In a classic jail, you assign one or more &lt;span class="caps"&gt;IP&lt;/span&gt; addresses to the jail. The jail shares the host&amp;#8217;s network stack - it uses the host&amp;#8217;s interfaces, the host&amp;#8217;s routing table, the host&amp;#8217;s firewall. The kernel simply restricts which addresses the jailed processes can bind to and communicate&amp;nbsp;from.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Classic Jail networking:

  ┌───────────────────────────────────────────────┐
  │                  Host Kernel                  │
  │                                               │
  │   Network Stack (shared)                      │
  │   ┌───────────┐                               │
  │   │  vtnet0   │  203.0.113.50                 │
  │   │           │  10.0.0.11 (bound to jail A)  │
  │   │           │  10.0.0.12 (bound to jail B)  │
  │   └───────────┘                               │
  │                                               │
  │   ┌─────────────┐    ┌─────────────┐          │
  │   │   Jail A    │    │   Jail B    │          │
  │   │ can only    │    │ can only    │          │
  │   │ bind to     │    │ bind to     │          │
  │   │ 10.0.0.11   │    │ 10.0.0.12   │          │
  │   └─────────────┘    └─────────────┘          │
  └───────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is simple and lightweight. No extra interfaces, no bridges, no routing between host and jail. The jail just sees &amp;#8220;its&amp;#8221; &lt;span class="caps"&gt;IP&lt;/span&gt; on the host&amp;#8217;s interface. Classic jails work well when you need basic process isolation and don&amp;#8217;t need the jail to run its own firewall, its own routing, or anything that requires full control over a network&amp;nbsp;stack.&lt;/p&gt;
&lt;p&gt;The limitation: because the jail shares the host&amp;#8217;s network stack, it cannot run services that need raw socket access, can&amp;#8217;t configure its own routes, can&amp;#8217;t run its own &lt;span class="caps"&gt;DHCP&lt;/span&gt; client, and can&amp;#8217;t have its own firewall rules. The jail has an &lt;span class="caps"&gt;IP&lt;/span&gt; address, but it doesn&amp;#8217;t have a&amp;nbsp;network.&lt;/p&gt;
&lt;h3 id="vnet-virtual-network-jails"&gt;&lt;span class="caps"&gt;VNET&lt;/span&gt; (Virtual Network)&amp;nbsp;Jails&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;VNET&lt;/span&gt; jails get their own complete network stack. Not just an &lt;span class="caps"&gt;IP&lt;/span&gt; - a full, independent network stack with its own interfaces, its own routing table, its own &lt;span class="caps"&gt;ARP&lt;/span&gt; cache, and the ability to run network services that would be impossible in a classic&amp;nbsp;jail.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;VNET Jail networking:

  ┌────────────────────────────────────────────────────────────────┐
  │                        Host Kernel                             │
  │                                                                │
  │  Host Network Stack            Jail A Network Stack            │
  │  ┌──────────┐                  ┌──────────┐                    │
  │  │ vtnet0   │                  │  vnet0   │                    │
  │  │ bridge0  │                  │  lo0     │                    │
  │  │ e0a_jail │                  │ (own routing table)           │
  │  └──────────┘                  └──────────┘                    │
  │       │                              │                         │
  │       └──────── epair ───────────────┘                         │
  │            (virtual ethernet cable)                            │
  └────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is what you want for production workloads. A &lt;span class="caps"&gt;VNET&lt;/span&gt; jail can run its own &lt;span class="caps"&gt;DNS&lt;/span&gt; resolver, its own firewall (yes, pf inside a jail), handle its own IPv6 router advertisements, and generally behave like a separate machine. From the network&amp;#8217;s perspective, it &lt;em&gt;is&lt;/em&gt; a separate machine - just one that happens to share a kernel with the&amp;nbsp;host.&lt;/p&gt;
&lt;p&gt;The trade-off is complexity. &lt;span class="caps"&gt;VNET&lt;/span&gt; jails need virtual interfaces, bridges, and explicit routing between the host and jail networks. That complexity is what the rest of this article&amp;nbsp;explains.&lt;/p&gt;
&lt;h3 id="when-to-use-which"&gt;When to Use&amp;nbsp;Which&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Classic Jail&lt;/th&gt;
&lt;th&gt;&lt;span class="caps"&gt;VNET&lt;/span&gt; Jail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple process isolation&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service needs to bind to a specific &lt;span class="caps"&gt;IP&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service needs raw sockets&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jail needs its own routing table&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jail needs its own firewall&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jail runs &lt;span class="caps"&gt;DNS&lt;/span&gt; resolver (unbound, etc.)&lt;/td&gt;
&lt;td&gt;Tricky&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jail needs &lt;span class="caps"&gt;DHCP&lt;/span&gt; client&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance overhead&lt;/td&gt;
&lt;td&gt;Negligible&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For anything resembling a real server workload - a mail server, a web server with its own &lt;span class="caps"&gt;TLS&lt;/span&gt; stack, a database - &lt;span class="caps"&gt;VNET&lt;/span&gt; is the right choice. Classic jails still make sense for batch jobs, build environments, or situations where the jail is purely a filesystem and process&amp;nbsp;boundary.&lt;/p&gt;
&lt;h2 id="the-vnet-networking-model"&gt;The &lt;span class="caps"&gt;VNET&lt;/span&gt; Networking&amp;nbsp;Model&lt;/h2&gt;
&lt;p&gt;This is where most people get lost, so let&amp;#8217;s build the mental model piece by&amp;nbsp;piece.&lt;/p&gt;
&lt;h3 id="epair-interfaces-virtual-ethernet-cables"&gt;Epair Interfaces: Virtual Ethernet&amp;nbsp;Cables&lt;/h3&gt;
&lt;p&gt;An &lt;code&gt;epair&lt;/code&gt; is a pair of virtual Ethernet interfaces connected back-to-back. Think of it as a virtual Ethernet cable with a plug on each end. Creating one gives you two&amp;nbsp;interfaces: &lt;code&gt;epair0a&lt;/code&gt; and &lt;code&gt;epair0b&lt;/code&gt;. Whatever goes into one end comes out the&amp;nbsp;other.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create an epair - this gives you epair0a and epair0b&lt;/span&gt;
ifconfig&lt;span class="w"&gt; &lt;/span&gt;epair&lt;span class="w"&gt; &lt;/span&gt;create
&lt;span class="c1"&gt;# Output: epair0a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;a&lt;/code&gt; side stays on the host.&amp;nbsp;The &lt;code&gt;b&lt;/code&gt; side gets moved into the jail. They&amp;#8217;re connected at the kernel level - traffic sent&amp;nbsp;into &lt;code&gt;epair0a&lt;/code&gt; appears&amp;nbsp;on &lt;code&gt;epair0b&lt;/code&gt;, and vice versa. No routing, no forwarding decision - it&amp;#8217;s a direct Layer 2 link, like plugging an Ethernet cable between two physical&amp;nbsp;machines.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;  Host                          Jail
  ┌──────────┐                  ┌──────────┐
  │          │    epair &amp;quot;wire&amp;quot;  │          │
  │  e0a_web ├──────────────────┤ e0b_web  │
  │          │                  │ (→vnet0) │
  └──────────┘                  └──────────┘
     stays                        moves
     on host                      into jail
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In practice, you rename both sides to something meaningful&amp;nbsp;(like &lt;code&gt;e0a_mailserver&lt;/code&gt; and &lt;code&gt;e0b_mailserver&lt;/code&gt;) so you can tell which pair belongs to which jail when you have a dozen of&amp;nbsp;them.&lt;/p&gt;
&lt;h3 id="the-bridge-connecting-jails-together"&gt;The Bridge: Connecting Jails&amp;nbsp;Together&lt;/h3&gt;
&lt;p&gt;Each epair connects exactly one jail to the host. But jails usually need to talk to each other and to the outside world. That&amp;#8217;s&amp;nbsp;where &lt;code&gt;bridge0&lt;/code&gt; comes&amp;nbsp;in.&lt;/p&gt;
&lt;p&gt;A bridge is a virtual network switch. You add the host-side&amp;nbsp;(&lt;code&gt;a&lt;/code&gt;) end of each epair to the bridge, assign the bridge an &lt;span class="caps"&gt;IP&lt;/span&gt; address (which becomes the jails&amp;#8217; default gateway), and now all jails are on the same Layer 2 network&amp;nbsp;segment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                        ┌─────────────────────────────────────────┐
                        │              bridge0                    │
                        │          10.0.0.1/24                    │
                        │    2001:db8:1234:5678::1/64             │
  Internet              │                                         │
     │                  │  ┌──────────┐  ┌──────────┐  ┌────────┐ │
     │   NAT (pf)       │  │e0a_mail  │  │e0a_web   │  │e0a_mon │ │
  ┌──┴──────┐           │  └────┬─────┘  └────┬─────┘  └───┬────┘ │
  │ vtnet0  │           └───────┼─────────────┼────────────┼──────┘
  │ (host)  │                   │             │            │
  └─────────┘             ┌─────┴─────┐ ┌─────┴─────┐ ┌────┴──────┐
                          │ mailserver│ │  webmail  │ │ monitor   │
                          │ 10.0.0.11 │ │ 10.0.0.12 │ │ 10.0.0.13 │
                          └───────────┘ └───────────┘ └───────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This topology is the same as plugging physical servers into a physical switch. The bridge does &lt;span class="caps"&gt;MAC&lt;/span&gt; learning, forwards frames between ports, and the jails communicate at Layer 2. The host is the gateway - it has an &lt;span class="caps"&gt;IP&lt;/span&gt; on the bridge and runs pf to &lt;span class="caps"&gt;NAT&lt;/span&gt; the jail traffic out to the internet (for IPv4) and to filter what comes&amp;nbsp;in.&lt;/p&gt;
&lt;p&gt;For IPv6, if you have a large enough allocation, you can route a subnet directly to the bridge and give each jail a public IPv6 address. No &lt;span class="caps"&gt;NAT&lt;/span&gt; needed. This is exactly how dual-stack jail hosting should&amp;nbsp;work.&lt;/p&gt;
&lt;h3 id="putting-it-together-the-lifecycle-of-a-vnet-jail"&gt;Putting It Together: The Lifecycle of a &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;Jail&lt;/h3&gt;
&lt;p&gt;When a &lt;span class="caps"&gt;VNET&lt;/span&gt; jail starts, several things happen in sequence.&amp;nbsp;The &lt;code&gt;exec.prestart&lt;/code&gt; commands&amp;nbsp;in &lt;code&gt;jail.conf&lt;/code&gt; run &lt;em&gt;before&lt;/em&gt; the jail is created, on the&amp;nbsp;host:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create the&amp;nbsp;epair:&lt;/strong&gt; &lt;code&gt;ifconfig epair create&lt;/code&gt; produces &lt;code&gt;epair0a&lt;/code&gt; and &lt;code&gt;epair0b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rename and bring up both&amp;nbsp;ends:&lt;/strong&gt; &lt;code&gt;ifconfig epair0a up name e0a_mailserver&lt;/code&gt;, same for&amp;nbsp;the &lt;code&gt;b&lt;/code&gt; side&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add the host side to the&amp;nbsp;bridge:&lt;/strong&gt; &lt;code&gt;ifconfig bridge0 addm e0a_mailserver&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then the jail starts.&amp;nbsp;The &lt;code&gt;vnet.interface&lt;/code&gt; directive moves&amp;nbsp;the &lt;code&gt;b&lt;/code&gt;-side interface into the jail&amp;#8217;s network&amp;nbsp;stack:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Move &lt;code&gt;e0b_mailserver&lt;/code&gt; into the jail&lt;/strong&gt; - it disappears from the host&amp;#8217;s interface&amp;nbsp;list&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inside the&amp;nbsp;jail&amp;#8217;s &lt;code&gt;rc.conf&lt;/code&gt;:&lt;/strong&gt;&amp;nbsp;rename &lt;code&gt;e0b_mailserver&lt;/code&gt; to &lt;code&gt;vnet0&lt;/code&gt;, assign &lt;span class="caps"&gt;IP&lt;/span&gt; addresses, set the default route to the bridge &lt;span class="caps"&gt;IP&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When the jail&amp;nbsp;stops, &lt;code&gt;exec.poststop&lt;/code&gt; cleans&amp;nbsp;up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Destroy &lt;code&gt;e0a_mailserver&lt;/code&gt;&lt;/strong&gt; - this automatically destroys the&amp;nbsp;paired &lt;code&gt;e0b_mailserver&lt;/code&gt; too&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;An important detail about&amp;nbsp;the &lt;code&gt;exec.prestart&lt;/code&gt; lines: each line runs in its own shell invocation.&amp;nbsp;The &lt;code&gt;$epair0&lt;/code&gt; variable from&amp;nbsp;the &lt;code&gt;ifconfig epair create&lt;/code&gt; call only exists within the same line, which is why the create, rename and bring-up are chained&amp;nbsp;with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; into a single line in the config&amp;nbsp;below.&lt;/p&gt;
&lt;p&gt;Here&amp;#8217;s what this looks like&amp;nbsp;in &lt;code&gt;jail.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;mailserver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Filesystem and process isolation&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/jails/mailserver&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mailserver&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;enforce_statfs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;devfs_ruleset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="c1"&gt;# reset environment before executing jail commands&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Standard start/stop&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/sh /etc/rc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/sh /etc/rc.shutdown&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consolelog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jails&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mailserver_console&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# VNET networking&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;e0b_mailserver&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Create epair, rename, bridge - runs on the host before jail starts&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;epair0=\$(ifconfig epair create) &amp;amp;&amp;amp; ifconfig \${epair0} up name e0a_mailserver &amp;amp;&amp;amp; ifconfig \${epair0%a}b up name e0b_mailserver&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig bridge0 addm e0a_mailserver&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig e0a_mailserver description &lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt;vnet0 host interface for Jail mailserver&lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Clean up - runs on the host after jail stops&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poststop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig e0a_mailserver destroy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the&amp;nbsp;corresponding &lt;code&gt;rc.conf&lt;/code&gt; inside the jail&amp;nbsp;at &lt;code&gt;/jails/mailserver/etc/rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rename the interface that was moved in by vnet.interface&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_e0b_mailserver_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# IPv4&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.0.0.11 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.0.0.1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# IPv6 (public address, no NAT needed)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1234:5678::11/64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1234:5678::1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Description for ifconfig output&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jail interface for bridge0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Standard jail housekeeping&lt;/span&gt;
&lt;span class="nv"&gt;syslogd_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-ss&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_submit_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_outbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_msp_queue_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;cron_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-J 60&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;moused_nondefault_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;dumpdev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Actual services&lt;/span&gt;
&lt;span class="nv"&gt;postfix_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;dovecot_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rspamd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;local_unbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A few things to note about&amp;nbsp;the &lt;code&gt;rc.conf&lt;/code&gt; pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;syslogd_flags="-ss"&lt;/code&gt;&lt;/strong&gt; disables syslogd&amp;#8217;s network socket. Without this, syslogd inside the jail tries to open a socket it shouldn&amp;#8217;t&amp;nbsp;need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sendmail_*="NO"&lt;/code&gt;&lt;/strong&gt; disables all sendmail components. Even if you&amp;#8217;re running Postfix, the base system&amp;#8217;s sendmail cron jobs will try to start&amp;nbsp;otherwise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cron_flags="-J 60"&lt;/code&gt;&lt;/strong&gt; tells cron to stagger job execution with a random delay of up to 60 seconds. Prevents all jails from firing cron jobs at the exact same&amp;nbsp;second.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="host-configuration-enabling-the-infrastructure"&gt;Host Configuration: Enabling the&amp;nbsp;Infrastructure&lt;/h2&gt;
&lt;p&gt;The host needs a few things configured&amp;nbsp;in &lt;code&gt;/etc/rc.conf&lt;/code&gt; before jails can use &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;networking:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create the bridge at boot&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Bridge gets the gateway IP for the jail network&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.0.0.1 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1234:5678::1/64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Enable IP forwarding (the host is the jails&amp;#39; router)&lt;/span&gt;
&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Enable the jail subsystem and packet filter&lt;/span&gt;
&lt;span class="nv"&gt;jail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;gateway_enable&lt;/code&gt; lines are critical. Without them, the kernel won&amp;#8217;t forward packets between the bridge (jail network) and the external interface. Your jails would have &lt;span class="caps"&gt;IP&lt;/span&gt; addresses but no connectivity beyond the&amp;nbsp;bridge.&lt;/p&gt;
&lt;h2 id="firewalling-with-pf"&gt;Firewalling with&amp;nbsp;pf&lt;/h2&gt;
&lt;p&gt;With &lt;span class="caps"&gt;VNET&lt;/span&gt; jails, the host is the router. All traffic between jails and the internet passes through the host&amp;#8217;s network stack, which means pf on the host controls&amp;nbsp;everything.&lt;/p&gt;
&lt;h3 id="nat-for-ipv4"&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt; for&amp;nbsp;IPv4&lt;/h3&gt;
&lt;p&gt;Unless you have a large IPv4 allocation (unlikely and expensive), your jails will use private addresses on the bridge and &lt;span class="caps"&gt;NAT&lt;/span&gt; through the host&amp;#8217;s public &lt;span class="caps"&gt;IP&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.0.0.0/24&amp;quot;&lt;/span&gt;

&lt;span class="cp"&gt;# NAT jail traffic going to the internet&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The parentheses&amp;nbsp;around &lt;code&gt;($ext_if)&lt;/code&gt; tell pf to dynamically resolve the interface&amp;#8217;s current &lt;span class="caps"&gt;IP&lt;/span&gt;. If the host&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt; changes (&lt;span class="caps"&gt;DHCP&lt;/span&gt;), the &lt;span class="caps"&gt;NAT&lt;/span&gt; rule adapts&amp;nbsp;automatically.&lt;/p&gt;
&lt;h3 id="redirecting-inbound-traffic"&gt;Redirecting Inbound&amp;nbsp;Traffic&lt;/h3&gt;
&lt;p&gt;For services that need to be reachable from the internet over IPv4,&amp;nbsp;use &lt;code&gt;rdr&lt;/code&gt; to forward incoming connections to the jail&amp;#8217;s private&amp;nbsp;address:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;mail_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.0.0.11&amp;quot;&lt;/span&gt;

&lt;span class="cp"&gt;# SMTP from anywhere&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_ipv4&lt;/span&gt;

&lt;span class="cp"&gt;# IMAP and submission&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4190&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_ipv4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="ipv6-no-nat-needed"&gt;IPv6: No &lt;span class="caps"&gt;NAT&lt;/span&gt;&amp;nbsp;Needed&lt;/h3&gt;
&lt;p&gt;With IPv6, each jail has a globally routable address. No &lt;span class="caps"&gt;NAT&lt;/span&gt;,&amp;nbsp;no &lt;code&gt;rdr&lt;/code&gt; - just pass&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mail_ipv6 = &amp;quot;2001:db8:1234:5678::11&amp;quot;

pass in quick on $ext_if inet6 proto tcp from any to $mail_ipv6 \
    port 25 flags S/SA keep state
pass in quick on $ext_if inet6 proto tcp from any to $mail_ipv6 \
    port {143, 587, 4190} flags S/SA keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is one of the practical advantages of deploying jails on a host with IPv6: the networking model becomes dramatically simpler. Each jail is a first-class citizen on the internet, directly addressable, with filtering handled by pass/block rules rather than &lt;span class="caps"&gt;NAT&lt;/span&gt; and redirection&amp;nbsp;gymnastics.&lt;/p&gt;
&lt;h3 id="jail-egress"&gt;Jail&amp;nbsp;Egress&lt;/h3&gt;
&lt;p&gt;Jails also need to initiate outbound connections (&lt;span class="caps"&gt;DNS&lt;/span&gt; queries, package downloads, sending mail). Allow this with a broad egress rule on the&amp;nbsp;bridge:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;jail_net = &amp;quot;10.0.0.0/24&amp;quot;
jail_net6 = &amp;quot;2001:db8:1234:5678::/64&amp;quot;

&lt;span class="gh"&gt;#&lt;/span&gt; Allow jails to reach the internet, but not each other&amp;#39;s private range
pass in quick on bridge0 from $jail_net to ! $jail_net keep state
pass in quick on bridge0 inet6 from $jail_net6 to ! $jail_net6 keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;! $jail_net&lt;/code&gt; exclusion means this rule only matches traffic leaving the jail network towards the internet. Traffic between jails on the same bridge is switched at Layer 2 and never passes through pf in the default FreeBSD configuration (controlled&amp;nbsp;by &lt;code&gt;net.link.bridge.pfil_member&lt;/code&gt; and &lt;code&gt;net.link.bridge.pfil_bridge&lt;/code&gt; sysctls, both off by default). If you need to restrict inter-jail communication, that requires either separate bridges per jail or&amp;nbsp;enabling &lt;code&gt;pfil_member&lt;/code&gt; and adding explicit block rules on the bridge member&amp;nbsp;interfaces.&lt;/p&gt;
&lt;h2 id="devfs-rules-controlling-device-access"&gt;devfs Rules: Controlling Device&amp;nbsp;Access&lt;/h2&gt;
&lt;p&gt;When a jail&amp;nbsp;has &lt;code&gt;mount.devfs&lt;/code&gt; enabled, it gets&amp;nbsp;a &lt;code&gt;/dev&lt;/code&gt; filesystem. But you don&amp;#8217;t want a jail to see the host&amp;#8217;s disk devices, &lt;span class="caps"&gt;USB&lt;/span&gt; devices, or hardware random number generators it shouldn&amp;#8217;t touch. devfs rulesets control which device nodes appear inside the&amp;nbsp;jail.&lt;/p&gt;
&lt;p&gt;The relevant rulesets are defined&amp;nbsp;in &lt;code&gt;/etc/devfs.rules&lt;/code&gt; on the host. FreeBSD ships with a default set, and ruleset 4&amp;nbsp;(&lt;code&gt;devfsrules_jail&lt;/code&gt;) is specifically designed for&amp;nbsp;jails:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# View the built-in rulesets&lt;/span&gt;
devfs&lt;span class="w"&gt; &lt;/span&gt;rule&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;show
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ruleset 4 typically hides everything and then unhides only what a jail&amp;nbsp;needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/dev/null&lt;/code&gt;, &lt;code&gt;/dev/zero&lt;/code&gt;, &lt;code&gt;/dev/random&lt;/code&gt;, &lt;code&gt;/dev/urandom&lt;/code&gt; - standard Unix&amp;nbsp;devices&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/fd/*&lt;/code&gt; - file&amp;nbsp;descriptors&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/stdin&lt;/code&gt;, &lt;code&gt;/dev/stdout&lt;/code&gt;, &lt;code&gt;/dev/stderr&lt;/code&gt; - standard&amp;nbsp;streams&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What it &lt;em&gt;doesn&amp;#8217;t&lt;/em&gt;&amp;nbsp;include: &lt;code&gt;/dev/ad*&lt;/code&gt;, &lt;code&gt;/dev/da*&lt;/code&gt; (disk&amp;nbsp;devices), &lt;code&gt;/dev/mem&lt;/code&gt; (physical&amp;nbsp;memory), &lt;code&gt;/dev/kmem&lt;/code&gt; (kernel&amp;nbsp;memory), &lt;code&gt;/dev/io&lt;/code&gt; (I/O port access). A jailed process cannot read raw disk data or probe kernel memory, even as&amp;nbsp;root.&lt;/p&gt;
&lt;p&gt;Notably, ruleset 4 also does &lt;em&gt;not&lt;/em&gt;&amp;nbsp;unhide &lt;code&gt;/dev/bpf*&lt;/code&gt; (Berkeley Packet Filter devices). This matters for &lt;span class="caps"&gt;VNET&lt;/span&gt; jails: without bpf, tools&amp;nbsp;like &lt;code&gt;tcpdump&lt;/code&gt; and &lt;code&gt;dhclient&lt;/code&gt; inside the jail won&amp;#8217;t work. If you need either, you&amp;#8217;ll want a custom ruleset that adds bpf access - shown&amp;nbsp;below.&lt;/p&gt;
&lt;p&gt;You reference the ruleset&amp;nbsp;in &lt;code&gt;jail.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;devfs_ruleset = 4;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you need to customize device access (for example, giving a jail access&amp;nbsp;to &lt;code&gt;/dev/pf&lt;/code&gt; for its own packet filter,&amp;nbsp;or &lt;code&gt;/dev/zvol/*&lt;/code&gt; for &lt;span class="caps"&gt;ZFS&lt;/span&gt; volume access), you create a custom ruleset&amp;nbsp;in &lt;code&gt;/etc/devfs.rules&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[devfsrules_jail_with_pf=100]&lt;/span&gt;
&lt;span class="na"&gt;add include $devfsrules_hide_all&lt;/span&gt;
&lt;span class="na"&gt;add include $devfsrules_unhide_basic&lt;/span&gt;
&lt;span class="na"&gt;add include $devfsrules_unhide_login&lt;/span&gt;
&lt;span class="na"&gt;add path &amp;#39;bpf*&amp;#39; unhide&lt;/span&gt;
&lt;span class="na"&gt;add path pf unhide&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then reference it&amp;nbsp;as &lt;code&gt;devfs_ruleset = 100&lt;/code&gt; in the jail&amp;#8217;s&amp;nbsp;configuration.&lt;/p&gt;
&lt;h2 id="security-what-jails-actually-prevent"&gt;Security: What Jails Actually&amp;nbsp;Prevent&lt;/h2&gt;
&lt;p&gt;Jails enforce security at the kernel level. Here&amp;#8217;s what a root user inside a jail &lt;em&gt;cannot&lt;/em&gt; do, regardless of their privilege&amp;nbsp;level:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Load or unload kernel modules&lt;/strong&gt; -&amp;nbsp;no &lt;code&gt;kldload&lt;/code&gt;,&amp;nbsp;no &lt;code&gt;kldunload&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mount or unmount filesystems&lt;/strong&gt; -&amp;nbsp;unless &lt;code&gt;enforce_statfs&lt;/code&gt; and &lt;code&gt;allow.mount*&lt;/code&gt; are explicitly&amp;nbsp;relaxed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modify the host&amp;#8217;s network&lt;/strong&gt; - can&amp;#8217;t touch the host&amp;#8217;s interfaces, routes, or&amp;nbsp;firewall&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;See processes outside the jail&lt;/strong&gt;&amp;nbsp;- &lt;code&gt;ps aux&lt;/code&gt; shows only jail-local&amp;nbsp;processes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access raw disk devices&lt;/strong&gt; - blocked by devfs&amp;nbsp;rules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modify the running kernel&lt;/strong&gt; - no access&amp;nbsp;to &lt;code&gt;/dev/mem&lt;/code&gt;, &lt;code&gt;/dev/kmem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create device nodes&lt;/strong&gt; -&amp;nbsp;no &lt;code&gt;mknod&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modify the system clock&lt;/strong&gt; -&amp;nbsp;no &lt;code&gt;adjtime&lt;/code&gt;,&amp;nbsp;no &lt;code&gt;settimeofday&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="enforce_statfs"&gt;enforce_statfs&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;enforce_statfs&lt;/code&gt; directive controls what the jail can see about mounted&amp;nbsp;filesystems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enforce_statfs = 0&lt;/code&gt; - jail sees all mount points (don&amp;#8217;t use&amp;nbsp;this)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enforce_statfs = 1&lt;/code&gt; - jail sees mount points beneath its&amp;nbsp;root&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enforce_statfs = 2&lt;/code&gt; - jail sees only its own mount points&amp;nbsp;(recommended)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At level&amp;nbsp;2, &lt;code&gt;df&lt;/code&gt; inside the jail shows only the jail&amp;#8217;s own filesystems. The jail can&amp;#8217;t discover the host&amp;#8217;s &lt;span class="caps"&gt;ZFS&lt;/span&gt; pool layout, other jails&amp;#8217; mount points, or the overall disk&amp;nbsp;topology.&lt;/p&gt;
&lt;h2 id="managing-jails"&gt;Managing&amp;nbsp;Jails&lt;/h2&gt;
&lt;h3 id="starting-and-stopping"&gt;Starting and&amp;nbsp;Stopping&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Start all jails&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;start

&lt;span class="c1"&gt;# Start a specific jail&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;mailserver

&lt;span class="c1"&gt;# Stop a specific jail&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;webmail

&lt;span class="c1"&gt;# Restart (stop + start) a specific jail&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;mailserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="entering-a-running-jail"&gt;Entering a Running&amp;nbsp;Jail&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get a shell inside a jail&lt;/span&gt;
jexec&lt;span class="w"&gt; &lt;/span&gt;mailserver&lt;span class="w"&gt; &lt;/span&gt;/bin/sh

&lt;span class="c1"&gt;# Run a specific command&lt;/span&gt;
jexec&lt;span class="w"&gt; &lt;/span&gt;mailserver&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;update

&lt;span class="c1"&gt;# See running jails&lt;/span&gt;
jls
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;jls&lt;/code&gt; shows all active jails with their &lt;span class="caps"&gt;JID&lt;/span&gt; (Jail &lt;span class="caps"&gt;ID&lt;/span&gt;), &lt;span class="caps"&gt;IP&lt;/span&gt; addresses, hostname, and path. It&amp;#8217;s the FreeBSD equivalent&amp;nbsp;of &lt;code&gt;docker ps&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="filesystem-zfs-datasets"&gt;Filesystem: &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;nbsp;Datasets&lt;/h3&gt;
&lt;p&gt;While you can use any directory as a jail root, &lt;span class="caps"&gt;ZFS&lt;/span&gt; datasets are the standard approach for&amp;nbsp;production:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a dataset for your jail infrastructure&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;zroot/jails

&lt;span class="c1"&gt;# Create a dataset for a specific jail&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;zroot/jails/mailserver

&lt;span class="c1"&gt;# Install a fresh userland into it&lt;/span&gt;
bsdinstall&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;/jails/mailserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; gives you snapshots&amp;nbsp;(&lt;code&gt;zfs snapshot zroot/jails/mailserver@before-upgrade&lt;/code&gt;), cloning (create a new jail from an existing snapshot in seconds), quotas, compression, and send/receive for backup and migration. This is comparable to Docker&amp;#8217;s layered images, but without the overlay filesystem&amp;nbsp;complexity.&lt;/p&gt;
&lt;h3 id="nullfs-mounts-sharing-data-between-host-and-jail"&gt;Nullfs Mounts: Sharing Data Between Host and&amp;nbsp;Jail&lt;/h3&gt;
&lt;p&gt;Sometimes a jail needs access to data that lives outside its filesystem root.&amp;nbsp;The &lt;code&gt;nullfs&lt;/code&gt; mount (a loopback mount, similar&amp;nbsp;to &lt;code&gt;mount --bind&lt;/code&gt; on Linux) makes a host directory visible inside the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# In jail.conf&lt;/span&gt;
&lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/vmail /jails/mailserver/var/vmail nullfs rw 0 0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This mounts the&amp;nbsp;host&amp;#8217;s &lt;code&gt;/var/vmail&lt;/code&gt; at &lt;code&gt;/jails/mailserver/var/vmail&lt;/code&gt;. The mail jail sees it as a local directory. This is useful for shared mail spools, database directories on fast storage, or any data that should persist independently of the jail&amp;#8217;s&amp;nbsp;lifecycle.&lt;/p&gt;
&lt;h2 id="a-complete-example-three-jail-architecture"&gt;A Complete Example: Three-Jail&amp;nbsp;Architecture&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s a complete topology for a generic server running three &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;jails:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                                  Internet
                                     │
                            ┌────────┴────────┐
                            │     vtnet0      │
                            │   203.0.113.50  │
                            │  (public IPv4)  │
                            └────────┬────────┘
                                     │
                                 pf (NAT/RDR)
                                     │
     ┌───────────────────────────────┴──────────────────────────────┐
     │                          bridge0                             │
     │                      10.0.0.1/24                             │
     │               2001:db8:1234:5678::1/64                       │
     │                                                              │
     │  ┌───────────────┐  ┌──────────────┐  ┌───────────────────┐  │
     │  │e0a_mailserver │  │ e0a_webmail  │  │  e0a_monitor      │  │
     │  └───────┬───────┘  └──────┬───────┘  └─────────┬─────────┘  │
     └──────────┼─────────────────┼────────────────────┼────────────┘
                │ epair           │ epair              │ epair
     ┌──────────┴──────┐  ┌───────┴───────┐  ┌─────────┴──────────┐
     │   mailserver    │  │    webmail    │  │      monitor       │
     │   10.0.0.11     │  │   10.0.0.12   │  │     10.0.0.13      │
     │                 │  │               │  │                    │
     │   Postfix       │  │    nginx      │  │   Prometheus       │
     │   Dovecot       │  │    php-fpm    │  │   Grafana          │
     │   Rspamd        │  │               │  │                    │
     │   Unbound       │  │               │  │                    │
     └─────────────────┘  └───────────────┘  └────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each jail gets its own epair, its own &lt;span class="caps"&gt;IP&lt;/span&gt;, and runs only the services it needs. The mailserver jail handles &lt;span class="caps"&gt;SMTP&lt;/span&gt;, &lt;span class="caps"&gt;IMAP&lt;/span&gt;, and spam filtering. The webmail jail runs a web interface. The monitor jail runs Prometheus and Grafana for metrics&amp;nbsp;collection.&lt;/p&gt;
&lt;p&gt;This separation means a vulnerability in the webmail application (&lt;span class="caps"&gt;PHP&lt;/span&gt;, web framework) doesn&amp;#8217;t automatically grant access to the mail spool, the &lt;span class="caps"&gt;SMTP&lt;/span&gt; relay, or the host system. Each blast radius is&amp;nbsp;contained.&lt;/p&gt;
&lt;h2 id="common-pitfalls"&gt;Common&amp;nbsp;Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;gateway_enable&lt;/code&gt;:&lt;/strong&gt; Your jails will get IPs, the bridge will be up, but nothing can reach the internet. The host kernel silently drops forwarded&amp;nbsp;packets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not&amp;nbsp;creating &lt;code&gt;/var/log/jails/&lt;/code&gt;:&lt;/strong&gt; If&amp;nbsp;the &lt;code&gt;exec.consolelog&lt;/code&gt; directory doesn&amp;#8217;t exist, jail startup fails silently. Create it ahead of&amp;nbsp;time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/var/log/jails
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Interface naming conflicts:&lt;/strong&gt; If two jails try to create epairs simultaneously without unique names, you get collisions. The naming convention&amp;nbsp;in &lt;code&gt;jail.conf&lt;/code&gt; (&lt;code&gt;e0a_&amp;lt;jailname&amp;gt;&lt;/code&gt; / &lt;code&gt;e0b_&amp;lt;jailname&amp;gt;&lt;/code&gt;) avoids&amp;nbsp;this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;DNS&lt;/span&gt; resolution inside jails:&lt;/strong&gt; Jails don&amp;#8217;t automatically inherit the&amp;nbsp;host&amp;#8217;s &lt;code&gt;/etc/resolv.conf&lt;/code&gt;. Either copy it into the jail&amp;#8217;s root, run a local resolver like unbound inside the jail, or&amp;nbsp;point &lt;code&gt;resolv.conf&lt;/code&gt; at the bridge &lt;span class="caps"&gt;IP&lt;/span&gt; if the host runs a resolver&amp;nbsp;there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forgetting to enable jail services:&lt;/strong&gt;&amp;nbsp;After &lt;code&gt;bsdinstall jail&lt;/code&gt;, the base system is minimal. Services need to be installed&amp;nbsp;(&lt;code&gt;pkg -j mailserver install postfix&lt;/code&gt;) and enabled in the&amp;nbsp;jail&amp;#8217;s &lt;code&gt;rc.conf&lt;/code&gt;. Don&amp;#8217;t&amp;nbsp;forget &lt;code&gt;sshd_enable="YES"&lt;/code&gt; if you want to &lt;span class="caps"&gt;SSH&lt;/span&gt; into the jail directly&amp;nbsp;(though &lt;code&gt;jexec&lt;/code&gt; from the host is often&amp;nbsp;sufficient).&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Jails are not a historical curiosity. They&amp;#8217;re a production-grade isolation mechanism backed by over 25 years of kernel-level enforcement. &lt;span class="caps"&gt;VNET&lt;/span&gt; gives each jail a real network identity, epairs provide the plumbing, bridges create the fabric, and pf ties it all together with filtering and &lt;span class="caps"&gt;NAT&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;The mental model to carry away: a &lt;span class="caps"&gt;VNET&lt;/span&gt; jail is a separate machine that shares a kernel with the host. It has its own network stack, its own filesystem view, and its own process space. What it doesn&amp;#8217;t have is the ability to affect the host - and that&amp;#8217;s enforced by the kernel, not by&amp;nbsp;convention.&lt;/p&gt;
&lt;p&gt;The next article in this series will cover &lt;span class="caps"&gt;ZFS&lt;/span&gt; - the filesystem that makes jail management, snapshots, and backups actually practical at&amp;nbsp;scale.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook -&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=jail&amp;amp;sektion=8"&gt;jail(8) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=jail.conf&amp;amp;sektion=5"&gt;jail.conf(5) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=devfs.rules&amp;amp;sektion=5"&gt;devfs.rules(5) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=epair&amp;amp;sektion=4"&gt;epair(4) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=if_bridge&amp;amp;sektion=4"&gt;if_bridge(4) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=pf.conf&amp;amp;sektion=5"&gt;pf.conf(5) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://papers.freebsd.org/2000/phk-jails/"&gt;Poul-Henning Kamp, Robert &lt;span class="caps"&gt;N. M.&lt;/span&gt; Watson - Jails: Confining the Omnipotent Root&lt;/a&gt; - the original 2000 paper introducing&amp;nbsp;Jails&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="jails"/><category term="vnet"/><category term="networking"/><category term="containers"/><category term="isolation"/><category term="security"/><category term="pf"/></entry><entry><title>Neovim Crash Course for Sysadmins: The 20% That Solve 80% of the Pain</title><link href="https://blog.hofstede.it/neovim-crash-course-for-sysadmins-the-20-that-solve-80-of-the-pain/" rel="alternate"/><published>2026-02-27T00:00:00+01:00</published><updated>2026-02-27T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-27:/neovim-crash-course-for-sysadmins-the-20-that-solve-80-of-the-pain/</id><summary type="html">&lt;p&gt;Not another hjkl tutorial. This is the stuff you still get wrong after years of using Vim - efficient navigation, copy/paste that actually works, &lt;span class="caps"&gt;YAML&lt;/span&gt;-specific workflows, and the motions that turn config file editing from a chore into a&amp;nbsp;joy.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Neovim Crash Course for Sysadmins" src="https://blog.hofstede.it/images/2026-02-27-neovim-crashkurs-sysadmin-devops.png" title="Neovim Crash Course for Sysadmins"&gt;&lt;/p&gt;
&lt;p&gt;This is not a beginner&amp;#8217;s guide. If you&amp;#8217;ve used Vim daily for a year and still occasionally fight with paste behavior, you&amp;#8217;re the target audience. This article covers the things I got wrong (or never properly learned) after fifteen years of daily Vim usage - and the moment everything&amp;nbsp;clicked.&lt;/p&gt;
&lt;p&gt;The focus is practical: editing configuration files, &lt;span class="caps"&gt;YAML&lt;/span&gt;, shell scripts, and infrastructure code. No Vim philosophy lectures. No &amp;#8220;Vim is a language&amp;#8221; metaphors. Just the patterns that make the biggest difference for sysadmins and DevOps engineers who spend their days in&amp;nbsp;terminals.&lt;/p&gt;
&lt;h2 id="table-of-contents"&gt;Table of&amp;nbsp;Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#motions-that-actually-matter"&gt;Motions That Actually&amp;nbsp;Matter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#jumping-not-crawling"&gt;Jumping, Not&amp;nbsp;Crawling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-power-of-text-objects"&gt;The Power of Text&amp;nbsp;Objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#copypaste-the-thing-youve-been-doing-wrong"&gt;Copy/Paste: The Thing You&amp;#8217;ve Been Doing&amp;nbsp;Wrong&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#search-and-replace-scoped-to-what-you-need"&gt;Search and Replace: Scoped to What You&amp;nbsp;Need&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#yaml-the-pain-and-the-antidote"&gt;&lt;span class="caps"&gt;YAML&lt;/span&gt;: The Pain and the&amp;nbsp;Antidote&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#registers-and-macros-automation-for-the-lazy"&gt;Registers and Macros: Automation for the&amp;nbsp;Lazy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#splits-buffers-and-efficient-file-navigation"&gt;Splits, Buffers, and Efficient File&amp;nbsp;Navigation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-neovim-advantage-lsp-and-treesitter"&gt;The Neovim Advantage: &lt;span class="caps"&gt;LSP&lt;/span&gt; and&amp;nbsp;Treesitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#practical-combos-the-cheat-sheet"&gt;Practical Combos: The Cheat&amp;nbsp;Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wrapping-up"&gt;Wrapping&amp;nbsp;Up&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="motions-that-actually-matter"&gt;Motions That Actually&amp;nbsp;Matter&lt;/h2&gt;
&lt;p&gt;Everyone&amp;nbsp;knows &lt;code&gt;hjkl&lt;/code&gt;. Most people&amp;nbsp;know &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;. Here&amp;#8217;s what separates fast editing from &amp;#8220;I&amp;#8217;ll just use&amp;nbsp;sed&amp;#8221;:&lt;/p&gt;
&lt;h3 id="jumping-not-crawling"&gt;Jumping, Not&amp;nbsp;Crawling&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Motion&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f{char}&lt;/code&gt; / &lt;code&gt;F{char}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jump to next/previous occurrence&amp;nbsp;of &lt;code&gt;{char}&lt;/code&gt; on the line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t{char}&lt;/code&gt; / &lt;code&gt;T{char}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jump to just before&amp;nbsp;next/previous &lt;code&gt;{char}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;;&lt;/code&gt; and &lt;code&gt;,&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repeat&amp;nbsp;last &lt;code&gt;f&lt;/code&gt;/&lt;code&gt;t&lt;/code&gt; forward/backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jump to matching bracket/parenthesis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{&lt;/code&gt; / &lt;code&gt;}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jump to previous/next empty line (paragraph)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl-d&lt;/code&gt; / &lt;code&gt;Ctrl-u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Half-page down/up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt; / &lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search word under cursor forward/backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go to local definition of word under cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;f&lt;/code&gt;/&lt;code&gt;t&lt;/code&gt; family is criminally underused. Editing a firewall rule and need to change the port&amp;nbsp;number? &lt;code&gt;f:&lt;/code&gt; jumps to the&amp;nbsp;colon, &lt;code&gt;l&lt;/code&gt; moves one&amp;nbsp;right, &lt;code&gt;cw&lt;/code&gt; changes the word. Three keystrokes instead of&amp;nbsp;mashing &lt;code&gt;w&lt;/code&gt; twelve&amp;nbsp;times.&lt;/p&gt;
&lt;h3 id="the-power-of-text-objects"&gt;The Power of Text&amp;nbsp;Objects&lt;/h3&gt;
&lt;p&gt;This is where Vim stops being a text editor and starts being a scalpel. Text objects work with any operator&amp;nbsp;(&lt;code&gt;d&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt;):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iw&lt;/code&gt; / &lt;code&gt;aw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inner word / a word (includes trailing space)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ciw&lt;/code&gt; - change word under cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i"&lt;/code&gt; / &lt;code&gt;a"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inside quotes / around quotes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ci"&lt;/code&gt; - change contents of quoted string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i(&lt;/code&gt; / &lt;code&gt;a(&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inside parentheses / around parens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;di(&lt;/code&gt; - delete contents&amp;nbsp;between &lt;code&gt;()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i{&lt;/code&gt; / &lt;code&gt;a{&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inside braces / around braces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ci{&lt;/code&gt; - change block contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip&lt;/code&gt; / &lt;code&gt;ap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inner paragraph / a paragraph&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yip&lt;/code&gt; - yank entire paragraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;it&lt;/code&gt; / &lt;code&gt;at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inside &lt;span class="caps"&gt;XML&lt;/span&gt;/&lt;span class="caps"&gt;HTML&lt;/span&gt; tag / around tag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cit&lt;/code&gt; - change tag contents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="visualizing-text-objects"&gt;Visualizing Text&amp;nbsp;Objects&lt;/h3&gt;
&lt;p&gt;Text objects are easier to grasp with a concrete example. Given this&amp;nbsp;code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;function_name() {
    some_value = true
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;di{&lt;/code&gt; deletes &lt;code&gt;some_value = true&lt;/code&gt; - everything &lt;em&gt;between&lt;/em&gt; the braces,&amp;nbsp;leaving &lt;code&gt;{ }&lt;/code&gt; intact&lt;/li&gt;
&lt;li&gt;&lt;code&gt;da{&lt;/code&gt; deletes &lt;code&gt;{ some_value = true }&lt;/code&gt; - the braces &lt;span class="caps"&gt;AND&lt;/span&gt; their&amp;nbsp;contents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Same logic applies to&amp;nbsp;quotes: &lt;code&gt;di"&lt;/code&gt; deletes just the string&amp;nbsp;contents, &lt;code&gt;da"&lt;/code&gt; deletes the quotes too. For&amp;nbsp;paragraphs, &lt;code&gt;dip&lt;/code&gt; deletes the&amp;nbsp;text, &lt;code&gt;dap&lt;/code&gt; also removes the trailing blank&amp;nbsp;line.&lt;/p&gt;
&lt;p&gt;Real-world example - you have a Jinja2 template&amp;nbsp;variable &lt;code&gt;{{ old_value }}&lt;/code&gt; and need to change&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ci{  →  deletes old_value, leaves you in insert mode between the braces
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One combo instead of navigating, selecting, deleting,&amp;nbsp;typing.&lt;/p&gt;
&lt;h2 id="copypaste-the-thing-youve-been-doing-wrong"&gt;Copy/Paste: The Thing You&amp;#8217;ve Been Doing&amp;nbsp;Wrong&lt;/h2&gt;
&lt;p&gt;This was my fifteen-year blind&amp;nbsp;spot.&lt;/p&gt;
&lt;h3 id="the-problem"&gt;The&amp;nbsp;Problem&lt;/h3&gt;
&lt;p&gt;You visually select some lines&amp;nbsp;with &lt;code&gt;v&lt;/code&gt;, yank&amp;nbsp;with &lt;code&gt;y&lt;/code&gt;, move somewhere,&amp;nbsp;hit &lt;code&gt;p&lt;/code&gt; - and the text lands in the middle of a line instead of on a new line below. You undo,&amp;nbsp;try &lt;code&gt;P&lt;/code&gt;, and it ends up in the middle of the line &lt;em&gt;above&lt;/em&gt;.&amp;nbsp;Familiar?&lt;/p&gt;
&lt;h3 id="why-it-happens"&gt;Why It&amp;nbsp;Happens&lt;/h3&gt;
&lt;p&gt;Vim tracks whether a yank was &lt;strong&gt;characterwise&lt;/strong&gt;, &lt;strong&gt;linewise&lt;/strong&gt;, or &lt;strong&gt;blockwise&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;v&lt;/code&gt; (characterwise)&lt;/strong&gt;&amp;nbsp;→ &lt;code&gt;y&lt;/code&gt; creates a characterwise register&amp;nbsp;→ &lt;code&gt;p&lt;/code&gt; inserts&amp;nbsp;inline&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;V&lt;/code&gt; (linewise)&lt;/strong&gt;&amp;nbsp;→ &lt;code&gt;y&lt;/code&gt; creates a linewise register&amp;nbsp;→ &lt;code&gt;p&lt;/code&gt; inserts below current&amp;nbsp;line&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl-v&lt;/code&gt; (blockwise)&lt;/strong&gt;&amp;nbsp;→ &lt;code&gt;y&lt;/code&gt; creates a blockwise register&amp;nbsp;→ &lt;code&gt;p&lt;/code&gt; inserts as a&amp;nbsp;column&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The yank type determines the paste behavior. That&amp;#8217;s it. That&amp;#8217;s the whole&amp;nbsp;mystery.&lt;/p&gt;
&lt;h3 id="the-fix"&gt;The&amp;nbsp;Fix&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;V&lt;/code&gt; (Visual Line) when you want to copy/paste lines.&lt;/strong&gt; This is the single biggest quality-of-life&amp;nbsp;improvement:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;V       → enter visual line mode
jjj     → select lines downward
y       → yank (linewise)
}       → jump to where you want it
p       → paste below current line. Clean. New line. No surprises.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="quick-reference"&gt;Quick&amp;nbsp;Reference&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Want to&amp;#8230;&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Yank current line&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yank 5 lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5yy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yank a paragraph&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yank lines visually&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V&lt;/code&gt; + move&amp;nbsp;+ &lt;code&gt;y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paste below&lt;/td&gt;
&lt;td&gt;&lt;code&gt;p&lt;/code&gt; (with linewise yank)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paste above&lt;/td&gt;
&lt;td&gt;&lt;code&gt;P&lt;/code&gt; (with linewise yank)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force paste as new line&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:put&lt;/code&gt; (below)&amp;nbsp;/ &lt;code&gt;:put!&lt;/code&gt; (above)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;:put&lt;/code&gt; command is your safety net - it &lt;em&gt;always&lt;/em&gt; pastes as a new line, regardless of how you yanked. Useful when you&amp;#8217;ve already yanked characterwise and don&amp;#8217;t want to redo&amp;nbsp;it.&lt;/p&gt;
&lt;h3 id="named-registers-your-clipboard-slots"&gt;Named Registers: Your Clipboard&amp;nbsp;Slots&lt;/h3&gt;
&lt;p&gt;Vim has 26 named registers&amp;nbsp;(&lt;code&gt;"a&lt;/code&gt; through &lt;code&gt;"z&lt;/code&gt;). Use&amp;nbsp;them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s"&gt;&amp;quot;ayy    → yank current line into register a&lt;/span&gt;
&lt;span class="s"&gt;&amp;quot;bV3jy  → yank 4 lines into register b&lt;/span&gt;
&lt;span class="s"&gt;&amp;quot;ap     → paste from register a&lt;/span&gt;
&lt;span class="s"&gt;&amp;quot;bp     → paste from register b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The system clipboard is&amp;nbsp;register &lt;code&gt;"+&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;quot;+yy    → yank line to system clipboard
&amp;quot;+p     → paste from system clipboard
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For Neovim, you can unify the clipboard by adding to your&amp;nbsp;config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clipboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;unnamedplus&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;p&lt;/code&gt; use the system clipboard directly. Whether you want this is a matter of taste - I prefer keeping Vim&amp;#8217;s registers separate and&amp;nbsp;using &lt;code&gt;"+&lt;/code&gt; explicitly when I need the system&amp;nbsp;clipboard.&lt;/p&gt;
&lt;h2 id="search-and-replace-scoped-to-what-you-need"&gt;Search and Replace: Scoped to What You&amp;nbsp;Need&lt;/h2&gt;
&lt;h3 id="global-replace"&gt;Global&amp;nbsp;Replace&lt;/h3&gt;
&lt;p&gt;The classic everyone&amp;nbsp;knows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;%s&lt;span class="sr"&gt;/old/&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;/&lt;span class="k"&gt;g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;%&lt;/code&gt; means the entire&amp;nbsp;file. &lt;code&gt;g&lt;/code&gt; means all occurrences per&amp;nbsp;line.&lt;/p&gt;
&lt;h3 id="replace-only-in-visual-selection"&gt;Replace Only in Visual&amp;nbsp;Selection&lt;/h3&gt;
&lt;p&gt;Select a block&amp;nbsp;with &lt;code&gt;V&lt;/code&gt; (or &lt;code&gt;v&lt;/code&gt;), then&amp;nbsp;press &lt;code&gt;:&lt;/code&gt;. Vim auto-fills the&amp;nbsp;range:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;s&lt;span class="sr"&gt;/old/&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;/&lt;span class="k"&gt;g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This replaces only within the selected &lt;strong&gt;lines&lt;/strong&gt;. But here&amp;#8217;s the subtlety:&amp;nbsp;with &lt;code&gt;V&lt;/code&gt; (Visual Line), this replaces in the entire lines. If you selected&amp;nbsp;with &lt;code&gt;v&lt;/code&gt; (characterwise) and want to restrict the match to &lt;em&gt;exactly&lt;/em&gt; the highlighted&amp;nbsp;text:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;s&lt;span class="sr"&gt;/\%Vold/&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;/&lt;span class="k"&gt;g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;\%V&lt;/code&gt; atom constrains the match to the visual selection boundary, not just the lines it spans. Niche, but invaluable when you need surgical&amp;nbsp;precision.&lt;/p&gt;
&lt;h3 id="useful-flags"&gt;Useful&amp;nbsp;Flags&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All occurrences per line (not just first)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirm each replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Case insensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Case sensitive&amp;nbsp;(overrides &lt;code&gt;ignorecase&lt;/code&gt; setting)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Count matches without replacing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;c&lt;/code&gt; flag is&amp;nbsp;underrated. &lt;code&gt;:%s/foo/bar/gc&lt;/code&gt; lets you step through every match and decide&amp;nbsp;with &lt;code&gt;y&lt;/code&gt;/&lt;code&gt;n&lt;/code&gt;. Much safer than blind replace on a production&amp;nbsp;config.&lt;/p&gt;
&lt;h3 id="multi-file-replace-with-argdo-cfdo"&gt;Multi-File Replace with :argdo /&amp;nbsp;:cfdo&lt;/h3&gt;
&lt;p&gt;Need to replace across multiple files? Open them&amp;nbsp;and:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;args *.yaml
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;argdo&lt;/span&gt; %s&lt;span class="sr"&gt;/old_value/&lt;/span&gt;new_value/&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or use the quickfix list after a&amp;nbsp;grep:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;vimgrep&lt;/span&gt; &lt;span class="sr"&gt;/pattern/&lt;/span&gt; **/*.yaml
&lt;span class="p"&gt;:&lt;/span&gt;cfdo %s&lt;span class="sr"&gt;/pattern/&lt;/span&gt;replacement/&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="yaml-the-pain-and-the-antidote"&gt;&lt;span class="caps"&gt;YAML&lt;/span&gt;: The Pain and the&amp;nbsp;Antidote&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;YAML&lt;/span&gt; editing is where Vim either shines or makes you want to throw your keyboard. Here&amp;#8217;s how to make it&amp;nbsp;shine.&lt;/p&gt;
&lt;h3 id="indentation-the-basics"&gt;Indentation: The&amp;nbsp;Basics&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;      → indent current line one &lt;span class="nb"&gt;shiftwidth&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&amp;lt;&lt;/span&gt;      → unindent current line
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;ip     → indent entire paragraph
&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;     → indent &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="nb"&gt;lines&lt;/span&gt;
V5j&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;    → visual select &lt;span class="m"&gt;6&lt;/span&gt; &lt;span class="nb"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; indent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In Visual&amp;nbsp;mode, &lt;code&gt;&amp;gt;&lt;/code&gt; indents and then &lt;em&gt;exits&lt;/em&gt; Visual mode.&amp;nbsp;Use &lt;code&gt;gv&lt;/code&gt; to reselect the same area, or better - just&amp;nbsp;use &lt;code&gt;.&lt;/code&gt; to&amp;nbsp;repeat:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;V5j&amp;gt;    → select and indent
.       → indent the same lines again (still selected by `&amp;#39;&amp;lt;,&amp;#39;&amp;gt;`)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="set-up-your-yaml-defaults"&gt;Set Up Your &lt;span class="caps"&gt;YAML&lt;/span&gt;&amp;nbsp;Defaults&lt;/h3&gt;
&lt;p&gt;Essential in your Neovim&amp;nbsp;config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_create_autocmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;FileType&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;yaml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt_local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shiftwidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt_local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tabstop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt_local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;softtabstop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt_local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expandtab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt_local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursorcolumn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;-- vertical line at cursor column&lt;/span&gt;
  &lt;span class="kr"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;cursorcolumn&lt;/code&gt; draws a vertical highlight at your cursor&amp;#8217;s column position - an instant visual guide for &lt;span class="caps"&gt;YAML&lt;/span&gt; indentation&amp;nbsp;alignment.&lt;/p&gt;
&lt;h3 id="moving-blocks-up-and-down"&gt;Moving Blocks Up and&amp;nbsp;Down&lt;/h3&gt;
&lt;p&gt;Rearranging &lt;span class="caps"&gt;YAML&lt;/span&gt; keys or Ansible&amp;nbsp;tasks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;m&lt;/span&gt;&lt;span class="p"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;    → move current line down one
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;m&lt;/span&gt;&lt;span class="m"&gt;-2&lt;/span&gt;    → move current line &lt;span class="k"&gt;up&lt;/span&gt; one
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or visually select a block and move&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;V3j     → select &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="nb"&gt;lines&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;m&lt;/span&gt; &amp;#39;&lt;span class="p"&gt;&amp;gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; → move block down one line
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;m&lt;/span&gt; &amp;#39;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;-2&lt;/span&gt; → move block &lt;span class="k"&gt;up&lt;/span&gt; one line
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Bind these for&amp;nbsp;convenience:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Move lines up/down in visual mode&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;J&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;:m &amp;#39;&amp;gt;+1&amp;lt;CR&amp;gt;gv=gv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;K&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;:m &amp;#39;&amp;lt;-2&amp;lt;CR&amp;gt;gv=gv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now &lt;code&gt;J&lt;/code&gt;/&lt;code&gt;K&lt;/code&gt; in Visual mode moves the selected block and re-indents. Invaluable for reordering Ansible&amp;nbsp;tasks.&lt;/p&gt;
&lt;h3 id="folding-yaml-sections"&gt;Folding &lt;span class="caps"&gt;YAML&lt;/span&gt;&amp;nbsp;Sections&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldmethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;indent&amp;#39;&lt;/span&gt;   &lt;span class="c1"&gt;-- YAML&amp;#39;s structure IS its indentation&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldlevelstart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;     &lt;span class="c1"&gt;-- start with everything unfolded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;za&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle fold under cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close all folds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open all folds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zc&lt;/code&gt; / &lt;code&gt;zo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close / open one fold&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Folding by indent works perfectly for &lt;span class="caps"&gt;YAML&lt;/span&gt; because indentation &lt;em&gt;is&lt;/em&gt; the structure. A folded Ansible playbook shows you just the task names - like a table of&amp;nbsp;contents.&lt;/p&gt;
&lt;h2 id="registers-and-macros-automation-for-the-lazy"&gt;Registers and Macros: Automation for the&amp;nbsp;Lazy&lt;/h2&gt;
&lt;h3 id="the-dot-command"&gt;The Dot&amp;nbsp;Command&lt;/h3&gt;
&lt;p&gt;The most powerful single key in&amp;nbsp;Vim: &lt;code&gt;.&lt;/code&gt; repeats the last&amp;nbsp;change.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ciw&amp;quot;new_value&amp;quot;&amp;lt;Esc&amp;gt;   → change word to &amp;quot;new_value&amp;quot;
n                      → jump to next search match
.                      → apply the same change
n.n.n.                 → repeat across all matches
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This pattern - search, change, repeat - handles 90% of &amp;#8220;I need to change this in five places&amp;#8221; without reaching&amp;nbsp;for &lt;code&gt;:%s&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="quick-macros"&gt;Quick&amp;nbsp;Macros&lt;/h3&gt;
&lt;p&gt;Record&amp;nbsp;with &lt;code&gt;q{register}&lt;/code&gt;, replay&amp;nbsp;with &lt;code&gt;@{register}&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;qa&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recording&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nl"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jump&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;colon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;
&lt;span class="n"&gt;ciw&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;&amp;lt;C-r&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="err"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deleted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;close&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="n"&gt;Esc&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;
&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;down&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recording&lt;/span&gt;

&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nv"&gt;@a&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;replay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note: &lt;code&gt;ciw"&amp;lt;C-r&amp;gt;""&lt;/code&gt; is the native way&amp;nbsp;- &lt;code&gt;ciw&lt;/code&gt; deletes the&amp;nbsp;word, &lt;code&gt;"&lt;/code&gt; starts the quoted&amp;nbsp;string, &lt;code&gt;&amp;lt;C-r&amp;gt;"&lt;/code&gt; (Ctrl-r followed by double-quote) pastes the deleted word from the default register, and the&amp;nbsp;final &lt;code&gt;"&lt;/code&gt; closes&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;For &lt;span class="caps"&gt;YAML&lt;/span&gt; quoting, this is also a great use case for &lt;strong&gt;vim-surround&lt;/strong&gt; or &lt;strong&gt;mini.surround&lt;/strong&gt;. With&amp;nbsp;mini.surround, &lt;code&gt;ysiw"&lt;/code&gt; wraps the word under cursor in quotes in a single command. No macro&amp;nbsp;needed.&lt;/p&gt;
&lt;h3 id="replaying-the-last-macro"&gt;Replaying the Last&amp;nbsp;Macro&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@@&lt;/code&gt; repeats the last played macro. Combined with a&amp;nbsp;count: &lt;code&gt;20@@&lt;/code&gt; - fire and&amp;nbsp;forget.&lt;/p&gt;
&lt;h2 id="splits-buffers-and-efficient-file-navigation"&gt;Splits, Buffers, and Efficient File&amp;nbsp;Navigation&lt;/h2&gt;
&lt;p&gt;Sysadmins often edit multiple related files: the playbook &lt;em&gt;and&lt;/em&gt; the inventory, the nginx config &lt;em&gt;and&lt;/em&gt; the upstream&amp;nbsp;block, &lt;code&gt;pf.conf&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; the anchor&amp;nbsp;file.&lt;/p&gt;
&lt;h3 id="splits"&gt;Splits&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;sp&lt;/span&gt; filename    → horizontal split
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;vsp&lt;/span&gt; filename   → &lt;span class="k"&gt;vertical&lt;/span&gt; split
Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;w&lt;/span&gt; &lt;span class="k"&gt;h&lt;/span&gt;&lt;span class="sr"&gt;/j/&lt;/span&gt;&lt;span class="k"&gt;k&lt;/span&gt;/&lt;span class="k"&gt;l&lt;/span&gt; → navigate between splits
Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;        → equalize split sizes
Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;w&lt;/span&gt; _        → maximize current horizontal split
Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;w&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;        → maximize current &lt;span class="k"&gt;vertical&lt;/span&gt; split
Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;w&lt;/span&gt; &lt;span class="k"&gt;o&lt;/span&gt;        → &lt;span class="k"&gt;close&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; other splits &lt;span class="p"&gt;(&lt;/span&gt;:&lt;span class="k"&gt;only&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="buffers"&gt;Buffers&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;e&lt;/span&gt; filename     → open &lt;span class="k"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; current buffer
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;ls&lt;/span&gt;             → &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; open &lt;span class="k"&gt;buffers&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;bn&lt;/span&gt; &lt;span class="sr"&gt;/ :bp       → next /&lt;/span&gt; &lt;span class="k"&gt;previous&lt;/span&gt; buffer
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;b&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;partial&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;    → switch &lt;span class="k"&gt;to&lt;/span&gt; buffer by partial name &lt;span class="k"&gt;match&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;bd&lt;/span&gt;             → &lt;span class="k"&gt;close&lt;/span&gt; buffer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;:b&lt;/code&gt; with tab completion and partial matching is surprisingly&amp;nbsp;fast: &lt;code&gt;:b pf&amp;lt;Tab&amp;gt;&lt;/code&gt; jumps&amp;nbsp;to &lt;code&gt;pf.conf&lt;/code&gt; if it&amp;#8217;s&amp;nbsp;open.&lt;/p&gt;
&lt;h3 id="jumping-between-recent-files"&gt;Jumping Between Recent&amp;nbsp;Files&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;o&lt;/span&gt;  → &lt;span class="k"&gt;jump&lt;/span&gt; back through &lt;span class="k"&gt;jump&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
Ctrl&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;i&lt;/span&gt;  → &lt;span class="k"&gt;jump&lt;/span&gt; forward
``      → &lt;span class="k"&gt;jump&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; last position before latest &lt;span class="k"&gt;jump&lt;/span&gt;
`&amp;quot;      → &lt;span class="k"&gt;jump&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; position when last editing this &lt;span class="k"&gt;file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Ctrl-o&lt;/code&gt; is the &amp;#8220;undo for navigation.&amp;#8221; Jumped somewhere&amp;nbsp;with &lt;code&gt;*&lt;/code&gt; or &lt;code&gt;gd&lt;/code&gt; and want to go&amp;nbsp;back? &lt;code&gt;Ctrl-o&lt;/code&gt;. Multiple times if&amp;nbsp;needed.&lt;/p&gt;
&lt;h2 id="the-neovim-advantage-lsp-and-treesitter"&gt;The Neovim Advantage: &lt;span class="caps"&gt;LSP&lt;/span&gt; and&amp;nbsp;Treesitter&lt;/h2&gt;
&lt;p&gt;If you&amp;#8217;re still on vanilla Vim, here&amp;#8217;s what you&amp;#8217;re missing. Neovim&amp;#8217;s built-in &lt;span class="caps"&gt;LSP&lt;/span&gt; client and Treesitter integration transform &lt;span class="caps"&gt;YAML&lt;/span&gt;&amp;nbsp;editing:&lt;/p&gt;
&lt;h3 id="yaml-language-server"&gt;&lt;span class="caps"&gt;YAML&lt;/span&gt; Language&amp;nbsp;Server&lt;/h3&gt;
&lt;p&gt;With &lt;code&gt;yaml-language-server&lt;/code&gt; configured, you&amp;nbsp;get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Schema validation&lt;/strong&gt; - red squiggles when your Kubernetes manifest has a wrong&amp;nbsp;field&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto-completion&lt;/strong&gt;&amp;nbsp;- &lt;code&gt;Ctrl-x Ctrl-o&lt;/code&gt; suggests valid keys for your&amp;nbsp;schema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hover docs&lt;/strong&gt;&amp;nbsp;- &lt;code&gt;K&lt;/code&gt; shows documentation for the key under&amp;nbsp;cursor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A minimal &lt;span class="caps"&gt;LSP&lt;/span&gt; setup&amp;nbsp;in &lt;code&gt;init.lua&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Requires nvim-lspconfig plugin&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;lspconfig&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;yamlls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;schemas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://json.schemastore.org/ansible-playbook&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;playbook*.yml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://json.schemastore.org/github-workflow&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;.github/workflows/*.yml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="treesitter"&gt;Treesitter&lt;/h3&gt;
&lt;p&gt;Treesitter gives you syntax-aware text objects and&amp;nbsp;highlighting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nvim-treesitter.configs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="n"&gt;ensure_installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;yaml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;bash&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;lua&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;json&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;toml&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;python&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;highlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With Treesitter, indentation and folding become structure-aware instead of relying on raw indent levels. It&amp;#8217;s the difference between &amp;#8220;this line has 4 spaces&amp;#8221; and &amp;#8220;this is a mapping value inside a sequence&amp;nbsp;item.&amp;#8221;&lt;/p&gt;
&lt;h2 id="practical-combos-the-cheat-sheet"&gt;Practical Combos: The Cheat&amp;nbsp;Sheet&lt;/h2&gt;
&lt;p&gt;A reference for the patterns that come up daily in sysadmin&amp;nbsp;work:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Keystrokes&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Change a value in quotes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ci"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes contents between quotes, enters insert mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete everything inside braces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;di{&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clears the block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yank a whole &lt;span class="caps"&gt;YAML&lt;/span&gt; section&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yap&lt;/code&gt; or &lt;code&gt;yip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Direct paragraph yank (no visual mode needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comment 10 lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl-v 9j I# &amp;lt;Esc&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block&amp;nbsp;insert &lt;code&gt;#&lt;/code&gt; at start of 10 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncomment 10 lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl-v 9j ll x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block&amp;nbsp;select &lt;code&gt;#&lt;/code&gt; (two columns) and delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace in selection only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V&lt;/code&gt; select, &lt;code&gt;:'&amp;lt;,'&amp;gt;s/a/b/g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scoped search-replace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indent a block more&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V&lt;/code&gt; select, &lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shift right&amp;nbsp;by &lt;code&gt;shiftwidth&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V&lt;/code&gt; select, &lt;code&gt;:sort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alphabetical sort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove duplicate lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V&lt;/code&gt; select, &lt;code&gt;:sort u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sort and deduplicate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reformat a long line&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gq&lt;/code&gt; + motion&amp;nbsp;(&lt;code&gt;gqip&lt;/code&gt; for paragraph)&lt;/td&gt;
&lt;td&gt;Wrap&amp;nbsp;to &lt;code&gt;textwidth&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat last change everywhere&lt;/td&gt;
&lt;td&gt;&lt;code&gt;n.n.n.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search next, apply same edit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Save file you opened without sudo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:w !sudo tee %&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes via sudo without restarting the editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Undo by time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:earlier 5m&lt;/code&gt; / &lt;code&gt;:later 5m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jump to file state 5 minutes ago/ahead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping&amp;nbsp;Up&lt;/h2&gt;
&lt;p&gt;Vim rewards investment on a curve - the first week is brutal, the first year is productive, and the next decade is about discovering that you&amp;#8217;ve been doing basic things inefficiently the entire time. The patterns in this article represent the subset that matters most for infrastructure work: efficient navigation, correct yank/paste semantics, scoped replacements, and &lt;span class="caps"&gt;YAML&lt;/span&gt;-specific&amp;nbsp;workflows.&lt;/p&gt;
&lt;p&gt;The best way to internalize these is not to memorize the table above but to deliberately use &lt;em&gt;one new pattern&lt;/em&gt; each day until it becomes muscle memory. Start&amp;nbsp;with &lt;code&gt;V&lt;/code&gt; instead&amp;nbsp;of &lt;code&gt;v&lt;/code&gt;. Then graduate&amp;nbsp;to &lt;code&gt;ci"&lt;/code&gt;. Then text objects. Each one&amp;nbsp;compounds.&lt;/p&gt;
&lt;p&gt;If you want a ready-made Neovim configuration optimized for Ansible, Python, and &lt;span class="caps"&gt;YAML&lt;/span&gt; work, check out &lt;a href="https://codeberg.org/Larvitz/nvim-ansible"&gt;nvim-ansible on Codeberg&lt;/a&gt; - it implements most of the patterns and plugins discussed in this&amp;nbsp;article.&lt;/p&gt;
&lt;p&gt;And if you&amp;#8217;ve been using Vim for fifteen years and just learned&amp;nbsp;that &lt;code&gt;V&lt;/code&gt; solves your paste problems - you&amp;#8217;re in good&amp;nbsp;company.&lt;/p&gt;</content><category term="Linux"/><category term="neovim"/><category term="vim"/><category term="sysadmin"/><category term="devops"/><category term="yaml"/><category term="ansible"/><category term="productivity"/></entry><entry><title>Running Your Own AS: Going Multi-Homed with iBGP and three Transits</title><link href="https://blog.hofstede.it/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/" rel="alternate"/><published>2026-02-26T00:00:00+01:00</published><updated>2026-02-26T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-26:/running-your-own-as-going-multi-homed-with-ibgp-and-three-transits/</id><summary type="html">&lt;p&gt;Expanding a single &lt;span class="caps"&gt;BGP&lt;/span&gt; router into a two-PoP distributed network: adding a Vultr edge router with native &lt;span class="caps"&gt;BGP&lt;/span&gt; peering, three upstream &lt;span class="caps"&gt;GRE&lt;/span&gt; providers and iBGP to tie it together - plus the stateless &lt;span class="caps"&gt;PF&lt;/span&gt; rules that make transit routing actually&amp;nbsp;work.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-02-26-going-multihomed-bgp-freebsd.png" title="Going Multi-Homed: Header image"&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;previous article&lt;/a&gt; covered the basics: obtain an &lt;span class="caps"&gt;AS&lt;/span&gt; number and IPv6 prefix, build a single FreeBSD &lt;span class="caps"&gt;BGP&lt;/span&gt; router with two upstream providers, and tunnel the prefix to downstream servers. That setup works. But it has a property that feels wrong once you&amp;#8217;ve operated it for a while: all your traffic converges on one point. One machine, one location, two upstreams. Everything in, everything out, through the same&amp;nbsp;box.&lt;/p&gt;
&lt;p&gt;Multi-homing addresses this at multiple levels. More upstream providers give inbound path diversity. A second Point of Presence at a different network means traffic from different parts of the internet can enter through different doors, and you can engineer which traffic uses which path. This article documents the evolution from that single-router setup to a distributed network: two FreeBSD &lt;span class="caps"&gt;BGP&lt;/span&gt; routers connected by iBGP and three upstream &lt;span class="caps"&gt;GRE&lt;/span&gt;-based transit&amp;nbsp;providers.&lt;/p&gt;
&lt;p&gt;The routing concepts involved - iBGP, local-preference, next-hop-self, stateless transit filtering - are the same ones any real network operator deals with. The hardware is two virtual&amp;nbsp;machines.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on addresses:&lt;/strong&gt; All provider-assigned &lt;span class="caps"&gt;IP&lt;/span&gt; addresses, tunnel endpoints, and management IPs have been replaced with &lt;a href="https://www.rfc-editor.org/rfc/rfc5737"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 5737&lt;/a&gt; / &lt;a href="https://www.rfc-editor.org/rfc/rfc3849"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 3849&lt;/a&gt; documentation ranges. &lt;span class="caps"&gt;AS201379&lt;/span&gt; and the prefix 2a06:9801:1c::/48 are real public &lt;span class="caps"&gt;BGP&lt;/span&gt; resources and shown as-is. Upstream &lt;span class="caps"&gt;AS&lt;/span&gt; numbers (&lt;span class="caps"&gt;AS209533&lt;/span&gt;, &lt;span class="caps"&gt;AS209735&lt;/span&gt;, &lt;span class="caps"&gt;AS212895&lt;/span&gt;, &lt;span class="caps"&gt;AS64515&lt;/span&gt;) are equally visible in public routing&amp;nbsp;tables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-add-more"&gt;Why Add&amp;nbsp;More?&lt;/h2&gt;
&lt;p&gt;The original two-upstream setup had one failure domain and one point of path selection. Three practical problems motivated the&amp;nbsp;expansion:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Route diversity.&lt;/strong&gt; Different parts of the internet have different peering arrangements. A network that transits heavily through Vultr&amp;#8217;s infrastructure prefers paths that show up in Vultr&amp;#8217;s &lt;span class="caps"&gt;BGP&lt;/span&gt;. A network at &lt;span class="caps"&gt;DE&lt;/span&gt;-&lt;span class="caps"&gt;CIX&lt;/span&gt; Frankfurt prefers the shorter path through a Frankfurt exchange point. With one upstream set, you can only be optimally reachable for the networks whose traffic happens to prefer your specific&amp;nbsp;providers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Single point of failure.&lt;/strong&gt; One router means one failure domain. A reboot or misconfiguration takes down all reachability&amp;nbsp;simultaneously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traffic engineering granularity.&lt;/strong&gt; &lt;span class="caps"&gt;AS&lt;/span&gt;-path prepending lets you make one path look less preferred, but only when the receiving network actually has a choice of paths. Multiple announcement points give that&amp;nbsp;choice.&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The setup has two tiers: a core router&amp;nbsp;(&lt;code&gt;hobgp&lt;/code&gt;) at a Hetzner facility in Nuremberg that handles three &lt;span class="caps"&gt;GRE&lt;/span&gt; upstreams and an edge router&amp;nbsp;(&lt;code&gt;vtbgp&lt;/code&gt;) on Vultr in Frankfurt that announces our prefix natively into Vultr&amp;#8217;s network. The two routers are connected by a &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel running&amp;nbsp;iBGP.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    ┌──────────────────────────────────────────────────┐
                    │                Default-Free Zone                  │
                    └──┬─────────────┬──────────────┬──────────────────┘
                       │             │              │
                   AS209533      AS209735       AS212895
                   (iFog)        (Lagrange)     (route64)
                       │             │              │
                  GRE tunnel    GRE tunnel     GRE tunnel
                       │             │              │
                  ┌────┴─────────────┴──────────────┴────────────┐
                  │                  hobgp (Core)                 │
                  │          FreeBSD + FRR, AS201379              │
                  │          2a06:9801:1c::/48                    │
                  └──────────────────────┬────────────────────────┘
                                         │
                                    GIF tunnel (iBGP)
                                         │
                  ┌──────────────────────┴────────────────────────┐
                  │                  vtbgp (Edge)                  │
                  │          FreeBSD + FRR, AS201379              │
                  │          Native BGP → AS64515 (Vultr)          │
                  └───────────────────────────────────────────────┘
                                         │
                                     AS64515
                                      (Vultr)
                                         │
                              ┌──────────┴──────────┐
                              │   Default-Free Zone   │
                              └───────────────────────┘

hobgp also tunnels downstream (as in the previous article):
                           ┌────────────┐
                           │   radon    │
                           │  :1000::/64│
                           └────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key routing properties of this&amp;nbsp;design: &lt;code&gt;hobgp&lt;/code&gt; is the actual forwarding hub - all traffic to and from downstream servers flows through&amp;nbsp;it. &lt;code&gt;vtbgp&lt;/code&gt; is an announcement-only node; traffic that arrives there is immediately sent through the iBGP tunnel&amp;nbsp;to &lt;code&gt;hobgp&lt;/code&gt; for forwarding. This asymmetry is intentional and efficient: Vultr&amp;#8217;s infrastructure sees our prefix announced locally, which shortens the path for Vultr-connected networks, without&amp;nbsp;requiring &lt;code&gt;vtbgp&lt;/code&gt; to carry the full routing table or forward arbitrary internet&amp;nbsp;traffic.&lt;/p&gt;
&lt;h2 id="the-core-router-hobgp"&gt;The Core Router:&amp;nbsp;hobgp&lt;/h2&gt;
&lt;h3 id="network-configuration"&gt;Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;rc.conf&lt;/code&gt; now manages three &lt;span class="caps"&gt;GRE&lt;/span&gt; upstreams and a &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel to the edge router, in addition to the downstream server tunnels from the original&amp;nbsp;setup:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hobgp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kld_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;if_gif if_gre&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Physical interface (Hetzner)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.10/32 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1c19::1/64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Loopback: router&amp;#39;s address within our /48&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_lo0_alias0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c::1 prefixlen 64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Tunnel interfaces&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0 gif1 gre0 gre1 gre2&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GRE to iFog (BGPTunnel.com free transit)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 198.51.100.44&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:300::2 prefixlen 126&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Transit-iFog-Frankfurt BGPTunnel&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GRE to Lagrange (UK transit, backup path)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 198.51.100.45&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre1_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 fd00:ca::2 prefixlen 64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre1_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Transit-Lagrange-UK&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GRE to route64.org (Frankfurt transit)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 198.51.100.46&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre2_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:400::2 prefixlen 64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre2_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Transit-route64-Frankfurt&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF to downstream VPS (radon)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 203.0.113.10 mtu 1480&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:ffff::1 2a06:9801:1c:ffff::2 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Tunnel-to-Radon-Server&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF to Vultr edge router (vtbgp) - iBGP link&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 203.0.113.20 mtu 1480&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif1_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:fffe::1 2a06:9801:1c:fffe::2 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif1_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Tunnel-to-Vultr-Edge&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Routing&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myblock cloud vtbgp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_myblock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48 -reject&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_cloud&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c:1000::/64 2a06:9801:1c:ffff::2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_vtbgp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c:fffe::2/128 -interface gif1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The iBGP link (gif1) uses addresses from our own /48&amp;nbsp;(&lt;code&gt;2a06:9801:1c:fffe::/64&lt;/code&gt;). This is convenient - those addresses are already routable within our infrastructure, so the iBGP session doesn&amp;#8217;t depend on any provider-assigned addresses being&amp;nbsp;reachable.&lt;/p&gt;
&lt;h3 id="frr-configuration"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;The &lt;span class="caps"&gt;FRR&lt;/span&gt; configuration grows to four &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions. The substantive additions over the original are local-preference assignment on inbound route-maps, and the iBGP session to&amp;nbsp;vtbgp:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;frr version 10.5.1
frr defaults traditional
hostname hobgp
log syslog informational
service integrated-vtysh-config
!
ipv6 prefix-list PL-MY-NET seq 5 permit 2a06:9801:1c::/48
!
! [PL-BOGONS: same comprehensive bogon filter as original - see previous article]
!
route-map RM-IFOG-BGPTUNNEL-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
 set local-preference 150
exit
!
route-map RM-IFOG-BGPTUNNEL-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-LAGRANGE-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
exit
!
route-map RM-LAGRANGE-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
 set as-path prepend 201379 201379
exit
!
route-map RM-ROUTE64-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
exit
!
route-map RM-ROUTE64-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-IBGP-IN permit 10
 set local-preference 200
exit
!
route-map RM-IBGP-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
ipv6 route 2a06:9801:1c::/48 blackhole
ipv6 route 2a06:9801:1c:fffe::2/128 gif1
!
router bgp 201379
 bgp router-id 198.51.100.10
 no bgp default ipv4-unicast
 neighbor 2001:db8:300::1 remote-as 209533
 neighbor 2001:db8:300::1 description Upstream-iFog-Frankfurt
 neighbor 2001:db8:300::1 update-source 2001:db8:300::2
 neighbor fd00:ca::1 remote-as 209735
 neighbor fd00:ca::1 description Upstream-Lagrange-UK
 neighbor fd00:ca::1 update-source fd00:ca::2
 neighbor 2001:db8:400::1 remote-as 212895
 neighbor 2001:db8:400::1 description Upstream-Route64-FRA
 neighbor 2001:db8:400::1 update-source 2001:db8:400::2
 neighbor 2a06:9801:1c:fffe::2 remote-as 201379
 neighbor 2a06:9801:1c:fffe::2 description Edge-Vultr-Frankfurt
 neighbor 2a06:9801:1c:fffe::2 update-source 2a06:9801:1c:fffe::1
 !
 address-family ipv6 unicast
  network 2a06:9801:1c::/48
  neighbor 2001:db8:300::1 activate
  neighbor 2001:db8:300::1 soft-reconfiguration inbound
  neighbor 2001:db8:300::1 maximum-prefix 250000 90 restart 30
  neighbor 2001:db8:300::1 route-map RM-IFOG-BGPTUNNEL-IN in
  neighbor 2001:db8:300::1 route-map RM-IFOG-BGPTUNNEL-OUT out
  neighbor fd00:ca::1 activate
  neighbor fd00:ca::1 soft-reconfiguration inbound
  neighbor fd00:ca::1 maximum-prefix 250000 90 restart 30
  neighbor fd00:ca::1 route-map RM-LAGRANGE-IN in
  neighbor fd00:ca::1 route-map RM-LAGRANGE-OUT out
  neighbor 2001:db8:400::1 activate
  neighbor 2001:db8:400::1 soft-reconfiguration inbound
  neighbor 2001:db8:400::1 maximum-prefix 250000 90 restart 30
  neighbor 2001:db8:400::1 route-map RM-ROUTE64-IN in
  neighbor 2001:db8:400::1 route-map RM-ROUTE64-OUT out
  neighbor 2a06:9801:1c:fffe::2 activate
  neighbor 2a06:9801:1c:fffe::2 soft-reconfiguration inbound
  neighbor 2a06:9801:1c:fffe::2 route-map RM-IBGP-IN in
  neighbor 2a06:9801:1c:fffe::2 route-map RM-IBGP-OUT out
 exit-address-family
exit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="local-preference-the-outbound-traffic-hierarchy"&gt;Local Preference: The Outbound Traffic&amp;nbsp;Hierarchy&lt;/h4&gt;
&lt;p&gt;Local-preference (&lt;span class="caps"&gt;LP&lt;/span&gt;) is the primary mechanism for outbound path selection within an &lt;span class="caps"&gt;AS&lt;/span&gt;. &lt;span class="caps"&gt;BGP&lt;/span&gt; evaluates &lt;span class="caps"&gt;LP&lt;/span&gt; before &lt;span class="caps"&gt;AS&lt;/span&gt;-path length, making it the dominant decision factor. The hierarchy here&amp;nbsp;is:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Neighbor&lt;/th&gt;
&lt;th&gt;Local Preference&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vtbgp (iBGP/Vultr)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Preferred exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iFog BGPTunnel&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;Secondary exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lagrange&lt;/td&gt;
&lt;td&gt;100 (default)&lt;/td&gt;
&lt;td&gt;Tertiary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;route64&lt;/td&gt;
&lt;td&gt;100 (default)&lt;/td&gt;
&lt;td&gt;Tertiary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With &lt;span class="caps"&gt;LP&lt;/span&gt; 200 on iBGP-learned routes, hobgp prefers to send outbound traffic through vtbgp and into Vultr&amp;#8217;s network wherever vtbgp has a route. iFog serves as backup when vtbgp lacks a route or the tunnel is down. Lagrange and route64 act as last resort, carrying traffic to parts of the internet not well-served by the other&amp;nbsp;two.&lt;/p&gt;
&lt;p&gt;For inbound traffic - what the rest of the internet chooses - the tool is different: you control what you announce and how attractive you make&amp;nbsp;it. &lt;code&gt;RM-LAGRANGE-OUT&lt;/code&gt; adds two extra prepends&amp;nbsp;(&lt;code&gt;set as-path prepend 201379 201379&lt;/code&gt;), making our prefix appear three &lt;span class="caps"&gt;AS&lt;/span&gt;-hops away via Lagrange. Most networks see a shorter path via iFog or Vultr and prefer those, demoting Lagrange to a true&amp;nbsp;backup.&lt;/p&gt;
&lt;h4 id="a-note-on-free-transit"&gt;A Note on Free&amp;nbsp;Transit&lt;/h4&gt;
&lt;p&gt;Both iFog&amp;#8217;s &lt;a href="https://bgptunnel.com"&gt;BGPTunnel.com&lt;/a&gt; service and &lt;a href="https://route64.org"&gt;route64.org&lt;/a&gt; offer free &lt;span class="caps"&gt;BGP&lt;/span&gt; transit aimed at hobbyists and researchers. BGPTunnel provides a &lt;span class="caps"&gt;GRE&lt;/span&gt; tunnel to iFog&amp;#8217;s Frankfurt infrastructure and announces your prefix to their peers. route64 works similarly. Neither carries SLAs suitable for production, but for learning and redundancy in a hobby setup they&amp;#8217;re genuinely useful. The community around these services is active and documentation is&amp;nbsp;good.&lt;/p&gt;
&lt;h2 id="the-edge-router-vtbgp"&gt;The Edge Router:&amp;nbsp;vtbgp&lt;/h2&gt;
&lt;p&gt;The second router runs on Vultr in Frankfurt. Its role is narrow: announce 2a06:9801:1c::/48 into Vultr&amp;#8217;s network and pass routing information back to hobgp via iBGP. It does not forward arbitrary internet&amp;nbsp;traffic.&lt;/p&gt;
&lt;h3 id="why-vultr"&gt;Why&amp;nbsp;Vultr?&lt;/h3&gt;
&lt;p&gt;Vultr offers a &lt;span class="caps"&gt;BGP&lt;/span&gt; service where customers announce their own prefixes through Vultr&amp;#8217;s infrastructure. Vultr&amp;#8217;s network (&lt;span class="caps"&gt;AS20473&lt;/span&gt; for customer connectivity, &lt;span class="caps"&gt;AS64515&lt;/span&gt; for &lt;span class="caps"&gt;BGP&lt;/span&gt; peering sessions) has good upstream diversity and significant peering at major exchanges. Networks that peer with or transit through Vultr have a direct path to a locally-announced prefix, resulting in measurably shorter routes than if they had to reach across to an independent transit&amp;nbsp;provider.&lt;/p&gt;
&lt;h3 id="network-configuration_1"&gt;Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtbgp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kld_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;if_gif tcpmd5&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Physical interface (Vultr-assigned)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 203.0.113.20/23 -rxcsum -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:f480::20/64 -rxcsum6 -tso6 accept_rtadv&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;203.0.113.1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF tunnel back to core (hobgp)&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 203.0.113.20 198.51.100.10 mtu 1480&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:fffe::2 2a06:9801:1c:fffe::1 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Uplink-to-hobgp&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Routing&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myblock vultrbgp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_myblock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48 -interface gif0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_vultrbgp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:19f0:ffff::1 fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;frr_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipsec_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rtsold_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rtsold_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-aF&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three details stand&amp;nbsp;out:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ipv6_route_myblock&lt;/code&gt;&lt;/strong&gt; points the entire /48 at gif0. Any traffic for our prefix arriving at vtbgp via Vultr&amp;#8217;s infrastructure gets forwarded through the tunnel to hobgp, which handles actual routing to downstream&amp;nbsp;servers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ipv6_route_vultrbgp&lt;/code&gt;&lt;/strong&gt; is a host route to Vultr&amp;#8217;s &lt;span class="caps"&gt;BGP&lt;/span&gt; peering address&amp;nbsp;(&lt;code&gt;2001:19f0:ffff::1&lt;/code&gt;) via the link-local next-hop. Vultr&amp;#8217;s peering address isn&amp;#8217;t directly connected - it&amp;#8217;s reachable via Vultr&amp;#8217;s own routing - so this static route tells the kernel where to send packets destined for it. &lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;uses &lt;code&gt;ebgp-multihop 2&lt;/code&gt; to allow the &lt;span class="caps"&gt;BGP&lt;/span&gt; session to traverse that extra&amp;nbsp;hop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;rtsold&lt;/code&gt;&lt;/strong&gt; handles &lt;span class="caps"&gt;SLAAC&lt;/span&gt; for vtbgp&amp;#8217;s provider-assigned IPv6 on vtnet0, receiving the gateway via router&amp;nbsp;advertisements.&lt;/p&gt;
&lt;p&gt;A reader might wonder how vtbgp reaches the internet for its own traffic - software updates, &lt;span class="caps"&gt;DNS&lt;/span&gt;, the &lt;span class="caps"&gt;BGP&lt;/span&gt; session setup itself - given that it deliberately doesn&amp;#8217;t install the 240,000 &lt;span class="caps"&gt;BGP&lt;/span&gt; routes into the kernel. The answer is the&amp;nbsp;IPv4 &lt;code&gt;defaultrouter="203.0.113.1"&lt;/code&gt; in &lt;code&gt;rc.conf&lt;/code&gt;. vtbgp&amp;#8217;s own outbound traffic uses a plain static default route provided by Vultr, completely independent of the &lt;span class="caps"&gt;BGP&lt;/span&gt; routing table. The &lt;span class="caps"&gt;BGP&lt;/span&gt; table exists solely for &lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;#8217;s own decision-making and for propagation to hobgp via&amp;nbsp;iBGP.&lt;/p&gt;
&lt;h3 id="tcp-md5-authentication"&gt;&lt;span class="caps"&gt;TCP&lt;/span&gt;-&lt;span class="caps"&gt;MD5&lt;/span&gt;&amp;nbsp;Authentication&lt;/h3&gt;
&lt;p&gt;Vultr requires &lt;span class="caps"&gt;TCP&lt;/span&gt;-&lt;span class="caps"&gt;MD5&lt;/span&gt; authentication on &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions. This is decades-old, &lt;span class="caps"&gt;MD5&lt;/span&gt; is not modern, and it&amp;#8217;s standard across the industry - practically every carrier requires it for &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions. It prevents trivial session disruption from third parties who don&amp;#8217;t know the shared&amp;nbsp;key.&lt;/p&gt;
&lt;p&gt;On FreeBSD, &lt;span class="caps"&gt;TCP&lt;/span&gt;-&lt;span class="caps"&gt;MD5&lt;/span&gt; is implemented via the IPsec subsystem. The security associations live&amp;nbsp;in &lt;code&gt;/etc/ipsec.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;add 2001:db8:f480::20 2001:19f0:ffff::1 tcp 0x1000 -A tcp-md5 &amp;quot;your-shared-secret&amp;quot;;
add 2001:19f0:ffff::1 2001:db8:f480::20 tcp 0x1000 -A tcp-md5 &amp;quot;your-shared-secret&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;SPI&lt;/span&gt; value (0x1000) is fixed and matches what &lt;span class="caps"&gt;FRR&lt;/span&gt; expects.&amp;nbsp;With &lt;code&gt;ipsec_enable="YES"&lt;/code&gt; in &lt;code&gt;rc.conf&lt;/code&gt; and the IPsec SAs loaded, &lt;span class="caps"&gt;FRR&lt;/span&gt; picks up the authentication automatically&amp;nbsp;when &lt;code&gt;password&lt;/code&gt; is set on the neighbor. The shared secret comes from Vultr&amp;#8217;s &lt;span class="caps"&gt;BGP&lt;/span&gt; configuration&amp;nbsp;panel.&lt;/p&gt;
&lt;h3 id="frr-configuration_1"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Configuration&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;frr version 10.5.1
frr defaults traditional
hostname vtbgp
log syslog informational
service integrated-vtysh-config
!
ipv6 prefix-list PL-MY-NET seq 5 permit 2a06:9801:1c::/48
! [PL-BOGONS: same bogon filter as hobgp]
!
route-map RM-VULTR-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
exit
!
route-map RM-VULTR-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
exit
!
route-map RM-IBGP-OUT permit 10
exit
!
route-map RM-KERNEL-DENY deny 10
exit
!
router bgp 201379
 bgp router-id 203.0.113.20
 no bgp default ipv4-unicast
 neighbor 2001:19f0:ffff::1 remote-as 64515
 neighbor 2001:19f0:ffff::1 description Vultr-Core
 neighbor 2001:19f0:ffff::1 password your-shared-secret
 neighbor 2001:19f0:ffff::1 ebgp-multihop 2
 neighbor 2a06:9801:1c:fffe::1 remote-as 201379
 neighbor 2a06:9801:1c:fffe::1 description Core-hobgp
 neighbor 2a06:9801:1c:fffe::1 update-source 2a06:9801:1c:fffe::2
 !
 address-family ipv6 unicast
  neighbor 2001:19f0:ffff::1 activate
  neighbor 2001:19f0:ffff::1 soft-reconfiguration inbound
  neighbor 2001:19f0:ffff::1 route-map RM-VULTR-IN in
  neighbor 2001:19f0:ffff::1 route-map RM-VULTR-OUT out
  neighbor 2a06:9801:1c:fffe::1 activate
  neighbor 2a06:9801:1c:fffe::1 next-hop-self
  neighbor 2a06:9801:1c:fffe::1 route-map RM-IBGP-OUT out
 exit-address-family
exit
!
ipv6 protocol bgp route-map RM-KERNEL-DENY
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Two things here deserve careful&amp;nbsp;explanation.&lt;/p&gt;
&lt;h4 id="rm-kernel-deny-keep-the-routing-table-clean"&gt;&lt;code&gt;RM-KERNEL-DENY&lt;/code&gt;: Keep the Routing Table&amp;nbsp;Clean&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ipv6 protocol bgp route-map RM-KERNEL-DENY&lt;/code&gt; applies a deny-all route-map to every &lt;span class="caps"&gt;BGP&lt;/span&gt; route before &lt;span class="caps"&gt;FRR&lt;/span&gt; tries to install it into the kernel&amp;#8217;s forwarding table. The result: &lt;span class="caps"&gt;FRR&lt;/span&gt; maintains its full &lt;span class="caps"&gt;BGP&lt;/span&gt; table internally (~240,000 routes), but none of those routes appear in the&amp;nbsp;kernel.&lt;/p&gt;
&lt;p&gt;This is intentional. vtbgp&amp;#8217;s job is to &lt;strong&gt;announce&lt;/strong&gt; our prefix, not to &lt;strong&gt;forward arbitrary internet traffic&lt;/strong&gt;. The only forwarding it does is for traffic arriving destined for 2a06:9801:1c::/48, which is handled by the static route pointing at gif0. Pushing 240,000 routes into the kernel would be wasteful memory consumption with no&amp;nbsp;benefit.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt; still uses its internal table for its own purposes and still sends routes to hobgp via iBGP - they just never touch the kernel&amp;#8217;s &lt;span class="caps"&gt;FIB&lt;/span&gt; on&amp;nbsp;vtbgp.&lt;/p&gt;
&lt;p&gt;A note for readers familiar with &lt;span class="caps"&gt;FRR&lt;/span&gt; internals: the standard layer for controlling kernel route installation is Zebra, &lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;#8217;s routing daemon that mediates between the protocol daemons (&lt;span class="caps"&gt;BGP&lt;/span&gt;, &lt;span class="caps"&gt;OSPF&lt;/span&gt;, etc.) and the &lt;span class="caps"&gt;OS&lt;/span&gt;. An alternative&amp;nbsp;to &lt;code&gt;RM-KERNEL-DENY&lt;/code&gt; is configuring Zebra directly to not install &lt;span class="caps"&gt;BGP&lt;/span&gt; routes into the kernel. Both approaches achieve the same&amp;nbsp;result; &lt;code&gt;RM-KERNEL-DENY&lt;/code&gt; intercepts routes at the &lt;span class="caps"&gt;BGP&lt;/span&gt; level before they reach Zebra at all. At this scale the distinction is academic, but Zebra is the canonical control point for &lt;span class="caps"&gt;RIB&lt;/span&gt;-to-&lt;span class="caps"&gt;FIB&lt;/span&gt; separation in production&amp;nbsp;deployments.&lt;/p&gt;
&lt;h4 id="next-hop-self-making-ibgp-routes-usable"&gt;&lt;code&gt;next-hop-self&lt;/code&gt;: Making iBGP Routes&amp;nbsp;Usable&lt;/h4&gt;
&lt;p&gt;When vtbgp receives a route from Vultr&amp;nbsp;(next-hop: &lt;code&gt;2001:19f0:ffff::1&lt;/code&gt;) and re-advertises it to hobgp via iBGP, the standard iBGP behavior is to preserve the original next-hop. hobgp can&amp;#8217;t&amp;nbsp;reach &lt;code&gt;2001:19f0:ffff::1&lt;/code&gt; directly - that&amp;#8217;s inside Vultr&amp;#8217;s network, reachable from vtbgp but not from&amp;nbsp;hobgp.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;next-hop-self&lt;/code&gt; rewrites the next-hop to vtbgp&amp;#8217;s own address&amp;nbsp;(&lt;code&gt;2a06:9801:1c:fffe::2&lt;/code&gt;) before sending routes to hobgp. hobgp reaches that address via gif1, so the routes become actionable: &amp;#8220;to get to this destination, send traffic down the iBGP tunnel to vtbgp.&amp;#8221;&amp;nbsp;Without &lt;code&gt;next-hop-self&lt;/code&gt;, hobgp would receive routes with unreachable next-hops and ignore&amp;nbsp;them.&lt;/p&gt;
&lt;h2 id="ibgp-connecting-the-two-routers"&gt;iBGP: Connecting the Two&amp;nbsp;Routers&lt;/h2&gt;
&lt;p&gt;iBGP (internal &lt;span class="caps"&gt;BGP&lt;/span&gt;) runs between routers in the same &lt;span class="caps"&gt;AS&lt;/span&gt;. It has different semantics from eBGP in two important&amp;nbsp;ways:&lt;/p&gt;
&lt;p&gt;First, the &lt;strong&gt;split-horizon rule&lt;/strong&gt;: routes learned via iBGP are not re-advertised to other iBGP peers. For a two-router setup this doesn&amp;#8217;t matter - each router hears from the other directly. For larger networks with three or more routers, you&amp;#8217;d need route reflection or a full mesh; with two, a single session is a complete&amp;nbsp;solution.&lt;/p&gt;
&lt;p&gt;Second, &lt;strong&gt;local-preference propagation&lt;/strong&gt;: &lt;span class="caps"&gt;LP&lt;/span&gt; is carried across iBGP sessions. Routes that vtbgp receives from Vultr carry &lt;span class="caps"&gt;LP&lt;/span&gt; 100 (default) until&amp;nbsp;hobgp&amp;#8217;s &lt;code&gt;RM-IBGP-IN&lt;/code&gt; overrides them to 200. This means hobgp&amp;#8217;s outbound preference for the Vultr path is configured entirely on hobgp - vtbgp doesn&amp;#8217;t need to know anything about the preference&amp;nbsp;hierarchy.&lt;/p&gt;
&lt;p&gt;The session itself is mechanically simple. On hobgp, vtbgp is a neighbor&amp;nbsp;with &lt;code&gt;remote-as 201379&lt;/code&gt; (our own &lt;span class="caps"&gt;AS&lt;/span&gt; number). &lt;span class="caps"&gt;FRR&lt;/span&gt; recognizes same-&lt;span class="caps"&gt;AS&lt;/span&gt; neighbors as iBGP peers automatically. The session runs over the gif1/gif0 tunnel pair, authenticated by virtue of the tunnel endpoints being within our own address&amp;nbsp;space.&lt;/p&gt;
&lt;h2 id="pf-on-a-transit-router-the-stateless-requirement"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; on a Transit Router: The Stateless&amp;nbsp;Requirement&lt;/h2&gt;
&lt;p&gt;Operating a transit router exposes a fundamental tension in stateful packet filtering. On a server, stateful rules are ideal: track &lt;span class="caps"&gt;TCP&lt;/span&gt; connections, match replies to established state, drop unsolicited packets. On a transit router, stateful rules for through-traffic break&amp;nbsp;things.&lt;/p&gt;
&lt;p&gt;The reason is &lt;strong&gt;asymmetric routing&lt;/strong&gt;. When hobgp receives a packet on gre0 (via iFog) destined for a host behind gif0, &lt;span class="caps"&gt;PF&lt;/span&gt; creates a state entry associating that flow with gre0. The destination host responds, the packet traverses gif0 back to hobgp, and the best exit path might be gre1 (Lagrange) - different from the arrival interface. &lt;span class="caps"&gt;PF&lt;/span&gt; sees the reply on gre1 with no matching state on that interface and drops&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;This isn&amp;#8217;t an edge case. Path asymmetry is the norm on the public internet. A request and its reply routinely traverse different providers, different exchange points, different physical&amp;nbsp;paths.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Request: Client ──&amp;gt; [iFog / gre0] ──&amp;gt; hobgp ──&amp;gt; [gif0] ──&amp;gt; Server
                                                               │
Reply:   Client &amp;lt;── [Lagrange / gre1] &amp;lt;── hobgp &amp;lt;── [gif0] &amp;lt;──┘
                    ^^^^^^^^^^^^^^^^^^^
                    PF drops it: state was created on gre0, reply arrives on gre1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The solution is to keep transit traffic&amp;nbsp;stateless:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Transit traffic: no state, no asymmetry problems
pass in quick inet6 from any to $my_network_v6 no state
pass out quick inet6 from any to $my_network_v6 no state
pass in quick inet6 from $my_network_v6 to any no state
pass out quick inet6 from $my_network_v6 to any no state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These four rules cover all IPv6 traffic to or from&amp;nbsp;2a06:9801:1c::/48. &lt;code&gt;no state&lt;/code&gt; means &lt;span class="caps"&gt;PF&lt;/span&gt; allows the packets without creating any tracking entry. Each packet is evaluated independently on its own&amp;nbsp;merits.&lt;/p&gt;
&lt;p&gt;But not everything can be stateless. The router itself makes outbound connections - &lt;span class="caps"&gt;BGP&lt;/span&gt; keepalives, &lt;span class="caps"&gt;DNS&lt;/span&gt; queries, software updates - and those need state tracking to match replies. The control plane (&lt;span class="caps"&gt;SSH&lt;/span&gt;, &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions) also needs state. The solution is a three-tier &lt;span class="caps"&gt;PF&lt;/span&gt; structure where ordering determines which rule&amp;nbsp;wins:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Tier 1: Control plane - stateful, locked to known peers
pass in quick proto tcp from &amp;lt;bgp_peers&amp;gt; to any port 179 keep state
pass out quick proto tcp from any to &amp;lt;bgp_peers&amp;gt; port 179 keep state

&lt;span class="gh"&gt;#&lt;/span&gt; Tier 2: Router&amp;#39;s own traffic - stateful for outbound connectivity
pass in  quick inet6 from any to $my_router_ip keep state
pass out quick inet6 from $my_router_ip to any keep state

&lt;span class="gh"&gt;#&lt;/span&gt; Tier 3: Transit traffic - stateless for asymmetric routing
pass in quick inet6 from any to $my_network_v6 no state
pass out quick inet6 from any to $my_network_v6 no state
pass in quick inet6 from $my_network_v6 to any no state
pass out quick inet6 from $my_network_v6 to any no state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;evaluates &lt;code&gt;quick&lt;/code&gt; rules in order and stops at the first match. Traffic&amp;nbsp;to &lt;code&gt;$my_router_ip&lt;/code&gt; (2a06:9801:1c::1, the router&amp;#8217;s address within our prefix) matches Tier 2 and gets state tracking. Everything else in the /48 falls through to Tier 3 and flows&amp;nbsp;stateless.&lt;/p&gt;
&lt;h3 id="nat-for-router-originated-traffic"&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt; for Router-Originated&amp;nbsp;Traffic&lt;/h3&gt;
&lt;p&gt;There&amp;#8217;s a related problem: hobgp&amp;#8217;s physical interface has a Hetzner-assigned IPv6 address&amp;nbsp;(&lt;code&gt;2001:db8:1c19::1&lt;/code&gt;). When the router sends traffic out a &lt;span class="caps"&gt;GRE&lt;/span&gt; tunnel using that address as the source, it&amp;#8217;s sending from an address with no relation to our announced prefix. Transit providers may find this confusing; more practically, return traffic to that address may take a completely different path or simply&amp;nbsp;fail.&lt;/p&gt;
&lt;p&gt;The fix is &lt;span class="caps"&gt;NAT&lt;/span&gt; on the &lt;span class="caps"&gt;GRE&lt;/span&gt; tunnel interfaces, translating non-/48 source addresses to the router&amp;#8217;s &lt;span class="caps"&gt;PI&lt;/span&gt;&amp;nbsp;address:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;peer_ifog&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_router_ip&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;peer_lagrange&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_router_ip&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;peer_route64&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_router_ip&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gif1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_router_ip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Reading the first rule: packets exiting gre0, with a source address not in our /48, destined for anywhere other than the iFog &lt;span class="caps"&gt;BGP&lt;/span&gt; peer itself, get their source rewritten&amp;nbsp;to &lt;code&gt;$my_router_ip&lt;/code&gt; (2a06:9801:1c::1). The peer exclusion is critical - &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions must use the tunnel link address, not the NATed address, for the session to work. Everything else the router originates - traceroutes, pings, package fetches - appears on the internet as 2a06:9801:1c::1, an address inside our announced&amp;nbsp;prefix.&lt;/p&gt;
&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;vtysh -c 'show bgp ipv6 summary'&lt;/code&gt; on hobgp shows four sessions in established&amp;nbsp;state:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Neighbor             V    AS   MsgRcvd   MsgSent  Up/Down   State/PfxRcd  Desc
2001:db8:300::1      4  209533  1873391    10601  16:21:35       237902    Upstream-iFog-FRA
fd00:ca::1           4  209735  1267858     1771  1d05h26m       239352    Upstream-Lagrange-UK
2001:db8:400::1      4  212895   609199     1771  1d05h26m       238542    Upstream-Route64-FRA
2a06:9801:1c:fffe::2 4  201379  1370565     1771  1d05h26m       229257    Edge-Vultr-Frankfurt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Four peers, three carrying the full &lt;span class="caps"&gt;DFZ&lt;/span&gt; table (~237-239K prefixes each), one iBGP peer with a partial Vultr view (~229K prefixes). All in established&amp;nbsp;state.&lt;/p&gt;
&lt;p&gt;The real test is whether different networks actually use different paths. &lt;span class="caps"&gt;MTR&lt;/span&gt; traces from three vantage points answer this&amp;nbsp;directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From Deutsche Telekom (heavy Vultr&amp;nbsp;peering):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;HOST: client.example.com                                    Loss%  Snt  Last   Avg
  ...
  9.|-- constantcompany-ic-375791.ip.twelve99-cust.net       0.0%   10  25.9  26.7
 10.|-- ethernetae1-sr2.fkt3.constant.com                    0.0%   10  23.4  29.6
 11.|-- 2001:19f0:6c00:154::33                               0.0%   10  24.1  23.7
 12.|-- vtbgp.example.com                                    0.0%   10  26.8  26.3
 13.|-- hobgp.example.com                                    0.0%   10  27.1  26.6
 14.|-- radon.example.com                                    0.0%   10  27.9  31.2
 15.|-- 2a06:9801:1c:1000::10                                0.0%   10  27.1  30.4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hops 9-11 are Constant Company infrastructure - that&amp;#8217;s Vultr&amp;#8217;s upstream. The traffic enters via vtbgp (hop 12), crosses the iBGP tunnel to hobgp (hop 13), and reaches the downstream server. The Vultr announcement is doing exactly what it should for traffic from &lt;span class="caps"&gt;DTAG&lt;/span&gt;-connected&amp;nbsp;networks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From Netcup (via &lt;span class="caps"&gt;AORTA&lt;/span&gt;/Vienna Internet&amp;nbsp;Exchange):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;HOST: server.example.com                                    Loss%  Snt  Last   Avg
  ...
  9.|-- 2001:7f8:15e::64                                     0.0%   10  16.7  16.7
 10.|-- 2a0d:5440:1::f                                       0.0%   10  15.8  15.9
 11.|-- hobgp.example.com                                    0.0%   10  19.9  19.6
 12.|-- radon.example.com                                    0.0%   10  20.7  21.4
 13.|-- 2a06:9801:1c:1000::10                                0.0%   10  20.8  22.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hop 9&amp;nbsp;(&lt;code&gt;2001:7f8:15e::64&lt;/code&gt;) is an internet exchange peering &lt;span class="caps"&gt;LAN&lt;/span&gt; address&amp;nbsp;(&lt;code&gt;2001:7f8::/29&lt;/code&gt; is &lt;span class="caps"&gt;IANA&lt;/span&gt;-allocated &lt;span class="caps"&gt;IXP&lt;/span&gt; space). Hop 10&amp;nbsp;(&lt;code&gt;2a0d:5440:1::f&lt;/code&gt;) belongs to route64 (&lt;span class="caps"&gt;AS212895&lt;/span&gt;). The traffic entered via route64 at an exchange point and arrived directly at hobgp - no vtbgp&amp;nbsp;involved.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From another Hetzner server&amp;nbsp;(Falkenstein):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;HOST: server3.example.com                                   Loss%  Snt  Last   Avg
  ...
  8.|-- 2001:7f8:11c:1:0:20:9533:1                          0.0%   10   5.5   5.6
  9.|-- hobgp.example.com                                    0.0%   10   9.5   9.6
 10.|-- radon.example.com                                    0.0%   10  10.3  16.4
 11.|-- 2a06:9801:1c:1000::10                                0.0%   10  10.5  11.3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hop 8 is a &lt;span class="caps"&gt;DE&lt;/span&gt;-&lt;span class="caps"&gt;CIX&lt;/span&gt; Frankfurt switching address&amp;nbsp;(the &lt;code&gt;2001:7f8:11c&lt;/code&gt; range belongs to &lt;span class="caps"&gt;DE&lt;/span&gt;-&lt;span class="caps"&gt;CIX&lt;/span&gt;). Traffic exits &lt;span class="caps"&gt;DE&lt;/span&gt;-&lt;span class="caps"&gt;CIX&lt;/span&gt; and hits hobgp in single-digit milliseconds - iFog&amp;#8217;s Frankfurt presence at the same exchange. The prefix is reachable from Hetzner in under&amp;nbsp;10ms.&lt;/p&gt;
&lt;p&gt;Three different paths, three different entry points. Each path is geometrically shorter for its respective traffic class than routing everything through a single upstream would&amp;nbsp;be.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Meta:&lt;/strong&gt; The destination address in every trace above&amp;nbsp;- &lt;code&gt;2a06:9801:1c:1000::10&lt;/code&gt; - is this blog. If you&amp;#8217;re reading this article over IPv6, you just arrived here via one of those&amp;nbsp;paths.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="what-this-costs"&gt;What This&amp;nbsp;Costs&lt;/h2&gt;
&lt;p&gt;Running your own IPv6 &lt;span class="caps"&gt;AS&lt;/span&gt; is surprisingly inexpensive. Here are the actual numbers for this setup, with monthly equivalents in&amp;nbsp;euros.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Actual cost&lt;/th&gt;
&lt;th&gt;Monthly (approx. €)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="caps"&gt;ASN&lt;/span&gt; registration (one-time)&lt;/td&gt;
&lt;td&gt;£15&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="caps"&gt;ASN&lt;/span&gt; annual fee&lt;/td&gt;
&lt;td&gt;£54.99/year&lt;/td&gt;
&lt;td&gt;€5.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="caps"&gt;PA&lt;/span&gt; /48 IPv6 block&lt;/td&gt;
&lt;td&gt;£7/year&lt;/td&gt;
&lt;td&gt;€0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner &lt;span class="caps"&gt;CX23&lt;/span&gt; (core router, hobgp)&lt;/td&gt;
&lt;td&gt;€4.15/month&lt;/td&gt;
&lt;td&gt;€4.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vultr vc2-1c-1gb (edge router, vtbgp)&lt;/td&gt;
&lt;td&gt;$6.00/month&lt;/td&gt;
&lt;td&gt;€5.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lagrange Cloud transit (100 Mbps)&lt;/td&gt;
&lt;td&gt;£2/month&lt;/td&gt;
&lt;td&gt;€2.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iFog BGPTunnel.com (100 Mbps)&lt;/td&gt;
&lt;td&gt;Free (non-commercial)&lt;/td&gt;
&lt;td&gt;€0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;route64.org (200 Mbps)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;€0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vultr &lt;span class="caps"&gt;BGP&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Included in &lt;span class="caps"&gt;VPS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;€0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total recurring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ €18/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That is less than many people pay for a single mid-range &lt;span class="caps"&gt;VPS&lt;/span&gt; - and it buys a redundant multi-homed autonomous system with 1.4 Gbps of available transit&amp;nbsp;capacity.&lt;/p&gt;
&lt;p&gt;The reason IPv6-only is so accessible comes down to address space economics. An IPv4 &lt;span class="caps"&gt;PA&lt;/span&gt; block costs thousands of euros per year through a sponsoring &lt;span class="caps"&gt;LIR&lt;/span&gt; and requires documented justification. An IPv6 /48 costs €8. The transit situation is similarly skewed: iFog and route64 offer free &lt;span class="caps"&gt;GRE&lt;/span&gt;-based transit specifically because IPv6-only hobbyists place negligible load on their networks. The free tier is a real service, not a&amp;nbsp;trial.&lt;/p&gt;
&lt;p&gt;The Vultr edge router is the largest recurring cost after the &lt;span class="caps"&gt;ASN&lt;/span&gt; fees, and it&amp;#8217;s paying for something specific: native &lt;span class="caps"&gt;BGP&lt;/span&gt; peering with Vultr&amp;#8217;s infrastructure, which reaches networks that simply don&amp;#8217;t have a short path to any other upstream in this setup. If path diversity from Vultr-connected networks isn&amp;#8217;t a priority, the edge router is optional and the setup collapses back to a single core router at under €5/month in compute&amp;nbsp;costs.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Stateless transit rules are not optional.&lt;/strong&gt; Getting this wrong produces subtle failures: &lt;span class="caps"&gt;ICMP&lt;/span&gt; works but &lt;span class="caps"&gt;TCP&lt;/span&gt; stalls, small packets flow freely but large ones don&amp;#8217;t, connections from one direction succeed while the other direction silently drops. When troubleshooting transit routing on &lt;span class="caps"&gt;PF&lt;/span&gt;, the first question is always whether state tracking is interfering with asymmetric&amp;nbsp;paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iBGP between two routers is straightforward.&lt;/strong&gt; The iBGP full-mesh requirement sounds complicated, but with exactly two routers it&amp;#8217;s trivially satisfied by one session on each side. The main configuration concern&amp;nbsp;is &lt;code&gt;next-hop-self&lt;/code&gt;, which is required whenever the iBGP peers can&amp;#8217;t directly reach the eBGP next-hops learned from&amp;nbsp;upstream.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;RM-KERNEL-DENY&lt;/code&gt; is useful for announcement-only nodes.&lt;/strong&gt; A router whose job is to announce a prefix and forward traffic for that prefix back through a tunnel doesn&amp;#8217;t need 240,000 kernel routes. Keeping the kernel&amp;#8217;s &lt;span class="caps"&gt;RIB&lt;/span&gt; clean saves memory, avoids confusing interactions with static routes, and makes the router&amp;#8217;s actual forwarding intent&amp;nbsp;explicit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vultr&amp;#8217;s &lt;span class="caps"&gt;BGP&lt;/span&gt; service reaches a meaningfully different part of the internet.&lt;/strong&gt; The &lt;span class="caps"&gt;DTAG&lt;/span&gt; traceroute shows traffic entering via Vultr&amp;#8217;s Constant Company upstream - a path that simply doesn&amp;#8217;t exist via iFog or route64. The two sets of upstreams complement each other. For traffic where both would work, local-preference makes the decision; for traffic where only one path exists, the choice is made for&amp;nbsp;you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Free transit providers provide real diversity.&lt;/strong&gt; BGPTunnel.com, route64.org, and similar services offer transit that&amp;#8217;s genuinely usable for learning and redundancy. Three upstreams with different Frankfurt exchange presence means real path diversity without transit&amp;nbsp;fees.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The gap between a single-router &lt;span class="caps"&gt;BGP&lt;/span&gt; setup and a distributed multi-homed network is mostly conceptual, not computational. The same FreeBSD tools - &lt;span class="caps"&gt;FRR&lt;/span&gt;, &lt;span class="caps"&gt;PF&lt;/span&gt;, &lt;span class="caps"&gt;GIF&lt;/span&gt;/&lt;span class="caps"&gt;GRE&lt;/span&gt; tunnels - handle the expanded setup. iBGP adds one session. The third upstream adds another &lt;span class="caps"&gt;GRE&lt;/span&gt; tunnel and two route-maps. The edge router requires &lt;span class="caps"&gt;TCP&lt;/span&gt;-&lt;span class="caps"&gt;MD5&lt;/span&gt; and a careful decision to keep routes out of the&amp;nbsp;kernel.&lt;/p&gt;
&lt;p&gt;What changes is what the setup can do. Three entry points mean inbound traffic takes geometrically shorter paths for a larger portion of the internet. Local-preference makes outbound path selection explicit and tunable. The iBGP architecture means either router can be rebooted without taking down the other&amp;#8217;s upstream&amp;nbsp;sessions.&lt;/p&gt;
&lt;p&gt;The next logical evolution would be geographic diversity - a PoP in a different region with genuinely independent upstream connectivity. But Frankfurt with three providers and two PoPs is already a substantial improvement over a single machine with one vendor&amp;#8217;s connectivity, and the lessons from building it transfer directly to any larger&amp;nbsp;scale.&lt;/p&gt;
&lt;p&gt;Each of those traceroutes ends the same way: hop 11 or hop 13 or hop 15, an address in 2a06:9801:1c::/48. The path that gets it there varies by two thousand kilometres and half a dozen autonomous systems, chosen automatically by the routing protocol. That&amp;#8217;s what multi-homing looks like when it&amp;#8217;s&amp;nbsp;working.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bgptunnel.com/"&gt;BGPTunnel.com - Free &lt;span class="caps"&gt;BGP&lt;/span&gt; Tunnels via&amp;nbsp;iFog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://route64.org/"&gt;route64.org - Free IPv6 &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;Transit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.vultr.com/configuring-bgp-on-vultr"&gt;Vultr &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.frrouting.org/en/latest/bgp.html"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt; Documentation: iBGP and&amp;nbsp;next-hop-self&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/firewalls/#pf-stateful"&gt;FreeBSD Handbook: &lt;span class="caps"&gt;PF&lt;/span&gt; Stateful&amp;nbsp;Tracking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc2385"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 2385 - Protection of &lt;span class="caps"&gt;BGP&lt;/span&gt; Sessions via &lt;span class="caps"&gt;TCP&lt;/span&gt; &lt;span class="caps"&gt;MD5&lt;/span&gt;&amp;nbsp;Signature&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bgp.tools/"&gt;bgp.tools - &lt;span class="caps"&gt;BGP&lt;/span&gt; Looking&amp;nbsp;Glass&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Networking"/><category term="freebsd"/><category term="bgp"/><category term="networking"/><category term="ipv6"/><category term="frr"/><category term="pf"/><category term="ibgp"/><category term="multihoming"/></entry><entry><title>HTTP/3 on FreeBSD: Getting QUIC Working with nginx in a Bastille Jail</title><link href="https://blog.hofstede.it/http3-on-freebsd-getting-quic-working-with-nginx-in-a-bastille-jail/" rel="alternate"/><published>2026-02-21T00:00:00+01:00</published><updated>2026-02-21T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-21:/http3-on-freebsd-getting-quic-working-with-nginx-in-a-bastille-jail/</id><summary type="html">&lt;p&gt;What looked like a simple nginx config change turned into a tour through &lt;span class="caps"&gt;SSL&lt;/span&gt; library incompatibilities, pf firewall rules for a new protocol, and a multi-worker affinity problem that only shows up under real traffic. A complete guide to getting &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 working with nginx 1.28 on FreeBSD 15.0 inside a Bastille&amp;nbsp;jail.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Nginx" src="https://blog.hofstede.it/images/2026-02-21-http3-quic-nginx-freebsd.png" title="Nginx Logo"&gt;&lt;/p&gt;
&lt;p&gt;What looks like a simple nginx config change turns out to involve &lt;span class="caps"&gt;SSL&lt;/span&gt; library compatibility, firewall rules for a new protocol, and a subtle multi-process routing problem that only surfaces under real traffic. This documents what it actually took to get &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 (&lt;span class="caps"&gt;QUIC&lt;/span&gt;) working on nginx 1.28 inside a FreeBSD 15.0 Bastille jail - serving the Mastodon instance at &lt;a href="https://burningboard.net"&gt;burningboard.net&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re running nginx on FreeBSD and want &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3, this should save you several hours of&amp;nbsp;troubleshooting.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD 12+ (this guide uses FreeBSD 15.0-&lt;span class="caps"&gt;RELEASE&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;nginx 1.25+&amp;nbsp;with &lt;code&gt;HTTPV3=on&lt;/code&gt; and &lt;code&gt;HTTPV3_BORING=on&lt;/code&gt; (built from&amp;nbsp;ports)&lt;/li&gt;
&lt;li&gt;A valid &lt;span class="caps"&gt;TLS&lt;/span&gt; certificate (Let&amp;#8217;s Encrypt or&amp;nbsp;similar)&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;UDP&lt;/span&gt; port 443 open in your&amp;nbsp;firewall&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;DNS&lt;/span&gt; A and &lt;span class="caps"&gt;AAAA&lt;/span&gt; records pointing to your&amp;nbsp;server&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-1-the-listen-directives"&gt;Step 1: The Listen&amp;nbsp;Directives&lt;/h2&gt;
&lt;p&gt;The most common first mistake:&amp;nbsp;replacing &lt;code&gt;listen 443 ssl&lt;/code&gt; with &lt;code&gt;listen 443 quic&lt;/code&gt;. &lt;span class="caps"&gt;QUIC&lt;/span&gt; doesn&amp;#8217;t replace &lt;span class="caps"&gt;TCP&lt;/span&gt; - it runs alongside it. Browsers always connect via regular &lt;span class="caps"&gt;HTTPS&lt;/span&gt; (&lt;span class="caps"&gt;TCP&lt;/span&gt;) first, then upgrade to &lt;span class="caps"&gt;QUIC&lt;/span&gt;/&lt;span class="caps"&gt;HTTP3&lt;/span&gt; only after seeing&amp;nbsp;an &lt;code&gt;Alt-Svc&lt;/code&gt; header announcing that &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 is&amp;nbsp;available.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wrong:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;http2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This makes nginx listen only on &lt;span class="caps"&gt;UDP&lt;/span&gt;. No browser can connect at all because the initial connection is always &lt;span class="caps"&gt;TCP&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Correct:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;http2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Both &lt;span class="caps"&gt;TCP&lt;/span&gt;&amp;nbsp;(&lt;code&gt;ssl&lt;/code&gt;) and &lt;span class="caps"&gt;UDP&lt;/span&gt;&amp;nbsp;(&lt;code&gt;quic&lt;/code&gt;) listeners must be present in the same server&amp;nbsp;block.&lt;/p&gt;
&lt;p&gt;The reason &lt;span class="caps"&gt;TCP&lt;/span&gt; can never be skipped is that the &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 upgrade is self-announcing: the server tells the browser about &lt;span class="caps"&gt;QUIC&lt;/span&gt; support &lt;em&gt;inside&lt;/em&gt; an &lt;span class="caps"&gt;HTTP&lt;/span&gt; response, which first requires a working &lt;span class="caps"&gt;TCP&lt;/span&gt; connection. Here is the full&amp;nbsp;sequence:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; Client                              nginx (:443)
   │                                     │
   │   ① TCP + TLS handshake             │
   ├────────────────────────────────────&amp;gt;┤  \
   │&amp;lt;────────────────────────────────────┤   TCP (always required for first contact)
   │                                     │  /
   │   ② First request - still over TCP  │
   ├─── GET / ──────────────────────────&amp;gt;┤  \
   │&amp;lt;── 200 OK ──────────────────────────┤   TCP
   │    Alt-Svc: h3=&amp;quot;:443&amp;quot;; ma=86400     │  /  ← browser learns HTTP/3 is available
   │                                     │
   │   (browser caches Alt-Svc)          │
   │                                     │
   │   ③ Subsequent requests via QUIC    │
   ├═══ QUIC handshake (UDP) ═══════════&amp;gt;┤  \
   │&amp;lt;════════════════════════════════════┤   UDP (HTTP/3)
   ├═══ GET /page ═══════════════════════&amp;gt;┤   lower latency, multiplexed
   │&amp;lt;═══ 200 OK ══════════════════════════┤  /
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is why both&amp;nbsp;the &lt;code&gt;ssl&lt;/code&gt; and &lt;code&gt;quic&lt;/code&gt; listen directives are always required together: one to serve the first response that announces &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3, the other to handle all subsequent &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;connections.&lt;/p&gt;
&lt;h2 id="step-2-the-alt-svc-header"&gt;Step 2: The Alt-Svc&amp;nbsp;Header&lt;/h2&gt;
&lt;p&gt;Browsers discover &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 support through&amp;nbsp;the &lt;code&gt;Alt-Svc&lt;/code&gt; response header. Without it, no browser will ever attempt a &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;connection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;always&lt;/code&gt; keyword ensures the header is sent even on error responses and redirects, not just successful&amp;nbsp;200s.&lt;/p&gt;
&lt;h3 id="the-add_header-inheritance-trap"&gt;The &lt;code&gt;add_header&lt;/code&gt; Inheritance&amp;nbsp;Trap&lt;/h3&gt;
&lt;p&gt;This is a subtle but critical nginx&amp;nbsp;behavior: &lt;code&gt;add_header&lt;/code&gt; directives inside&amp;nbsp;a &lt;code&gt;location&lt;/code&gt; block &lt;strong&gt;completely override&lt;/strong&gt;&amp;nbsp;all &lt;code&gt;add_header&lt;/code&gt; directives from the&amp;nbsp;parent &lt;code&gt;server&lt;/code&gt; block. If you&amp;nbsp;set &lt;code&gt;Alt-Svc&lt;/code&gt; at the server level but have&amp;nbsp;any &lt;code&gt;location&lt;/code&gt; block with its&amp;nbsp;own &lt;code&gt;add_header&lt;/code&gt; (for &lt;code&gt;Cache-Control&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, etc.),&amp;nbsp;the &lt;code&gt;Alt-Svc&lt;/code&gt; header silently disappears for those&amp;nbsp;locations.&lt;/p&gt;
&lt;p&gt;The fix:&amp;nbsp;add &lt;code&gt;Alt-Svc&lt;/code&gt; to&amp;nbsp;every &lt;code&gt;location&lt;/code&gt; block that already has&amp;nbsp;any &lt;code&gt;add_header&lt;/code&gt; directive.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/assets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cache-Control&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Cache-Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$upstream_cache_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Must be here too!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify coverage with&amp;nbsp;curl:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check the root path&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;-sI&lt;span class="w"&gt; &lt;/span&gt;https://yourdomain.com/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;alt-svc

&lt;span class="c1"&gt;# Check a subpath - if this is empty, you have the inheritance problem&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;-sI&lt;span class="w"&gt; &lt;/span&gt;https://yourdomain.com/some/path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;alt-svc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="step-3-the-ssl-library-problem"&gt;Step 3: The &lt;span class="caps"&gt;SSL&lt;/span&gt; Library&amp;nbsp;Problem&lt;/h2&gt;
&lt;p&gt;With a correct config and firewall in place, browsers may still refuse &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3. Capturing &lt;span class="caps"&gt;UDP&lt;/span&gt; traffic on port 443 inside the nginx jail reveals the&amp;nbsp;problem:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tcpdump&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;vnet0&lt;span class="w"&gt; &lt;/span&gt;udp&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With stock OpenSSL, the server responds with tiny 51-byte packets to every &lt;span class="caps"&gt;QUIC&lt;/span&gt; connection attempt. These are &lt;span class="caps"&gt;QUIC&lt;/span&gt; version negotiation failures - the handshake is breaking at the &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 framing layer, not the &lt;span class="caps"&gt;TLS&lt;/span&gt;&amp;nbsp;layer.&lt;/p&gt;
&lt;h3 id="the-misleading-openssl-s_client-test"&gt;The&amp;nbsp;Misleading &lt;code&gt;openssl s_client&lt;/code&gt; Test&lt;/h3&gt;
&lt;p&gt;A common false positive when diagnosing&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;s_client&lt;span class="w"&gt; &lt;/span&gt;-connect&lt;span class="w"&gt; &lt;/span&gt;yourdomain.com:443&lt;span class="w"&gt; &lt;/span&gt;-quic&lt;span class="w"&gt; &lt;/span&gt;-alpn&lt;span class="w"&gt; &lt;/span&gt;h3
&lt;span class="c1"&gt;# Shows: CONNECTED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This only validates &lt;span class="caps"&gt;TLS&lt;/span&gt;-level &lt;span class="caps"&gt;QUIC&lt;/span&gt; support. The actual &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 protocol layer on top can still be broken - and with stock OpenSSL on FreeBSD, it&amp;nbsp;is.&lt;/p&gt;
&lt;h3 id="why-stock-openssl-doesnt-work"&gt;Why Stock OpenSSL Doesn&amp;#8217;t&amp;nbsp;Work&lt;/h3&gt;
&lt;p&gt;nginx&amp;#8217;s &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 implementation was originally built against BoringSSL. While OpenSSL 3.2+ added &lt;span class="caps"&gt;QUIC&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt; support, there are known compatibility issues at the &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 framing layer. The &lt;span class="caps"&gt;TLS&lt;/span&gt; handshake may succeed, but actual &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 data transfer&amp;nbsp;fails.&lt;/p&gt;
&lt;h3 id="rebuilding-nginx-with-boringssl"&gt;Rebuilding nginx with&amp;nbsp;BoringSSL&lt;/h3&gt;
&lt;p&gt;The FreeBSD nginx port offers several &lt;span class="caps"&gt;SSL&lt;/span&gt; backends for &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3. Check the available&amp;nbsp;options:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;make&lt;span class="w"&gt; &lt;/span&gt;-C&lt;span class="w"&gt; &lt;/span&gt;/usr/ports/www/nginx&lt;span class="w"&gt; &lt;/span&gt;showconfig&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;HTTPV3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In nginx 1.28.2 the relevant options&amp;nbsp;are:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;HTTPV3=on: Enable HTTP/3 protocol support
HTTPV3_BORING=on: Use security/boringssl
HTTPV3_LSSL=off: Use security/libressl-devel
HTTPV3_QTLS=off: Use security/openssl33-quictls
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Enable &lt;code&gt;HTTPV3&lt;/code&gt; and &lt;code&gt;HTTPV3_BORING&lt;/code&gt;, then&amp;nbsp;rebuild:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/usr/ports/www/nginx
make&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# Enable HTTPV3 and HTTPV3_BORING&lt;/span&gt;
pkg&lt;span class="w"&gt; &lt;/span&gt;unlock&lt;span class="w"&gt; &lt;/span&gt;nginx&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# If the package is locked&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;clean
make&lt;span class="w"&gt; &lt;/span&gt;deinstall
make&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;install
pkg&lt;span class="w"&gt; &lt;/span&gt;lock&lt;span class="w"&gt; &lt;/span&gt;nginx&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;# Re-lock if desired&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;nginx&lt;span class="w"&gt; &lt;/span&gt;restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify the new binary links against&amp;nbsp;BoringSSL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;nginx&lt;span class="w"&gt; &lt;/span&gt;-V&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;nginx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nginx&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nt"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;28&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;2&lt;/span&gt;
&lt;span class="nt"&gt;built&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;OpenSSL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;compatible&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;BoringSSL&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;running&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;BoringSSL&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;TLS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;SNI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;support&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After rebuilding, tcpdump shows proper-sized &lt;span class="caps"&gt;QUIC&lt;/span&gt; handshake packets (1200, 892, 223 bytes) instead of the 51-byte&amp;nbsp;rejections.&lt;/p&gt;
&lt;h2 id="step-4-the-worker-process-affinity-problem"&gt;Step 4: The Worker Process Affinity&amp;nbsp;Problem&lt;/h2&gt;
&lt;p&gt;With BoringSSL in place, the first &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 request succeeds - but subsequent requests on the same connection fail&amp;nbsp;with &lt;code&gt;ERR_QUIC_PROTOCOL_ERROR_QUIC_PUBLIC_RESET&lt;/code&gt; in&amp;nbsp;Chrome.&lt;/p&gt;
&lt;p&gt;Setting &lt;code&gt;worker_processes 1&lt;/code&gt; in &lt;code&gt;nginx.conf&lt;/code&gt; makes &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 work perfectly. This confirms the diagnosis: &lt;span class="caps"&gt;QUIC&lt;/span&gt; packets from the same connection are being routed to different worker processes. A worker that didn&amp;#8217;t establish the connection responds with a &amp;#8220;public reset&amp;#8221; packet, killing&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;The contrast between the broken and fixed&amp;nbsp;behaviour:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;WITHOUT reuseport - one shared UDP socket, OS picks any worker per packet:

  pkt 1 (conn A) ──┐
  pkt 2 (conn A) ──┼──&amp;gt; [ :443 UDP ] ──&amp;gt; OS ──&amp;gt; Worker 1  ✓ (owns conn A)
  pkt 3 (conn A) ──┘                       └──&amp;gt; Worker 2  ✗ (no context)
                                                     │
                                               sends RST → connection killed


WITH reuseport + quic_retry - one socket per worker, OS hashes by connection:

  pkt 1 (conn A) ──┐                    ┌──&amp;gt; Worker 1 socket ──&amp;gt; Worker 1  ✓
  pkt 2 (conn A) ──┼──&amp;gt; SO_REUSEPORT_LB ┤
  pkt 3 (conn A) ──┘  (hashes 4-tuple)  ├──&amp;gt; Worker 2 socket ──&amp;gt; Worker 2
                                         └──&amp;gt; Worker 3 socket ──&amp;gt; Worker 3

  All packets for the same connection always reach the same worker.
  quic_retry ensures the correct worker is pinned from the very first packet.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="the-linux-solution-not-available-on-freebsd"&gt;The Linux Solution (Not Available on&amp;nbsp;FreeBSD)&lt;/h3&gt;
&lt;p&gt;On Linux, nginx solves this&amp;nbsp;with &lt;code&gt;quic_bpf on&lt;/code&gt;, which uses eBPF to route &lt;span class="caps"&gt;QUIC&lt;/span&gt; packets to the correct worker based on the connection &lt;span class="caps"&gt;ID&lt;/span&gt;. FreeBSD doesn&amp;#8217;t have eBPF - this option doesn&amp;#8217;t&amp;nbsp;exist.&lt;/p&gt;
&lt;h3 id="the-freebsd-solution-reuseport-quic_retry"&gt;The FreeBSD&amp;nbsp;Solution: &lt;code&gt;reuseport&lt;/code&gt; + &lt;code&gt;quic_retry&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Two directives work together to solve this on&amp;nbsp;FreeBSD:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;reuseport&lt;/code&gt;&lt;/strong&gt; on the &lt;span class="caps"&gt;QUIC&lt;/span&gt; listeners creates per-worker listening sockets. FreeBSD 12+&amp;nbsp;supports &lt;code&gt;SO_REUSEPORT_LB&lt;/code&gt;, which load-balances incoming &lt;span class="caps"&gt;UDP&lt;/span&gt; packets across workers while keeping each connection pinned to one worker socket. Add it to &lt;strong&gt;one&lt;/strong&gt; server block only - typically the primary vhost - to avoid duplicate binding&amp;nbsp;errors:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Primary server block only:&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;reuseport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;reuseport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# All other server blocks:&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;quic_retry on&lt;/code&gt;&lt;/strong&gt; forces a &lt;span class="caps"&gt;QUIC&lt;/span&gt; retry handshake (address validation) before establishing the connection. This extra round-trip ensures the correct worker process handles the entire &lt;span class="caps"&gt;QUIC&lt;/span&gt; session. Add it to&amp;nbsp;the &lt;code&gt;http&lt;/code&gt; block&amp;nbsp;in &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;quic_retry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify &lt;code&gt;reuseport&lt;/code&gt; is active by checking for multiple &lt;span class="caps"&gt;UDP&lt;/span&gt;&amp;nbsp;sockets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sockstat&lt;span class="w"&gt; &lt;/span&gt;-4&lt;span class="w"&gt; &lt;/span&gt;-6&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;nginx&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see multiple &lt;span class="caps"&gt;UDP&lt;/span&gt; entries on port 443 - one per worker&amp;nbsp;process.&lt;/p&gt;
&lt;h2 id="step-5-firewall-configuration"&gt;Step 5: Firewall&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;QUIC&lt;/span&gt; runs over &lt;span class="caps"&gt;UDP&lt;/span&gt;, so you need explicit &lt;span class="caps"&gt;UDP&lt;/span&gt; rules for port 443 alongside the existing &lt;span class="caps"&gt;TCP&lt;/span&gt; rules. The exact configuration depends on your network&amp;nbsp;topology.&lt;/p&gt;
&lt;p&gt;In this setup, nginx runs inside a Bastille jail. IPv6 is directly routed - the jail holds a public IPv6 address, so no &lt;span class="caps"&gt;RDR&lt;/span&gt; or &lt;span class="caps"&gt;NAT&lt;/span&gt; is needed for IPv6 &lt;span class="caps"&gt;QUIC&lt;/span&gt;. For IPv4, the host&amp;#8217;s pf forwards traffic from the public &lt;span class="caps"&gt;IP&lt;/span&gt; to the jail&amp;#8217;s private address&amp;nbsp;using &lt;code&gt;rdr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The important detail: &lt;span class="caps"&gt;QUIC&lt;/span&gt; (&lt;span class="caps"&gt;UDP&lt;/span&gt;) needs its&amp;nbsp;own &lt;code&gt;rdr&lt;/code&gt; rule on the host, separate from the &lt;span class="caps"&gt;TCP&lt;/span&gt; one.&amp;nbsp;A &lt;code&gt;pass&lt;/code&gt; rule alone is not enough -&amp;nbsp;without &lt;code&gt;rdr&lt;/code&gt;, &lt;span class="caps"&gt;UDP&lt;/span&gt; packets arriving at the public &lt;span class="caps"&gt;IP&lt;/span&gt; are still addressed to the public &lt;span class="caps"&gt;IP&lt;/span&gt; when the filter rules run, so&amp;nbsp;a &lt;code&gt;pass ... to $frontend_v4&lt;/code&gt; (jail &lt;span class="caps"&gt;IP&lt;/span&gt;) rule will never match&amp;nbsp;them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# --- RDR ---&lt;/span&gt;
&lt;span class="cp"&gt;# Redirect TCP 80/443 to the nginx jail&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;

&lt;span class="cp"&gt;# Redirect UDP 443 (QUIC) to the nginx jail - separate rule required&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;

&lt;span class="cp"&gt;# --- Pass rules ---&lt;/span&gt;
&lt;span class="cp"&gt;# HTTP/HTTPS (TCP)&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="cp"&gt;# QUIC (UDP) - IPv4 via NAT/rdr above, IPv6 direct&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Don&amp;#8217;t forget both IPv4 and IPv6 if your server is dual-stack. For a server without a Bastille jail - nginx running directly on the host -&amp;nbsp;the &lt;code&gt;rdr&lt;/code&gt; rules are not needed; only&amp;nbsp;the &lt;code&gt;pass&lt;/code&gt; rules for both &lt;span class="caps"&gt;TCP&lt;/span&gt; and &lt;span class="caps"&gt;UDP&lt;/span&gt;.&lt;/p&gt;
&lt;h2 id="step-6-performance-tuning"&gt;Step 6: Performance&amp;nbsp;Tuning&lt;/h2&gt;
&lt;h3 id="tls-13-early-data-0-rtt"&gt;&lt;span class="caps"&gt;TLS&lt;/span&gt; 1.3 Early Data (0-&lt;span class="caps"&gt;RTT&lt;/span&gt;)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_early_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Speeds up reconnections by allowing data in the first &lt;span class="caps"&gt;TLS&lt;/span&gt; flight. Early data is vulnerable to replay attacks, so pass the status to your&amp;nbsp;backend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Early-Data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ssl_early_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add this to&amp;nbsp;all &lt;code&gt;proxy_pass&lt;/code&gt; locations so your application can reject replayed requests on sensitive endpoints (&lt;span class="caps"&gt;POST&lt;/span&gt;, &lt;span class="caps"&gt;DELETE&lt;/span&gt;,&amp;nbsp;etc.).&lt;/p&gt;
&lt;h3 id="session-tickets"&gt;Session&amp;nbsp;Tickets&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_session_tickets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Required for &lt;span class="caps"&gt;QUIC&lt;/span&gt; 0-&lt;span class="caps"&gt;RTT&lt;/span&gt; resumption. Unlike the common advice to disable session tickets for &lt;span class="caps"&gt;TLS&lt;/span&gt; 1.2 security, they&amp;#8217;re important for &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;performance.&lt;/p&gt;
&lt;h2 id="complete-example-configuration"&gt;Complete Example&amp;nbsp;Configuration&lt;/h2&gt;
&lt;h3 id="nginxconf"&gt;nginx.conf&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;worker_processes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;error_log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/var/log/nginx/error.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;worker_connections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;http&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;include&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s"&gt;mime.types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;default_type&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;application/octet-stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;sendfile&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;tcp_nopush&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;tcp_nodelay&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;keepalive_timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# QUIC settings&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;quic_retry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_early_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# SSL global settings&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_protocols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;TLSv1.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_prefer_server_ciphers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_session_cache&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_session_tickets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# gzip compression&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;gzip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;gzip_vary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;gzip_proxied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;gzip_comp_level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;gzip_types&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;text/css&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;application/javascript&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="s"&gt;text/xml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;application/xml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;application/xml+rss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;text/javascript&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="s"&gt;image/svg+xml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;image/x-icon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/usr/local/etc/nginx/vhosts/*.conf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="primary-vhost-with-reuseport"&gt;Primary vhost&amp;nbsp;(with &lt;code&gt;reuseport&lt;/code&gt;)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;reuseport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;reuseport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;http2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;/path/to/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/path/to/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# ... your config ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;@proxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Early-Data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ssl_early_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# ... your proxy config ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="additional-vhosts-without-reuseport"&gt;Additional vhosts&amp;nbsp;(without &lt;code&gt;reuseport&lt;/code&gt;)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;[::]:443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;http2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;other.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;/path/to/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/path/to/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alt-Svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;h3=&amp;quot;:443&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;ma=86400&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# ... your config ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="testing-and-verification"&gt;Testing and&amp;nbsp;Verification&lt;/h2&gt;
&lt;h3 id="server-side"&gt;Server-side&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Verify QUIC TLS handshake (note: only tests TLS layer, not full HTTP/3 framing)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;s_client&lt;span class="w"&gt; &lt;/span&gt;-connect&lt;span class="w"&gt; &lt;/span&gt;yourdomain.com:443&lt;span class="w"&gt; &lt;/span&gt;-quic&lt;span class="w"&gt; &lt;/span&gt;-alpn&lt;span class="w"&gt; &lt;/span&gt;h3

&lt;span class="c1"&gt;# Check Alt-Svc is present on all paths&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;-sI&lt;span class="w"&gt; &lt;/span&gt;https://yourdomain.com/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;alt-svc
curl&lt;span class="w"&gt; &lt;/span&gt;-sI&lt;span class="w"&gt; &lt;/span&gt;https://yourdomain.com/some/path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;alt-svc

&lt;span class="c1"&gt;# Monitor live QUIC traffic (inside the nginx jail)&lt;/span&gt;
tcpdump&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;vnet0&lt;span class="w"&gt; &lt;/span&gt;udp&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;

&lt;span class="c1"&gt;# Verify reuseport sockets - expect multiple UDP entries on 443&lt;/span&gt;
sockstat&lt;span class="w"&gt; &lt;/span&gt;-4&lt;span class="w"&gt; &lt;/span&gt;-6&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;nginx&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="client-side"&gt;Client-side&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Online checker:&lt;/strong&gt; &lt;a href="https://http3check.net"&gt;http3check.net&lt;/a&gt; - verifies both Alt-Svc and &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;connectivity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser DevTools:&lt;/strong&gt; Network tab → right-click column headers → enable &amp;#8220;Protocol&amp;#8221;. Look&amp;nbsp;for &lt;code&gt;h3&lt;/code&gt; (may require a second page load after Alt-Svc is&amp;nbsp;cached)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chrome &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;internals:&lt;/strong&gt; &lt;code&gt;chrome://net-export/&lt;/code&gt; captures a netlog, analyzable at &lt;a href="https://netlog-viewer.appspot.com"&gt;netlog-viewer.appspot.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="debugging-tips"&gt;Debugging&amp;nbsp;Tips&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;If Chrome marks &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;as &lt;code&gt;is_broken: true&lt;/code&gt;, clear cached data via Settings → Clear browsing data → Cached images and&amp;nbsp;files&lt;/li&gt;
&lt;li&gt;Use an incognito window for clean tests without cached Alt-Svc&amp;nbsp;state&lt;/li&gt;
&lt;li&gt;Check nginx error&amp;nbsp;logs: &lt;code&gt;grep -i quic /var/log/nginx/error.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For deep debugging, temporarily&amp;nbsp;set &lt;code&gt;error_log /var/log/nginx/quic-debug.log debug;&lt;/code&gt; - remove it again promptly, it fills up&amp;nbsp;fast&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary-of-pitfalls"&gt;Summary of&amp;nbsp;Pitfalls&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;listen 443 ssl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Site completely down&lt;/td&gt;
&lt;td&gt;Add&amp;nbsp;both &lt;code&gt;ssl&lt;/code&gt; and &lt;code&gt;quic&lt;/code&gt; listeners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;Alt-Svc&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;Browsers never try &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;Alt-Svc&lt;/code&gt; with &lt;code&gt;always&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add_header&lt;/code&gt; inheritance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Alt-Svc&lt;/code&gt; missing on some paths&lt;/td&gt;
&lt;td&gt;Repeat &lt;code&gt;Alt-Svc&lt;/code&gt; in&amp;nbsp;every &lt;code&gt;location&lt;/code&gt; with &lt;code&gt;add_header&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock OpenSSL&lt;/td&gt;
&lt;td&gt;51-byte &lt;span class="caps"&gt;QUIC&lt;/span&gt;&amp;nbsp;rejections, &lt;code&gt;openssl s_client&lt;/code&gt; lies&lt;/td&gt;
&lt;td&gt;Rebuild nginx&amp;nbsp;with &lt;code&gt;HTTPV3_BORING=on&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker affinity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ERR_QUIC_PROTOCOL_ERROR_QUIC_PUBLIC_RESET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;reuseport&lt;/code&gt; + &lt;code&gt;quic_retry on&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;span class="caps"&gt;UDP&lt;/span&gt; &lt;code&gt;rdr&lt;/code&gt; in pf&lt;/td&gt;
&lt;td&gt;IPv4 &lt;span class="caps"&gt;QUIC&lt;/span&gt; silently unreachable despite pass rules&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;rdr&lt;/code&gt; for &lt;span class="caps"&gt;UDP&lt;/span&gt; 443 alongside the &lt;span class="caps"&gt;TCP&lt;/span&gt; one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall blocks &lt;span class="caps"&gt;UDP&lt;/span&gt; 443&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;QUIC&lt;/span&gt; unreachable from clients&lt;/td&gt;
&lt;td&gt;Add &lt;span class="caps"&gt;UDP&lt;/span&gt;&amp;nbsp;443 &lt;code&gt;pass&lt;/code&gt; rules for IPv4 and IPv6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Getting &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 working on FreeBSD with nginx requires more legwork than on Linux, mostly because of the &lt;span class="caps"&gt;SSL&lt;/span&gt; library situation and the absence&amp;nbsp;of &lt;code&gt;quic_bpf&lt;/code&gt;. The four things that actually&amp;nbsp;matter:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;BoringSSL&lt;/strong&gt; - rebuild from ports&amp;nbsp;with &lt;code&gt;HTTPV3=on&lt;/code&gt; and &lt;code&gt;HTTPV3_BORING=on&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;quic_retry on&lt;/code&gt;&lt;/strong&gt; - ensures correct worker routing during session&amp;nbsp;establishment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reuseport&lt;/code&gt;&lt;/strong&gt; on &lt;span class="caps"&gt;QUIC&lt;/span&gt; listeners - per-worker &lt;span class="caps"&gt;UDP&lt;/span&gt; sockets for reliable packet&amp;nbsp;distribution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Alt-Svc&lt;/code&gt; in every location block&lt;/strong&gt; - the&amp;nbsp;silent &lt;code&gt;add_header&lt;/code&gt; inheritance gotcha will bite&amp;nbsp;you&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In a jail setup with IPv4 &lt;span class="caps"&gt;NAT&lt;/span&gt;, add a&amp;nbsp;dedicated &lt;code&gt;rdr&lt;/code&gt; rule for &lt;span class="caps"&gt;UDP&lt;/span&gt; 443 -&amp;nbsp;a &lt;code&gt;pass&lt;/code&gt; rule alone isn&amp;#8217;t&amp;nbsp;enough.&lt;/p&gt;
&lt;p&gt;Once all of this is in place, &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 works reliably and delivers noticeably lower latency, especially for connection-heavy workloads like a Mastodon&amp;nbsp;instance.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nginx.org/en/docs/http/ngx_http_v3_module.html"&gt;nginx &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 module&amp;nbsp;documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://boringssl.googlesource.com/boringssl/"&gt;BoringSSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://http3check.net"&gt;http3check.net&lt;/a&gt; - online &lt;span class="caps"&gt;HTTP&lt;/span&gt;/3 availability&amp;nbsp;checker&lt;/li&gt;
&lt;li&gt;&lt;a href="https://netlog-viewer.appspot.com"&gt;Chrome netlog&amp;nbsp;viewer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook –&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="nginx"/><category term="http3"/><category term="quic"/><category term="networking"/><category term="jails"/><category term="bastille"/></entry><entry><title>Integrating LaCrosse Sensors into Home Assistant via JeeLink on FreeBSD</title><link href="https://blog.hofstede.it/integrating-lacrosse-sensors-into-home-assistant-via-jeelink-on-freebsd/" rel="alternate"/><published>2026-02-19T00:00:00+01:00</published><updated>2026-02-19T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-19:/integrating-lacrosse-sensors-into-home-assistant-via-jeelink-on-freebsd/</id><summary type="html">&lt;p&gt;Using a JeeLink &lt;span class="caps"&gt;USB&lt;/span&gt; stick inside a FreeBSD jail to bridge legacy LaCrosse temperature and humidity sensors to Home Assistant over &lt;span class="caps"&gt;MQTT&lt;/span&gt;&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="LaCrosse Sensor" src="https://blog.hofstede.it/images/2026-02-19-lacrosse-jeelink-homeassistant-freebsd.png" title="LaCrosse Temperature Sensor"&gt;&lt;/p&gt;
&lt;p&gt;Home Assistant runs great inside a bhyve &lt;span class="caps"&gt;VM&lt;/span&gt; on FreeBSD, but &lt;span class="caps"&gt;USB&lt;/span&gt; passthrough into bhyve is not straightforward. When I needed to integrate a ConBee &lt;span class="caps"&gt;II&lt;/span&gt; Zigbee dongle, the solution was to run zigbee2mqtt inside a FreeBSD jail - where &lt;span class="caps"&gt;USB&lt;/span&gt; devices can be exposed via devfs rules - and forward everything over &lt;span class="caps"&gt;MQTT&lt;/span&gt;. This same pattern turns out to work perfectly for other &lt;span class="caps"&gt;USB&lt;/span&gt; receivers, like the JeeLink with its legacy LaCrosse temperature and humidity&amp;nbsp;sensors.&lt;/p&gt;
&lt;p&gt;The JeeLink is a small &lt;span class="caps"&gt;USB&lt;/span&gt; stick with an &lt;span class="caps"&gt;RFM12B&lt;/span&gt; radio module running the LaCrosseITPlusReader sketch. It picks up transmissions from LaCrosse &lt;span class="caps"&gt;TX&lt;/span&gt;-series sensors on 868 MHz, decodes them, and outputs the raw data over a serial port. All we need is a small bridge script that reads the serial data and publishes it to &lt;span class="caps"&gt;MQTT&lt;/span&gt;.&lt;/p&gt;
&lt;h2 id="the-stack"&gt;The&amp;nbsp;Stack&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[ LaCrosse TX sensors ]
        |  868 MHz
        v
[ JeeLink USB (FT232R) ] --&amp;gt; /dev/cuaU1
        |
        v
[ FreeBSD Jail ]
  lacrosse2mqtt.py
        |  MQTT
        v
[ Mosquitto Broker ]
        |
        v
[ Home Assistant (bhyve VM) ]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The jail already exists for zigbee2mqtt, so adding a second serial device and a Python script is minimal additional&amp;nbsp;overhead.&lt;/p&gt;
&lt;h2 id="identifying-the-device"&gt;Identifying the&amp;nbsp;Device&lt;/h2&gt;
&lt;p&gt;FreeBSD recognizes the JeeLink&amp;#8217;s &lt;span class="caps"&gt;FTDI&lt;/span&gt; chip out of the&amp;nbsp;box:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;usbconfig&lt;span class="w"&gt; &lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ugen0.4: &amp;lt;FT232 Serial (UART) IC Future Technology Devices International, Ltd&amp;gt; at usbus0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The kernel attaches it via&amp;nbsp;the &lt;code&gt;uftdi&lt;/code&gt; driver, which maps to&amp;nbsp;a &lt;code&gt;/dev/cuaU*&lt;/code&gt; serial device. If you already have another &lt;span class="caps"&gt;USB&lt;/span&gt; serial device (like a Zigbee dongle&amp;nbsp;on &lt;code&gt;/dev/cuaU0&lt;/code&gt;), the JeeLink will appear&amp;nbsp;as &lt;code&gt;/dev/cuaU1&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="passing-the-device-into-the-jail"&gt;Passing the Device into the&amp;nbsp;Jail&lt;/h2&gt;
&lt;p&gt;In &lt;code&gt;/etc/devfs.rules&lt;/code&gt;, extend the jail&amp;#8217;s ruleset to include the new serial&amp;nbsp;port:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[devfsrules_jail_z2m=10]
add include $devfsrules_hide_all
add include $devfsrules_unhide_login
add path &amp;#39;cuaU0&amp;#39; unhide
add path &amp;#39;cuaU1&amp;#39; unhide
add path &amp;#39;ugen*&amp;#39; unhide
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The jail configuration&amp;nbsp;in &lt;code&gt;/etc/jail.conf&lt;/code&gt; references this&amp;nbsp;ruleset:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;z2m {
    # ... other settings ...
    devfs_ruleset = 10;
    enforce_statfs = 1;
    allow.mount.devfs;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Restart the jail for the new device to appear inside&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="verifying-the-serial-connection"&gt;Verifying the Serial&amp;nbsp;Connection&lt;/h2&gt;
&lt;p&gt;Inside the jail,&amp;nbsp;use &lt;code&gt;cu&lt;/code&gt; to confirm the JeeLink is transmitting and the baud rate is correct. The LaCrosseITPlusReader sketch runs at 57600&amp;nbsp;baud:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cu&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;/dev/cuaU1&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;57600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If everything is working, you&amp;#8217;ll see a header line followed by sensor&amp;nbsp;readings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[LaCrosseITPlusReader.10.1s (RFM12B f:868300 r:17241)]
OK 9 8 1 4 22 106
OK 9 44 129 4 182 52
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Exit &lt;code&gt;cu&lt;/code&gt; with &lt;code&gt;~.&lt;/code&gt; and move on. If you see garbage, double-check the baud rate - this was by far the most common cause of confusion during my&amp;nbsp;setup.&lt;/p&gt;
&lt;h2 id="decoding-the-data-format"&gt;Decoding the Data&amp;nbsp;Format&lt;/h2&gt;
&lt;p&gt;Each &lt;code&gt;OK 9&lt;/code&gt; line from the JeeLink represents a sensor&amp;nbsp;reading:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;OK 9 &amp;lt;ID&amp;gt; &amp;lt;flags&amp;gt; &amp;lt;temp_high&amp;gt; &amp;lt;temp_low&amp;gt; &amp;lt;humidity&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Temperature&lt;/strong&gt; is a 16-bit value divided by 10, offset by&amp;nbsp;100:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;temp = (temp_high * 256 + temp_low) / 10.0 - 100.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For example,&amp;nbsp;bytes &lt;code&gt;4 182&lt;/code&gt; decode&amp;nbsp;to &lt;code&gt;(4 * 256 + 182) / 10.0 - 100.0 = 20.6°C&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Humidity&lt;/strong&gt; is the raw byte value in percent. A value&amp;nbsp;of &lt;code&gt;52&lt;/code&gt; means 52% relative&amp;nbsp;humidity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flags&lt;/strong&gt; encode battery status: bit 7&amp;nbsp;(&lt;code&gt;0x80&lt;/code&gt;) indicates a new battery was just inserted, bit 0&amp;nbsp;(&lt;code&gt;0x01&lt;/code&gt;) signals a weak&amp;nbsp;battery.&lt;/p&gt;
&lt;h2 id="the-bridge-script"&gt;The Bridge&amp;nbsp;Script&lt;/h2&gt;
&lt;p&gt;Install the dependencies inside the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;py311-serial&lt;span class="w"&gt; &lt;/span&gt;py311-paho-mqtt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then&amp;nbsp;create &lt;code&gt;/usr/local/bin/lacrosse2mqtt.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;serial&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;paho.mqtt.client&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;mqtt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%(asctime)s&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(message)s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;SERIAL_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/dev/cuaU1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;BAUD&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;57600&lt;/span&gt;
&lt;span class="n"&gt;MQTT_HOST&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;MQTT_PORT&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1883&lt;/span&gt;
&lt;span class="n"&gt;MQTT_USER&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;your_mqtt_user&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;MQTT_PASS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;your_mqtt_password&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;SENSOR_IDS&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;               &lt;span class="c1"&gt;# whitelist: only your own sensor IDs&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mqtt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mqtt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallbackAPIVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERSION2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username_pw_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MQTT_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MQTT_PASS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MQTT_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MQTT_PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loop_start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;ser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serial&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SERIAL_PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BAUD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Connected to JeeLink on &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SERIAL_PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ignore&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;OK 9&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sensor_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SENSOR_IDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;flags&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;temp&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
        &lt;span class="n"&gt;humidity&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;new_batt&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weak_batt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Sensor &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;%.1f&lt;/span&gt;&lt;span class="s2"&gt;°C, &lt;/span&gt;&lt;span class="si"&gt;%d%%&lt;/span&gt;&lt;span class="s2"&gt; rH (new_batt=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; weak_batt=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_batt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weak_batt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lacrosse/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sensor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/temperature&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;retain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/humidity&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/battery_low&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weak_batt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;retain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;SENSOR_IDS&lt;/code&gt; whitelist is important. The 868 MHz band is shared, and you will pick up sensors from neighboring households. Temporarily remove the filter to discover your sensor&amp;#8217;s &lt;span class="caps"&gt;ID&lt;/span&gt; - cross-reference it against the physical display or move the sensor around to identify which readings are&amp;nbsp;yours.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;retain=True&lt;/code&gt; flag on every publish call is equally important. Without it, Home Assistant will show the entity as unavailable after a restart, because the broker has no stored value to replay to new&amp;nbsp;subscribers.&lt;/p&gt;
&lt;h2 id="running-as-a-service"&gt;Running as a&amp;nbsp;Service&lt;/h2&gt;
&lt;p&gt;Create an rc script&amp;nbsp;at &lt;code&gt;/usr/local/etc/rc.d/lacrosse2mqtt&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c1"&gt;# PROVIDE: lacrosse2mqtt&lt;/span&gt;
&lt;span class="c1"&gt;# REQUIRE: NETWORKING&lt;/span&gt;
&lt;span class="c1"&gt;# KEYWORD: shutdown&lt;/span&gt;

.&lt;span class="w"&gt; &lt;/span&gt;/etc/rc.subr

&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lacrosse2mqtt&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rcvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lacrosse2mqtt_enable&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/usr/local/bin/python3&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;command_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/usr/local/bin/lacrosse2mqtt.py&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/run/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pid&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;logfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/log/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;start_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_start&amp;quot;&lt;/span&gt;

lacrosse2mqtt_start&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Starting &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;...&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/usr/sbin/daemon&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;logfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;command_args&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

load_rc_config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;lacrosse2mqtt_enable&lt;/span&gt;&lt;span class="p"&gt;:=NO&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

run_rc_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Enable and start&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/rc.d/lacrosse2mqtt
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;lacrosse2mqtt_enable=&amp;quot;YES&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/rc.conf
service&lt;span class="w"&gt; &lt;/span&gt;lacrosse2mqtt&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify&amp;nbsp;with &lt;code&gt;service lacrosse2mqtt status&lt;/code&gt; and &lt;code&gt;tail -f /var/log/lacrosse2mqtt.log&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="verifying-mqtt"&gt;Verifying &lt;span class="caps"&gt;MQTT&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Before touching Home Assistant, confirm messages are arriving at the&amp;nbsp;broker:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mosquitto_sub&lt;span class="w"&gt; &lt;/span&gt;-h&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;your_mqtt_user&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;your_mqtt_password&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lacrosse/#&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;lacrosse/44/temperature 20.7
lacrosse/44/humidity 51
lacrosse/44/battery_low 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If nothing shows up, check the log file and make sure the &lt;span class="caps"&gt;MQTT&lt;/span&gt; credentials are&amp;nbsp;correct.&lt;/p&gt;
&lt;h2 id="home-assistant-configuration"&gt;Home Assistant&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Add the sensors&amp;nbsp;to &lt;code&gt;configuration.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;mqtt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;sensor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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="s"&gt;&amp;quot;Living&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Room&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Temperature&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;unique_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lacrosse_44_temperature&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;state_topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lacrosse/44/temperature&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;unit_of_measurement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;°C&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;device_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;temperature&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;state_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;measurement&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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="s"&gt;&amp;quot;Living&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Room&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Humidity&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;unique_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lacrosse_44_humidity&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;state_topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lacrosse/44/humidity&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;unit_of_measurement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;device_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;humidity&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;state_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;measurement&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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="s"&gt;&amp;quot;Living&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Room&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Sensor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Battery&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;unique_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lacrosse_44_battery&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;state_topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lacrosse/44/battery_low&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;device_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;battery&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Check the configuration under &lt;strong&gt;Developer Tools&lt;/strong&gt;, restart Home Assistant, and the entities will appear under &lt;strong&gt;Settings &amp;gt; Devices &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt; Services &amp;gt; Entities&lt;/strong&gt;. You can also verify &lt;span class="caps"&gt;MQTT&lt;/span&gt; messages are arriving inside &lt;span class="caps"&gt;HA&lt;/span&gt; at &lt;strong&gt;Settings &amp;gt; Devices &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt; Services &amp;gt; &lt;span class="caps"&gt;MQTT&lt;/span&gt; &amp;gt; Configure&lt;/strong&gt; using the &amp;#8220;Listen to a topic&amp;#8221; field&amp;nbsp;with &lt;code&gt;lacrosse/#&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="adding-more-sensors"&gt;Adding More&amp;nbsp;Sensors&lt;/h2&gt;
&lt;p&gt;When you add a new LaCrosse&amp;nbsp;sensor:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Insert fresh batteries and let it transmit for a&amp;nbsp;minute&lt;/li&gt;
&lt;li&gt;Watch the log&amp;nbsp;(&lt;code&gt;tail -f /var/log/lacrosse2mqtt.log&lt;/code&gt;) for the new sensor &lt;span class="caps"&gt;ID&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Add the &lt;span class="caps"&gt;ID&lt;/span&gt;&amp;nbsp;to &lt;code&gt;SENSOR_IDS&lt;/code&gt; in the Python&amp;nbsp;script&lt;/li&gt;
&lt;li&gt;Restart the service&amp;nbsp;(&lt;code&gt;service lacrosse2mqtt restart&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Add the corresponding &lt;span class="caps"&gt;MQTT&lt;/span&gt; sensors&amp;nbsp;to &lt;code&gt;configuration.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Garbage output from the serial port:&lt;/strong&gt; Wrong baud rate. The JeeLink with LaCrosseITPlusReader uses 57600. Verify interactively&amp;nbsp;with &lt;code&gt;cu&lt;/code&gt; before anything&amp;nbsp;else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sensor IDs from neighbors:&lt;/strong&gt; Expected on the shared 868 MHz band. Use&amp;nbsp;the &lt;code&gt;SENSOR_IDS&lt;/code&gt; whitelist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;new_batt=True&lt;/code&gt; after battery change:&lt;/strong&gt; Normal. The sensor sets this flag temporarily after a battery insertion. It clears on its&amp;nbsp;own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Entity shows &amp;#8220;—&amp;#8221; in Home Assistant:&lt;/strong&gt; &lt;span class="caps"&gt;MQTT&lt;/span&gt; messages are not being retained. Check&amp;nbsp;that &lt;code&gt;retain=True&lt;/code&gt; is set in&amp;nbsp;all &lt;code&gt;client.publish()&lt;/code&gt; calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;MQTT&lt;/span&gt; connection refused:&lt;/strong&gt; Your broker likely requires authentication. Add credentials&amp;nbsp;via &lt;code&gt;client.username_pw_set()&lt;/code&gt; in the&amp;nbsp;script.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serial device&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/dev/cuaU1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baud rate&lt;/td&gt;
&lt;td&gt;57600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JeeLink sketch&lt;/td&gt;
&lt;td&gt;LaCrosseITPlusReader 10.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="caps"&gt;MQTT&lt;/span&gt; topic structure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lacrosse/&amp;lt;sensor_id&amp;gt;/temperature&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature formula&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(high * 256 + low) / 10.0 - 100.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humidity&lt;/td&gt;
&lt;td&gt;Direct byte value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The pattern here - &lt;span class="caps"&gt;USB&lt;/span&gt; device in a FreeBSD jail, data bridged over &lt;span class="caps"&gt;MQTT&lt;/span&gt; - generalizes well beyond LaCrosse sensors. Any &lt;span class="caps"&gt;USB&lt;/span&gt;-based &lt;span class="caps"&gt;RF&lt;/span&gt; receiver that can&amp;#8217;t be passed directly into a bhyve &lt;span class="caps"&gt;VM&lt;/span&gt; can be handled this way. If you&amp;#8217;re already running zigbee2mqtt in a jail, adding another serial device is just a few lines in your devfs rules and a small Python script&amp;nbsp;away.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="further-reading"&gt;Further&amp;nbsp;Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/rinie/LaCrosseITPlusReader"&gt;LaCrosseITPlusReader&amp;nbsp;firmware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jeelabs.net/projects/hardware/wiki/JeeLink"&gt;JeeLink&amp;nbsp;hardware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.home-assistant.io/integrations/mqtt/"&gt;Home Assistant &lt;span class="caps"&gt;MQTT&lt;/span&gt;&amp;nbsp;integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook:&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html"&gt;paho-mqtt Python&amp;nbsp;client&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="jails"/><category term="home-assistant"/><category term="mqtt"/><category term="iot"/><category term="sensors"/></entry><entry><title>RHEL on ZFS Root: An Unholy Experiment</title><link href="https://blog.hofstede.it/rhel-on-zfs-root-an-unholy-experiment/" rel="alternate"/><published>2026-02-14T00:00:00+01:00</published><updated>2026-02-14T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-14:/rhel-on-zfs-root-an-unholy-experiment/</id><summary type="html">&lt;p&gt;Running Red Hat Enterprise Linux on &lt;span class="caps"&gt;ZFS&lt;/span&gt; root is not supported. I did it anyway. Here&amp;#8217;s how this cursed configuration came to be, why you shouldn&amp;#8217;t replicate it, and what the proper alternative looks&amp;nbsp;like.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Some experiments are born from necessity. Others from curiosity. This one was born from a Friday evening, a homelab &lt;span class="caps"&gt;VM&lt;/span&gt; with nothing to lose, and the thought: &amp;#8220;I wonder if convert2rhel will just&amp;#8230; let me do&amp;nbsp;this.&amp;#8221;&lt;/p&gt;
&lt;p&gt;&lt;img alt="RHEL on ZFS" src="https://blog.hofstede.it/images/2026-02-14-rhel-zfs-root.png" title="Red Hat Enterprise Linux 9.7 on ZFS Root - because why not"&gt;&lt;/p&gt;
&lt;p&gt;Let me be absolutely clear from the start: &lt;strong&gt;this is a terrible idea&lt;/strong&gt;. Red Hat does not support &lt;span class="caps"&gt;ZFS&lt;/span&gt; on root. The supported solution in the &lt;span class="caps"&gt;RHEL&lt;/span&gt; ecosystem is Stratis. If you do this in production, you&amp;#8217;re on your own. If you call Red Hat support with &lt;span class="caps"&gt;ZFS&lt;/span&gt; issues, they will politely suggest you reproduce the problem on a supported configuration before they even look at&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;This was purely an experiment for the lolz. Here&amp;#8217;s what I did and why you&amp;nbsp;shouldn&amp;#8217;t.&lt;/p&gt;
&lt;h2 id="the-proof-it-works-for-now"&gt;The Proof It Works (For&amp;nbsp;Now)&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[root@testhost ~]# uname -a
Linux testhost 5.14.0-611.16.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Sun Dec 7 05:52:24 EST 2025 x86_64 x86_64 x86_64 GNU/Linux

[root@testhost ~]# cat /etc/os-release 
NAME=&amp;quot;Red Hat Enterprise Linux&amp;quot;
VERSION=&amp;quot;9.7 (Plow)&amp;quot;
ID=&amp;quot;rhel&amp;quot;
ID_LIKE=&amp;quot;fedora&amp;quot;
VERSION_ID=&amp;quot;9.7&amp;quot;
PRETTY_NAME=&amp;quot;Red Hat Enterprise Linux 9.7 (Plow)&amp;quot;

[root@testhost ~]# zpool list
NAME    SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
rpool  67.5G  4.09G  63.4G        -         -     3%     6%  1.00x    ONLINE  -

[root@testhost ~]# zfs list
NAME         USED  AVAIL  REFER  MOUNTPOINT
rpool       4.09G  61.3G    96K  none
rpool/home   176K  61.3G    96K  legacy
rpool/root  4.09G  61.3G  2.65G  /

[root@testhost ~]# lsmod | grep zfs
zfs                  7024640  8
spl                   217088  1 zfs

[root@testhost ~]# dmesg | grep zfs
[    1.466549] zfs: module license &amp;#39;CDDL&amp;#39; taints kernel.
[    1.467131] zfs: module license taints kernel.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yes, that&amp;#8217;s genuine &lt;span class="caps"&gt;RHEL&lt;/span&gt; 9.7 running on a &lt;span class="caps"&gt;ZFS&lt;/span&gt; root pool. No, I don&amp;#8217;t recommend&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="the-how-a-tale-of-two-bad-decisions"&gt;The How: A Tale of Two Bad&amp;nbsp;Decisions&lt;/h2&gt;
&lt;p&gt;The path to this abomination was&amp;nbsp;straightforward:&lt;/p&gt;
&lt;h3 id="step-1-rocky-linux-9-on-zfs-root"&gt;Step 1: Rocky Linux 9 on &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;nbsp;Root&lt;/h3&gt;
&lt;p&gt;Start with Rocky Linux 9 installed on &lt;span class="caps"&gt;ZFS&lt;/span&gt; root using the excellent &lt;a href="https://openzfs.github.io/openzfs-docs/Getting%20Started/RHEL-based%20distro/Root%20on%20ZFS.html"&gt;OpenZFS documentation&lt;/a&gt;. This guide is well-maintained and produces a working&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;At this point, you have a perfectly functional Rocky Linux system with &lt;span class="caps"&gt;ZFS&lt;/span&gt; root. You could stop here. You &lt;em&gt;should&lt;/em&gt; stop here. I did not stop&amp;nbsp;here.&lt;/p&gt;
&lt;h3 id="step-2-convert2rhel-with-extreme-prejudice"&gt;Step 2: Convert2RHEL (With Extreme&amp;nbsp;Prejudice)&lt;/h3&gt;
&lt;p&gt;This is where things get cursed. Convert2RHEL is designed to convert CentOS, Rocky, or AlmaLinux systems to genuine &lt;span class="caps"&gt;RHEL&lt;/span&gt;. It performs extensive pre-flight checks to ensure the system is in a supported state before&amp;nbsp;conversion.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; root is very much &lt;em&gt;not&lt;/em&gt; a supported&amp;nbsp;state.&lt;/p&gt;
&lt;p&gt;The tool correctly identified numerous&amp;nbsp;problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Non-standard bootloader configuration (rEFInd instead of &lt;span class="caps"&gt;GRUB&lt;/span&gt; in the usual&amp;nbsp;location)&lt;/li&gt;
&lt;li&gt;Unexpected partition&amp;nbsp;layout&lt;/li&gt;
&lt;li&gt;Missing dracut modules for standard&amp;nbsp;setups&lt;/li&gt;
&lt;li&gt;Various &amp;#8220;are you sure this is a real system?&amp;#8221; sanity&amp;nbsp;checks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The solution? Disable the checks. All of them. Every single one that stands between you and your bad&amp;nbsp;decision.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;convert2rhel&lt;span class="w"&gt; &lt;/span&gt;--disable-submgr-checks&lt;span class="w"&gt; &lt;/span&gt;--no-rpm-va&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--disablerepo&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--enablerepo&lt;span class="w"&gt; &lt;/span&gt;rhel-9-for-x86_64-baseos-rpms&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--enablerepo&lt;span class="w"&gt; &lt;/span&gt;rhel-9-for-x86_64-appstream-rpms
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That handles some of them. For the rest, I had to patch convert2rhel directly, commenting out validation functions that insisted on standard configurations. If you&amp;#8217;ve ever edited a vendor tool&amp;#8217;s source code to make it stop telling you &amp;#8220;no,&amp;#8221; you know the feeling. It&amp;#8217;s the system administration equivalent of cutting the warning label off a&amp;nbsp;mattress.&lt;/p&gt;
&lt;h3 id="step-3-reboot-and-pray"&gt;Step 3: Reboot and&amp;nbsp;Pray&lt;/h3&gt;
&lt;p&gt;After the conversion completes, you need to manually clean up the remnants of the Rocky release packages. A&amp;nbsp;few &lt;code&gt;rpm -e&lt;/code&gt; invocations,&amp;nbsp;some &lt;code&gt;dnf distro-sync&lt;/code&gt; runs, and a quick check that the &lt;span class="caps"&gt;ZFS&lt;/span&gt; dracut modules are still intact in the initramfs. Nothing too&amp;nbsp;dramatic.&lt;/p&gt;
&lt;p&gt;Then comes the moment of truth. You&amp;nbsp;type &lt;code&gt;reboot&lt;/code&gt;, stare at the console, and&amp;nbsp;wait.&lt;/p&gt;
&lt;p&gt;The rEFInd menu appears. The kernel loads. &lt;span class="caps"&gt;ZFS&lt;/span&gt; imports the pool. Systemd starts doing its thing. And then - a login prompt. &lt;span class="caps"&gt;RHEL&lt;/span&gt; 9.7 on &lt;span class="caps"&gt;ZFS&lt;/span&gt; root, fully subscribed and receiving updates from Red Hat&amp;#8217;s repositories. It actually&amp;nbsp;worked.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;m not going to pretend I wasn&amp;#8217;t&amp;nbsp;surprised.&lt;/p&gt;
&lt;h2 id="why-this-is-a-bad-idea"&gt;Why This Is A Bad&amp;nbsp;Idea&lt;/h2&gt;
&lt;p&gt;Let me count the&amp;nbsp;ways:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No Support&lt;/strong&gt;: Red Hat will not help you with &lt;span class="caps"&gt;ZFS&lt;/span&gt; issues. Period. The first thing support will ask is &amp;#8220;can you reproduce this on a supported&amp;nbsp;filesystem?&amp;#8221;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kernel Updates&lt;/strong&gt;: &lt;span class="caps"&gt;ZFS&lt;/span&gt; is out-of-tree. Every kernel update triggers a &lt;span class="caps"&gt;DKMS&lt;/span&gt; rebuild of the &lt;span class="caps"&gt;ZFS&lt;/span&gt; modules. This usually works, but when it doesn&amp;#8217;t, you&amp;#8217;re debugging it yourself with no root&amp;nbsp;filesystem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dracut Complexity&lt;/strong&gt;: The initramfs needs &lt;span class="caps"&gt;ZFS&lt;/span&gt; modules and tools to mount the root pool. Any dracut configuration drift could leave you with an unbootable&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Convert2RHEL Wasn&amp;#8217;t Designed For This&lt;/strong&gt;: By bypassing validation checks, you&amp;#8217;re in uncharted territory. Future convert2rhel updates might break or refuse to&amp;nbsp;run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SELinux Contexts&lt;/strong&gt;: &lt;span class="caps"&gt;ZFS&lt;/span&gt; supports xattrs&amp;nbsp;(and &lt;code&gt;xattr=sa&lt;/code&gt; is the default on Linux), so basic SELinux labeling works. But edge cases will bite you: dataset send/receive can lose security labels, full relabels are slow, and some &lt;span class="caps"&gt;ZFS&lt;/span&gt; operations don&amp;#8217;t trigger the expected SELinux hooks. It mostly works, but &amp;#8220;mostly&amp;#8221; is doing a lot of heavy lifting in that&amp;nbsp;sentence.&lt;/p&gt;
&lt;h2 id="the-proper-solution-stratis"&gt;The Proper Solution:&amp;nbsp;Stratis&lt;/h2&gt;
&lt;p&gt;If you want advanced storage features on &lt;span class="caps"&gt;RHEL&lt;/span&gt;, the supported path is &lt;a href="https://stratis-storage.github.io/"&gt;Stratis&lt;/a&gt;. Stratis&amp;nbsp;provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pool-based storage&amp;nbsp;management&lt;/li&gt;
&lt;li&gt;Thin&amp;nbsp;provisioning&lt;/li&gt;
&lt;li&gt;Snapshots&lt;/li&gt;
&lt;li&gt;Encryption&amp;nbsp;support&lt;/li&gt;
&lt;li&gt;Full Red Hat&amp;nbsp;support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stratis uses device-mapper for thin provisioning and &lt;span class="caps"&gt;XFS&lt;/span&gt; as the filesystem layer, providing a modern storage management interface while staying within the supported stack. It&amp;#8217;s what Red Hat engineers will recommend when you ask about advanced filesystem&amp;nbsp;features.&lt;/p&gt;
&lt;p&gt;To use Stratis on &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;9:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;stratisd&lt;span class="w"&gt; &lt;/span&gt;stratis-cli
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;stratisd
sudo&lt;span class="w"&gt; &lt;/span&gt;stratis&lt;span class="w"&gt; &lt;/span&gt;pool&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;mypool&lt;span class="w"&gt; &lt;/span&gt;/dev/sdb
sudo&lt;span class="w"&gt; &lt;/span&gt;stratis&lt;span class="w"&gt; &lt;/span&gt;filesystem&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;mypool&lt;span class="w"&gt; &lt;/span&gt;myfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The resulting filesystem can then be added&amp;nbsp;to &lt;code&gt;/etc/fstab&lt;/code&gt; with&amp;nbsp;the &lt;code&gt;x-systemd.requires=stratisd.service&lt;/code&gt; mount&amp;nbsp;option.&lt;/p&gt;
&lt;h2 id="what-actually-works"&gt;What Actually&amp;nbsp;Works&lt;/h2&gt;
&lt;p&gt;Surprisingly, most things just work. Package updates from &lt;span class="caps"&gt;RHEL&lt;/span&gt; repositories install without complaint. Systemd services start and stop like they should. Networking, user management, firewalld - all blissfully unaware that they&amp;#8217;re sitting on a filesystem Oracle has lawyers for. SELinux mostly cooperates too, since &lt;span class="caps"&gt;ZFS&lt;/span&gt; defaults&amp;nbsp;to &lt;code&gt;xattr=sa&lt;/code&gt; on Linux and handles standard labeling&amp;nbsp;fine.&lt;/p&gt;
&lt;h2 id="what-might-not-work"&gt;What Might Not&amp;nbsp;Work&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Major version upgrades&lt;/strong&gt;: Leapp (&lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;#8217;s in-place upgrade tool) will absolutely not appreciate what you&amp;#8217;ve done to this system. Going to &lt;span class="caps"&gt;RHEL&lt;/span&gt; 10 will require creative problem-solving, or more likely, a fresh&amp;nbsp;install&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rescue mode&lt;/strong&gt;: &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;#8217;s rescue environments assume your root is on ext4 or &lt;span class="caps"&gt;XFS&lt;/span&gt;. Booting into rescue mode on a &lt;span class="caps"&gt;ZFS&lt;/span&gt; root system gives you a very unhelpful&amp;nbsp;shell&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated diagnostics&lt;/strong&gt;: Sosreport and friends&amp;nbsp;expect &lt;code&gt;/&lt;/code&gt; to be on a filesystem they recognize. Some checks will simply fail or produce&amp;nbsp;nonsense&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Support tickets&lt;/strong&gt;: &amp;#8220;Can you reproduce this on a supported filesystem?&amp;#8221; will be the response to every case you&amp;nbsp;open&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-honest-assessment"&gt;The Honest&amp;nbsp;Assessment&lt;/h2&gt;
&lt;p&gt;This works because the Linux kernel doesn&amp;#8217;t particularly care what filesystem&amp;nbsp;holds &lt;code&gt;/&lt;/code&gt;. As long as the initramfs can mount it and userspace can find its libraries, the system boots. &lt;span class="caps"&gt;ZFS&lt;/span&gt; is mature, well-tested software. It handles the filesystem layer just&amp;nbsp;fine.&lt;/p&gt;
&lt;p&gt;But &amp;#8220;works&amp;#8221; and &amp;#8220;supported&amp;#8221; are very different things. This configuration works today, will probably work tomorrow, and could break spectacularly on the next kernel update. I wouldn&amp;#8217;t run anything I care about on&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Red Hat Enterprise Linux on &lt;span class="caps"&gt;ZFS&lt;/span&gt; root is technically possible. It&amp;#8217;s also a bad idea. I did it because I wanted to see if I could, and because Friday evenings in the homelab are for experiments that would get you fired if you tried them at&amp;nbsp;work.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re reading this thinking &amp;#8220;I should try this&amp;#8221; - do it on a &lt;span class="caps"&gt;VM&lt;/span&gt; you&amp;#8217;re prepared to destroy. And maybe don&amp;#8217;t mention it in your next job&amp;nbsp;interview.&lt;/p&gt;
&lt;p&gt;For everyone else: Stratis exists, it&amp;#8217;s supported, and Red Hat engineers won&amp;#8217;t quietly judge you for using&amp;nbsp;it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/Getting%20Started/RHEL-based%20distro/Root%20on%20ZFS.html"&gt;OpenZFS: Root on &lt;span class="caps"&gt;ZFS&lt;/span&gt; for &lt;span class="caps"&gt;RHEL&lt;/span&gt;-based&amp;nbsp;distros&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/converting_from_an_rpm-based_linux_distribution_to_rhel/"&gt;Convert2RHEL&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stratis-storage.github.io/"&gt;Stratis Storage&amp;nbsp;Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/managing_file_systems/managing-layered-local-storage-with-stratis_managing-file-systems"&gt;Red Hat Stratis&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;My homelab &lt;span class="caps"&gt;VM&lt;/span&gt; didn&amp;#8217;t deserve this treatment, but it got it anyway. The things we do for a blog&amp;nbsp;post.&lt;/p&gt;</content><category term="Linux"/><category term="linux"/><category term="rhel"/><category term="zfs"/><category term="rocky"/><category term="convert2rhel"/><category term="unsupported"/></entry><entry><title>SELinux: A Practical Guide for Fedora and RHEL</title><link href="https://blog.hofstede.it/selinux-a-practical-guide-for-fedora-and-rhel/" rel="alternate"/><published>2026-02-13T00:00:00+01:00</published><updated>2026-02-13T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-13:/selinux-a-practical-guide-for-fedora-and-rhel/</id><summary type="html">&lt;p&gt;Moving beyond &amp;#8220;setenforce 0&amp;#8221; - a practical guide to understanding, troubleshooting, and working with SELinux on Fedora and Red Hat Enterprise&amp;nbsp;Linux.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="SELinux" src="https://blog.hofstede.it/images/2026-02-13-selinux-practical-guide.png" title="SELinux Architecture"&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;ve spent time on Fedora or &lt;span class="caps"&gt;RHEL&lt;/span&gt; systems, you&amp;#8217;ve encountered SELinux. And if you&amp;#8217;re like many administrators, your first instinct when something doesn&amp;#8217;t work is to check if SELinux is the culprit. The temptation to&amp;nbsp;run &lt;code&gt;setenforce 0&lt;/code&gt; is strong - but it&amp;#8217;s also the wrong&amp;nbsp;approach.&lt;/p&gt;
&lt;p&gt;SELinux (Security-Enhanced Linux) implements mandatory access control (&lt;span class="caps"&gt;MAC&lt;/span&gt;) at the kernel level. Unlike traditional Unix permissions (discretionary access control), SELinux policies are enforced regardless of file ownership or traditional permissions. A process running as root can still be blocked from accessing files if the SELinux policy doesn&amp;#8217;t permit&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;This guide focuses on practical skills: understanding what SELinux is doing, troubleshooting denials, and configuring policies correctly. We&amp;#8217;ll cover the concepts you need without getting lost in policy&amp;nbsp;theory.&lt;/p&gt;
&lt;p&gt;Most of the tools in this guide&amp;nbsp;(&lt;code&gt;semanage&lt;/code&gt;, &lt;code&gt;audit2allow&lt;/code&gt;, &lt;code&gt;restorecon&lt;/code&gt;) come from&amp;nbsp;the &lt;code&gt;policycoreutils-python-utils&lt;/code&gt; package, which isn&amp;#8217;t always installed on minimal&amp;nbsp;systems:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;policycoreutils-python-utils&lt;span class="w"&gt; &lt;/span&gt;setroubleshoot-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="why-selinux-matters"&gt;Why SELinux&amp;nbsp;Matters&lt;/h2&gt;
&lt;p&gt;SELinux isn&amp;#8217;t security theater. It provides genuine&amp;nbsp;defense-in-depth:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Confinement&lt;/strong&gt;: Even compromised root processes are limited by SELinux policy. A web server exploit that gains root can&amp;#8217;t suddenly read &lt;span class="caps"&gt;SSH&lt;/span&gt; keys or modify system&amp;nbsp;binaries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Least privilege&lt;/strong&gt;: Policies define exactly what each process type can access. Apache doesn&amp;#8217;t need to&amp;nbsp;read &lt;code&gt;/etc/shadow&lt;/code&gt;, so it&amp;nbsp;can&amp;#8217;t.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit trail&lt;/strong&gt;: Every denial is logged. Attempted intrusions leave fingerprints even when&amp;nbsp;blocked.&lt;/p&gt;
&lt;p&gt;The alternative - running in permissive mode or disabling SELinux entirely - removes this protection layer. The goal is to work &lt;em&gt;with&lt;/em&gt; SELinux, not around&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="selinux-modes"&gt;SELinux&amp;nbsp;Modes&lt;/h2&gt;
&lt;p&gt;SELinux operates in three&amp;nbsp;modes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enforcing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Policy is enforced, denials logged and blocked&lt;/td&gt;
&lt;td&gt;Production systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Permissive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Policy checked, denials logged but not blocked&lt;/td&gt;
&lt;td&gt;Debugging, policy development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disabled&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SELinux entirely inactive&lt;/td&gt;
&lt;td&gt;Not recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Check current&amp;nbsp;status:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sestatus
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output shows the mode, policy name, and other&amp;nbsp;details:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Temporarily switch&amp;nbsp;modes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Switch to permissive (survives until reboot)&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;setenforce&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Return to enforcing&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;setenforce&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For permanent changes,&amp;nbsp;edit &lt;code&gt;/etc/selinux/config&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;SELINUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;enforcing&lt;/span&gt;
&lt;span class="na"&gt;SELINUXTYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;targeted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;targeted&lt;/code&gt; policy is standard on Fedora and &lt;span class="caps"&gt;RHEL&lt;/span&gt; - it confines specific daemons while leaving user processes largely unrestricted. Other policy types exist&amp;nbsp;(&lt;code&gt;mls&lt;/code&gt; for multi-level security) but are&amp;nbsp;niche.&lt;/p&gt;
&lt;h2 id="understanding-selinux-contexts"&gt;Understanding SELinux&amp;nbsp;Contexts&lt;/h2&gt;
&lt;p&gt;Everything in SELinux revolves around &lt;strong&gt;contexts&lt;/strong&gt; (also called labels). Every process, file, socket, and network port has a security context that determines what can access&amp;nbsp;what.&lt;/p&gt;
&lt;h3 id="viewing-contexts"&gt;Viewing&amp;nbsp;Contexts&lt;/h3&gt;
&lt;p&gt;Files and&amp;nbsp;directories:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-Z&lt;span class="w"&gt; &lt;/span&gt;/var/www/html/index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Processes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ps&lt;span class="w"&gt; &lt;/span&gt;-Z&lt;span class="w"&gt; &lt;/span&gt;-C&lt;span class="w"&gt; &lt;/span&gt;httpd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;LABEL                              PID TTY          TIME CMD
system_u:system_r:httpd_t:s0      1234 ?        00:00:01 httpd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ports:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;semanage&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;http
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="context-format"&gt;Context&amp;nbsp;Format&lt;/h3&gt;
&lt;p&gt;Contexts have four colon-separated&amp;nbsp;fields:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;user:role:type:level
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For practical administration, the &lt;strong&gt;type&lt;/strong&gt; field&amp;nbsp;(&lt;code&gt;httpd_sys_content_t&lt;/code&gt;, &lt;code&gt;httpd_t&lt;/code&gt;) matters most. Types ending&amp;nbsp;in &lt;code&gt;_t&lt;/code&gt; are what policies match&amp;nbsp;against.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;system_u&lt;/code&gt;, &lt;code&gt;unconfined_u&lt;/code&gt;): SELinux user, maps to Linux&amp;nbsp;users&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;system_r&lt;/code&gt;, &lt;code&gt;object_r&lt;/code&gt;): Groups types for role-based&amp;nbsp;access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;httpd_t&lt;/code&gt;, &lt;code&gt;httpd_sys_content_t&lt;/code&gt;): The primary policy&amp;nbsp;discriminator&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;s0&lt;/code&gt;): &lt;span class="caps"&gt;MLS&lt;/span&gt;/&lt;span class="caps"&gt;MCS&lt;/span&gt; sensitivity level (usually s0 in targeted&amp;nbsp;policy)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-policy-works"&gt;How Policy&amp;nbsp;Works&lt;/h3&gt;
&lt;p&gt;The policy defines rules&amp;nbsp;like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;A process with&amp;nbsp;type &lt;code&gt;httpd_t&lt;/code&gt; can read files with&amp;nbsp;type &lt;code&gt;httpd_sys_content_t&lt;/code&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is why placing a file&amp;nbsp;in &lt;code&gt;/var/www/html/&lt;/code&gt; doesn&amp;#8217;t automatically make it readable by Apache - the context must match. A file&amp;nbsp;with &lt;code&gt;user_home_t&lt;/code&gt; context won&amp;#8217;t be accessible even if owned&amp;nbsp;by &lt;code&gt;apache:apache&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="common-administrative-tasks"&gt;Common Administrative&amp;nbsp;Tasks&lt;/h2&gt;
&lt;h3 id="changing-file-contexts"&gt;Changing File&amp;nbsp;Contexts&lt;/h3&gt;
&lt;p&gt;When you move or create files outside standard locations, they won&amp;#8217;t have the correct context. This often catches people off&amp;nbsp;guard: &lt;code&gt;cp&lt;/code&gt; inherits the destination directory&amp;#8217;s context,&amp;nbsp;but &lt;code&gt;mv&lt;/code&gt; preserves the original context. Moving a file from your home directory&amp;nbsp;into &lt;code&gt;/var/www/html/&lt;/code&gt; leaves it&amp;nbsp;with &lt;code&gt;user_home_t&lt;/code&gt; - and Apache can&amp;#8217;t read it despite the file being in the right&amp;nbsp;place.&lt;/p&gt;
&lt;p&gt;Two approaches exist to fix&amp;nbsp;contexts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Temporary (relabel gets reset)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chcon&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;httpd_sys_content_t&lt;span class="w"&gt; &lt;/span&gt;/srv/www/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Persistent (survives relabels)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define the context mapping&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;httpd_sys_content_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/srv/www(/.*)?&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Apply the context&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/srv/www/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;semanage fcontext&lt;/code&gt; command adds rules to the&amp;nbsp;policy. &lt;code&gt;restorecon&lt;/code&gt; applies those rules. This is the correct approach for&amp;nbsp;production.&lt;/p&gt;
&lt;p&gt;List custom context&amp;nbsp;mappings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;srv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="managing-port-labels"&gt;Managing Port&amp;nbsp;Labels&lt;/h3&gt;
&lt;p&gt;Running a service on a non-standard port? The port needs the correct&amp;nbsp;label:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow Apache to listen on port 8081&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;http_port_t&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Remove a custom port&amp;nbsp;mapping:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;semanage&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;http_port_t&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="selinux-booleans"&gt;SELinux&amp;nbsp;Booleans&lt;/h3&gt;
&lt;p&gt;Booleans toggle specific policy behaviors without rewriting policy. They&amp;#8217;re the preferred way to enable common&amp;nbsp;functionality:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# List booleans related to HTTP&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;boolean&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;httpd

&lt;span class="c1"&gt;# Or using getsebool&lt;/span&gt;
getsebool&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;httpd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Common &lt;span class="caps"&gt;HTTP&lt;/span&gt;&amp;nbsp;booleans:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow Apache to connect to network (for reverse proxy)&lt;/span&gt;
setsebool&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;httpd_can_network_connect&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# Allow Apache to send email&lt;/span&gt;
setsebool&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;httpd_can_sendmail&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# Allow home directory access&lt;/span&gt;
setsebool&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;httpd_enable_homedirs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# Allow network relay (for mod_proxy)&lt;/span&gt;
setsebool&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;httpd_can_network_relay&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-P&lt;/code&gt; flag makes changes persistent. Without it, changes reset on&amp;nbsp;reboot.&lt;/p&gt;
&lt;h2 id="troubleshooting-selinux-denials"&gt;Troubleshooting SELinux&amp;nbsp;Denials&lt;/h2&gt;
&lt;p&gt;When something doesn&amp;#8217;t work and you suspect SELinux, here&amp;#8217;s the systematic&amp;nbsp;approach:&lt;/p&gt;
&lt;h3 id="step-1-confirm-its-selinux"&gt;Step 1: Confirm It&amp;#8217;s&amp;nbsp;SELinux&lt;/h3&gt;
&lt;p&gt;Temporarily switch to&amp;nbsp;permissive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;setenforce&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the problem goes away, SELinux is the cause. Switch back to enforcing while you&amp;nbsp;debug:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;setenforce&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="step-2-find-the-denial"&gt;Step 2: Find the&amp;nbsp;Denial&lt;/h3&gt;
&lt;p&gt;SELinux denials are logged&amp;nbsp;to &lt;code&gt;/var/log/audit/audit.log&lt;/code&gt;. The format is dense, but tools&amp;nbsp;help:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check recent denials&lt;/span&gt;
ausearch&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;AVC&lt;span class="w"&gt; &lt;/span&gt;-ts&lt;span class="w"&gt; &lt;/span&gt;recent

&lt;span class="c1"&gt;# More readable format&lt;/span&gt;
ausearch&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;AVC&lt;span class="w"&gt; &lt;/span&gt;-ts&lt;span class="w"&gt; &lt;/span&gt;recent&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;audit2allow&lt;span class="w"&gt; &lt;/span&gt;-w
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Example&amp;nbsp;output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Type: AVC
Summary:

SELinux is preventing /usr/sbin/httpd from write access on the directory /var/www/uploads.

*****  Plugin catchall_boolean (89.3 confidence) suggests   ******************

If you want to allow httpd to have unified access to all httpd content
Then you must tell SELinux about this by enabling the &amp;#39;httpd_unified&amp;#39; boolean.

Do
setsebool -P httpd_unified 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For systems&amp;nbsp;without &lt;code&gt;auditd&lt;/code&gt;,&amp;nbsp;check &lt;code&gt;/var/log/messages&lt;/code&gt; for &lt;code&gt;setroubleshoot&lt;/code&gt; messages.&lt;/p&gt;
&lt;h3 id="step-3-understand-the-denial"&gt;Step 3: Understand the&amp;nbsp;Denial&lt;/h3&gt;
&lt;p&gt;Raw audit&amp;nbsp;entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;type=AVC msg=audit(1708032000.123:456): avc:  denied  { write } for  pid=1234 comm=&amp;quot;httpd&amp;quot; name=&amp;quot;uploads&amp;quot; dev=&amp;quot;dm-0&amp;quot; ino=789 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:httpd_sys_content_t:s0 tclass=dir permissive=0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Key&amp;nbsp;fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scontext&lt;/code&gt;: Source context (the&amp;nbsp;process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcontext&lt;/code&gt;: Target context (what was&amp;nbsp;accessed)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tclass&lt;/code&gt;: Object class (file, dir,&amp;nbsp;socket)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{ write }&lt;/code&gt;: The denied&amp;nbsp;permission&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-4-fix-the-denial"&gt;Step 4: Fix the&amp;nbsp;Denial&lt;/h3&gt;
&lt;p&gt;Options, from best to&amp;nbsp;worst:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Boolean&lt;/strong&gt; (preferred for common&amp;nbsp;cases):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;audit2allow&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;-a
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Often suggests a boolean. Enable&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Context fix&lt;/strong&gt; (for mislabeled&amp;nbsp;files):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check current context&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;-Z&lt;span class="w"&gt; &lt;/span&gt;/var/www/uploads/

&lt;span class="c1"&gt;# Fix to appropriate type&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;httpd_sys_rw_content_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/www/uploads(/.*)?&amp;quot;&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/var/www/uploads/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3. Custom policy module&lt;/strong&gt; (for application-specific&amp;nbsp;needs):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generate policy from audit log&lt;/span&gt;
ausearch&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;myapp&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--raw&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;audit2allow&lt;span class="w"&gt; &lt;/span&gt;-M&lt;span class="w"&gt; &lt;/span&gt;myapp

&lt;span class="c1"&gt;# Apply the module&lt;/span&gt;
semodule&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;myapp.pp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;4. Per-domain permissive&lt;/strong&gt; (temporary&amp;nbsp;workaround):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Make httpd permissive while keeping system enforcing&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;permissive&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;httpd_t
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Never disable SELinux globally for an application-specific&amp;nbsp;issue.&lt;/p&gt;
&lt;h2 id="selinux-and-containers"&gt;SELinux and&amp;nbsp;Containers&lt;/h2&gt;
&lt;p&gt;Podman and containers have specific SELinux considerations. The container runtime automatically sets up isolation, but volume mounts require&amp;nbsp;attention.&lt;/p&gt;
&lt;h3 id="container-file-labels"&gt;Container File&amp;nbsp;Labels&lt;/h3&gt;
&lt;p&gt;When mounting host directories into containers, SELinux contexts matter. Podman provides flags to handle&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# The :z flag relabels the directory for container use&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;/srv/data:/data:z&lt;span class="w"&gt; &lt;/span&gt;nginx

&lt;span class="c1"&gt;# The :Z flag makes the volume private to this container&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;/srv/data:/data:Z&lt;span class="w"&gt; &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;:z&lt;/strong&gt; (lowercase): Relabels&amp;nbsp;with &lt;code&gt;container_file_t&lt;/code&gt;, shared among&amp;nbsp;containers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;:Z&lt;/strong&gt; (uppercase): Private labeling, only this container can&amp;nbsp;access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: Both flags relabel the &lt;em&gt;host&lt;/em&gt; directory. If another service on the host depends on the original&amp;nbsp;label, &lt;code&gt;:z&lt;/code&gt; or &lt;code&gt;:Z&lt;/code&gt; will break it. Only use these on directories dedicated to container&amp;nbsp;use.&lt;/p&gt;
&lt;h3 id="quadlet-example"&gt;Quadlet&amp;nbsp;Example&lt;/h3&gt;
&lt;p&gt;In Podman Quadlet&amp;nbsp;files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:latest&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/web:/usr/share/nginx/html:z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;:z&lt;/code&gt; flag ensures SELinux doesn&amp;#8217;t block the container from reading mounted&amp;nbsp;files.&lt;/p&gt;
&lt;h3 id="checking-container-contexts"&gt;Checking Container&amp;nbsp;Contexts&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Container processes run in container_t&lt;/span&gt;
ps&lt;span class="w"&gt; &lt;/span&gt;-Z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;container

&lt;span class="c1"&gt;# Files in container volumes should have container_file_t&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;-Z&lt;span class="w"&gt; &lt;/span&gt;/srv/web
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="relabeling-the-filesystem"&gt;Relabeling the&amp;nbsp;Filesystem&lt;/h2&gt;
&lt;p&gt;If contexts get completely mangled (or after restoring from backup), force a full&amp;nbsp;relabel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create relabel trigger&lt;/span&gt;
touch&lt;span class="w"&gt; &lt;/span&gt;/.autorelabel

&lt;span class="c1"&gt;# Reboot&lt;/span&gt;
reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The system relabels all files on next boot. This takes time on large&amp;nbsp;filesystems.&lt;/p&gt;
&lt;p&gt;For targeted&amp;nbsp;relabeling:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/path/to/directory
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="common-scenarios"&gt;Common&amp;nbsp;Scenarios&lt;/h2&gt;
&lt;h3 id="web-server-with-non-standard-document-root"&gt;Web Server with Non-Standard Document&amp;nbsp;Root&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Set context for new web root&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;httpd_sys_content_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/srv/www(/.*)?&amp;quot;&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/srv/www

&lt;span class="c1"&gt;# For writable directories (uploads, cache)&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;httpd_sys_rw_content_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/srv/www/uploads(/.*)?&amp;quot;&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/srv/www/uploads
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="database-with-custom-data-directory"&gt;Database with Custom Data&amp;nbsp;Directory&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# PostgreSQL&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;postgresql_db_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/srv/pgdata(/.*)?&amp;quot;&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/srv/pgdata

&lt;span class="c1"&gt;# MariaDB/MySQL&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;mysqld_db_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/srv/mysqldata(/.*)?&amp;quot;&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/srv/mysqldata
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="ssh-on-non-standard-port"&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; on Non-Standard&amp;nbsp;Port&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add port to ssh_port_t&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;ssh_port_t&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2222&lt;/span&gt;

&lt;span class="c1"&gt;# Verify&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;ssh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="home-directory-web-access"&gt;Home Directory Web&amp;nbsp;Access&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enable userdir module access&lt;/span&gt;
setsebool&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;httpd_enable_homedirs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# For specific user home access (more restrictive)&lt;/span&gt;
setsebool&lt;span class="w"&gt; &lt;/span&gt;-P&lt;span class="w"&gt; &lt;/span&gt;httpd_read_user_content&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="useful-commands-reference"&gt;Useful Commands&amp;nbsp;Reference&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Check status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sestatus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List file contexts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ls -Z&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List process contexts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ps -Z&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change file context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chcon -t type file&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add persistent context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;semanage fcontext -a -t type "/path(/.*)?"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apply contexts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;restorecon -Rv /path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List booleans&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getsebool -a&lt;/code&gt; or &lt;code&gt;semanage boolean -l&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set boolean&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setsebool -P boolean_name 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List port labels&lt;/td&gt;
&lt;td&gt;&lt;code&gt;semanage port -l&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add port label&lt;/td&gt;
&lt;td&gt;&lt;code&gt;semanage port -a -t type -p tcp port&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search denials&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ausearch -m AVC -ts recent&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explain denials&lt;/td&gt;
&lt;td&gt;&lt;code&gt;audit2allow -w -a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create policy module&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ausearch --raw \| audit2allow -M name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install module&lt;/td&gt;
&lt;td&gt;&lt;code&gt;semodule -i name.pp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List modules&lt;/td&gt;
&lt;td&gt;&lt;code&gt;semodule -l&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="best-practices"&gt;Best&amp;nbsp;Practices&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Never disable SELinux globally&lt;/strong&gt;. If an application has issues, troubleshoot the specific&amp;nbsp;denial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use booleans first&lt;/strong&gt;. Many common use cases have pre-built&amp;nbsp;toggles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prefer context changes over policy changes&lt;/strong&gt;. Labeling files correctly is cleaner than adding policy&amp;nbsp;rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document customizations&lt;/strong&gt;.&amp;nbsp;Track &lt;code&gt;semanage&lt;/code&gt; commands in configuration management (Ansible, Salt) so systems remain&amp;nbsp;reproducible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test in permissive first&lt;/strong&gt;. When developing policy, run in permissive to collect all denials before switching to&amp;nbsp;enforcing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;audit2allow -w&lt;/code&gt; for explanations&lt;/strong&gt;. Before creating a policy module, understand what you&amp;#8217;re&amp;nbsp;allowing.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;SELinux is a powerful security layer that protects systems even when traditional permissions are bypassed. The learning curve is real, but the skills are essential for Fedora and &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;nbsp;administration.&lt;/p&gt;
&lt;p&gt;The key insight: SELinux isn&amp;#8217;t trying to break your applications. When denials occur, it&amp;#8217;s usually because files or ports are mislabeled, or a boolean needs to be set. The tools exist to diagnose and fix these issues correctly - use them instead of disabling the&amp;nbsp;protection.&lt;/p&gt;
&lt;p&gt;Once you understand contexts, booleans, and the troubleshooting workflow, SELinux becomes a tool you work with rather than an obstacle to work&amp;nbsp;around.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/10/html/using_selinux/"&gt;Red Hat SELinux&amp;nbsp;Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/audit2allow.1.html"&gt;audit2allow man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/semanage.8.html"&gt;semanage man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SELinux was born from the &lt;span class="caps"&gt;NSA&lt;/span&gt;&amp;#8217;s research into mandatory access controls - military-grade security that somehow became a default on every Fedora workstation and &lt;span class="caps"&gt;RHEL&lt;/span&gt; server. Most of the time it&amp;#8217;s invisible, doing its job without asking. When it does push back, it&amp;#8217;s specific enough to tell you exactly what went wrong and how to fix it. In a world where privilege escalation makes regular headlines, a kernel-level layer that constrains even root isn&amp;#8217;t an inconvenience - it&amp;#8217;s quiet&amp;nbsp;insurance.&lt;/p&gt;</content><category term="Linux"/><category term="linux"/><category term="fedora"/><category term="rhel"/><category term="selinux"/><category term="security"/><category term="containers"/><category term="podman"/></entry><entry><title>Podman 5.8: Quadlet Multi-File Install, Automatic SQLite Migration, and the Road to 6.0</title><link href="https://blog.hofstede.it/podman-58-quadlet-multi-file-install-automatic-sqlite-migration-and-the-road-to-60/" rel="alternate"/><published>2026-02-12T00:00:00+01:00</published><updated>2026-02-12T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-12:/podman-58-quadlet-multi-file-install-automatic-sqlite-migration-and-the-road-to-60/</id><summary type="html">&lt;p&gt;Podman 5.8 lands with multi-file Quadlet installs, automatic BoltDB-to-SQLite migration in preparation for Podman 6.0, and several quality-of-life improvements for container&amp;nbsp;workflows.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-02-12-podman-5-8-release.png" title="Podman Logo"&gt;&lt;/p&gt;
&lt;p&gt;Podman 5.8 dropped this week, and while it&amp;#8217;s not a flashy major release, it lays important groundwork for the upcoming 6.0 transition. The headline features are a significantly improved Quadlet install workflow and an automatic database migration that moves users from BoltDB to SQLite - quietly preparing the ecosystem for the next&amp;nbsp;generation.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;ve been running Quadlet-based deployments (I wrote about that approach in detail in my &lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;production-grade Podman Quadlets guide&lt;/a&gt;), several of these changes directly improve that&amp;nbsp;workflow.&lt;/p&gt;
&lt;h2 id="quadlet-multi-file-install"&gt;Quadlet Multi-File&amp;nbsp;Install&lt;/h2&gt;
&lt;p&gt;The biggest usability improvement in 5.8 is multi-file support&amp;nbsp;for &lt;code&gt;podman quadlet install&lt;/code&gt;. Previously, installing a multi-container setup meant feeding each Quadlet unit file to the command individually. Now you can bundle multiple Quadlet definitions into a single file, separated&amp;nbsp;by &lt;code&gt;---&lt;/code&gt; delimiters, with each section identified by&amp;nbsp;a &lt;code&gt;# FileName=&amp;lt;name&amp;gt;&lt;/code&gt; header.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# FileName=myapp-db.container&lt;/span&gt;
&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/postgres:17&lt;/span&gt;
&lt;span class="na"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;# FileName=myapp-web.container&lt;/span&gt;
&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:latest&lt;/span&gt;
&lt;span class="na"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;# FileName=myapp.network&lt;/span&gt;
&lt;span class="k"&gt;[Network]&lt;/span&gt;
&lt;span class="na"&gt;Subnet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10.89.1.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This brings Quadlet closer to the &amp;#8220;single manifest&amp;#8221; experience people are used to from Docker Compose or Kubernetes &lt;span class="caps"&gt;YAML&lt;/span&gt;, while retaining the systemd-native integration that makes Quadlet compelling in the first place. For complex deployments with multiple containers, networks, and volumes, this cuts the friction&amp;nbsp;significantly.&lt;/p&gt;
&lt;h2 id="automatic-boltdb-to-sqlite-migration"&gt;Automatic BoltDB to SQLite&amp;nbsp;Migration&lt;/h2&gt;
&lt;p&gt;This is the change that matters most for the long term. Podman has been transitioning its internal database from BoltDB to SQLite, and 5.8 makes this migration automatic: legacy BoltDB databases will convert to SQLite upon system&amp;nbsp;reboot.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# If automatic migration fails, you can trigger it manually&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;system&lt;span class="w"&gt; &lt;/span&gt;migrate&lt;span class="w"&gt; &lt;/span&gt;--migrate-db
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The motivation is straightforward. SQLite offers better concurrent access, more reliable crash recovery, and a proven track record at scale. BoltDB served Podman well, but as container workloads grow more complex - particularly with pods and Quadlet-managed services - the limitations of a simple key-value store become&amp;nbsp;apparent.&lt;/p&gt;
&lt;p&gt;This is explicitly preparation for Podman 6.0, where SQLite will be the only supported backend. If you&amp;#8217;re running 5.8, the migration happens transparently. If something goes wrong, the manual migration command is your&amp;nbsp;fallback.&lt;/p&gt;
&lt;h2 id="apparmor-support-in-quadlet"&gt;AppArmor Support in&amp;nbsp;Quadlet&lt;/h2&gt;
&lt;p&gt;Container Quadlet files now accept&amp;nbsp;an &lt;code&gt;AppArmor&lt;/code&gt; key for configuring container AppArmor profiles directly in the unit&amp;nbsp;definition:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:latest&lt;/span&gt;
&lt;span class="na"&gt;AppArmor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;my-custom-profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For distributions that use AppArmor as their &lt;span class="caps"&gt;MAC&lt;/span&gt; system (Debian, Ubuntu, &lt;span class="caps"&gt;SUSE&lt;/span&gt;), this removes a gap in Quadlet&amp;#8217;s security configuration. Previously, you&amp;#8217;d need to pass AppArmor settings through the less&amp;nbsp;ergonomic &lt;code&gt;PodmanArgs&lt;/code&gt; escape&amp;nbsp;hatch.&lt;/p&gt;
&lt;h2 id="performance-and-usability-improvements"&gt;Performance and Usability&amp;nbsp;Improvements&lt;/h2&gt;
&lt;p&gt;Several smaller changes round out the&amp;nbsp;release:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;podman exec --no-session&lt;/code&gt;&lt;/strong&gt; disables session tracking for exec commands. If you&amp;#8217;re running frequent exec calls against a container - health checks, monitoring scripts, batch operations - the session tracking overhead adds up. This flag trades tracking for raw&amp;nbsp;speed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;podman update --ulimit&lt;/code&gt;&lt;/strong&gt; lets you modify container ulimits on a running container without recreating it. Useful when you realize your database container needs more open file descriptors and you&amp;#8217;d rather not cycle the whole&amp;nbsp;service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;VM&lt;/span&gt; artifact path optimization&lt;/strong&gt;&amp;nbsp;improves &lt;code&gt;podman artifact add&lt;/code&gt; when working&amp;nbsp;with &lt;code&gt;podman machine&lt;/code&gt; VMs. Files on shared paths are now loaded directly from the &lt;span class="caps"&gt;VM&lt;/span&gt; filesystem rather than streaming through the &lt;span class="caps"&gt;REST&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt; - a notable speedup for large&amp;nbsp;artifacts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;podman secret create -&lt;/code&gt;&lt;/strong&gt; no longer requires pipe input. You can now type a secret directly at the terminal prompt, which is more intuitive for interactive&amp;nbsp;use.&lt;/p&gt;
&lt;h2 id="bugfixes"&gt;Bugfixes&lt;/h2&gt;
&lt;p&gt;The release addresses a range of issues across the&amp;nbsp;stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Healthcheck start period timing now works correctly with Kubernetes &lt;span class="caps"&gt;YAML&lt;/span&gt;&amp;nbsp;deployments&lt;/li&gt;
&lt;li&gt;Environment variable precedence&amp;nbsp;in &lt;code&gt;kube play&lt;/code&gt; follows the expected&amp;nbsp;order&lt;/li&gt;
&lt;li&gt;Volume mount path handling is more robust across edge&amp;nbsp;cases&lt;/li&gt;
&lt;li&gt;Authentication handling improvements for private&amp;nbsp;registries&lt;/li&gt;
&lt;li&gt;Windows path processing fixes for cross-platform&amp;nbsp;workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="updated-dependencies"&gt;Updated&amp;nbsp;Dependencies&lt;/h2&gt;
&lt;p&gt;Podman 5.8 ships with Buildah v1.43.0, containers/storage v1.62.0, containers/image v5.39.1, and containers/common&amp;nbsp;v0.67.0.&lt;/p&gt;
&lt;h2 id="looking-ahead"&gt;Looking&amp;nbsp;Ahead&lt;/h2&gt;
&lt;p&gt;The SQLite migration signals where Podman is heading. Version 6.0 will likely bring more significant architectural changes, and 5.8 is doing the responsible thing: migrating users incrementally rather than forcing a flag day. If you&amp;#8217;re running Podman in production, updating to 5.8 sooner rather than later gives you a smooth transition&amp;nbsp;path.&lt;/p&gt;
&lt;p&gt;For Quadlet users in particular, the multi-file install support makes managing complex deployments meaningfully easier. Combined with the AppArmor integration and the ongoing maturation of the Quadlet system, Podman continues to build a compelling case as a production container runtime that doesn&amp;#8217;t need a daemon or an orchestrator to be&amp;nbsp;useful.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/containers/podman/releases/tag/v5.8.0"&gt;Podman 5.8.0 Release&amp;nbsp;Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxiac.com/podman-5-8-introduces-quadlet-multi-file-install-and-sqlite-migration/"&gt;Linuxiac: Podman 5.8 Introduces Quadlet Multi-File Install and SQLite&amp;nbsp;Migration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"&gt;Podman Quadlet&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/"&gt;Production-Grade Container Deployment with Podman Quadlets&lt;/a&gt; - my earlier deep-dive into Quadlet-based&amp;nbsp;deployments&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="podman"/><category term="containers"/><category term="quadlet"/><category term="systemd"/><category term="linux"/><category term="rhel"/><category term="sqlite"/></entry><entry><title>Adding Fediverse Comments to a Pelican Blog</title><link href="https://blog.hofstede.it/adding-fediverse-comments-to-a-pelican-blog/" rel="alternate"/><published>2026-02-10T00:00:00+01:00</published><updated>2026-02-10T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-10:/adding-fediverse-comments-to-a-pelican-blog/</id><summary type="html">&lt;p&gt;How I added a Mastodon-based comment system to this Pelican blog - no databases, no third-party services, just the Fediverse&amp;#8217;s public &lt;span class="caps"&gt;API&lt;/span&gt; and a bit of&amp;nbsp;JavaScript.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Every static site eventually faces the comment question. Disqus tracks your readers. Self-hosted solutions like Commento or Isso need a server-side component and a database. Most options either compromise privacy or add operational complexity that feels disproportionate for a personal&amp;nbsp;blog.&lt;/p&gt;
&lt;p&gt;Then I came across &lt;a href="https://jan.wildeboer.net/2023/02/Jekyll-Mastodon-Comments/"&gt;Jan Wildeboer&amp;#8217;s approach&lt;/a&gt; for his Jekyll blog: use Mastodon as the comment backend. Every blog post gets a corresponding Mastodon post. Replies to that post become the article&amp;#8217;s comments, fetched client-side through the public Mastodon &lt;span class="caps"&gt;API&lt;/span&gt;. No tracking, no database, no server-side logic. Just the Fediverse doing what it already&amp;nbsp;does.&lt;/p&gt;
&lt;p&gt;I liked the idea enough to port it to Pelican. This article explains how it works, how it&amp;#8217;s integrated, and how you could do the&amp;nbsp;same.&lt;/p&gt;
&lt;h2 id="the-concept"&gt;The&amp;nbsp;Concept&lt;/h2&gt;
&lt;p&gt;The Mastodon &lt;span class="caps"&gt;API&lt;/span&gt; exposes a public endpoint that returns the full conversation context for any&amp;nbsp;post:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;GET https://instance.tld/api/v1/statuses/{id}/context
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The response includes&amp;nbsp;an &lt;code&gt;ancestors&lt;/code&gt; array (the thread above the post) and&amp;nbsp;a &lt;code&gt;descendants&lt;/code&gt; array (all replies). We only care&amp;nbsp;about &lt;code&gt;descendants&lt;/code&gt; - those are the comments. No authentication is required for public&amp;nbsp;posts.&lt;/p&gt;
&lt;p&gt;The workflow is&amp;nbsp;straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Publish your blog&amp;nbsp;article&lt;/li&gt;
&lt;li&gt;Post about it on&amp;nbsp;Mastodon&lt;/li&gt;
&lt;li&gt;Add the Mastodon post&amp;#8217;s metadata to your article&amp;#8217;s&amp;nbsp;frontmatter&lt;/li&gt;
&lt;li&gt;Rebuild the&amp;nbsp;site&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From that point on, anyone who replies to your Mastodon post will have their reply appear as a comment on the&amp;nbsp;blog.&lt;/p&gt;
&lt;h2 id="integration-into-pelican"&gt;Integration into&amp;nbsp;Pelican&lt;/h2&gt;
&lt;h3 id="article-metadata"&gt;Article&amp;nbsp;Metadata&lt;/h3&gt;
&lt;p&gt;Pelican lets you define arbitrary metadata fields in article frontmatter. I added three fields to link an article to its Mastodon&amp;nbsp;post:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Mastodon_Host: burningboard.net
Mastodon_User: Larvitz
Mastodon_Id: 116035059515573876
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mastodon_Host&lt;/strong&gt; - the domain of the Mastodon&amp;nbsp;instance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mastodon_User&lt;/strong&gt; - your handle on that&amp;nbsp;instance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mastodon_Id&lt;/strong&gt; - the numeric status &lt;span class="caps"&gt;ID&lt;/span&gt; (grab it from the post &lt;span class="caps"&gt;URL&lt;/span&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These become available in templates&amp;nbsp;as &lt;code&gt;article.mastodon_host&lt;/code&gt;, &lt;code&gt;article.mastodon_user&lt;/code&gt;,&amp;nbsp;and &lt;code&gt;article.mastodon_id&lt;/code&gt;. Articles without these fields simply don&amp;#8217;t get a comment&amp;nbsp;section.&lt;/p&gt;
&lt;h3 id="the-comment-template"&gt;The Comment&amp;nbsp;Template&lt;/h3&gt;
&lt;p&gt;The comment system lives in a single Jinja2 include file&amp;nbsp;at &lt;code&gt;themes/pelican-alchemy-custom/templates/include/comments.html&lt;/code&gt;. The entire thing is wrapped in a&amp;nbsp;conditional:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;article.mastodon_id&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fediverse-comments&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;...
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gets included at the bottom of the article&amp;nbsp;template:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;{&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;include/comments.html&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since my blog uses the &lt;a href="https://github.com/nairobilug/pelican-alchemy"&gt;Alchemy&lt;/a&gt; theme, I override the&amp;nbsp;default &lt;code&gt;article.html&lt;/code&gt; in a custom template directory loaded via&amp;nbsp;Pelican&amp;#8217;s &lt;code&gt;THEME_TEMPLATES_OVERRIDES&lt;/code&gt; setting. This keeps the base theme untouched as a git submodule while allowing targeted&amp;nbsp;modifications.&lt;/p&gt;
&lt;h3 id="the-javascript"&gt;The&amp;nbsp;JavaScript&lt;/h3&gt;
&lt;p&gt;The client-side logic is compact. On page load, it fetches the conversation context and renders each&amp;nbsp;reply:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://{{ article.mastodon_host }}/api/v1/statuses/{{ article.mastodon_id }}/context&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;descendants&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;descendants&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;descendants&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;descendants&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;// Build and render each comment&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// &amp;quot;No comments yet&amp;quot; message&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each reply from the &lt;span class="caps"&gt;API&lt;/span&gt; includes the author&amp;#8217;s display name, avatar, profile &lt;span class="caps"&gt;URL&lt;/span&gt;, the reply content (as &lt;span class="caps"&gt;HTML&lt;/span&gt;), a timestamp, and engagement counts (replies, boosts, favourites). The script builds an &lt;span class="caps"&gt;HTML&lt;/span&gt; structure for each comment and appends it to the&amp;nbsp;page.&lt;/p&gt;
&lt;p&gt;One nice detail: Mastodon custom emojis in display names. The &lt;span class="caps"&gt;API&lt;/span&gt; returns&amp;nbsp;an &lt;code&gt;emojis&lt;/code&gt; array for each account, with shortcodes and image URLs. The script&amp;nbsp;replaces &lt;code&gt;:shortcode:&lt;/code&gt; patterns in display names with the actual emoji images, so usernames render&amp;nbsp;correctly.&lt;/p&gt;
&lt;h3 id="xss-protection"&gt;&lt;span class="caps"&gt;XSS&lt;/span&gt;&amp;nbsp;Protection&lt;/h3&gt;
&lt;p&gt;Since Mastodon post content arrives as &lt;span class="caps"&gt;HTML&lt;/span&gt;, rendering it directly would be a textbook &lt;span class="caps"&gt;XSS&lt;/span&gt; vulnerability. The implementation handles this in two&amp;nbsp;layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Manual escaping&lt;/strong&gt; - all user-controlled strings (display names, URLs, avatar paths) are run through&amp;nbsp;an &lt;code&gt;escapeHtml()&lt;/code&gt; function before being placed into the comment&amp;nbsp;markup&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOMPurify&lt;/strong&gt; - the assembled comment &lt;span class="caps"&gt;HTML&lt;/span&gt; is sanitized through &lt;a href="https://github.com/cure53/DOMPurify"&gt;DOMPurify&lt;/a&gt; before being injected into the &lt;span class="caps"&gt;DOM&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mastodon-comments-list&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;RETURN_DOM_FRAGMENT&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The DOMPurify library is served as a local static file rather than loaded from a &lt;span class="caps"&gt;CDN&lt;/span&gt;, avoiding external dependencies. It&amp;#8217;s registered&amp;nbsp;in &lt;code&gt;pelicanconf.py&lt;/code&gt; as a static&amp;nbsp;path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;STATIC_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;images&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;extra/custom.css&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;extra/robots.txt&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;extra/purify.min.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;EXTRA_PATH_METADATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;extra/purify.min.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;purify.min.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="interaction-flow"&gt;Interaction&amp;nbsp;Flow&lt;/h3&gt;
&lt;p&gt;Readers who want to comment need a Mastodon (or other ActivityPub) account. The template provides a &amp;#8220;Copy post link&amp;#8221; button that copies the Mastodon post &lt;span class="caps"&gt;URL&lt;/span&gt; to the clipboard. They can then search for that &lt;span class="caps"&gt;URL&lt;/span&gt; on their own instance, find the post, and reply. Their reply will show up on the blog the next time someone loads the page - no rebuild required, since everything is fetched&amp;nbsp;live.&lt;/p&gt;
&lt;h3 id="styling"&gt;Styling&lt;/h3&gt;
&lt;p&gt;The comment section is styled to match the blog&amp;#8217;s Solarized Dark theme. Each comment is a flexbox row with a circular avatar on the left and the content on the right. Author names link to their Mastodon profiles. Dates link to the original reply. Engagement metrics (replies, boosts, favourites) are displayed in a muted&amp;nbsp;footer.&lt;/p&gt;
&lt;h2 id="trade-offs"&gt;Trade-offs&lt;/h2&gt;
&lt;p&gt;This approach has real limitations worth&amp;nbsp;acknowledging:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JavaScript required&lt;/strong&gt; - the comments won&amp;#8217;t load without it.&amp;nbsp;A &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; fallback tells the&amp;nbsp;reader.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content warnings are ignored&lt;/strong&gt; - Mastodon&amp;#8217;s &lt;span class="caps"&gt;CW&lt;/span&gt; feature isn&amp;#8217;t reflected in the rendered&amp;nbsp;comments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Media attachments are excluded&lt;/strong&gt; - only text content is&amp;nbsp;displayed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moderation is coarse&lt;/strong&gt; - you moderate through Mastodon itself. Blocking an account or deleting the source post affects all comments. There&amp;#8217;s no per-comment moderation from the blog&amp;nbsp;side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The source post must stay up&lt;/strong&gt; - if you delete the Mastodon post, the comments disappear. Bookmark your&amp;nbsp;posts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;CORS&lt;/span&gt; dependency&lt;/strong&gt; - the Mastodon instance must allow cross-origin requests to the &lt;span class="caps"&gt;API&lt;/span&gt;, which is the default for most&amp;nbsp;instances.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the other hand, the advantages are compelling for a personal blog: zero infrastructure, full reader privacy, decentralized identity, and comments from people who already have Fediverse accounts - which self-selects for a community I actually want to hear&amp;nbsp;from.&lt;/p&gt;
&lt;h2 id="the-full-picture"&gt;The Full&amp;nbsp;Picture&lt;/h2&gt;
&lt;p&gt;The implementation touches exactly four&amp;nbsp;things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;themes/pelican-alchemy-custom/templates/include/comments.html&lt;/code&gt;&lt;/strong&gt; - the template with &lt;span class="caps"&gt;HTML&lt;/span&gt; structure and JavaScript&amp;nbsp;logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;themes/pelican-alchemy-custom/templates/article.html&lt;/code&gt;&lt;/strong&gt; - the article template, with&amp;nbsp;one &lt;code&gt;{% include %}&lt;/code&gt; line&amp;nbsp;added&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;content/extra/purify.min.js&lt;/code&gt;&lt;/strong&gt; - DOMPurify library as a static&amp;nbsp;asset&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;content/extra/custom.css&lt;/code&gt;&lt;/strong&gt; - styling for the comment&amp;nbsp;section&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Plus three metadata fields in any article that should have comments enabled. That&amp;#8217;s it. No plugins, no external services, no build-time processing. The comments are entirely a client-side&amp;nbsp;feature.&lt;/p&gt;
&lt;p&gt;You can see it in action on my &lt;a href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/"&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; article&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="attribution"&gt;Attribution&lt;/h2&gt;
&lt;p&gt;This implementation is directly inspired by &lt;a href="https://jan.wildeboer.net"&gt;Jan Wildeboer&lt;/a&gt; (&lt;a href="https://social.wildeboer.net/@jwildeboer"&gt;@jwildeboer@social.wildeboer.net&lt;/a&gt;), who &lt;a href="https://jan.wildeboer.net/2023/02/Jekyll-Mastodon-Comments/"&gt;published this approach&lt;/a&gt; for his Jekyll blog in February 2023. I took his logic and ported it to Pelican&amp;#8217;s Jinja2 templating system. The core idea - using Mastodon&amp;#8217;s public conversation &lt;span class="caps"&gt;API&lt;/span&gt; as a comment backend - is entirely&amp;nbsp;his.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jan.wildeboer.net/2023/02/Jekyll-Mastodon-Comments/"&gt;Jan Wildeboer - Client-side comments with Mastodon on a static Jekyll&amp;nbsp;website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.joinmastodon.org/methods/statuses/#context"&gt;Mastodon &lt;span class="caps"&gt;API&lt;/span&gt; - Get context of a&amp;nbsp;status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cure53/DOMPurify"&gt;DOMPurify - &lt;span class="caps"&gt;XSS&lt;/span&gt; sanitizer for &lt;span class="caps"&gt;HTML&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getpelican.com/"&gt;Pelican - Static Site&amp;nbsp;Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nairobilug/pelican-alchemy"&gt;Pelican Alchemy&amp;nbsp;Theme&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Meta"/><category term="pelican"/><category term="fediverse"/><category term="mastodon"/><category term="activitypub"/><category term="blog"/><category term="javascript"/></entry><entry><title>Running Your Own AS: BGP on FreeBSD with FRR, GRE Tunnels, and Policy Routing</title><link href="https://blog.hofstede.it/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/" rel="alternate"/><published>2026-02-08T00:00:00+01:00</published><updated>2026-02-08T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-08:/running-your-own-as-bgp-on-freebsd-with-frr-gre-tunnels-and-policy-routing/</id><summary type="html">&lt;p&gt;How I obtained my own &lt;span class="caps"&gt;AS&lt;/span&gt; number and IPv6 prefix, set up a FreeBSD &lt;span class="caps"&gt;BGP&lt;/span&gt; router with &lt;span class="caps"&gt;FRR&lt;/span&gt;, and built a tunnel overlay to bring globally routable addresses to servers that already have provider-assigned IPv6 - using dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; policy routing to make both&amp;nbsp;coexist.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-02-08-running-your-own-as-bgp-freebsd.png" title="Running Your Own AS: Header image"&gt;&lt;/p&gt;
&lt;p&gt;Running your own Autonomous System on the public internet sounds like something reserved for ISPs and large enterprises. It&amp;#8217;s not. With sponsoring LIRs making &lt;span class="caps"&gt;AS&lt;/span&gt; numbers and IPv6 prefixes accessible to individuals, and FreeBSD providing the routing tools to make it work, you can announce your own address space to the Default-Free Zone from a single virtual&amp;nbsp;machine.&lt;/p&gt;
&lt;p&gt;This article walks through the complete setup: obtaining resources from &lt;span class="caps"&gt;RIPE&lt;/span&gt; via a sponsoring &lt;span class="caps"&gt;LIR&lt;/span&gt;, configuring a FreeBSD &lt;span class="caps"&gt;BGP&lt;/span&gt; router with &lt;span class="caps"&gt;FRR&lt;/span&gt;, building &lt;span class="caps"&gt;GRE&lt;/span&gt;/&lt;span class="caps"&gt;GIF&lt;/span&gt; tunnels to distribute prefixes to remote servers, and solving the routing challenge that arises when a server needs to speak from two different IPv6 address spaces&amp;nbsp;simultaneously.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on addresses:&lt;/strong&gt; All provider-assigned &lt;span class="caps"&gt;IP&lt;/span&gt; addresses, hostnames, and management IPs in this article have been replaced with &lt;a href="https://www.rfc-editor.org/rfc/rfc5737"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 5737&lt;/a&gt; / &lt;a href="https://www.rfc-editor.org/rfc/rfc3849"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 3849&lt;/a&gt; documentation ranges. My own &lt;span class="caps"&gt;AS&lt;/span&gt; number (&lt;span class="caps"&gt;AS201379&lt;/span&gt;) and prefix (2a06:9801:1c::/48) are public &lt;span class="caps"&gt;BGP&lt;/span&gt; resources and shown as-is. The upstream &lt;span class="caps"&gt;AS&lt;/span&gt; numbers (&lt;span class="caps"&gt;AS34927&lt;/span&gt;, &lt;span class="caps"&gt;AS209735&lt;/span&gt;) are equally visible in public routing&amp;nbsp;tables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-run-your-own-as"&gt;Why Run Your Own &lt;span class="caps"&gt;AS&lt;/span&gt;?&lt;/h2&gt;
&lt;p&gt;Provider-assigned IPv6 addresses are tied to that provider. Move to a different hoster and your addresses change - along with &lt;span class="caps"&gt;DNS&lt;/span&gt; records, firewall rules, reputation, and every system that references them. With your own &lt;span class="caps"&gt;AS&lt;/span&gt; and prefix, your addresses follow you. Migrate a server, update a tunnel endpoint, and traffic flows again without touching a single service&amp;nbsp;configuration.&lt;/p&gt;
&lt;p&gt;There are also less practical reasons. Understanding &lt;span class="caps"&gt;BGP&lt;/span&gt; transforms how you think about internet routing. Watching your prefix propagate through the &lt;span class="caps"&gt;DFZ&lt;/span&gt; and appear on looking glasses worldwide is genuinely satisfying. And if you run services across multiple providers, having provider-independent addressing simplifies the architecture&amp;nbsp;considerably.&lt;/p&gt;
&lt;h2 id="obtaining-resources"&gt;Obtaining&amp;nbsp;Resources&lt;/h2&gt;
&lt;p&gt;To announce prefixes on the internet, you need two things from a Regional Internet Registry (in Europe, that&amp;#8217;s &lt;span class="caps"&gt;RIPE&lt;/span&gt; &lt;span class="caps"&gt;NCC&lt;/span&gt;):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;An &lt;span class="caps"&gt;AS&lt;/span&gt; number&lt;/strong&gt; - your identity in &lt;span class="caps"&gt;BGP&lt;/span&gt;. Mine is &lt;span class="caps"&gt;AS201379&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An IPv6 prefix&lt;/strong&gt; - the address space you&amp;#8217;ll announce. I received&amp;nbsp;2a06:9801:1c::/48.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As an individual, you don&amp;#8217;t need to become a &lt;span class="caps"&gt;RIPE&lt;/span&gt; member (which involves fees and bureaucracy). Instead, you work with a &lt;strong&gt;sponsoring &lt;span class="caps"&gt;LIR&lt;/span&gt;&lt;/strong&gt; - an existing &lt;span class="caps"&gt;RIPE&lt;/span&gt; member who sponsors your resource registration. Several LIRs cater to hobbyists and small operators. The process typically&amp;nbsp;involves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filling out a request form with your intended use&amp;nbsp;case&lt;/li&gt;
&lt;li&gt;Creating the appropriate &lt;span class="caps"&gt;RIPE&lt;/span&gt; database objects (aut-num, inet6num,&amp;nbsp;route6)&lt;/li&gt;
&lt;li&gt;Setting up &lt;span class="caps"&gt;RPKI&lt;/span&gt; ROAs (Route Origin Authorizations) to cryptographically bind your prefix to your &lt;span class="caps"&gt;AS&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the paperwork is done, you need upstream connectivity - someone willing to carry your &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions and announce your routes to the rest of the&amp;nbsp;internet.&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;p&gt;The setup involves two tiers: a &lt;span class="caps"&gt;BGP&lt;/span&gt; router that peers with upstream providers, and downstream servers that receive tunneled subnets from the router&amp;#8217;s&amp;nbsp;/48.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    ┌──────────────────────────────┐
                    │     Default-Free Zone         │
                    └──────┬──────────────┬─────────┘
                           │              │
                    AS34927 (iFog)   AS209735 (Lagrange)
                           │              │
                      GRE tunnel     Direct peering
                           │              │
                    ┌──────┴──────────────┴─────────┐
                    │    router01 (BGP Router)       │
                    │     FreeBSD + FRR              │
                    │     AS201379                   │
                    │     2a06:9801:1c::/48          │
                    └──────┬──────────────┬─────────┘
                           │              │
                      GIF tunnel     GIF tunnel
                      (proto 41)     (proto 41)
                           │              │
                    ┌──────┴───┐   ┌──────┴──────────┐
                    │  vps01   │   │  dcgw01          │
                    │  VPS     │   │  DC OPNsense     │
                    │  :1000:: │   │  :2000::/62      │
                    │  /64     │   │                   │
                    └──────────┘   └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;BGP&lt;/span&gt; router&amp;nbsp;(&lt;code&gt;router01&lt;/code&gt;) announces 2a06:9801:1c::/48 to two upstream providers and maintains a blackhole route for the aggregate. Individual /64s (and a /62 for my Colocation datacenter) are tunneled to downstream servers via &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnels (IPv6-in-IPv4 encapsulation). Each server receives real, globally routable addresses from my prefix while keeping its existing provider-assigned IPv6 fully&amp;nbsp;operational.&lt;/p&gt;
&lt;h2 id="the-bgp-router"&gt;The &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;Router&lt;/h2&gt;
&lt;p&gt;The router runs on a FreeBSD &lt;span class="caps"&gt;VM&lt;/span&gt; at a colocation facility with direct connectivity to two upstream networks. Let&amp;#8217;s walk through each&amp;nbsp;layer.&lt;/p&gt;
&lt;h3 id="network-configuration"&gt;Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;The&amp;nbsp;router&amp;#8217;s &lt;code&gt;/etc/rc.conf&lt;/code&gt; sets up the physical interface, tunnel interfaces, and static&amp;nbsp;routes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;router01&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Security&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Physical interface&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.10/24 -rxcsum -txcsum -rxcsum6 -txcsum6 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:100::96/64&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:100::1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Loopback alias for originated prefix&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_lo0_alias0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c::1 prefixlen 64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Tunnel interfaces&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0 gif1 gre0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kld_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;if_gif if_gre&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GRE Tunnel to transit provider (iFog)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 198.51.100.44&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:300::2 2001:db8:300::1 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gre0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Transit-iFog&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF Tunnel to VPS (vps01)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 203.0.113.10&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:ffff::1 2a06:9801:1c:ffff::2 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Tunnel-to-VPS&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_cloud&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c:1000::/64 2a06:9801:1c:ffff::2&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF Tunnel to datacenter firewall (dcgw01)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tunnel 198.51.100.10 192.0.2.50&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif1_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:ffff::3 2a06:9801:1c:ffff::4 prefixlen 128&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif1_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Tunnel-to-Datacenter&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_dc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c:2000::/62 2a06:9801:1c:ffff::4&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Blackhole route for the aggregate + downstream routes&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myblock cloud dc&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_route_myblock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48 -reject&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pflog_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;frr_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A few things worth&amp;nbsp;explaining:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The blackhole route&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;-reject&lt;/code&gt; for the /48) is essential. Without it, traffic for unassigned subnets within your prefix would follow the default route back to the upstream, creating a routing loop. The blackhole ensures unrouted traffic is dropped&amp;nbsp;locally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Point-to-point tunnel addresses&lt;/strong&gt; use /128 prefixes on&amp;nbsp;the &lt;code&gt;2a06:9801:1c:ffff::/64&lt;/code&gt; link subnet. Each tunnel gets a pair of addresses from this&amp;nbsp;range.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Downstream routes&lt;/strong&gt; point specific subnets at the far end of each tunnel. The /64 for the &lt;span class="caps"&gt;VPS&lt;/span&gt; and /62 for the datacenter are routed to their respective tunnel&amp;nbsp;endpoints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;GRE&lt;/span&gt; vs &lt;span class="caps"&gt;GIF&lt;/span&gt;&lt;/strong&gt;: The iFog peering uses &lt;span class="caps"&gt;GRE&lt;/span&gt; because that&amp;#8217;s what the provider requires. The downstream tunnels use &lt;span class="caps"&gt;GIF&lt;/span&gt; (protocol 41, IPv6-in-IPv4) which is simpler and has less&amp;nbsp;overhead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="frr-configuration"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt; (Free Range Routing) handles the &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions. The configuration lives&amp;nbsp;at &lt;code&gt;/usr/local/etc/frr/frr.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;frr version 10.5.1
frr defaults traditional
hostname router01
log syslog informational
service integrated-vtysh-config
!
ipv6 prefix-list PL-MY-NET seq 5 permit 2a06:9801:1c::/48
!
ipv6 prefix-list PL-BOGONS seq 5 deny ::/0 le 7
ipv6 prefix-list PL-BOGONS seq 10 deny ::/8
ipv6 prefix-list PL-BOGONS seq 15 deny 100::/8
ipv6 prefix-list PL-BOGONS seq 20 deny 200::/7
ipv6 prefix-list PL-BOGONS seq 25 deny 400::/6
ipv6 prefix-list PL-BOGONS seq 30 deny 800::/5
ipv6 prefix-list PL-BOGONS seq 35 deny 1000::/4
ipv6 prefix-list PL-BOGONS seq 40 deny 4000::/3
ipv6 prefix-list PL-BOGONS seq 45 deny 6000::/3
ipv6 prefix-list PL-BOGONS seq 50 deny 8000::/3
ipv6 prefix-list PL-BOGONS seq 55 deny a000::/3
ipv6 prefix-list PL-BOGONS seq 60 deny c000::/3
ipv6 prefix-list PL-BOGONS seq 65 deny e000::/4
ipv6 prefix-list PL-BOGONS seq 70 deny f000::/5
ipv6 prefix-list PL-BOGONS seq 75 deny f800::/6
ipv6 prefix-list PL-BOGONS seq 80 deny fc00::/7
ipv6 prefix-list PL-BOGONS seq 85 deny fe80::/10
ipv6 prefix-list PL-BOGONS seq 90 deny fec0::/10
ipv6 prefix-list PL-BOGONS seq 95 deny ff00::/8
ipv6 prefix-list PL-BOGONS seq 100 deny 2a06:9801:1c::/48
ipv6 prefix-list PL-BOGONS seq 105 deny ::/0 ge 49
ipv6 prefix-list PL-BOGONS seq 110 permit ::/0 le 48
!
route-map RM-IFOG-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
 set community 34927:9501 34927:9301 additive
exit
!
route-map RM-LAGRANGE-OUT permit 10
 match ipv6 address prefix-list PL-MY-NET
 set as-path prepend 201379 201379
exit
!
route-map RM-IFOG-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
exit
!
route-map RM-LAGRANGE-IN permit 10
 match ipv6 address prefix-list PL-BOGONS
exit
!
ipv6 route 2a06:9801:1c::/48 blackhole
!
router bgp 201379
 bgp router-id 198.51.100.10
 no bgp default ipv4-unicast
 neighbor 2001:db8:300::1 remote-as 34927
 neighbor 2001:db8:300::1 description Upstream-iFog
 neighbor 2001:db8:300::1 ttl-security hops 1
 neighbor 2001:db8:300::1 update-source gre0
 neighbor 2001:db8:100::ff remote-as 209735
 neighbor 2001:db8:100::ff description Upstream-Lagrange
 neighbor 2001:db8:100::ff ttl-security hops 1
 neighbor 2001:db8:100::ff update-source 2001:db8:100::96
 !
 address-family ipv6 unicast
  network 2a06:9801:1c::/48
  neighbor 2001:db8:300::1 activate
  neighbor 2001:db8:300::1 soft-reconfiguration inbound
  neighbor 2001:db8:300::1 maximum-prefix 250000 90 restart 30
  neighbor 2001:db8:300::1 route-map RM-IFOG-IN in
  neighbor 2001:db8:300::1 route-map RM-IFOG-OUT out
  neighbor 2001:db8:100::ff activate
  neighbor 2001:db8:100::ff soft-reconfiguration inbound
  neighbor 2001:db8:100::ff maximum-prefix 250000 90 restart 30
  neighbor 2001:db8:100::ff route-map RM-LAGRANGE-IN in
  neighbor 2001:db8:100::ff route-map RM-LAGRANGE-OUT out
 exit-address-family
exit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;#8217;s a lot happening here. Let me break down the key design&amp;nbsp;decisions.&lt;/p&gt;
&lt;h4 id="prefix-lists"&gt;Prefix&amp;nbsp;Lists&lt;/h4&gt;
&lt;p&gt;Two prefix lists control what gets sent and&amp;nbsp;received:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;PL&lt;/span&gt;-&lt;span class="caps"&gt;MY&lt;/span&gt;-&lt;span class="caps"&gt;NET&lt;/span&gt;&lt;/strong&gt;: Matches only our /48. Used in outbound route-maps to ensure we only ever announce our own&amp;nbsp;prefix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;PL&lt;/span&gt;-&lt;span class="caps"&gt;BOGONS&lt;/span&gt;&lt;/strong&gt;: A comprehensive bogon filter for inbound routes. This rejects non-routable address space (link-local, &lt;span class="caps"&gt;ULA&lt;/span&gt;, multicast, documentation ranges), our own prefix (to prevent loops), and anything more specific than a /48 or less specific than a /8. The&amp;nbsp;final &lt;code&gt;permit ::/0 le 48&lt;/code&gt; at the end accepts everything that survived the deny&amp;nbsp;rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The bogon filter deserves emphasis. Accepting bad routes from peers can cause anything from black-holed traffic to becoming an unwitting participant in route hijacks. Filter aggressively on&amp;nbsp;inbound.&lt;/p&gt;
&lt;h4 id="route-maps"&gt;Route&amp;nbsp;Maps&lt;/h4&gt;
&lt;p&gt;Each peer gets its own pair of inbound/outbound route&amp;nbsp;maps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Outbound to iFog&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;RM-IFOG-OUT&lt;/code&gt;): Announces our /48 with &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;communities &lt;code&gt;34927:9501&lt;/code&gt; and &lt;code&gt;34927:9301&lt;/code&gt;. These are iFog-specific communities that control route propagation - in this case, requesting announcement to specific peering&amp;nbsp;partners.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbound to Lagrange&lt;/strong&gt;&amp;nbsp;(&lt;code&gt;RM-LAGRANGE-OUT&lt;/code&gt;): Announces our /48 with &lt;span class="caps"&gt;AS&lt;/span&gt;-path prepending (adds our &lt;span class="caps"&gt;ASN&lt;/span&gt; twice). This makes the Lagrange path appear longer to the rest of the internet, steering inbound traffic to prefer the iFog path. Useful for traffic engineering when one upstream has better&amp;nbsp;connectivity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inbound from both&lt;/strong&gt;: Apply the bogon filter to reject garbage&amp;nbsp;routes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="bgp-session-details"&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; Session&amp;nbsp;Details&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;no bgp default ipv4-unicast&lt;/code&gt;&lt;/strong&gt;: We&amp;#8217;re IPv6-only. Don&amp;#8217;t activate IPv4 address family by&amp;nbsp;default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ttl-security hops 1&lt;/code&gt;&lt;/strong&gt;: &lt;span class="caps"&gt;GTSM&lt;/span&gt; (Generalized &lt;span class="caps"&gt;TTL&lt;/span&gt; Security Mechanism) - reject &lt;span class="caps"&gt;BGP&lt;/span&gt; packets with &lt;span class="caps"&gt;TTL&lt;/span&gt; less than 254. This prevents remote attacks on the &lt;span class="caps"&gt;BGP&lt;/span&gt; session since only directly connected peers can send packets with &lt;span class="caps"&gt;TTL&lt;/span&gt;&amp;nbsp;255.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;soft-reconfiguration inbound&lt;/code&gt;&lt;/strong&gt;: Store received routes before applying filters. This lets you change inbound policy without resetting the &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;maximum-prefix 250000 90 restart 30&lt;/code&gt;&lt;/strong&gt;: Safety valve. If a peer sends more than 250,000 prefixes (or 90% of that as a warning), tear down the session and retry after 30 minutes. Protects against route leaks from&amp;nbsp;upstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="firewall-on-the-router"&gt;Firewall on the&amp;nbsp;Router&lt;/h3&gt;
&lt;p&gt;The &lt;span class="caps"&gt;BGP&lt;/span&gt; router&amp;#8217;s &lt;span class="caps"&gt;PF&lt;/span&gt; configuration protects the control plane while allowing data plane&amp;nbsp;forwarding:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Macros ---&lt;/span&gt;
&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;dc_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;vps_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gif0&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;trusted_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 198.51.100.100, 198.51.100.101 }&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;trusted_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 2001:db8:ffff:1::/64, 2001:db8:ffff:2::/64 }&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;bgp_peers_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 198.51.100.20 }&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;bgp_peers_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 2001:db8:100::ff }&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ifog_gre_endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.44&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ifog_bgp_peer&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:300::1&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2a06:9801:1c::/48&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;vps_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;203.0.113.10&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# --- Tables ---&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;bgp_peers_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;bgp_peers_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bogons&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fc00&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;fec0&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ff00&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Options ---&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scrub ---&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;vps_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1440&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;dc_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1140&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;

&lt;span class="c1"&gt;# --- Filtering ---&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bogons&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;
&lt;span class="n"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Control Plane ---&lt;/span&gt;

&lt;span class="c1"&gt;# SSH from trusted sources only&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# BGP (TCP 179) - strictly limited to known peers&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bgp_peers_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# GRE tunnel from iFog&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ifog_gre_endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ifog_bgp_peer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;179&lt;/span&gt;

&lt;span class="c1"&gt;# ICMPv6: essential for NDP, PMTUD, and diagnostics&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paramprob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;routersol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unreach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Data Plane ---&lt;/span&gt;

&lt;span class="c1"&gt;# Inbound traffic destined for our prefix&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gre0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Return traffic from downstream tunnels&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;vps_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;dc_tun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;my_network_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# GIF tunnel encapsulation (proto 41) from downstream endpoints&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;vps_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Outbound&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The firewall cleanly separates &lt;strong&gt;control plane&lt;/strong&gt; (&lt;span class="caps"&gt;SSH&lt;/span&gt;, &lt;span class="caps"&gt;BGP&lt;/span&gt; sessions) from &lt;strong&gt;data plane&lt;/strong&gt; (forwarded traffic). The control plane rules are strict: &lt;span class="caps"&gt;BGP&lt;/span&gt; is locked to known peer addresses, &lt;span class="caps"&gt;SSH&lt;/span&gt; to trusted management IPs. The data plane rules are simpler since the router just needs to forward packets between upstreams and downstream&amp;nbsp;tunnels.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;block in quick on $ext_if from { &amp;lt;bogons&amp;gt;, $my_network_v6 }&lt;/code&gt; rule is important - it drops packets claiming to come from our own prefix arriving on the external interface. If someone on the internet spoofs a source address from our range, this catches it before it enters the forwarding&amp;nbsp;path.&lt;/p&gt;
&lt;p&gt;Note the per-tunnel &lt;span class="caps"&gt;MSS&lt;/span&gt; clamping in the scrub section. Each tunnel has different overhead (&lt;span class="caps"&gt;GRE&lt;/span&gt; adds more headers than &lt;span class="caps"&gt;GIF&lt;/span&gt;), so the &lt;span class="caps"&gt;MSS&lt;/span&gt; values differ. Getting this wrong causes mysterious connection stalls with large&amp;nbsp;packets.&lt;/p&gt;
&lt;h2 id="the-downstream-server-dual-stack-with-policy-routing"&gt;The Downstream Server: Dual-Stack with Policy&amp;nbsp;Routing&lt;/h2&gt;
&lt;p&gt;This is where things get interesting. The &lt;span class="caps"&gt;VPS&lt;/span&gt;&amp;nbsp;(&lt;code&gt;vps01&lt;/code&gt;) already has provider-assigned IPv6 from its hoster. Jails on this server use addresses from both address&amp;nbsp;spaces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Provider IPv6&lt;/strong&gt; (2001:db8:200:0:1000::/68) - the hoster&amp;#8217;s addresses, NATed to the&amp;nbsp;host&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;BGP&lt;/span&gt; IPv6&lt;/strong&gt; (2a06:9801:1c:1000::/64) - our own prefix, routed natively via the &lt;span class="caps"&gt;GIF&lt;/span&gt;&amp;nbsp;tunnel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private IPv4&lt;/strong&gt; (10.254.254.0/24) - NATed to the host&amp;#8217;s public&amp;nbsp;IPv4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The challenge: when a jail sends traffic from its &lt;span class="caps"&gt;BGP&lt;/span&gt; address (2a06:&amp;#8230;), that traffic must exit through the &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel to the &lt;span class="caps"&gt;BGP&lt;/span&gt; router - not through the default route to the &lt;span class="caps"&gt;VPS&lt;/span&gt; provider, where it would be dropped as spoofed. But traffic from the provider address must continue using the normal default&amp;nbsp;route.&lt;/p&gt;
&lt;p&gt;The solution is &lt;strong&gt;dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; policy routing&lt;/strong&gt; - FreeBSD&amp;#8217;s implementation of multiple routing&amp;nbsp;tables.&lt;/p&gt;
&lt;h3 id="how-dual-fib-works"&gt;How Dual-&lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;Works&lt;/h3&gt;
&lt;p&gt;FreeBSD supports multiple routing tables called FIBs (Forwarding Information Bases). Each &lt;span class="caps"&gt;FIB&lt;/span&gt; is an independent routing table with its own default route and entries. Interfaces and &lt;span class="caps"&gt;PF&lt;/span&gt; rules can assign traffic to a specific &lt;span class="caps"&gt;FIB&lt;/span&gt;, and the kernel consults the appropriate table when&amp;nbsp;forwarding.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FIB 0 (default):
  default --&amp;gt; vtnet0 --&amp;gt; VPS provider upstream
  Used by: host traffic, provider-addressed jail traffic

FIB 1:
  default --&amp;gt; gif0 --&amp;gt; BGP router (router01)
  Used by: BGP-addressed jail traffic (2a06:9801:1c::/48)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="network-configuration_1"&gt;Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;Here&amp;#8217;s the relevant portion of the&amp;nbsp;server&amp;#8217;s &lt;code&gt;/etc/rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vps01.example.com&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Primary interface - provider IPv4 and IPv6&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 203.0.113.10 netmask 255.255.252.0 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:200::2 prefixlen 68&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;203.0.113.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Jail bridge - three address spaces&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0 gif0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.254.254.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:200:0:1000::1 prefixlen 68&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_alias0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:1000::1 prefixlen 64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# GIF tunnel to BGP router - assigned to FIB 1&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fib 1 tunnel 203.0.113.10 198.51.100.10 tunnelfib 0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_gif0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2a06:9801:1c:ffff::2 2a06:9801:1c:ffff::1 prefixlen 128&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Enable forwarding&lt;/span&gt;
&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# FIB 1 routing table entries&lt;/span&gt;
&lt;span class="nv"&gt;static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fib1default jailleak bgplink&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_fib1default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-6 default -interface gif0 -fib 1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_jailleak&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-6 2001:db8:200:0:1000::/68 -interface bastille0 -fib 1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_bgplink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-6 2a06:9801:1c:1000::/64 -interface bastille0 -fib 1&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel configuration deserves a closer&amp;nbsp;look:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_gif0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fib 1 tunnel 203.0.113.10 198.51.100.10 tunnelfib 0&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This single line contains two critical&amp;nbsp;directives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fib 1&lt;/code&gt;&lt;/strong&gt;: The tunnel interface itself lives in &lt;span class="caps"&gt;FIB&lt;/span&gt; 1. Traffic arriving on gif0 and traffic routed out gif0 consults routing table&amp;nbsp;1.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tunnelfib 0&lt;/code&gt;&lt;/strong&gt;: But the outer IPv4 encapsulation (the 203.0.113.10 &amp;#8212;&amp;gt; 198.51.100.10 wrapper) uses &lt;span class="caps"&gt;FIB&lt;/span&gt; 0. This is essential - the IPv4 path to the &lt;span class="caps"&gt;BGP&lt;/span&gt; router goes through the provider&amp;#8217;s default route in &lt;span class="caps"&gt;FIB&lt;/span&gt; 0.&amp;nbsp;Without &lt;code&gt;tunnelfib 0&lt;/code&gt;, the encapsulated packets would try to use &lt;span class="caps"&gt;FIB&lt;/span&gt; 1&amp;#8217;s default route (which points at gif0 itself), creating a recursive&amp;nbsp;loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The three static routes in &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 complete the&amp;nbsp;picture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fib1default&lt;/code&gt;&lt;/strong&gt;: Default route in &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 exits through gif0 to the &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;jailleak&lt;/code&gt;&lt;/strong&gt;: Tells &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 that the provider&amp;#8217;s jail subnet is reachable via bastille0 (without this, return traffic in &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 for jails&amp;#8217; provider addresses would try to exit through&amp;nbsp;gif0)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bgplink&lt;/code&gt;&lt;/strong&gt;: Same for the &lt;span class="caps"&gt;BGP&lt;/span&gt; jail subnet - &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 needs to know these addresses are local on&amp;nbsp;bastille0&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="pf-the-routing-glue"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;: The Routing&amp;nbsp;Glue&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; is where the address-based routing decision happens. When a jail sends a packet from a &lt;span class="caps"&gt;BGP&lt;/span&gt; address, &lt;span class="caps"&gt;PF&lt;/span&gt; assigns it to &lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;1:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BGP&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;addressed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;traffic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gif0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;rtable 1&lt;/code&gt; directive is the key. It tells &lt;span class="caps"&gt;PF&lt;/span&gt; to route matching packets using &lt;span class="caps"&gt;FIB&lt;/span&gt; 1 instead of the default &lt;span class="caps"&gt;FIB&lt;/span&gt; 0. Since &lt;span class="caps"&gt;FIB&lt;/span&gt; 1&amp;#8217;s default route points out gif0 to the &lt;span class="caps"&gt;BGP&lt;/span&gt; router, these packets get encapsulated and sent to router01, which then forwards them to the internet with the correct source&amp;nbsp;address.&lt;/p&gt;
&lt;p&gt;For traffic arriving on the tunnel destined for jails, &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;uses &lt;code&gt;reply-to&lt;/code&gt; to ensure return traffic takes the same&amp;nbsp;path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Inbound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BGP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;traffic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ensures&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gif0&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_hub_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BGP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMPv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;also&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;needs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;correct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;tun_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_hub_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;bgp_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Without &lt;code&gt;reply-to&lt;/code&gt;, the kernel would consult &lt;span class="caps"&gt;FIB&lt;/span&gt; 0 for return traffic (since the jail itself isn&amp;#8217;t in &lt;span class="caps"&gt;FIB&lt;/span&gt; 1), and replies to &lt;span class="caps"&gt;BGP&lt;/span&gt;-addressed connections would exit through vtnet0 with the wrong source routing - getting dropped as spoofed by the provider.&amp;nbsp;The &lt;code&gt;reply-to&lt;/code&gt; directive forces &lt;span class="caps"&gt;PF&lt;/span&gt; to send reply packets back out the interface they arrived on, to the specified&amp;nbsp;next-hop.&lt;/p&gt;
&lt;h3 id="the-complete-picture"&gt;The Complete&amp;nbsp;Picture&lt;/h3&gt;
&lt;p&gt;Here&amp;#8217;s how a request to a &lt;span class="caps"&gt;BGP&lt;/span&gt;-addressed jail service&amp;nbsp;flows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; 1. Client sends packet to 2a06:9801:1c:1000::10 (web jail)
 2. Packet traverses the internet, reaching AS201379 via iFog or Lagrange
 3. router01 forwards it through gif0 tunnel to vps01
 4. vps01 receives proto 41 on vtnet0, decapsulates --&amp;gt; gif0
 5. PF matches: reply-to ($tun_if $bgp_hub_ip), creates state
 6. Packet forwarded to bastille0 --&amp;gt; jail
 7. Jail responds, packet exits on bastille0
 8. PF&amp;#39;s state table triggers reply-to: send via gif0 to bgp_hub_ip
 9. gif0 encapsulates (proto 41) using FIB 0 to reach router01
10. router01 receives, forwards to upstream --&amp;gt; internet --&amp;gt; client
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And for outbound connections initiated by the jail using its &lt;span class="caps"&gt;BGP&lt;/span&gt;&amp;nbsp;address:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;1. Jail sends packet from 2a06:9801:1c:1000::10
2. Packet arrives on bastille0
3. PF matches: &amp;quot;from $bgp_net --&amp;gt; rtable 1&amp;quot;
4. Kernel routes via FIB 1 --&amp;gt; default route --&amp;gt; gif0
5. gif0 encapsulates using FIB 0 --&amp;gt; vtnet0 --&amp;gt; router01
6. router01 receives, forwards to internet (source: 2a06:9801:1c:1000::10)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Meanwhile, the exact same jail can communicate using its provider address through the normal default route in &lt;span class="caps"&gt;FIB&lt;/span&gt; 0, with &lt;span class="caps"&gt;NAT&lt;/span&gt; to the host&amp;#8217;s address. Both address spaces coexist on the same interface, differentiated purely by &lt;span class="caps"&gt;PF&lt;/span&gt; rules and &lt;span class="caps"&gt;FIB&lt;/span&gt;&amp;nbsp;selection.&lt;/p&gt;
&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;p&gt;Once everything is running, verification is straightforward. From inside a jail with both&amp;nbsp;addresses:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Traffic from the provider address - NATed through the hoster&lt;/span&gt;
root@caddy:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# curl --interface 2001:db8:200:0:1000::10 https://ifconfig.co&lt;/span&gt;
&lt;span class="m"&gt;2001&lt;/span&gt;:db8:200::2

&lt;span class="c1"&gt;# Traffic from the BGP address - routed natively through the tunnel&lt;/span&gt;
root@caddy:~&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# curl --interface 2a06:9801:1c:1000::10 https://ifconfig.co&lt;/span&gt;
2a06:9801:1c:1000::10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first request shows the host&amp;#8217;s NATed provider address. The second shows the jail&amp;#8217;s real &lt;span class="caps"&gt;BGP&lt;/span&gt; address - confirming the packet traversed the tunnel and reached the internet through &lt;span class="caps"&gt;AS201379&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;A traceroute from an external host confirms the &lt;span class="caps"&gt;BGP&lt;/span&gt; path is&amp;nbsp;working:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ mtr -rw 2a06:9801:1c:1000::10
HOST:                              Loss%   Snt   Last   Avg  Best  Wrst StDev
 1.|-- [local-gateway]               0.0%    10    2.6   5.5   2.6  14.1   3.6
    ...
 9.|-- [transit-provider-edge]       0.0%    10   33.8  46.2  33.8  81.2  19.0
10.|-- [ifog-peering-fabric]         0.0%    10   33.5  46.7  33.5  87.0  18.6
11.|-- 2001:db8:300::2               0.0%    10   44.3  59.1  41.9 136.7  33.2
12.|-- 2a06:9801:1c:ffff::2         0.0%    10   72.7  98.9  68.8 198.8  42.4
13.|-- 2a06:9801:1c:1000::10        0.0%    10  164.1  83.1  63.5 164.1  33.4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Traffic enters via the transit provider (hops 9-10), traverses the &lt;span class="caps"&gt;GRE&lt;/span&gt; tunnel to router01 (hop 11), then the &lt;span class="caps"&gt;GIF&lt;/span&gt; tunnel to vps01 (hop 12,&amp;nbsp;the &lt;code&gt;2a06:9801:1c:ffff::2&lt;/code&gt; link address), and finally reaches the jail (hop 13). The prefix also shows up correctly on bgp.tools as active and originated by &lt;span class="caps"&gt;AS201379&lt;/span&gt; with both upstreams&amp;nbsp;visible.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;Lessons&amp;nbsp;Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;MSS&lt;/span&gt; clamping is non-negotiable with tunnels.&lt;/strong&gt; Every layer of encapsulation eats into the &lt;span class="caps"&gt;MTU&lt;/span&gt;. &lt;span class="caps"&gt;GIF&lt;/span&gt; adds 20 bytes (IPv4 header) to every packet. &lt;span class="caps"&gt;GRE&lt;/span&gt; adds more. If you don&amp;#8217;t clamp the &lt;span class="caps"&gt;TCP&lt;/span&gt; &lt;span class="caps"&gt;MSS&lt;/span&gt;, large packets get fragmented or dropped, causing mysterious failures where small requests work but large transfers stall.&amp;nbsp;Set &lt;code&gt;max-mss&lt;/code&gt; in &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s scrub rules for every tunnel interface, calculated as: &lt;span class="caps"&gt;MTU&lt;/span&gt; minus IPv6 header (40 bytes) minus &lt;span class="caps"&gt;TCP&lt;/span&gt; header (20&amp;nbsp;bytes).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;FIB&lt;/span&gt; separation is cleaner than source-based routing hacks.&lt;/strong&gt; FreeBSD&amp;#8217;s multi-&lt;span class="caps"&gt;FIB&lt;/span&gt; support is a first-class feature.&amp;nbsp;Using &lt;code&gt;rtable&lt;/code&gt; in &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;and &lt;code&gt;fib&lt;/code&gt;/&lt;code&gt;tunnelfib&lt;/code&gt; on interfaces gives you full control over which routing table handles which traffic. It&amp;#8217;s conceptually cleaner and more debuggable than alternatives like ip6tables &lt;span class="caps"&gt;MARK&lt;/span&gt; targets on&amp;nbsp;Linux.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bogon filtering matters even for small networks.&lt;/strong&gt; The internet is full of misconfigurations and occasional malice. Filtering inbound routes prevents your router from accepting nonsense that could black-hole traffic or worse. The cost is a few lines of configuration; the protection is&amp;nbsp;real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;reply-to&lt;/code&gt; solves asymmetric routing.&lt;/strong&gt; When traffic can arrive on multiple interfaces, the kernel&amp;#8217;s default &lt;span class="caps"&gt;FIB&lt;/span&gt; selection for return traffic may choose the wrong path. &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s &lt;code&gt;reply-to&lt;/code&gt; directive forces replies back out the arrival interface, which is exactly what you need for tunnel overlay&amp;nbsp;setups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start with two upstreams.&lt;/strong&gt; A single upstream means zero redundancy and no ability to do traffic engineering. Two upstreams give you failover and the ability to prefer one path over the other using &lt;span class="caps"&gt;AS&lt;/span&gt;-path prepending or communities. The operational complexity increase is&amp;nbsp;minimal.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Running your own &lt;span class="caps"&gt;AS&lt;/span&gt; on the internet is more accessible than most people assume. The barrier isn&amp;#8217;t technical complexity - it&amp;#8217;s knowing that the option exists. A FreeBSD &lt;span class="caps"&gt;VM&lt;/span&gt;, &lt;span class="caps"&gt;FRR&lt;/span&gt;, a couple of tunnels, and some careful &lt;span class="caps"&gt;PF&lt;/span&gt; rules give you provider-independent addressing, real &lt;span class="caps"&gt;BGP&lt;/span&gt; peering, and a deeper understanding of how the internet actually&amp;nbsp;works.&lt;/p&gt;
&lt;p&gt;The dual-&lt;span class="caps"&gt;FIB&lt;/span&gt; approach on the downstream server is the piece I&amp;#8217;m most satisfied with. It elegantly solves the &amp;#8220;two address spaces, one server&amp;#8221; problem without hacks: &lt;span class="caps"&gt;BGP&lt;/span&gt; traffic takes the tunnel, provider traffic takes the default route, and &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s &lt;code&gt;rtable&lt;/code&gt; directive makes the decision based purely on source address. Both paths coexist transparently, and the jails don&amp;#8217;t need to know anything about the routing&amp;nbsp;underneath.&lt;/p&gt;
&lt;p&gt;Is it overkill for a blog? Absolutely. But the same infrastructure carries every service I run, and having addresses that survive provider migrations has already paid for itself in operational simplicity. Besides, there&amp;#8217;s something deeply satisfying about seeing your own &lt;span class="caps"&gt;AS&lt;/span&gt; number show up in a&amp;nbsp;traceroute.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ripe.net/manage-ips-and-asns/resource-management/requesting-resources/"&gt;&lt;span class="caps"&gt;RIPE&lt;/span&gt; &lt;span class="caps"&gt;NCC&lt;/span&gt; - Requesting&amp;nbsp;Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.frrouting.org/en/latest/"&gt;&lt;span class="caps"&gt;FRR&lt;/span&gt;&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/firewalls/"&gt;FreeBSD Handbook: Firewalls (&lt;span class="caps"&gt;PF&lt;/span&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=setfib"&gt;FreeBSD&amp;nbsp;setfib(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bgp.tools/"&gt;bgp.tools&lt;/a&gt; - &lt;span class="caps"&gt;BGP&lt;/span&gt; looking glass and&amp;nbsp;analytics&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ripe.net/manage-ips-and-asns/resource-management/rpki/"&gt;&lt;span class="caps"&gt;RIPE&lt;/span&gt; &lt;span class="caps"&gt;RPKI&lt;/span&gt;&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc5082"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 5082 - &lt;span class="caps"&gt;GTSM&lt;/span&gt; (&lt;span class="caps"&gt;TTL&lt;/span&gt;&amp;nbsp;Security)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;The internet is a network of networks, and now you&amp;#8217;re one of them. There&amp;#8217;s a certain elegance in participating in the same routing protocol that glues together every network on the planet - from your single /48 all the way up to the Tier 1 carriers. &lt;span class="caps"&gt;BGP&lt;/span&gt; doesn&amp;#8217;t care about your size. It just cares that your routes are valid, your filters are clean, and your packets know where to&amp;nbsp;go.&lt;/p&gt;</content><category term="Networking"/><category term="freebsd"/><category term="bgp"/><category term="networking"/><category term="ipv6"/><category term="frr"/><category term="pf"/><category term="tunneling"/></entry><entry><title>PF Firewall on FreeBSD: A Practical Guide</title><link href="https://blog.hofstede.it/pf-firewall-on-freebsd-a-practical-guide/" rel="alternate"/><published>2026-02-06T00:00:00+01:00</published><updated>2026-02-06T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-02-06:/pf-firewall-on-freebsd-a-practical-guide/</id><summary type="html">&lt;p&gt;A comprehensive guide to configuring &lt;span class="caps"&gt;PF&lt;/span&gt; on FreeBSD, covering core concepts, practical configurations for jails and dual-stack networking, and advanced techniques including brute-force protection and bastion host setups with&amp;nbsp;authpf.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-02-06-pf-firewall-freebsd-guide.png" title="PF Firewall Tutorial: Header image"&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; (Packet Filter) is one of the most elegant firewall systems available on any operating system. Originally developed for OpenBSD and ported to FreeBSD, it combines a clean configuration syntax with powerful capabilities for filtering, &lt;span class="caps"&gt;NAT&lt;/span&gt;, traffic shaping, and logging. After running &lt;span class="caps"&gt;PF&lt;/span&gt; across multiple FreeBSD servers for years, I&amp;#8217;ve developed a consistent configuration pattern that balances security with&amp;nbsp;practicality.&lt;/p&gt;
&lt;p&gt;This guide covers everything from basic concepts to production configurations, with tips and patterns I&amp;#8217;ve refined through real-world deployment. Whether you&amp;#8217;re protecting a single server or a complex jail infrastructure, the principles here should give you a solid&amp;nbsp;foundation.&lt;/p&gt;
&lt;h2 id="core-concepts"&gt;Core&amp;nbsp;Concepts&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; processes rules in order, but with an important twist: the &lt;strong&gt;last matching rule wins&lt;/strong&gt; (unless you&amp;nbsp;use &lt;code&gt;quick&lt;/code&gt;). This means you typically structure rules from general to specific, with explicit blocks at the top and specific allows below.&amp;nbsp;The &lt;code&gt;quick&lt;/code&gt; keyword short-circuits evaluation - when&amp;nbsp;a &lt;code&gt;quick&lt;/code&gt; rule matches, processing stops&amp;nbsp;immediately.&lt;/p&gt;
&lt;p&gt;The configuration file lives&amp;nbsp;at &lt;code&gt;/etc/pf.conf&lt;/code&gt;. After editing, validate syntax&amp;nbsp;with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-nf&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If validation passes, load the&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To enable &lt;span class="caps"&gt;PF&lt;/span&gt; at boot, add&amp;nbsp;to &lt;code&gt;/etc/rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pflog_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since this guide covers &lt;span class="caps"&gt;NAT&lt;/span&gt; and routing traffic between jails and the internet, you must also enable packet forwarding. Without this, the firewall rules will load but traffic will die at the&amp;nbsp;interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="anatomy-of-a-pf-configuration"&gt;Anatomy of a &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;A&amp;nbsp;well-organized &lt;code&gt;pf.conf&lt;/code&gt; follows a predictable structure. Here&amp;#8217;s the skeleton I use across all&amp;nbsp;servers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Macros ---&lt;/span&gt;
&lt;span class="c1"&gt;# Named variables for interfaces and addresses&lt;/span&gt;

&lt;span class="c1"&gt;# --- Tables ---&lt;/span&gt;
&lt;span class="c1"&gt;# Dynamic lists of addresses&lt;/span&gt;

&lt;span class="c1"&gt;# --- Options ---&lt;/span&gt;
&lt;span class="c1"&gt;# Global behavior settings&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scrub ---&lt;/span&gt;
&lt;span class="c1"&gt;# Packet normalization&lt;/span&gt;

&lt;span class="c1"&gt;# --- NAT ---&lt;/span&gt;
&lt;span class="c1"&gt;# Network address translation&lt;/span&gt;

&lt;span class="c1"&gt;# --- RDR ---&lt;/span&gt;
&lt;span class="c1"&gt;# Port redirections&lt;/span&gt;

&lt;span class="c1"&gt;# --- Filtering ---&lt;/span&gt;
&lt;span class="c1"&gt;# The actual firewall rules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each section has a distinct purpose. Let&amp;#8217;s walk through them with practical&amp;nbsp;examples.&lt;/p&gt;
&lt;h2 id="macros-naming-your-network"&gt;Macros: Naming Your&amp;nbsp;Network&lt;/h2&gt;
&lt;p&gt;Macros make configurations readable and maintainable. Define them at the&amp;nbsp;top:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macros&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bridge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Bastille&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;If&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iocage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;might&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lo1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="nx"&gt;int_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;

&lt;span class="nx"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.100.0.0/24&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:1000:8000::/65&amp;quot;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;NAT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;single&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CIDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;
&lt;span class="nx"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:1000::1&amp;quot;&lt;/span&gt;

&lt;span class="nx"&gt;web_jail_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.100.0.10&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;web_jail_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:1000:8000::10&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;db_jail_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.100.0.20&amp;quot;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Lists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;braces&lt;/span&gt;
&lt;span class="nx"&gt;trusted_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{ 198.51.100.22, 203.0.113.50 }&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;trusted_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{ 2001:db8:ffff::/48 }&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using macros means changing an &lt;span class="caps"&gt;IP&lt;/span&gt; address requires editing one line, not hunting through the entire ruleset.&amp;nbsp;The &lt;code&gt;$macro&lt;/code&gt; syntax references them in&amp;nbsp;rules.&lt;/p&gt;
&lt;h2 id="tables-dynamic-address-lists"&gt;Tables: Dynamic Address&amp;nbsp;Lists&lt;/h2&gt;
&lt;p&gt;Tables are &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s mechanism for large or changing address sets. Unlike macros, tables can be modified at runtime without reloading the entire&amp;nbsp;ruleset:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; --- Tables ---
table &amp;lt;bruteforce&amp;gt; persist
table &amp;lt;jails_v4&amp;gt; { $jail_net }
table &amp;lt;jails_v6&amp;gt; { $jail_net6 }
table &amp;lt;trusted_v4&amp;gt; persist { $trusted_ipv4 }
table &amp;lt;trusted_v6&amp;gt; persist { $trusted_ipv6 }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;persist&lt;/code&gt; keyword keeps the table in memory even when empty. This is essential for tables populated dynamically&amp;nbsp;(like &lt;code&gt;&amp;lt;bruteforce&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Manage tables at&amp;nbsp;runtime:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Show table contents&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;show

&lt;span class="c1"&gt;# Add an address&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.100

&lt;span class="c1"&gt;# Remove an address&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.100

&lt;span class="c1"&gt;# Flush entire table&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;flush

&lt;span class="c1"&gt;# Load from file&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;trusted_v4&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.trusted.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Tables scale efficiently - &lt;span class="caps"&gt;PF&lt;/span&gt; uses radix trees internally, so even tables with hundreds of thousands of entries (like GeoIP databases) perform&amp;nbsp;well.&lt;/p&gt;
&lt;h2 id="options-global-behavior"&gt;Options: Global&amp;nbsp;Behavior&lt;/h2&gt;
&lt;p&gt;The options section configures &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s global&amp;nbsp;behavior:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; --- Options ---
set skip on lo0
set block-policy drop
set loginterface $ext_if
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Key options&amp;nbsp;explained:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;set skip on lo0&lt;/code&gt;&lt;/strong&gt;: Never filter loopback traffic. Critical for local&amp;nbsp;services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;set block-policy drop&lt;/code&gt;&lt;/strong&gt;: Silently drop blocked packets&amp;nbsp;(vs. &lt;code&gt;return&lt;/code&gt; which sends &lt;span class="caps"&gt;RST&lt;/span&gt;/&lt;span class="caps"&gt;ICMP&lt;/span&gt;). Drop is better for security - it gives attackers no&amp;nbsp;information.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;set loginterface&lt;/code&gt;&lt;/strong&gt;: Enable per-interface packet/byte counters. View&amp;nbsp;with &lt;code&gt;pfctl -si&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For large tables (GeoIP filtering, for example), increase the table entry&amp;nbsp;limit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;set limit table-entries 1000000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="scrub-packet-normalization"&gt;Scrub: Packet&amp;nbsp;Normalization&lt;/h2&gt;
&lt;p&gt;Scrubbing normalizes packets to prevent various attacks and fix fragmentation&amp;nbsp;issues:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;#&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nv"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;reassemble&lt;/span&gt;
&lt;span class="nv"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;fragment reassemble&lt;/code&gt; directive reassembles fragmented packets before filtering - essential because fragments can be used to evade stateless filters.&amp;nbsp;The &lt;code&gt;random-id&lt;/code&gt; directive randomizes &lt;span class="caps"&gt;IP&lt;/span&gt; IDs on outbound packets, making traffic analysis harder.&amp;nbsp;The &lt;code&gt;max-mss&lt;/code&gt; clamps &lt;span class="caps"&gt;TCP&lt;/span&gt; &lt;span class="caps"&gt;MSS&lt;/span&gt; to prevent fragmentation issues, particularly important for tunneled traffic or &lt;span class="caps"&gt;VPN&lt;/span&gt;&amp;nbsp;scenarios.&lt;/p&gt;
&lt;p&gt;For specific interfaces with lower MTUs (tunnels, VSwitch connections), add targeted scrub&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;scrub out on gif0 max-mss 1240
scrub out on $int_if max-mss 1410
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="nat-address-translation"&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt;: Address&amp;nbsp;Translation&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt; translates private addresses to public ones for outbound traffic. With FreeBSD jails on private networks, &lt;span class="caps"&gt;NAT&lt;/span&gt; is essential for&amp;nbsp;IPv4:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# --- NAT ---&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The parentheses&amp;nbsp;around &lt;code&gt;($ext_if)&lt;/code&gt; make the &lt;span class="caps"&gt;NAT&lt;/span&gt; rule dynamic - if the external interface&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt; changes (&lt;span class="caps"&gt;DHCP&lt;/span&gt;), &lt;span class="caps"&gt;PF&lt;/span&gt; automatically uses the new&amp;nbsp;address.&lt;/p&gt;
&lt;p&gt;For IPv6, if you have a routed prefix, skip &lt;span class="caps"&gt;NAT&lt;/span&gt; entirely and route natively - this is the recommended approach. Jails get real global addresses and IPv6 works end-to-end as&amp;nbsp;designed.&lt;/p&gt;
&lt;p&gt;If you must &lt;span class="caps"&gt;NAT&lt;/span&gt; IPv6 (not recommended, but sometimes necessary), the syntax is similar. Ensure the target is a single address, not a &lt;span class="caps"&gt;CIDR&lt;/span&gt;&amp;nbsp;block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;must&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;single&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;external&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;
&lt;span class="nx"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;jails_v6&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;host_ipv6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="rdr-port-forwarding"&gt;&lt;span class="caps"&gt;RDR&lt;/span&gt;: Port&amp;nbsp;Forwarding&lt;/h2&gt;
&lt;p&gt;Redirections forward incoming traffic to internal hosts. For a web server&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# --- RDR ---&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_jail_v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_jail_v6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;rdr pass&lt;/code&gt; syntax combines redirection with an implicit pass rule.&amp;nbsp;Without &lt;code&gt;pass&lt;/code&gt;, you&amp;#8217;d need a separate filter rule for the translated&amp;nbsp;traffic.&lt;/p&gt;
&lt;p&gt;For port translation (external port differs from&amp;nbsp;internal):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# External 2222 -&amp;gt; internal 22&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_runner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;deploy_jail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Restricting the source in &lt;span class="caps"&gt;RDR&lt;/span&gt; rules is a powerful pattern - the service is invisible to everyone except specified&amp;nbsp;sources.&lt;/p&gt;
&lt;h2 id="filtering-the-ruleset-core"&gt;Filtering: The Ruleset&amp;nbsp;Core&lt;/h2&gt;
&lt;p&gt;The filtering section is where access control happens. Start with default&amp;nbsp;deny:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; --- Filtering ---
&lt;span class="gh"&gt;#&lt;/span&gt; Block known bad actors immediately
block quick from &amp;lt;bruteforce&amp;gt;

&lt;span class="gh"&gt;#&lt;/span&gt; Default deny everything
block drop in log all
block drop out log all

&lt;span class="gh"&gt;#&lt;/span&gt; Allow all established connections out
pass out quick all keep state

&lt;span class="gh"&gt;#&lt;/span&gt; Anti-spoofing
antispoof quick for { $ext_if, bastille0 }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="understanding-quick-and-state"&gt;Understanding Quick and&amp;nbsp;State&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;quick&lt;/code&gt; keyword stops rule processing on match. Use it for definitive&amp;nbsp;decisions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Immediately drop known attackers - no further processing
block quick from &amp;lt;bruteforce&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;keep state&lt;/code&gt; directive enables stateful filtering. When an outbound connection is allowed, &lt;span class="caps"&gt;PF&lt;/span&gt; creates a state entry that automatically permits return traffic. This is why a&amp;nbsp;single &lt;code&gt;pass out&lt;/code&gt; rule handles both the outbound packet and inbound&amp;nbsp;replies.&lt;/p&gt;
&lt;p&gt;Note: While newer OpenBSD versions of &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;make &lt;code&gt;keep state&lt;/code&gt; implicit, FreeBSD&amp;#8217;s &lt;span class="caps"&gt;PF&lt;/span&gt; is based on an older codebase. Being explicit&amp;nbsp;with &lt;code&gt;keep state&lt;/code&gt; ensures connection tracking works exactly as intended across FreeBSD&amp;nbsp;versions.&lt;/p&gt;
&lt;h3 id="ssh-protection-pattern"&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; Protection&amp;nbsp;Pattern&lt;/h3&gt;
&lt;p&gt;Protecting &lt;span class="caps"&gt;SSH&lt;/span&gt; deserves special attention. This pattern restricts access to trusted sources, and automatically blocks brute-force&amp;nbsp;attempts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# SSH only from trusted sources&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Block and log all other SSH attempts (for monitoring)&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh_blocked&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The state options deserve&amp;nbsp;explanation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;max-src-conn 5&lt;/code&gt;&lt;/strong&gt;: Maximum 5 simultaneous connections per source &lt;span class="caps"&gt;IP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;max-src-conn-rate 3/30&lt;/code&gt;&lt;/strong&gt;: Maximum 3 new connections per 30 seconds per&amp;nbsp;source&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;overload &amp;lt;bruteforce&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Add violators to the bruteforce&amp;nbsp;table&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;flush global&lt;/code&gt;&lt;/strong&gt;: Kill all existing connections from the&amp;nbsp;offender&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;label&lt;/code&gt; on the block rule tags logged packets for easy&amp;nbsp;filtering.&lt;/p&gt;
&lt;h3 id="essential-icmp"&gt;Essential &lt;span class="caps"&gt;ICMP&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Never block all &lt;span class="caps"&gt;ICMP&lt;/span&gt; - it breaks essential network&amp;nbsp;functionality:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Essential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMPv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Useful&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMPv4&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The IPv6 types are critical:
- &lt;strong&gt;neighbrsol/neighbradv&lt;/strong&gt;: Neighbor Discovery (IPv6&amp;#8217;s &lt;span class="caps"&gt;ARP&lt;/span&gt; equivalent)
- &lt;strong&gt;toobig&lt;/strong&gt;: Path &lt;span class="caps"&gt;MTU&lt;/span&gt; Discovery (breaks connections if blocked)
- &lt;strong&gt;echoreq/echorep&lt;/strong&gt;: Ping (useful for&amp;nbsp;diagnostics)&lt;/p&gt;
&lt;h3 id="jail-egress-rules"&gt;Jail Egress&amp;nbsp;Rules&lt;/h3&gt;
&lt;p&gt;Allow jails to reach the internet while preventing them from directly accessing other internal&amp;nbsp;networks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Jails can reach anywhere except internal networks
pass in quick on bastille0 from &amp;lt;jails_v4&amp;gt; to ! 10.100.0.0/24 keep state
pass in quick on bastille0 inet6 from &amp;lt;jails_v6&amp;gt; to ! 2001:db8:1000:8000::/65 keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;!&lt;/code&gt; negation ensures jails can reach the internet but can&amp;#8217;t directly contact other jails (unless explicitly permitted&amp;nbsp;elsewhere).&lt;/p&gt;
&lt;h3 id="inter-jail-communication"&gt;Inter-Jail&amp;nbsp;Communication&lt;/h3&gt;
&lt;p&gt;When jails need to communicate (database connections, for example), add explicit&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# Web jail -&amp;gt; Database jail (PostgreSQL + Redis)&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_jail_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;db_jail_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This explicit allowlisting is preferable to permitting all inter-jail traffic - it documents and enforces your service&amp;nbsp;architecture.&lt;/p&gt;
&lt;h2 id="complete-example-configuration"&gt;Complete Example&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s a production-ready configuration for a server running multiple&amp;nbsp;jails:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Macros ---&lt;/span&gt;
&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Jail bridge interface (bastille0 for Bastille, bridge0 or lo1 for others)&lt;/span&gt;
&lt;span class="n"&gt;int_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.0/24&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1000:8000::/65&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1000::1&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;web_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.10&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;web_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1000:8000::10&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;db_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.20&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;deploy_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.25&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;trusted_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 198.51.100.22, 203.0.113.50 }&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;trusted_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 2001:db8:ffff::/48 }&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;deploy_runner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;192.0.2.100&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# --- Tables ---&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- Options ---&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scrub ---&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;

&lt;span class="c1"&gt;# --- NAT ---&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- RDR ---&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_v6&lt;/span&gt;

&lt;span class="c1"&gt;# Deployment SSH from CI runner only&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;deploy_runner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;deploy_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;

&lt;span class="c1"&gt;# --- Filtering ---&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="n"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Management SSH&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh_blocked&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Essential ICMP&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Web jail public access&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Inter-jail: web -&amp;gt; database&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;web_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;db_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Jail egress&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.100&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="tips-and-tricks"&gt;Tips and&amp;nbsp;Tricks&lt;/h2&gt;
&lt;h3 id="viewing-loaded-rules"&gt;Viewing Loaded&amp;nbsp;Rules&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Show all rules with rule numbers&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-sr

&lt;span class="c1"&gt;# Verbose output with statistics&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-sr&lt;span class="w"&gt; &lt;/span&gt;-v

&lt;span class="c1"&gt;# Show NAT rules&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-sn

&lt;span class="c1"&gt;# Show state table&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-ss

&lt;span class="c1"&gt;# Show interface statistics&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-si
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="logging-and-debugging"&gt;Logging and&amp;nbsp;Debugging&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; logs&amp;nbsp;to &lt;code&gt;pflog0&lt;/code&gt;, viewable with&amp;nbsp;tcpdump:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Live log viewing&lt;/span&gt;
tcpdump&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;-ttt&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;pflog0

&lt;span class="c1"&gt;# Read saved log file&lt;/span&gt;
tcpdump&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;-ttt&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;/var/log/pflog

&lt;span class="c1"&gt;# Filter by label&lt;/span&gt;
tcpdump&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;-ttt&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;pflog0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;label ssh_blocked&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Enable pflog in rc.conf and optionally configure rotation&amp;nbsp;in &lt;code&gt;/etc/newsyslog.conf&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="clearing-brute-force-entries"&gt;Clearing Brute-Force&amp;nbsp;Entries&lt;/h3&gt;
&lt;p&gt;Sometimes legitimate users get caught in rate&amp;nbsp;limiting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Show who&amp;#39;s blocked&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;show

&lt;span class="c1"&gt;# Unblock specific IP&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;198&lt;/span&gt;.51.100.50

&lt;span class="c1"&gt;# Flush all blocked IPs&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;flush
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Consider a cron job to expire old&amp;nbsp;entries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Clear entries older than 24 hours&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;bruteforce&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;expire&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="testing-changes-safely"&gt;Testing Changes&amp;nbsp;Safely&lt;/h3&gt;
&lt;p&gt;Before applying potentially locking changes, use the classic sysadmin safety&amp;nbsp;net:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Validate syntax first&lt;/span&gt;
pfctl&lt;span class="w"&gt; &lt;/span&gt;-nf&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf

&lt;span class="c1"&gt;# Apply with automatic rollback&lt;/span&gt;
&lt;span class="c1"&gt;# (If you get locked out, rules revert after timeout)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pfctl -f /etc/pf.conf.backup&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;at&lt;span class="w"&gt; &lt;/span&gt;now&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;minutes
pfctl&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf
&lt;span class="c1"&gt;# If everything works, cancel the at job&lt;/span&gt;
atq
atrm&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;job_number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note:&amp;nbsp;The &lt;code&gt;at&lt;/code&gt; command requires&amp;nbsp;the &lt;code&gt;atd&lt;/code&gt; daemon. On minimal installs, you may need to enable it&amp;nbsp;first:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# In /etc/rc.conf&lt;/span&gt;
&lt;span class="nv"&gt;atd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Then start it&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;atd&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="monitoring-specific-traffic"&gt;Monitoring Specific&amp;nbsp;Traffic&lt;/h3&gt;
&lt;p&gt;Add temporary rules with counters for&amp;nbsp;debugging:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pass in log on $ext_if proto tcp to port 443 label &amp;quot;https_debug&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then&amp;nbsp;watch:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-sr&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;https_debug
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="sidebar-authpf-for-bastion-hosts"&gt;Sidebar: authpf for Bastion&amp;nbsp;Hosts&lt;/h2&gt;
&lt;p&gt;For bastion hosts or jump servers, authpf provides per-user firewall rules that activate upon &lt;span class="caps"&gt;SSH&lt;/span&gt; login. Instead of static rules, the firewall dynamically permits access based on who&amp;#8217;s&amp;nbsp;connected.&lt;/p&gt;
&lt;p&gt;authpf works by loading a user-specific ruleset&amp;nbsp;(from &lt;code&gt;/etc/authpf/users/$USER/&lt;/code&gt; or &lt;code&gt;/etc/authpf/authpf.rules&lt;/code&gt;) when the user logs in via &lt;span class="caps"&gt;SSH&lt;/span&gt;. When they log out, the rules are removed. This creates a &amp;#8220;knock first&amp;#8221; security model where services are invisible until&amp;nbsp;authenticated.&lt;/p&gt;
&lt;p&gt;Basic&amp;nbsp;setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Set the user&amp;#8217;s shell&amp;nbsp;to &lt;code&gt;/usr/sbin/authpf&lt;/code&gt;:
   &lt;code&gt;bash
   pw usermod bastion_user -s /usr/sbin/authpf&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create &lt;code&gt;/etc/authpf/authpf.rules&lt;/code&gt; with rules to load on&amp;nbsp;login:
   &lt;code&gt;pf
   pass in quick on $ext_if from $user_ip to $internal_net&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The&amp;nbsp;macro &lt;code&gt;$user_ip&lt;/code&gt; is automatically set to the connecting client&amp;#8217;s&amp;nbsp;address.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach is powerful for restricting access to internal networks: users must &lt;span class="caps"&gt;SSH&lt;/span&gt; to the bastion first, then their &lt;span class="caps"&gt;IP&lt;/span&gt; gets temporary firewall access. When the &lt;span class="caps"&gt;SSH&lt;/span&gt; session ends, access is revoked. Combined with short-lived certificates or hardware tokens, authpf creates a robust zero-trust perimeter without &lt;span class="caps"&gt;VPN&lt;/span&gt;&amp;nbsp;complexity.&lt;/p&gt;
&lt;p&gt;For production deployments, consider authpf as an alternative to VPNs for administrative access - it&amp;#8217;s simpler to audit, integrates with existing &lt;span class="caps"&gt;SSH&lt;/span&gt; infrastructure, and provides per-session access&amp;nbsp;control.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; rewards careful configuration with robust, maintainable security. The patterns here - default deny, explicit allowlists, brute-force protection, and clean macro organization - scale from single servers to complex multi-jail&amp;nbsp;deployments.&lt;/p&gt;
&lt;p&gt;The key principles worth&amp;nbsp;remembering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Default deny with explicit allows&lt;/strong&gt;: Never assume traffic is&amp;nbsp;safe&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State tracking&lt;/strong&gt;: Let &lt;span class="caps"&gt;PF&lt;/span&gt; manage connection state rather than permitting return traffic&amp;nbsp;explicitly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tables for dynamic data&lt;/strong&gt;: Use tables for address lists that change or grow&amp;nbsp;large&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log selectively&lt;/strong&gt;: Log blocked traffic for analysis, but avoid logging high-volume allowed&amp;nbsp;traffic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test before applying&lt;/strong&gt;: Always validate syntax and have a rollback&amp;nbsp;plan&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s syntax takes time to internalize, but once it clicks, writing firewall rules becomes almost intuitive. The investment pays dividends in security, clarity, and the confidence that comes from truly understanding what your firewall&amp;nbsp;permits.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.openbsd.org/faq/pf/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; - The OpenBSD Packet Filter&lt;/a&gt; (&lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s canonical&amp;nbsp;documentation)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/firewalls/"&gt;FreeBSD Handbook:&amp;nbsp;Firewalls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=pf.conf"&gt;FreeBSD pf.conf(5) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=authpf"&gt;authpf(8) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;A good firewall is invisible when everything works and invaluable when something goes wrong. &lt;span class="caps"&gt;PF&lt;/span&gt; sits quietly in the kernel, inspecting every packet, blocking the noise, and letting through exactly what you&amp;#8217;ve specified. It&amp;#8217;s one of those tools that makes you appreciate the Unix philosophy: do one thing well, make it composable, keep it&amp;nbsp;simple.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="firewall"/><category term="pf"/><category term="security"/><category term="networking"/></entry><entry><title>Immutable Linux Desktops: Universal Blue, OSTree, and the Future of Desktop Linux</title><link href="https://blog.hofstede.it/immutable-linux-desktops-universal-blue-ostree-and-the-future-of-desktop-linux/" rel="alternate"/><published>2026-01-26T00:00:00+01:00</published><updated>2026-01-26T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-26:/immutable-linux-desktops-universal-blue-ostree-and-the-future-of-desktop-linux/</id><summary type="html">&lt;p&gt;Exploring atomic desktop Linux distributions, the technology stack behind them, and why Universal Blue&amp;#8217;s Aurora and Bazzite represent a compelling vision for reliable, maintainable desktop&amp;nbsp;systems.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Traditional Linux desktop installations accumulate state over time. Package upgrades modify system files in place, configuration changes drift from defaults, and after a few years, systems become unique snowflakes that are difficult to reproduce or repair. Atomic desktops take a fundamentally different approach: the operating system is treated as a single, versioned image that can be deployed, rolled back, or replaced&amp;nbsp;entirely.&lt;/p&gt;
&lt;p&gt;This model has proven itself in server infrastructure through CoreOS and similar projects. Now it&amp;#8217;s coming to the desktop through Fedora&amp;#8217;s atomic variants and, more ambitiously, through Universal Blue&amp;#8217;s community-built images like Aurora and&amp;nbsp;Bazzite.&lt;/p&gt;
&lt;h2 id="the-foundation-ostree"&gt;The Foundation:&amp;nbsp;OSTree&lt;/h2&gt;
&lt;p&gt;At the heart of atomic Linux systems lies &lt;a href="https://ostreedev.github.io/ostree/"&gt;OSTree&lt;/a&gt; (also known as libostree), a content-addressed filesystem designed to store and deploy complete bootable operating system trees. Think of it as &amp;#8220;git for operating systems&amp;#8221;. Though the comparison is imperfect, it captures the essential&amp;nbsp;concept.&lt;/p&gt;
&lt;p&gt;OSTree stores filesystem trees as immutable, deduplicated objects identified by their content hash. When you deploy a new system version,&amp;nbsp;OSTree:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Downloads only the changed objects (delta&amp;nbsp;updates)&lt;/li&gt;
&lt;li&gt;Hardlinks unchanged files from the existing&amp;nbsp;deployment&lt;/li&gt;
&lt;li&gt;Creates a new boot entry pointing to the new&amp;nbsp;tree&lt;/li&gt;
&lt;li&gt;Keeps the previous deployment intact for&amp;nbsp;rollback&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result is atomic updates - the system either fully transitions to the new state or remains unchanged. No more half-completed upgrades leaving your system in an inconsistent state. No more &amp;#8220;let me try rebooting again&amp;#8221; after a failed&amp;nbsp;update.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ sudo rpm-ostree status
State: idle
Deployments:
  fedora:fedora/43/x86_64/silverblue
                  Version: 43.20260126.0 (2026-01-26T00:26:25Z)
                   Commit: e36246b0837cfe0d1c09e3ee60205ed5be57d57260adc10a625f9353bbef3c40
             GPGSignature: Valid signature by C6E7F081CF80E13146676E88829B606631645531
                     Diff: 522 upgraded, 11 removed, 26 added

● fedora:fedora/43/x86_64/silverblue
                  Version: 43.1.6 (2025-10-23T03:11:18Z)
                   Commit: 4d40d281be93a88f3d559b5756df602f454f932f3c809a6a4250b91049ce40e8
             GPGSignature: Valid signature by C6E7F081CF80E13146676E88829B606631645531
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The bullet point indicates your current deployment. The previous deployment remains available - a&amp;nbsp;single &lt;code&gt;rpm-ostree rollback&lt;/code&gt; and reboot returns you to the exact state you&amp;nbsp;left.&lt;/p&gt;
&lt;h2 id="rpm-ostree-hybrid-package-management"&gt;rpm-ostree: Hybrid Package&amp;nbsp;Management&lt;/h2&gt;
&lt;p&gt;While pure OSTree deployments are completely immutable, desktop users need flexibility. Enter &lt;a href="https://coreos.github.io/rpm-ostree/"&gt;rpm-ostree&lt;/a&gt;, which provides a hybrid model: you get the reliability of image-based deployments while retaining the ability to layer additional &lt;span class="caps"&gt;RPM&lt;/span&gt;&amp;nbsp;packages.&lt;/p&gt;
&lt;p&gt;The base system remains a verified, signed image from your distribution. On top of this, rpm-ostree allows you&amp;nbsp;to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layer packages&lt;/strong&gt;: Install additional RPMs that persist across&amp;nbsp;updates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Override packages&lt;/strong&gt;: Replace base system packages with different&amp;nbsp;versions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove packages&lt;/strong&gt;: Exclude unwanted components from the base&amp;nbsp;image&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Layer a package on top of the base image&lt;/span&gt;
rpm-ostree&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;htop&lt;span class="w"&gt; &lt;/span&gt;neovim

&lt;span class="c1"&gt;# Override a base package with a newer version&lt;/span&gt;
rpm-ostree&lt;span class="w"&gt; &lt;/span&gt;override&lt;span class="w"&gt; &lt;/span&gt;replace&lt;span class="w"&gt; &lt;/span&gt;./updated-package.rpm

&lt;span class="c1"&gt;# Remove a base package you don&amp;#39;t need&lt;/span&gt;
rpm-ostree&lt;span class="w"&gt; &lt;/span&gt;override&lt;span class="w"&gt; &lt;/span&gt;remove&lt;span class="w"&gt; &lt;/span&gt;firefox
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each modification creates a new deployment, leaving the previous state intact. Updates from upstream are rebased on top of your layered packages, maintaining a coherent system&amp;nbsp;state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The key insight&lt;/strong&gt;: your customizations are explicit and tracked. Unlike traditional package managers where the system state is the accumulated result of thousands of operations over time, rpm-ostree maintains a clear separation between the base image and your&amp;nbsp;modifications.&lt;/p&gt;
&lt;h2 id="bootc-container-native-systems"&gt;bootc: Container-Native&amp;nbsp;Systems&lt;/h2&gt;
&lt;p&gt;The current standard in this space is &lt;a href="https://containers.github.io/bootc/"&gt;bootc&lt;/a&gt;, which takes the logical next step: if containers are the standard way to build and distribute software, why not use them for entire operating&amp;nbsp;systems?&lt;/p&gt;
&lt;p&gt;bootc systems are built as standard &lt;span class="caps"&gt;OCI&lt;/span&gt; container images. The same tools used for application containers - Podman, Docker, Buildah - can build bootable system images. The same registries that host application images can host operating system images. The entire container ecosystem becomes available for &lt;span class="caps"&gt;OS&lt;/span&gt;&amp;nbsp;development.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quay.io/fedora/fedora-bootc:43&lt;/span&gt;

&lt;span class="c"&gt;# Install additional packages&lt;/span&gt;
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;htop&lt;span class="w"&gt; &lt;/span&gt;neovim&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;clean&lt;span class="w"&gt; &lt;/span&gt;all

&lt;span class="c"&gt;# Add custom configuration&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;custom-config.conf&lt;span class="w"&gt; &lt;/span&gt;/etc/myapp/

&lt;span class="c"&gt;# Standard container build produces a bootable system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Building this Containerfile produces a complete, bootable operating system image. You can test it in a &lt;span class="caps"&gt;VM&lt;/span&gt;, push it to a registry, and deploy it to physical hardware. The same image works&amp;nbsp;everywhere.&lt;/p&gt;
&lt;p&gt;bootc uses OSTree under the hood for the actual deployment mechanics, but the authoring experience shifts entirely to container workflows. For organizations already invested in container tooling, this dramatically simplifies system&amp;nbsp;management.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Current state&lt;/strong&gt;: bootc is production-ready for servers and is making rapid progress on desktop support. Universal Blue&amp;#8217;s images are already built using container workflows, positioning them well for the full bootc&amp;nbsp;transition.&lt;/p&gt;
&lt;h2 id="fedora-atomic-desktops"&gt;Fedora Atomic&amp;nbsp;Desktops&lt;/h2&gt;
&lt;p&gt;Fedora provides official atomic desktop variants, each tailored to a specific desktop&amp;nbsp;environment:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Desktop Environment&lt;/th&gt;
&lt;th&gt;Target Users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fedora Silverblue&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;GNOME&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;General desktop users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fedora Kinoite&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;KDE&lt;/span&gt; Plasma&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;KDE&lt;/span&gt; enthusiasts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fedora Sway Atomic&lt;/td&gt;
&lt;td&gt;Sway&lt;/td&gt;
&lt;td&gt;Tiling &lt;span class="caps"&gt;WM&lt;/span&gt; users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fedora Budgie Atomic&lt;/td&gt;
&lt;td&gt;Budgie&lt;/td&gt;
&lt;td&gt;Budgie fans&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These provide the foundation: a stable, tested base with Fedora&amp;#8217;s regular release cadence. They&amp;#8217;re excellent choices for users who want the atomic model with minimal deviation from upstream&amp;nbsp;Fedora.&lt;/p&gt;
&lt;p&gt;However, Fedora&amp;#8217;s official images are intentionally conservative. They include only what&amp;#8217;s necessary for a functional desktop, leaving additional customization to the user through package&amp;nbsp;layering.&lt;/p&gt;
&lt;h2 id="universal-blue-opinionated-images-done-right"&gt;Universal Blue: Opinionated Images Done&amp;nbsp;Right&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://universal-blue.org/"&gt;Universal Blue&lt;/a&gt; takes the Fedora atomic base and builds upon it with thoughtful defaults, additional hardware support, and curated software selections. Rather than starting from scratch, they leverage Fedora&amp;#8217;s extensive testing and add value through careful&amp;nbsp;curation.&lt;/p&gt;
&lt;p&gt;The project produces several image&amp;nbsp;variants:&lt;/p&gt;
&lt;h3 id="aurora"&gt;Aurora&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://getaurora.dev/"&gt;Aurora&lt;/a&gt; is Universal Blue&amp;#8217;s flagship desktop image, based on Fedora Kinoite (&lt;span class="caps"&gt;KDE&lt;/span&gt; Plasma). It targets users who want a polished, productive desktop without manual&amp;nbsp;configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enhanced hardware support&lt;/strong&gt;: Additional firmware, codecs, and drivers included (UBlue images come with Nvidia drivers pre-installed and&amp;nbsp;signed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer tooling&lt;/strong&gt;: Distrobox pre-configured for containerized development&amp;nbsp;environments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality-of-life improvements&lt;/strong&gt;: Thoughtful defaults and missing pieces filled&amp;nbsp;in&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic updates&lt;/strong&gt;: System images update in the background and apply on&amp;nbsp;reboot&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aurora represents what a modern Linux desktop can be when someone with taste makes the integration decisions. The &lt;span class="caps"&gt;KDE&lt;/span&gt; Plasma experience feels cohesive and&amp;nbsp;complete.&lt;/p&gt;
&lt;h3 id="bazzite"&gt;Bazzite&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://bazzite.gg/"&gt;Bazzite&lt;/a&gt; targets gaming and &lt;span class="caps"&gt;HTPC&lt;/span&gt; use cases, building on the same solid foundation with gaming-specific&amp;nbsp;optimizations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Steam and Lutris&lt;/strong&gt;: Pre-installed and&amp;nbsp;configured&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graphics drivers&lt;/strong&gt;: Latest Mesa, Vulkan support, and vendor-specific&amp;nbsp;drivers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller support&lt;/strong&gt;: Out-of-box recognition for a wide range of game&amp;nbsp;controllers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance tuning&lt;/strong&gt;: Kernel parameters and system settings optimized for&amp;nbsp;gaming&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Steam Deck compatibility&lt;/strong&gt;: Specific images for Steam Deck and similar&amp;nbsp;handhelds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;HTPC&lt;/span&gt; mode&lt;/strong&gt;: Console-like experience for living room&amp;nbsp;setups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bazzite solves the perennial &amp;#8220;gaming on Linux&amp;#8221; setup problem. Instead of spending an afternoon configuring drivers, Proton, and various compatibility layers, you boot into a system where everything already&amp;nbsp;works.&lt;/p&gt;
&lt;h3 id="other-variants"&gt;Other&amp;nbsp;Variants&lt;/h3&gt;
&lt;p&gt;Universal Blue also&amp;nbsp;produces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bluefin&lt;/strong&gt;: &lt;span class="caps"&gt;GNOME&lt;/span&gt;-based developer workstation&amp;nbsp;image&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uCore&lt;/strong&gt;: Server-focused images based on Fedora&amp;nbsp;CoreOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework-specific images&lt;/strong&gt;: Optimized for Framework laptop&amp;nbsp;hardware&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-advantages-of-atomic-desktops"&gt;The Advantages of Atomic&amp;nbsp;Desktops&lt;/h2&gt;
&lt;h3 id="reliability-and-stability"&gt;Reliability and&amp;nbsp;Stability&lt;/h3&gt;
&lt;p&gt;The most significant advantage is predictability. Every user running Aurora 43.20260126 has &lt;em&gt;exactly&lt;/em&gt; the same base system. Support becomes tractable - you&amp;#8217;re not debugging the unique accumulated state of someone&amp;#8217;s five-year-old&amp;nbsp;installation.&lt;/p&gt;
&lt;p&gt;Updates are atomic: they either complete successfully or don&amp;#8217;t happen at all. The half-updated, inconsistent states that sometimes plague traditional distributions simply cannot&amp;nbsp;occur.&lt;/p&gt;
&lt;h3 id="painless-rollback"&gt;Painless&amp;nbsp;Rollback&lt;/h3&gt;
&lt;p&gt;Every update preserves the previous system state. If an update causes&amp;nbsp;problems:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rollback to the previous deployment&lt;/span&gt;
rpm-ostree&lt;span class="w"&gt; &lt;/span&gt;rollback
systemctl&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After reboot, you&amp;#8217;re running the exact system you had before the update. This isn&amp;#8217;t &amp;#8220;uninstall the broken package and hope&amp;#8221; - it&amp;#8217;s a complete restoration of the previous&amp;nbsp;state.&lt;/p&gt;
&lt;p&gt;This dramatically lowers the risk of updates. You can confidently run the latest packages knowing that recovery is&amp;nbsp;trivial.&lt;/p&gt;
&lt;h3 id="rebasing-switch-distributions-instantly"&gt;Rebasing: Switch Distributions&amp;nbsp;Instantly&lt;/h3&gt;
&lt;p&gt;Perhaps the most surprising capability: you can switch between compatible images without reinstalling. Running Aurora but want to try Bazzite for&amp;nbsp;gaming?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rebase to Bazzite&lt;/span&gt;
rpm-ostree&lt;span class="w"&gt; &lt;/span&gt;rebase&lt;span class="w"&gt; &lt;/span&gt;ostree-unverified-registry:ghcr.io/ublue-os/bazzite:latest&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# (Select the specific tag for your hardware/GPU)&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After reboot, you&amp;#8217;re running Bazzite. Your home directory, user data, and Flatpak applications remain untouched - only the base system changed. Don&amp;#8217;t like it? Rebase back to&amp;nbsp;Aurora.&lt;/p&gt;
&lt;p&gt;This works across any compatible OSTree-based images. You can move between Fedora&amp;#8217;s official images and Universal Blue&amp;#8217;s variants freely. The operating system becomes as swappable as a container image - because, fundamentally, that&amp;#8217;s what it&amp;nbsp;is.&lt;/p&gt;
&lt;h3 id="separation-of-concerns"&gt;Separation of&amp;nbsp;Concerns&lt;/h3&gt;
&lt;p&gt;Atomic desktops enforce a clean&amp;nbsp;separation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System image&lt;/strong&gt;: Managed by OSTree, updated atomically,&amp;nbsp;reproducible&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applications&lt;/strong&gt;: Installed via Flatpak, containerized, independent of&amp;nbsp;system&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User data&lt;/strong&gt;: Lives in your home directory, persists across&amp;nbsp;rebases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development tools&lt;/strong&gt;: Run in Distrobox containers, isolated from&amp;nbsp;system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation means you can experiment freely with your development environment without risking system stability. Your tooling runs in containers; your &lt;span class="caps"&gt;OS&lt;/span&gt; remains&amp;nbsp;pristine.&lt;/p&gt;
&lt;h3 id="reproducibility"&gt;Reproducibility&lt;/h3&gt;
&lt;p&gt;Need to set up a new machine identically to your current one? The process is&amp;nbsp;trivial:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the same base&amp;nbsp;image&lt;/li&gt;
&lt;li&gt;Apply the same layered packages (if&amp;nbsp;any)&lt;/li&gt;
&lt;li&gt;Sync your home&amp;nbsp;directory&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There&amp;#8217;s no &amp;#8220;reinstall all the packages I&amp;#8217;ve accumulated over five years&amp;#8221; step because your system state is declaratively defined by the image plus your explicit&amp;nbsp;modifications.&lt;/p&gt;
&lt;h2 id="working-with-atomic-desktops"&gt;Working with Atomic&amp;nbsp;Desktops&lt;/h2&gt;
&lt;h3 id="daily-workflow"&gt;Daily&amp;nbsp;Workflow&lt;/h3&gt;
&lt;p&gt;Day-to-day usage feels identical to traditional Linux. You use your applications, edit documents, browse the web. The atomic nature is invisible until you need&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;Updates happen automatically in the background. When a new image is ready, you&amp;#8217;ll see a notification. The update applies on your next reboot - no waiting for package downloads during your&amp;nbsp;workday.&lt;/p&gt;
&lt;h3 id="installing-software"&gt;Installing&amp;nbsp;Software&lt;/h3&gt;
&lt;p&gt;For graphical applications, Flatpak is the primary&amp;nbsp;method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;flatpak&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;flathub&lt;span class="w"&gt; &lt;/span&gt;org.mozilla.firefox
flatpak&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;flathub&lt;span class="w"&gt; &lt;/span&gt;com.spotify.Client
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Flatpak applications are containerized and independent of the base system. They update separately and work across all Linux&amp;nbsp;distributions.&lt;/p&gt;
&lt;p&gt;For command-line tools and development environments, Distrobox provides seamless container&amp;nbsp;integration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a development container&lt;/span&gt;
distrobox&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;dev&lt;span class="w"&gt; &lt;/span&gt;--image&lt;span class="w"&gt; &lt;/span&gt;fedora:43

&lt;span class="c1"&gt;# Enter the container - it feels like native&lt;/span&gt;
distrobox&lt;span class="w"&gt; &lt;/span&gt;enter&lt;span class="w"&gt; &lt;/span&gt;dev

&lt;span class="c1"&gt;# Inside the container, install whatever you need&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;gcc&lt;span class="w"&gt; &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;python3-devel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Distrobox containers integrate with your desktop - applications appear in your menu, files are accessible, and the experience is nearly&amp;nbsp;transparent.&lt;/p&gt;
&lt;p&gt;Crucially, you can export binaries or &lt;span class="caps"&gt;GUI&lt;/span&gt; apps from the container to the host. This means you can launch tools like &lt;span class="caps"&gt;VS&lt;/span&gt; Code or PyCharm directly from your system dock or application menu, just as if they were installed&amp;nbsp;natively.&lt;/p&gt;
&lt;h3 id="when-to-layer-packages"&gt;When to Layer&amp;nbsp;Packages&lt;/h3&gt;
&lt;p&gt;Some software legitimately needs system integration that Flatpak can&amp;#8217;t&amp;nbsp;provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Virtualization&lt;/strong&gt;: libvirt, &lt;span class="caps"&gt;QEMU&lt;/span&gt;,&amp;nbsp;virt-manager&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VPNs&lt;/strong&gt;: OpenVPN, WireGuard with system&amp;nbsp;integration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System services&lt;/strong&gt;: Custom daemons that run at&amp;nbsp;boot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel modules&lt;/strong&gt;: Out-of-tree drivers (though check if the image already includes&amp;nbsp;them)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For these cases, rpm-ostree layering is&amp;nbsp;appropriate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rpm-ostree&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;libvirt&lt;span class="w"&gt; &lt;/span&gt;virt-manager
systemctl&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The general guidance: use Flatpak when possible, Distrobox for development, and rpm-ostree layering only when neither alternative&amp;nbsp;works.&lt;/p&gt;
&lt;h2 id="considerations-and-trade-offs"&gt;Considerations and&amp;nbsp;Trade-offs&lt;/h2&gt;
&lt;p&gt;Atomic desktops aren&amp;#8217;t without&amp;nbsp;trade-offs:&lt;/p&gt;
&lt;h3 id="adjustment-period"&gt;Adjustment&amp;nbsp;Period&lt;/h3&gt;
&lt;p&gt;Users accustomed&amp;nbsp;to &lt;code&gt;dnf install whatever&lt;/code&gt; need to adapt their mental model. The system isn&amp;#8217;t broken - it&amp;#8217;s intentionally designed to guide you toward better patterns. This adjustment takes a few days for most&amp;nbsp;users.&lt;/p&gt;
&lt;h3 id="reboot-requirements"&gt;Reboot&amp;nbsp;Requirements&lt;/h3&gt;
&lt;p&gt;System changes require a reboot to take effect. While this ensures atomicity, it can feel inconvenient compared to traditional package managers. In practice, most users update infrequently enough that this isn&amp;#8217;t&amp;nbsp;burdensome.&lt;/p&gt;
&lt;h3 id="layered-package-limits"&gt;Layered Package&amp;nbsp;Limits&lt;/h3&gt;
&lt;p&gt;While you &lt;em&gt;can&lt;/em&gt; layer packages with rpm-ostree, heavy layering defeats the purpose. If you&amp;#8217;re layering fifty packages, you&amp;#8217;re fighting the model. Consider whether those packages truly need system integration or whether Flatpak/Distrobox would&amp;nbsp;work.&lt;/p&gt;
&lt;h3 id="some-niche-software"&gt;Some Niche&amp;nbsp;Software&lt;/h3&gt;
&lt;p&gt;Certain applications expect traditional &lt;span class="caps"&gt;FHS&lt;/span&gt; layouts or assume they can modify system files. Most mainstream software works fine, but occasional edge cases&amp;nbsp;exist.&lt;/p&gt;
&lt;h2 id="getting-started"&gt;Getting&amp;nbsp;Started&lt;/h2&gt;
&lt;h3 id="choosing-an-image"&gt;Choosing an&amp;nbsp;Image&lt;/h3&gt;
&lt;p&gt;For general desktop use with &lt;span class="caps"&gt;KDE&lt;/span&gt; Plasma: &lt;strong&gt;Aurora&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For gaming or &lt;span class="caps"&gt;HTPC&lt;/span&gt;: &lt;strong&gt;Bazzite&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For &lt;span class="caps"&gt;GNOME&lt;/span&gt; and developer workflows: &lt;strong&gt;Bluefin&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For conservative, upstream-focused experience: &lt;strong&gt;Fedora&amp;nbsp;Silverblue/Kinoite&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;Universal Blue images install like any other Linux distribution. Download the &lt;span class="caps"&gt;ISO&lt;/span&gt;, create a bootable &lt;span class="caps"&gt;USB&lt;/span&gt;, and run the installer. The Anaconda installer handles partitioning, user creation, and initial&amp;nbsp;setup.&lt;/p&gt;
&lt;p&gt;Post-installation, enable automatic updates if not already&amp;nbsp;enabled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;rpm-ostreed-automatic.timer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="first-steps"&gt;First&amp;nbsp;Steps&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Explore Flatpak&lt;/strong&gt;: Visit &lt;a href="https://flathub.org/"&gt;Flathub&lt;/a&gt; and install your essential&amp;nbsp;applications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up Distrobox&lt;/strong&gt;: Create a development container for your programming&amp;nbsp;environment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure your desktop&lt;/strong&gt;: &lt;span class="caps"&gt;KDE&lt;/span&gt; Plasma or &lt;span class="caps"&gt;GNOME&lt;/span&gt; settings work exactly as&amp;nbsp;expected&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify automatic updates&lt;/strong&gt;:&amp;nbsp;Check &lt;code&gt;rpm-ostree status&lt;/code&gt; to see your current&amp;nbsp;deployment&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="the-future"&gt;The&amp;nbsp;Future&lt;/h2&gt;
&lt;p&gt;The atomic desktop model aligns with where computing is heading. Containers have proven their value for applications; applying the same principles to the operating system itself is the logical&amp;nbsp;evolution.&lt;/p&gt;
&lt;p&gt;bootc represents the next major step, unifying system management with container tooling. Universal Blue is already building toward this future while maintaining stability&amp;nbsp;today.&lt;/p&gt;
&lt;p&gt;For users tired of nursing aging installations, wondering what broke after the last update, or simply wanting a system that stays out of their way, atomic desktops offer a compelling alternative. They&amp;#8217;re not experimental technology - they&amp;#8217;re the result of years of refinement in the server space, now polished for desktop&amp;nbsp;use.&lt;/p&gt;
&lt;p&gt;The best part: if you don&amp;#8217;t like it, you can always rebase&amp;nbsp;back.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://universal-blue.org/"&gt;Universal Blue&lt;/a&gt; - Project&amp;nbsp;home&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getaurora.dev/"&gt;Aurora&lt;/a&gt; - &lt;span class="caps"&gt;KDE&lt;/span&gt;-based desktop&amp;nbsp;image&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bazzite.gg/"&gt;Bazzite&lt;/a&gt; - Gaming-focused&amp;nbsp;image&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ostreedev.github.io/ostree/"&gt;OSTree&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coreos.github.io/rpm-ostree/"&gt;rpm-ostree&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://containers.github.io/bootc/"&gt;bootc&amp;nbsp;Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fedoraproject.org/atomic-desktops/"&gt;Fedora Atomic&amp;nbsp;Desktops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://flathub.org/"&gt;Flathub&lt;/a&gt; - Flatpak application&amp;nbsp;repository&lt;/li&gt;
&lt;li&gt;&lt;a href="https://distrobox.it/"&gt;Distrobox&lt;/a&gt; - Container integration for&amp;nbsp;development&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Thanks to the Universal Blue community for building these excellent images and to the Fedora team for providing the solid foundation they&amp;#8217;re built&amp;nbsp;upon.&lt;/p&gt;</content><category term="Linux"/><category term="linux"/><category term="fedora"/><category term="ostree"/><category term="rpm-ostree"/><category term="bootc"/><category term="universal-blue"/><category term="aurora"/><category term="bazzite"/><category term="atomic"/></entry><entry><title>Integrating FreeBSD 15 with FreeIPA: Native Kerberos and LDAP Authentication</title><link href="https://blog.hofstede.it/integrating-freebsd-15-with-freeipa-native-kerberos-and-ldap-authentication/" rel="alternate"/><published>2026-01-25T00:00:00+01:00</published><updated>2026-01-25T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-25:/integrating-freebsd-15-with-freeipa-native-kerberos-and-ldap-authentication/</id><summary type="html">&lt;p&gt;A clean approach to integrating FreeBSD 15 into a FreeIPA realm using native components - Kerberos for authentication, &lt;span class="caps"&gt;LDAP&lt;/span&gt; for identity, and no local user&amp;nbsp;management.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;&lt;img alt="Logo" src="https://blog.hofstede.it/images/2026-01-25-freebsd-freeipa-kerberos-ldap.jpg" title="FreeBSD 15 with FreeIPA: Header image"&gt;&lt;/p&gt;
&lt;p&gt;Most FreeIPA documentation assumes you&amp;#8217;re running Linux and will&amp;nbsp;use &lt;code&gt;ipa-client-install&lt;/code&gt; to join hosts to the realm. FreeBSD doesn&amp;#8217;t have this luxury. There&amp;#8217;s no official &lt;span class="caps"&gt;IPA&lt;/span&gt; client, and the enrollment scripts expect systemd and other Linux-specific components. But that&amp;#8217;s not necessarily a&amp;nbsp;disadvantage.&lt;/p&gt;
&lt;p&gt;FreeBSD&amp;#8217;s native Kerberos implementation and the&amp;nbsp;lightweight &lt;code&gt;nslcd&lt;/code&gt; daemon provide everything needed to integrate with FreeIPA cleanly. The result is arguably more elegant than the Linux approach: pure &lt;span class="caps"&gt;GSSAPI&lt;/span&gt; authentication via &lt;span class="caps"&gt;SSH&lt;/span&gt;, &lt;span class="caps"&gt;LDAP&lt;/span&gt;-backed identity lookups, and zero local user management. No &lt;span class="caps"&gt;SSSD&lt;/span&gt;, no realm daemon, no&amp;nbsp;complexity.&lt;/p&gt;
&lt;p&gt;This guide details integrating a FreeBSD 15.0 host into a Red Hat Identity Management (IdM) or FreeIPA realm using only native FreeBSD&amp;nbsp;components.&lt;/p&gt;
&lt;h2 id="design-philosophy"&gt;Design&amp;nbsp;Philosophy&lt;/h2&gt;
&lt;p&gt;The approach here follows what I call &amp;#8220;Clean and Sane&amp;nbsp;Engineering&amp;#8221;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: Pure Kerberos via &lt;span class="caps"&gt;GSSAPI&lt;/span&gt;. Users authenticate with their Kerberos tickets - no passwords transmitted over the&amp;nbsp;network&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identity&lt;/strong&gt;: &lt;span class="caps"&gt;LDAP&lt;/span&gt;&amp;nbsp;via &lt;code&gt;nslcd&lt;/code&gt; for user and group lookups. The &lt;span class="caps"&gt;NSS&lt;/span&gt; layer queries FreeIPA&amp;#8217;s &lt;span class="caps"&gt;LDAP&lt;/span&gt; directory&amp;nbsp;transparently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Management&lt;/strong&gt;: Stateless. No local users created manually. The host is a pure consumer of central identity&amp;nbsp;services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation of concerns mirrors how enterprise authentication should work: the &lt;span class="caps"&gt;KDC&lt;/span&gt; handles cryptographic verification, the directory provides attributes, and the local system just needs to know where to&amp;nbsp;ask.&lt;/p&gt;
&lt;h3 id="why-not-sssd"&gt;Why Not &lt;span class="caps"&gt;SSSD&lt;/span&gt;?&lt;/h3&gt;
&lt;p&gt;The Linux world has largely converged on &lt;span class="caps"&gt;SSSD&lt;/span&gt; (System Security Services Daemon) for this kind of integration. It&amp;#8217;s a powerful tool, but it&amp;#8217;s also a monolith with a sprawling dependency tree, D-Bus requirements, and complexity that feels foreign on a FreeBSD&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;Following Unix philosophy and &lt;span class="caps"&gt;KISS&lt;/span&gt; principles, we actively avoid such heavyweight solutions. Each component in this setup does one thing&amp;nbsp;well: &lt;code&gt;nslcd&lt;/code&gt; handles &lt;span class="caps"&gt;LDAP&lt;/span&gt; lookups, the&amp;nbsp;native &lt;code&gt;krb5&lt;/code&gt; library handles Kerberos, &lt;span class="caps"&gt;PAM&lt;/span&gt; modules handle session setup. These are small, focused tools that compose&amp;nbsp;together.&lt;/p&gt;
&lt;p&gt;Yes, the initial setup requires understanding how the pieces fit together. But the result is a solution that&amp;#8217;s lighter, more transparent, and equally robust. When something breaks, you can debug each component independently. There&amp;#8217;s no opaque daemon trying to be clever on your behalf. The configuration files are human-readable, the logs are straightforward, and the failure modes are&amp;nbsp;predictable.&lt;/p&gt;
&lt;p&gt;This trade-off, slightly more upfront complexity for long-term simplicity and maintainability, is at the heart of the FreeBSD philosophy. We&amp;#8217;re not fighting the system. We&amp;#8217;re working with its native&amp;nbsp;primitives.&lt;/p&gt;
&lt;h2 id="prerequisites-and-packages"&gt;Prerequisites and&amp;nbsp;Packages&lt;/h2&gt;
&lt;p&gt;The package requirements are minimal. Install the &lt;span class="caps"&gt;LDAP&lt;/span&gt; connector and the &lt;span class="caps"&gt;PAM&lt;/span&gt; module for automatic home directory&amp;nbsp;creation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;nss-pam-ldapd&lt;span class="w"&gt; &lt;/span&gt;pam_mkhomedir&lt;span class="w"&gt; &lt;/span&gt;sudo
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;nss-pam-ldapd&lt;/code&gt; package provides&amp;nbsp;the &lt;code&gt;nslcd&lt;/code&gt; daemon, which bridges FreeBSD&amp;#8217;s &lt;span class="caps"&gt;NSS&lt;/span&gt; (Name Service Switch) to &lt;span class="caps"&gt;LDAP&lt;/span&gt;. Unlike heavier solutions like &lt;span class="caps"&gt;SSSD&lt;/span&gt;, it does one thing well: translate &lt;span class="caps"&gt;NSS&lt;/span&gt; queries into &lt;span class="caps"&gt;LDAP&lt;/span&gt;&amp;nbsp;lookups.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;pam_mkhomedir&lt;/code&gt; module handles a common pain point: Creating home directories on first login. Without it, users would authenticate successfully but fail to get a shell&amp;nbsp;because &lt;code&gt;/home/username&lt;/code&gt; doesn&amp;#8217;t&amp;nbsp;exist.&lt;/p&gt;
&lt;h2 id="host-identity-and-keytab-provisioning"&gt;Host Identity and Keytab&amp;nbsp;Provisioning&lt;/h2&gt;
&lt;p&gt;Since there&amp;#8217;s&amp;nbsp;no &lt;code&gt;ipa-client-install&lt;/code&gt;, we provision the host identity manually. This is actually straightforward: Create the host entry on the &lt;span class="caps"&gt;IPA&lt;/span&gt; server and export a&amp;nbsp;keytab.&lt;/p&gt;
&lt;h3 id="on-the-ipa-server"&gt;On the &lt;span class="caps"&gt;IPA&lt;/span&gt;&amp;nbsp;Server&lt;/h3&gt;
&lt;p&gt;First, create the host entry. If &lt;span class="caps"&gt;DNS&lt;/span&gt; is integrated with &lt;span class="caps"&gt;IPA&lt;/span&gt;,&amp;nbsp;the &lt;code&gt;--ip-address&lt;/code&gt; flag will create the A record&amp;nbsp;automatically:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ipa&lt;span class="w"&gt; &lt;/span&gt;host-add&lt;span class="w"&gt; &lt;/span&gt;bsdhost.example.com&lt;span class="w"&gt; &lt;/span&gt;--ip-address&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.168.1.25
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then generate and export the keytab.&amp;nbsp;The &lt;code&gt;ipa-getkeytab&lt;/code&gt; command randomizes the host&amp;#8217;s password (just like machine account passwords in Active Directory) and saves the cryptographic key&amp;nbsp;material:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ipa-getkeytab&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;idm01.example.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;host/bsdhost.example.com@EXAMPLE.COM&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-k&lt;span class="w"&gt; &lt;/span&gt;/tmp/bsdhost.keytab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="on-the-freebsd-host"&gt;On the FreeBSD&amp;nbsp;Host&lt;/h3&gt;
&lt;p&gt;Transfer the keytab securely&amp;nbsp;(via &lt;code&gt;scp&lt;/code&gt;)&amp;nbsp;to &lt;code&gt;/etc/krb5.keytab&lt;/code&gt;. The permissions here&amp;nbsp;matter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chown&lt;span class="w"&gt; &lt;/span&gt;root:nslcd&lt;span class="w"&gt; &lt;/span&gt;/etc/krb5.keytab
chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;640&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/krb5.keytab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;nslcd&lt;/code&gt; daemon needs read access to this keytab to authenticate its &lt;span class="caps"&gt;LDAP&lt;/span&gt; queries via &lt;span class="caps"&gt;GSSAPI&lt;/span&gt;. Without this, directory lookups will fail when the host&amp;#8217;s cached credentials&amp;nbsp;expire.&lt;/p&gt;
&lt;p&gt;You also need to ensure, sshd can read the file (or &lt;span class="caps"&gt;SSH&lt;/span&gt; authentication will fail). That can be done either with ACLs or group membership&amp;nbsp;for &lt;code&gt;sshd&lt;/code&gt; in&amp;nbsp;the &lt;code&gt;nslcd&lt;/code&gt; group.&lt;/p&gt;
&lt;h2 id="kerberos-configuration"&gt;Kerberos&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;The Kerberos configuration is minimal. FreeBSD&amp;#8217;s&amp;nbsp;native &lt;code&gt;krb5&lt;/code&gt; implementation needs to know where the &lt;span class="caps"&gt;KDC&lt;/span&gt; is and which realm to&amp;nbsp;use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/krb5.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[libdefaults]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;default_realm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;EXAMPLE.COM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;dns_lookup_kdc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;dns_lookup_realm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;[realms]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;EXAMPLE.COM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="na"&gt;kdc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;idm01.example.com&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="na"&gt;admin_server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;idm01.example.com&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;[domain_realm]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;.example.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;EXAMPLE.COM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;example.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;EXAMPLE.COM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I explicitly disable &lt;span class="caps"&gt;DNS&lt;/span&gt; lookups for the &lt;span class="caps"&gt;KDC&lt;/span&gt;. While FreeIPA publishes &lt;span class="caps"&gt;SRV&lt;/span&gt; records, hardcoding the &lt;span class="caps"&gt;KDC&lt;/span&gt; hostname makes the configuration deterministic and easier to debug. In environments with multiple KDCs, you can list them all in&amp;nbsp;the &lt;code&gt;kdc&lt;/code&gt; directive.&lt;/p&gt;
&lt;p&gt;Verify the configuration&amp;nbsp;works:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;klist&lt;span class="w"&gt; &lt;/span&gt;-k
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This should display the host principal from the&amp;nbsp;keytab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------
   1 host/bsdhost.example.com@EXAMPLE.COM
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="ldap-configuration-with-nslcd"&gt;&lt;span class="caps"&gt;LDAP&lt;/span&gt; Configuration with&amp;nbsp;nslcd&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;nslcd&lt;/code&gt; daemon handles all &lt;span class="caps"&gt;LDAP&lt;/span&gt; communication. It runs as an unprivileged user, connects to FreeIPA&amp;#8217;s &lt;span class="caps"&gt;LDAP&lt;/span&gt; server, and responds to &lt;span class="caps"&gt;NSS&lt;/span&gt; queries over a Unix&amp;nbsp;socket.&lt;/p&gt;
&lt;p&gt;The key insight is using &lt;span class="caps"&gt;SASL&lt;/span&gt;/&lt;span class="caps"&gt;GSSAPI&lt;/span&gt; authentication&amp;nbsp;- &lt;code&gt;nslcd&lt;/code&gt; authenticates to &lt;span class="caps"&gt;LDAP&lt;/span&gt; using the host&amp;#8217;s Kerberos keytab rather than a bind password. This eliminates stored credentials and leverages the existing trust&amp;nbsp;relationship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/nslcd.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Run as unprivileged user
uid nslcd
gid nslcd

# Connection details
uri ldap://idm01.example.com
base dc=example,dc=com

# Authentication: Use the system keytab (Host Principal)
sasl_mech GSSAPI
sasl_realm EXAMPLE.COM

# Mapping: Force all IPA users to use /bin/sh
# IPA typically defaults to /bin/bash, which lives in /usr/local/bin on FreeBSD
map passwd loginShell &amp;quot;/bin/sh&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;loginShell&lt;/code&gt; mapping deserves explanation. FreeIPA&amp;nbsp;stores &lt;code&gt;/bin/bash&lt;/code&gt; as the default shell, but on FreeBSD, bash (if installed) lives&amp;nbsp;at &lt;code&gt;/usr/local/bin/bash&lt;/code&gt;. Rather than requiring every user to update their shell in &lt;span class="caps"&gt;IPA&lt;/span&gt;, we override it locally&amp;nbsp;to &lt;code&gt;/bin/sh&lt;/code&gt;, which is guaranteed to exist. Users who need bash can set it explicitly in their&amp;nbsp;profile.&lt;/p&gt;
&lt;p&gt;Enable and start the&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nslcd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;nslcd&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="nss-configuration"&gt;&lt;span class="caps"&gt;NSS&lt;/span&gt;&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Tell FreeBSD&amp;#8217;s name service switch to query &lt;span class="caps"&gt;LDAP&lt;/span&gt; after local files. The order matters - local accounts (root, system users) take precedence, with &lt;span class="caps"&gt;LDAP&lt;/span&gt; providing everything&amp;nbsp;else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/nsswitch.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;group: files ldap
passwd: files ldap
hosts: files dns
networks: files
shells: files
services: compat
protocols: files
rpc: files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We avoid the&amp;nbsp;legacy &lt;code&gt;compat&lt;/code&gt; mode for passwd and group -&amp;nbsp;the &lt;code&gt;files ldap&lt;/code&gt; ordering is cleaner and more&amp;nbsp;predictable.&lt;/p&gt;
&lt;p&gt;Test the integration&amp;nbsp;immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;id&lt;span class="w"&gt; &lt;/span&gt;someuser
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This should return &lt;span class="caps"&gt;UID&lt;/span&gt; and &lt;span class="caps"&gt;GID&lt;/span&gt; information from&amp;nbsp;FreeIPA:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uid=1234(someuser) gid=1234(someuser) groups=1234(someuser),5000(admins)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If this fails, check&amp;nbsp;that &lt;code&gt;nslcd&lt;/code&gt; is running and can read the keytab. The logs&amp;nbsp;in &lt;code&gt;/var/log/messages&lt;/code&gt; usually reveal the&amp;nbsp;problem.&lt;/p&gt;
&lt;h2 id="ssh-configuration"&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Configure &lt;span class="caps"&gt;SSH&lt;/span&gt; to accept Kerberos tickets via &lt;span class="caps"&gt;GSSAPI&lt;/span&gt;. This enables true single sign-on - users with valid Kerberos tickets can &lt;span class="caps"&gt;SSH&lt;/span&gt; without any password&amp;nbsp;prompt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/ssh/sshd_config&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Disable password authentication entirely
PasswordAuthentication no
KbdInteractiveAuthentication no

# Optional: Disable pubkey if relying purely on Kerberos
# PubkeyAuthentication no

# Kerberos / GSSAPI authentication
GSSAPIAuthentication yes
GSSAPICleanupCredentials yes
GSSAPIStrictAcceptorCheck no
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;GSSAPIStrictAcceptorCheck no&lt;/code&gt; setting relaxes hostname verification. This is sometimes necessary when the &lt;span class="caps"&gt;SSH&lt;/span&gt; server&amp;#8217;s hostname doesn&amp;#8217;t exactly match the principal in the keytab (e.g., connecting via &lt;span class="caps"&gt;IP&lt;/span&gt; or a &lt;span class="caps"&gt;CNAME&lt;/span&gt;).&lt;/p&gt;
&lt;p&gt;Restart &lt;span class="caps"&gt;SSH&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;service&lt;span class="w"&gt; &lt;/span&gt;sshd&lt;span class="w"&gt; &lt;/span&gt;restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="pam-configuration-for-home-directories"&gt;&lt;span class="caps"&gt;PAM&lt;/span&gt; Configuration for Home&amp;nbsp;Directories&lt;/h2&gt;
&lt;p&gt;When a user logs in for the first time, their home directory won&amp;#8217;t exist.&amp;nbsp;The &lt;code&gt;pam_mkhomedir&lt;/code&gt; module creates it automatically with appropriate&amp;nbsp;permissions.&lt;/p&gt;
&lt;p&gt;Edit the &lt;span class="caps"&gt;SSH&lt;/span&gt; &lt;span class="caps"&gt;PAM&lt;/span&gt; configuration to add the&amp;nbsp;module:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/pam.d/sshd&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;auth            required        pam_unix.so             no_warn try_first_pass

# account
account         required        pam_nologin.so
account         required        pam_login_access.so
account         required        pam_unix.so

# session
session         required        pam_mkhomedir.so        mode=0700
session         required        pam_permit.so

# password
password        required        pam_unix.so             no_warn try_first_pass
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;mode=0700&lt;/code&gt; ensures home directories are created with restrictive&amp;nbsp;permissions.&lt;/p&gt;
&lt;p&gt;Before this works, verify&amp;nbsp;that &lt;code&gt;/home&lt;/code&gt; exists as a symlink&amp;nbsp;to &lt;code&gt;/usr/home&lt;/code&gt; (the FreeBSD&amp;nbsp;convention):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;/home
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If &lt;code&gt;/home&lt;/code&gt; doesn&amp;#8217;t exist or isn&amp;#8217;t a&amp;nbsp;symlink:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/usr/home&lt;span class="w"&gt; &lt;/span&gt;/home
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="sudo-configuration"&gt;Sudo&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Instead of managing local group membership, leverage FreeIPA&amp;#8217;s groups directly. Members of the &lt;span class="caps"&gt;IPA&lt;/span&gt; &lt;code&gt;admins&lt;/code&gt; group get sudo access without any local configuration per&amp;nbsp;user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/sudoers&lt;/strong&gt; (or&amp;nbsp;use &lt;code&gt;visudo&lt;/code&gt;)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Allow members of the LDAP &amp;#39;admins&amp;#39; group full sudo access
%admins ALL=(ALL:ALL) ALL
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;%&lt;/code&gt; prefix tells sudo to&amp;nbsp;treat &lt;code&gt;admins&lt;/code&gt; as a group name.&amp;nbsp;Since &lt;code&gt;nslcd&lt;/code&gt; handles group lookups, this transparently queries&amp;nbsp;FreeIPA.&lt;/p&gt;
&lt;p&gt;For more granular control, you can define multiple group-based&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;%admins ALL=(ALL:ALL) ALL
%developers ALL=(ALL) /usr/local/bin/docker, /usr/local/bin/podman
%dba ALL=(postgres) ALL
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;p&gt;With everything configured, verify each&amp;nbsp;component:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Identity&amp;nbsp;Lookups&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;id&lt;span class="w"&gt; &lt;/span&gt;admin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Expected output shows UIDs and groups from &lt;span class="caps"&gt;IPA&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uid=1000(admin) gid=1000(admin) groups=1000(admin),5000(admins)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. Keytab&amp;nbsp;Status&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;klist&lt;span class="w"&gt; &lt;/span&gt;-k
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Should list the host&amp;nbsp;principal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;span class="caps"&gt;SSH&lt;/span&gt; Login&amp;nbsp;Test&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;From another machine with a valid Kerberos&amp;nbsp;ticket:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;kinit&lt;span class="w"&gt; &lt;/span&gt;someuser@EXAMPLE.COM
ssh&lt;span class="w"&gt; &lt;/span&gt;bsdhost.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The login should succeed without a password prompt. After&amp;nbsp;login:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Should&amp;nbsp;show &lt;code&gt;/home/someuser&lt;/code&gt; (created automatically&amp;nbsp;by &lt;code&gt;pam_mkhomedir&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Sudo&amp;nbsp;Test&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the user is in&amp;nbsp;the &lt;code&gt;admins&lt;/code&gt; group:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;whoami
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Should&amp;nbsp;return &lt;code&gt;root&lt;/code&gt; without asking for a password&amp;nbsp;(assuming &lt;code&gt;NOPASSWD&lt;/code&gt; is configured) or after entering the user&amp;#8217;s Kerberos&amp;nbsp;password.&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;nslcd fails to start or lookups&amp;nbsp;fail&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Check the keytab&amp;nbsp;permissions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;/etc/krb5.keytab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;nslcd&lt;/code&gt; user (or group) must have read access. Also verify the keytab contains valid&amp;nbsp;keys:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;klist&lt;span class="w"&gt; &lt;/span&gt;-k&lt;span class="w"&gt; &lt;/span&gt;/etc/krb5.keytab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; rejects &lt;span class="caps"&gt;GSSAPI&lt;/span&gt;&amp;nbsp;authentication&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ensure the hostname matches the keytab principal.&amp;nbsp;Check &lt;code&gt;/var/log/auth.log&lt;/code&gt; for details. Common&amp;nbsp;issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connecting via &lt;span class="caps"&gt;IP&lt;/span&gt; when the keytab has only the &lt;span class="caps"&gt;FQDN&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Clock skew between the FreeBSD host and &lt;span class="caps"&gt;KDC&lt;/span&gt; (Kerberos is sensitive to time&amp;nbsp;differences)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Users can authenticate but have no home&amp;nbsp;directory&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Verify &lt;code&gt;pam_mkhomedir&lt;/code&gt; is&amp;nbsp;in &lt;code&gt;/etc/pam.d/sshd&lt;/code&gt; and&amp;nbsp;that &lt;code&gt;/home&lt;/code&gt; is a symlink&amp;nbsp;to &lt;code&gt;/usr/home&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Group membership not&amp;nbsp;visible&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This usually&amp;nbsp;indicates &lt;code&gt;nslcd&lt;/code&gt; isn&amp;#8217;t querying group information correctly. Verify&amp;nbsp;the &lt;code&gt;base&lt;/code&gt; &lt;span class="caps"&gt;DN&lt;/span&gt;&amp;nbsp;in &lt;code&gt;nslcd.conf&lt;/code&gt; includes the container where groups are stored. For &lt;span class="caps"&gt;IPA&lt;/span&gt;, this is&amp;nbsp;typically &lt;code&gt;cn=groups,cn=accounts,dc=example,dc=com&lt;/code&gt;, but using&amp;nbsp;just &lt;code&gt;dc=example,dc=com&lt;/code&gt; as the base should work with subtree&amp;nbsp;searches.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Integrating FreeBSD with FreeIPA doesn&amp;#8217;t require complex third-party tools or Linux-specific software. The combination of native&amp;nbsp;Kerberos, &lt;code&gt;nslcd&lt;/code&gt; for &lt;span class="caps"&gt;LDAP&lt;/span&gt;, and standard &lt;span class="caps"&gt;PAM&lt;/span&gt; modules provides a clean, maintainable&amp;nbsp;solution.&lt;/p&gt;
&lt;p&gt;The key advantages of this&amp;nbsp;approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No local user management&lt;/strong&gt; - users exist only in &lt;span class="caps"&gt;IPA&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;True &lt;span class="caps"&gt;SSO&lt;/span&gt;&lt;/strong&gt; - Kerberos tickets authenticate &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;sessions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal attack surface&lt;/strong&gt; - no &lt;span class="caps"&gt;SSSD&lt;/span&gt;, no realm daemon, no Python&amp;nbsp;dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateless configuration&lt;/strong&gt; - rebuild the host without losing identity&amp;nbsp;integration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This pattern scales well across a fleet of FreeBSD hosts. Once you&amp;#8217;ve configured one system, the configuration files can be templated and deployed with Ansible or similar tools. The only per-host customization is the keytab, which &lt;span class="caps"&gt;IPA&lt;/span&gt; generates&amp;nbsp;automatically.&lt;/p&gt;
&lt;p&gt;For environments already invested in FreeIPA for Linux hosts, adding FreeBSD becomes trivial - just another host entry in the directory, with the same users, groups, and policies applying uniformly across the&amp;nbsp;infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://freeipa.readthedocs.io/"&gt;FreeIPA&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/security/#kerberos5"&gt;FreeBSD Handbook:&amp;nbsp;Kerberos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arthurdejong.org/nss-pam-ldapd/"&gt;nss-pam-ldapd&amp;nbsp;Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.mit.edu/kerberos/krb5-latest/doc/"&gt;&lt;span class="caps"&gt;MIT&lt;/span&gt; Kerberos&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=pam.conf"&gt;&lt;span class="caps"&gt;PAM&lt;/span&gt; Configuration (FreeBSD Man&amp;nbsp;Page)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="freeipa"/><category term="kerberos"/><category term="ldap"/><category term="authentication"/><category term="security"/></entry><entry><title>Reviving Life is Strange: Before the Storm on Modern Linux with a glibc Shim</title><link href="https://blog.hofstede.it/reviving-life-is-strange-before-the-storm-on-modern-linux-with-a-glibc-shim/" rel="alternate"/><published>2026-01-25T00:00:00+01:00</published><updated>2026-01-25T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-25:/reviving-life-is-strange-before-the-storm-on-modern-linux-with-a-glibc-shim/</id><summary type="html">&lt;p&gt;Fixing Life is Strange Before the Storm on modern Linux distributions by compiling a small shim library that restores deprecated glibc internal&amp;nbsp;functions.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Life is Strange: Before the Storm shipped with native Linux support back in 2017. That was a different era - glibc 2.26 was current, and some developers made the unfortunate choice of linking against internal, undocumented glibc symbols. Fast forward to 2026, and the game refuses to start on modern distributions like Fedora 43. The symbols it depends on no longer&amp;nbsp;exist.&lt;/p&gt;
&lt;p&gt;The fix is a small shim library that brings back the missing functions. After implementing it, I played through the entire game without&amp;nbsp;issues.&lt;/p&gt;
&lt;p&gt;&lt;img alt="LiS-BTS" src="https://blog.hofstede.it/images/2026-01-25-life-is-strange-before-the-storm-glibc-fix.png" title="Lige is Strange: Before the Storm on Linux"&gt;&lt;/p&gt;
&lt;h2 id="the-problem-missing-glibc-internals"&gt;The Problem: Missing glibc&amp;nbsp;Internals&lt;/h2&gt;
&lt;p&gt;When you try to launch the game on a modern system, it fails silently or crashes immediately. Running it from the terminal reveals the&amp;nbsp;culprit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;./LifeIsStrange: symbol lookup error: ./LifeIsStrange: undefined symbol: __libc_dlopen_mode, version GLIBC_PRIVATE
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The game&amp;#8217;s binary was linked&amp;nbsp;against &lt;code&gt;__libc_dlopen_mode&lt;/code&gt; and &lt;code&gt;__libc_dlsym&lt;/code&gt; - internal glibc functions that were never part of the public &lt;span class="caps"&gt;API&lt;/span&gt;. These symbols lived in a special version namespace&amp;nbsp;called &lt;code&gt;GLIBC_PRIVATE&lt;/code&gt;, which is exactly what it sounds like: private implementation details that upstream reserves the right to change at&amp;nbsp;will.&lt;/p&gt;
&lt;p&gt;In glibc 2.34, the dynamic loader was merged into libc proper, and these internal symbols were restructured. The functions still exist internally, but they&amp;#8217;re no longer exported with the same symbol names and versions that older binaries&amp;nbsp;expect.&lt;/p&gt;
&lt;h2 id="the-solution-a-compatibility-shim"&gt;The Solution: A Compatibility&amp;nbsp;Shim&lt;/h2&gt;
&lt;p&gt;Since the internal functions are gone, we provide replacements. The game doesn&amp;#8217;t actually need the internal implementation - it just needs &lt;em&gt;something&lt;/em&gt; that loads shared libraries. The standard &lt;span class="caps"&gt;POSIX&lt;/span&gt;&amp;nbsp;functions &lt;code&gt;dlopen&lt;/code&gt; and &lt;code&gt;dlsym&lt;/code&gt; do exactly&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;The trick is making our replacement functions appear with the exact symbol names and versions the game expects. &lt;span class="caps"&gt;GCC&lt;/span&gt;&amp;#8217;s &lt;code&gt;__symver__&lt;/code&gt; attribute handles&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;dlfcn.h&amp;gt;&lt;/span&gt;

&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;__symver__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;__libc_dlopen_mode@GLIBC_PRIVATE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;libc_dlopen_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;__symver__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;__libc_dlsym@GLIBC_PRIVATE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;libc_dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kr"&gt;restrict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kr"&gt;restrict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;__symver__&lt;/code&gt; attribute tells the linker to export these functions under specific versioned symbol names. When the game&amp;#8217;s dynamic linker searches&amp;nbsp;for &lt;code&gt;__libc_dlopen_mode@GLIBC_PRIVATE&lt;/code&gt;, it finds our shim instead of the missing glibc&amp;nbsp;symbol.&lt;/p&gt;
&lt;h2 id="building-the-shim-library"&gt;Building the Shim&amp;nbsp;Library&lt;/h2&gt;
&lt;p&gt;Create a working directory and add three&amp;nbsp;files:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;libc_dlopen_mode.c:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;dlfcn.h&amp;gt;&lt;/span&gt;

&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;__symver__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;__libc_dlopen_mode@GLIBC_PRIVATE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;libc_dlopen_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;__symver__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;__libc_dlsym@GLIBC_PRIVATE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;libc_dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kr"&gt;restrict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kr"&gt;restrict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;version.map:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;GLIBC_PRIVATE {
global:
    *;
};
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The version script defines our&amp;nbsp;custom &lt;code&gt;GLIBC_PRIVATE&lt;/code&gt; version tag. Without it, the linker wouldn&amp;#8217;t know how to apply the version specified&amp;nbsp;in &lt;code&gt;__symver__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Makefile:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt;

&lt;span class="nv"&gt;RES_LIB_NAME&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;liblibc_dlopen_mode
&lt;span class="nv"&gt;RES_LIB&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;RES_LIB_NAME&lt;span class="k"&gt;)&lt;/span&gt;.so
&lt;span class="nv"&gt;SRCS&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;libc_dlopen_mode.c

&lt;span class="nv"&gt;CFLAGS&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-fpic&lt;span class="w"&gt; &lt;/span&gt;-shared&lt;span class="w"&gt; &lt;/span&gt;-flto&lt;span class="w"&gt; &lt;/span&gt;-Wl,--version-script&lt;span class="w"&gt; &lt;/span&gt;-Wl,version.map&lt;span class="w"&gt; &lt;/span&gt;-Wl,--as-needed

&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;RES_LIB&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;$(RES_LIB)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;SRCS&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;.&lt;span class="n"&gt;map&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;CC&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;CFLAGS&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;SRCS&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;RM&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;RES_LIB&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Build and&amp;nbsp;install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;make
sudo&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;liblibc_dlopen_mode.so&lt;span class="w"&gt; &lt;/span&gt;/usr/local/lib/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can verify the symbols are correctly&amp;nbsp;versioned:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;nm&lt;span class="w"&gt; &lt;/span&gt;-D&lt;span class="w"&gt; &lt;/span&gt;/usr/local/lib/liblibc_dlopen_mode.so
&lt;span class="w"&gt;                 &lt;/span&gt;w&lt;span class="w"&gt; &lt;/span&gt;__cxa_finalize@GLIBC_2.2.5
&lt;span class="w"&gt;                 &lt;/span&gt;U&lt;span class="w"&gt; &lt;/span&gt;dlopen@GLIBC_2.34
&lt;span class="w"&gt;                 &lt;/span&gt;U&lt;span class="w"&gt; &lt;/span&gt;dlsym@GLIBC_2.34
&lt;span class="m"&gt;0000000000001100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;T&lt;span class="w"&gt; &lt;/span&gt;libc_dlopen_mode@GLIBC_PRIVATE
&lt;span class="m"&gt;0000000000001120&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;T&lt;span class="w"&gt; &lt;/span&gt;libc_dlsym@GLIBC_PRIVATE
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;T&lt;/code&gt; entries show our exported functions with the&amp;nbsp;correct &lt;code&gt;GLIBC_PRIVATE&lt;/code&gt; version&amp;nbsp;tag.&lt;/p&gt;
&lt;h2 id="configuring-steam"&gt;Configuring&amp;nbsp;Steam&lt;/h2&gt;
&lt;p&gt;Open Steam, right-click on Life is Strange: Before the Storm, and select Properties. In the Launch Options field,&amp;nbsp;add:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;LD_PRELOAD=/usr/local/lib/liblibc_dlopen_mode.so %command%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you&amp;#8217;re using GameMode for performance&amp;nbsp;optimization:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;LD_PRELOAD=/usr/local/lib/liblibc_dlopen_mode.so gamemoderun %command%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;LD_PRELOAD&lt;/code&gt; environment variable tells the dynamic linker to load our shim library before any others. When the game binary starts and&amp;nbsp;requests &lt;code&gt;__libc_dlopen_mode@GLIBC_PRIVATE&lt;/code&gt;, the linker finds it in our preloaded library instead of failing with an undefined symbol&amp;nbsp;error.&lt;/p&gt;
&lt;h2 id="why-this-works"&gt;Why This&amp;nbsp;Works&lt;/h2&gt;
&lt;p&gt;The game uses these internal functions to dynamically load additional libraries at runtime. The actual implementation details don&amp;#8217;t matter to the game - it just needs &lt;em&gt;a&lt;/em&gt; function that accepts a filename and flags, calls the kernel to map a shared object, and returns a handle. The&amp;nbsp;public &lt;code&gt;dlopen&lt;/code&gt; and &lt;code&gt;dlsym&lt;/code&gt; functions do exactly&amp;nbsp;this.&lt;/p&gt;
&lt;p&gt;Our shim acts as a translation layer: the game calls what it thinks are internal glibc functions, and we redirect those calls to the stable public &lt;span class="caps"&gt;API&lt;/span&gt;. The game can&amp;#8217;t tell the&amp;nbsp;difference.&lt;/p&gt;
&lt;h2 id="other-affected-games"&gt;Other Affected&amp;nbsp;Games&lt;/h2&gt;
&lt;p&gt;Life is Strange: Before the Storm isn&amp;#8217;t alone. Several games from the 2015-2018 era made the same mistake of depending on glibc internals. If you encounter similar &amp;#8220;undefined symbol&amp;#8221; errors&amp;nbsp;mentioning &lt;code&gt;GLIBC_PRIVATE&lt;/code&gt;, this same technique should work - just verify which specific symbols are missing and add corresponding wrapper&amp;nbsp;functions.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;p&gt;A few things to keep in&amp;nbsp;mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;This is a workaround, not a proper fix.&lt;/strong&gt; The correct solution would be for the game developers to rebuild against the public &lt;span class="caps"&gt;API&lt;/span&gt;. Unfortunately, many older Linux ports are&amp;nbsp;abandoned.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The shim intercepts calls globally&lt;/strong&gt; within the game process. This is fine for single-player games but be mindful when&amp;nbsp;using &lt;code&gt;LD_PRELOAD&lt;/code&gt; with any software that handles sensitive&amp;nbsp;data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Future glibc changes&lt;/strong&gt; could theoretically break even the&amp;nbsp;public &lt;code&gt;dlopen&lt;/code&gt;/&lt;code&gt;dlsym&lt;/code&gt; &lt;span class="caps"&gt;API&lt;/span&gt;, though this is extremely unlikely given &lt;span class="caps"&gt;POSIX&lt;/span&gt; compatibility&amp;nbsp;requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;A few lines of C and a Steam launch option are all it takes to revive this abandoned Linux port. The game runs flawlessly - I completed all three episodes and the bonus Farewell episode without a single&amp;nbsp;crash.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s a shame that native Linux ports from this era are becoming unplayable due to dependency rot, but at least the fix is straightforward. Sometimes the best compatibility layer is just a thin wrapper that pretends to be something it&amp;nbsp;isn&amp;#8217;t.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sourceware.org/pipermail/libc-alpha/2021-August/129718.html"&gt;glibc 2.34 Release Notes&lt;/a&gt; - Details on the dynamic loader&amp;nbsp;merge&lt;/li&gt;
&lt;li&gt;&lt;a href="https://store.steampowered.com/app/554620/Life_is_Strange_Before_the_Storm/"&gt;Life is Strange: Before the Storm on&amp;nbsp;Steam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html"&gt;&lt;span class="caps"&gt;GCC&lt;/span&gt; Symbol Versioning&lt;/a&gt; - Documentation&amp;nbsp;for &lt;code&gt;__symver__&lt;/code&gt; attribute&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/ld.so.8.html"&gt;LD_PRELOAD Trick&lt;/a&gt; - Dynamic linker&amp;nbsp;documentation&lt;/li&gt;
&lt;/ul&gt;</content><category term="Linux"/><category term="linux"/><category term="gaming"/><category term="steam"/><category term="fedora"/><category term="glibc"/><category term="linuxgaming"/></entry><entry><title>Self-Hosted CryptPad on FreeBSD with VNET Jails and Caddy</title><link href="https://blog.hofstede.it/self-hosted-cryptpad-on-freebsd-with-vnet-jails-and-caddy/" rel="alternate"/><published>2026-01-24T00:00:00+01:00</published><updated>2026-01-24T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-24:/self-hosted-cryptpad-on-freebsd-with-vnet-jails-and-caddy/</id><summary type="html">&lt;p&gt;Running CryptPad in a FreeBSD &lt;span class="caps"&gt;VNET&lt;/span&gt; jail with isolated networking, &lt;span class="caps"&gt;NAT&lt;/span&gt; via &lt;span class="caps"&gt;PF&lt;/span&gt;, and Caddy for &lt;span class="caps"&gt;TLS&lt;/span&gt;&amp;nbsp;termination.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;CryptPad is an end-to-end encrypted collaboration suite. Think Google Docs, but where the server never sees your content. It&amp;#8217;s a compelling option for privacy-conscious teams or anyone wanting to own their data. This post documents installing CryptPad in a FreeBSD &lt;span class="caps"&gt;VNET&lt;/span&gt; jail, served behind a Caddy reverse proxy, with network isolation enforced by &lt;span class="caps"&gt;PF&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;This guide assumes familiarity with FreeBSD jails, &lt;span class="caps"&gt;PF&lt;/span&gt;, and BastilleBSD. It focuses on architecture and pitfalls rather than introductory jail&amp;nbsp;setup.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Cryptpad" src="https://blog.hofstede.it/images/2026-01-24-cryptpad-freebsd-jail-caddy.png" title="Screenshot of Document in Cryptpad"&gt;&lt;/p&gt;
&lt;h2 id="why-cryptpad"&gt;Why&amp;nbsp;CryptPad?&lt;/h2&gt;
&lt;p&gt;Most collaborative document platforms require trusting the provider with your data. CryptPad takes a different approach: all encryption happens client-side in the browser. The server stores only encrypted blobs and has no way to decrypt your documents, even if compelled&amp;nbsp;to.&lt;/p&gt;
&lt;p&gt;Self-hosting adds another layer: your data never leaves infrastructure you control. Combined with FreeBSD&amp;#8217;s &lt;span class="caps"&gt;VNET&lt;/span&gt; jails for network isolation, this creates a robust privacy-respecting setup. Unlike tools such as Nextcloud or OnlyOffice, CryptPad’s threat model assumes the server itself is&amp;nbsp;untrusted.&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[ Internet ]
     |
     v
+-----------------------+
|    PF Firewall        |
|    NAT + RDR          |
+-----------+-----------+
            |
     +------+------+
     | bastille0   |  (bridge interface)
     | 10.254.254.1|
     +------+------+
            |
   +--------+--------+
   |                 |
   v                 v
+-------+      +-----------+
| Caddy |      | CryptPad  |
| Jail  |      |   Jail    |
| .10   |-----&amp;gt;|   .36     |
+-------+      +-----------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key security principle: jails live on a private &lt;span class="caps"&gt;RFC1918&lt;/span&gt; network&amp;nbsp;(&lt;code&gt;10.254.254.0/24&lt;/code&gt;) that is completely invisible to the internet. &lt;span class="caps"&gt;PF&lt;/span&gt; on the host performs &lt;span class="caps"&gt;NAT&lt;/span&gt; for outbound traffic and only redirects specific ports to specific jails. The CryptPad jail has no direct internet exposure - all traffic flows through the Caddy&amp;nbsp;jail.&lt;/p&gt;
&lt;h2 id="vnet-jail-networking"&gt;&lt;span class="caps"&gt;VNET&lt;/span&gt; Jail&amp;nbsp;Networking&lt;/h2&gt;
&lt;p&gt;This setup uses &lt;span class="caps"&gt;VNET&lt;/span&gt; jails, which give each jail its own complete network stack including interfaces, routing tables, and firewall state. Unlike traditional &lt;span class="caps"&gt;IP&lt;/span&gt;-based jails that share the host&amp;#8217;s network stack, &lt;span class="caps"&gt;VNET&lt;/span&gt; jails behave like independent networked&amp;nbsp;machines.&lt;/p&gt;
&lt;p&gt;The jails attach to a bridge interface on the&amp;nbsp;host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Host /etc/rc.conf (relevant networking section)&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.254.254.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The host acts as the default gateway for all jails. Each jail gets an &lt;span class="caps"&gt;IP&lt;/span&gt; from&amp;nbsp;the &lt;code&gt;10.254.254.0/24&lt;/code&gt; range:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ bastille list
 JID  Name           State  IP Address
 2    caddy          Up     10.254.254.10
 10   cryptpad       Up     10.254.254.36
 ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Creating the CryptPad jail with&amp;nbsp;Bastille:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;cryptpad&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;.0-RELEASE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.254.36&lt;span class="w"&gt; &lt;/span&gt;bastille0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-B&lt;/code&gt; flag creates a &lt;span class="caps"&gt;VNET&lt;/span&gt; jail attached to&amp;nbsp;the &lt;code&gt;bastille0&lt;/code&gt; bridge.&lt;/p&gt;
&lt;h2 id="pf-firewall-nat-and-selective-exposure"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; Firewall: &lt;span class="caps"&gt;NAT&lt;/span&gt; and Selective&amp;nbsp;Exposure&lt;/h2&gt;
&lt;p&gt;The critical security layer is &lt;span class="caps"&gt;PF&lt;/span&gt; on the host. It&amp;nbsp;provides:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt; for outbound traffic&lt;/strong&gt; - jails can reach the internet (for package updates, etc.) but appear to come from the host&amp;#8217;s public &lt;span class="caps"&gt;IP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port redirection&lt;/strong&gt; - only ports 80/443 are exposed, and only to the Caddy&amp;nbsp;jail&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default deny&lt;/strong&gt; - everything else is silently&amp;nbsp;dropped&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# /etc/pf.conf
ext_if = &amp;quot;vtnet0&amp;quot;
jail_net = &amp;quot;10.254.254.0/24&amp;quot;
frontend_v4 = &amp;quot;10.254.254.10&amp;quot;  # Caddy jail

table &amp;lt;jails_v4&amp;gt; { $jail_net }

set skip on lo0
set block-policy drop

# NAT: Jails -&amp;gt; Internet
nat on $ext_if inet from &amp;lt;jails_v4&amp;gt; to any -&amp;gt; ($ext_if)

# RDR: Internet -&amp;gt; Caddy jail only
rdr pass on $ext_if inet proto tcp to ($ext_if) port {80,443} -&amp;gt; $frontend_v4

# Default deny
block drop in log all
block drop out log all

# Allow established connections out
pass out quick all keep state

# Anti-spoofing
antispoof quick for { $ext_if, bastille0 }

# ICMP
pass in inet proto icmp icmp-type { echoreq, unreach }

# Jail egress (but not to other jails)
pass in quick on bastille0 from &amp;lt;jails_v4&amp;gt; to ! 10.254.254.0/24 keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice what&amp;#8217;s &lt;strong&gt;not&lt;/strong&gt; here: there&amp;#8217;s no rule allowing direct internet access to the CryptPad jail. Traffic to ports 80/443 is redirected&amp;nbsp;to &lt;code&gt;10.254.254.10&lt;/code&gt; (Caddy), which then proxies to CryptPad internally over the bridge network. The CryptPad jail is completely hidden from the&amp;nbsp;internet.&lt;/p&gt;
&lt;p&gt;This is defense in depth: even if CryptPad had a vulnerability, an attacker couldn&amp;#8217;t establish a reverse shell or exfiltrate data directly - they&amp;#8217;d have to go through Caddy&amp;nbsp;first.&lt;/p&gt;
&lt;h2 id="cryptpad-installation"&gt;CryptPad&amp;nbsp;Installation&lt;/h2&gt;
&lt;p&gt;Two domains are required: one for the main application and one for the sandbox (CryptPad&amp;#8217;s &lt;span class="caps"&gt;XSS&lt;/span&gt; protection mechanism). Both domains resolve to the host&amp;#8217;s public &lt;span class="caps"&gt;IP&lt;/span&gt;, where &lt;span class="caps"&gt;PF&lt;/span&gt; redirects them to&amp;nbsp;Caddy.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Inside the CryptPad jail, install the necessary packages. CryptPad requires Node.js 20+ and build tools for native&amp;nbsp;modules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;node24&lt;span class="w"&gt; &lt;/span&gt;npm-node24&lt;span class="w"&gt; &lt;/span&gt;gmake&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The full package list for a working installation looks like&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;brotli-1.2.0,1          git-tiny-2.52.0         node24-24.12.0
c-ares-1.34.6           gmake-4.4.1             npm-node24-11.7.0
corepack-0.34.5         icu-76.1,1              python311-3.11.14
libuv-1.51.0            llhttp-9.3.0            simdjson-4.2.4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="user-and-installation"&gt;User and&amp;nbsp;Installation&lt;/h2&gt;
&lt;p&gt;Never run CryptPad as root. Create a dedicated&amp;nbsp;user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pw&lt;span class="w"&gt; &lt;/span&gt;useradd&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;cryptpad&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;CryptPad User&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;/usr/local/cryptpad&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/bin/sh&lt;span class="w"&gt; &lt;/span&gt;-m
su&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;cryptpad
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/cryptpad/cryptpad.git&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="the-critical-installation-step"&gt;The Critical Installation&amp;nbsp;Step&lt;/h3&gt;
&lt;p&gt;This is where many installations fail. Running&amp;nbsp;only &lt;code&gt;npm install&lt;/code&gt; will leave you with a site that hangs on &amp;#8220;Loading&amp;#8230;&amp;#8221; and throws 404 errors&amp;nbsp;for &lt;code&gt;require.js&lt;/code&gt;. CryptPad needs three distinct build&amp;nbsp;steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Install backend dependencies&lt;/span&gt;
npm&lt;span class="w"&gt; &lt;/span&gt;install

&lt;span class="c1"&gt;# 2. Install frontend components (bower, bootstrap, require.js, etc.)&lt;/span&gt;
npm&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;install:components

&lt;span class="c1"&gt;# 3. Build static assets (pages, CSS, etc.)&lt;/span&gt;
npm&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The second step is easily missed because it&amp;#8217;s not obvious from the documentation. If you skip it, the application will appear to start but the browser will never finish&amp;nbsp;loading.&lt;/p&gt;
&lt;h2 id="configuration"&gt;Configuration&lt;/h2&gt;
&lt;p&gt;Copy the example configuration and edit&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp&lt;span class="w"&gt; &lt;/span&gt;config/config.example.js&lt;span class="w"&gt; &lt;/span&gt;config/config.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The critical settings&amp;nbsp;in &lt;code&gt;config/config.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Listen on the jail&amp;#39;s interface (not localhost)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;httpAddress&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;10.254.254.36&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Main domain - where users access CryptPad&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;httpUnsafeOrigin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://pad.example.com&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Sandbox domain - MUST be different from main domain&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;httpSafeOrigin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://sandbox.example.com&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Admin keys (add after registering your admin account)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;adminKeys&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// &amp;quot;[your-public-key-here]&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="understanding-the-two-domains"&gt;Understanding the Two&amp;nbsp;Domains&lt;/h3&gt;
&lt;p&gt;CryptPad&amp;#8217;s security model relies on browser same-origin policy. The main domain handles authentication and cryptographic operations. The sandbox domain loads the &lt;span class="caps"&gt;UI&lt;/span&gt; in an iframe with a different origin, preventing &lt;span class="caps"&gt;XSS&lt;/span&gt; attacks from accessing your encryption&amp;nbsp;keys.&lt;/p&gt;
&lt;p&gt;Both domains point to the same CryptPad instance, the application routes requests appropriately based on the Host header. This is why correctly passing the Host header through the reverse proxy is&amp;nbsp;essential.&lt;/p&gt;
&lt;h2 id="freebsd-service-setup"&gt;FreeBSD Service&amp;nbsp;Setup&lt;/h2&gt;
&lt;p&gt;CryptPad includes an rc.d script template. Copy it and enable the&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp&lt;span class="w"&gt; &lt;/span&gt;/usr/local/cryptpad/docs/rc.d-cryptpad&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/rc.d/cryptpad
chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/rc.d/cryptpad
sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cryptpad_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here&amp;#8217;s the actual rc.d script adapted for&amp;nbsp;FreeBSD:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c1"&gt;# PROVIDE: cryptpad&lt;/span&gt;
&lt;span class="c1"&gt;# REQUIRE: DAEMON&lt;/span&gt;
&lt;span class="c1"&gt;# KEYWORD: shutdown&lt;/span&gt;

.&lt;span class="w"&gt; &lt;/span&gt;/etc/rc.subr

&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cryptpad&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;start_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;start&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;stop_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stop&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rcvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cryptpad_enable

&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/run/cryptpad/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pid&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;CryptPad Service&amp;quot;&lt;/span&gt;

load_rc_config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

start&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/bin/mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/var/run/cryptpad
&lt;span class="w"&gt;    &lt;/span&gt;/usr/sbin/chown&lt;span class="w"&gt; &lt;/span&gt;cryptpad:cryptpad&lt;span class="w"&gt; &lt;/span&gt;/var/run/cryptpad

&lt;span class="w"&gt;    &lt;/span&gt;/usr/bin/su&lt;span class="w"&gt; &lt;/span&gt;cryptpad&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:~/bin &amp;amp;&amp;amp; \&lt;/span&gt;
&lt;span class="s2"&gt;        cd /usr/local/cryptpad &amp;amp;&amp;amp; \&lt;/span&gt;
&lt;span class="s2"&gt;        /usr/sbin/daemon -T &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; \&lt;/span&gt;
&lt;span class="s2"&gt;            -P /var/run/cryptpad/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_supervisor.pid \&lt;/span&gt;
&lt;span class="s2"&gt;            -p /var/run/cryptpad/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pid \&lt;/span&gt;
&lt;span class="s2"&gt;            -f -S -r /usr/local/bin/node server&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

stop&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/bin/kill&lt;span class="w"&gt; &lt;/span&gt;-9&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/var/run/cryptpad/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;_supervisor.pid&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/bin/kill&lt;span class="w"&gt; &lt;/span&gt;-15&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/var/run/cryptpad/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.pid&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_rc_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Start the&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;service&lt;span class="w"&gt; &lt;/span&gt;cryptpad&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A running CryptPad instance spawns multiple worker&amp;nbsp;processes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;USER       PID %CPU %MEM    VSZ   RSS TT  STAT COMMAND
cryptpad 89849  0.0  0.0  13068  2508  -  IsJ  daemon: /usr/local/bin/node[89850]
cryptpad 89850  0.0  1.1 839584 87964  -  SJ   /usr/local/bin/node server
cryptpad 89851  0.0  1.1 840864 88624  -  SJ   /usr/local/bin/node ./lib/http-worker.js
cryptpad 89852  0.0  1.1 842400 88000  -  SJ   /usr/local/bin/node ./lib/http-worker.js
...
cryptpad 89857  0.0  1.0 834088 79448  -  SJ   /usr/local/bin/node lib/workers/db-worker
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="caddy-reverse-proxy"&gt;Caddy Reverse&amp;nbsp;Proxy&lt;/h2&gt;
&lt;p&gt;Caddy runs in its own jail&amp;nbsp;(&lt;code&gt;10.254.254.10&lt;/code&gt;) and is the only jail exposed to the internet via &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s port redirection. Configure it to proxy both CryptPad domains to the application jail. The Host header passthrough is crucial! Without it, CryptPad&amp;#8217;s WebSocket security checks will&amp;nbsp;fail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pad.example.com {
    reverse_proxy 10.254.254.36:3000 {
        header_up Host {host}
        header_up X-Real-IP {remote}
    }

    header {
        Strict-Transport-Security &amp;quot;max-age=31536000; includeSubdomains&amp;quot;
        X-XSS-Protection &amp;quot;1; mode=block&amp;quot;
        X-Content-Type-Options &amp;quot;nosniff&amp;quot;
        X-Frame-Options &amp;quot;SAMEORIGIN&amp;quot;
        Referrer-Policy &amp;quot;same-origin&amp;quot;
    }
}

sandbox.example.com {
    reverse_proxy 10.254.254.36:3000 {
        header_up Host {host}
        header_up X-Real-IP {remote}
    }

    header {
        Strict-Transport-Security &amp;quot;max-age=31536000; includeSubdomains&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that both domains proxy to the same backend&amp;nbsp;(&lt;code&gt;10.254.254.36:3000&lt;/code&gt;). Caddy handles &lt;span class="caps"&gt;TLS&lt;/span&gt; automatically via Let&amp;#8217;s Encrypt. Since the jails communicate over the private bridge network, this internal traffic doesn&amp;#8217;t need&amp;nbsp;encryption.&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;h3 id="site-hangs-on-loading"&gt;Site Hangs on&amp;nbsp;&amp;#8220;Loading&amp;#8230;&amp;#8221;&lt;/h3&gt;
&lt;p&gt;This is almost always caused by missing frontend components. Check if require.js&amp;nbsp;exists:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;/usr/local/cryptpad/www/components/requirejs/require.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If missing, you&amp;nbsp;skipped &lt;code&gt;npm run install:components&lt;/code&gt;. Fix&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;su&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;cryptpad
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/usr/local/cryptpad
npm&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;install:components
npm&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;build
&lt;span class="nb"&gt;exit&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;cryptpad&lt;span class="w"&gt; &lt;/span&gt;restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="websocket-connection-failures"&gt;WebSocket Connection&amp;nbsp;Failures&lt;/h3&gt;
&lt;p&gt;If you see WebSocket errors in the browser console,&amp;nbsp;verify:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Host header is being passed through your reverse&amp;nbsp;proxy&lt;/li&gt;
&lt;li&gt;Both domains are correctly configured&amp;nbsp;in &lt;code&gt;config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;httpAddress&lt;/code&gt; in config.js matches the jail&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="white-screen"&gt;White&amp;nbsp;Screen&lt;/h3&gt;
&lt;p&gt;Usually a sandbox domain misconfiguration. The main and sandbox domains must be different but both must resolve to your CryptPad instance. Check browser console for &lt;span class="caps"&gt;CORS&lt;/span&gt; or &lt;span class="caps"&gt;CSP&lt;/span&gt;&amp;nbsp;errors.&lt;/p&gt;
&lt;h2 id="adding-an-admin-account"&gt;Adding an Admin&amp;nbsp;Account&lt;/h2&gt;
&lt;p&gt;After the first successful&amp;nbsp;login:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Register a normal account through the web&amp;nbsp;interface&lt;/li&gt;
&lt;li&gt;Go to Settings and find your public signing&amp;nbsp;key&lt;/li&gt;
&lt;li&gt;Add it to&amp;nbsp;the &lt;code&gt;adminKeys&lt;/code&gt; array&amp;nbsp;in &lt;code&gt;config/config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Restart&amp;nbsp;CryptPad&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;adminKeys&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[cryptpad-admin@pad.example.com/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The admin panel then becomes available&amp;nbsp;at &lt;code&gt;https://pad.example.com/admin/&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;CryptPad in a FreeBSD &lt;span class="caps"&gt;VNET&lt;/span&gt; jail provides a solid foundation for privacy-respecting collaboration. The layered security model gives defense in&amp;nbsp;depth:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;VNET&lt;/span&gt; jails&lt;/strong&gt; provide complete network stack&amp;nbsp;isolation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; &lt;span class="caps"&gt;NAT&lt;/span&gt;&lt;/strong&gt; hides all jails behind &lt;span class="caps"&gt;RFC1918&lt;/span&gt;&amp;nbsp;addresses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selective port redirection&lt;/strong&gt; exposes only the Caddy&amp;nbsp;frontend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-end encryption&lt;/strong&gt; means even you as administrator cannot read&amp;nbsp;documents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main gotcha is the three-step build process - if your installation hangs on loading, that&amp;#8217;s almost certainly the culprit. Get that right, ensure both domains are configured, and you&amp;#8217;ll have a working instance that&amp;#8217;s invisible to port scanners and attackers probing for Node.js&amp;nbsp;vulnerabilities.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cryptpad.org/"&gt;CryptPad&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cryptpad/cryptpad"&gt;CryptPad GitHub&amp;nbsp;Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/"&gt;Caddy Web&amp;nbsp;Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook -&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bastillebsd.org/"&gt;BastilleBSD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openbsd.org/faq/pf/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; - The OpenBSD Packet&amp;nbsp;Filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="jails"/><category term="self-hosting"/><category term="privacy"/></entry><entry><title>Self-Hosting Email on FreeBSD: A Secure, Jailed Setup with Postfix and Dovecot</title><link href="https://blog.hofstede.it/self-hosting-email-on-freebsd-a-secure-jailed-setup-with-postfix-and-dovecot/" rel="alternate"/><published>2026-01-18T00:00:00+01:00</published><updated>2026-01-18T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-18:/self-hosting-email-on-freebsd-a-secure-jailed-setup-with-postfix-and-dovecot/</id><summary type="html">&lt;p&gt;A technical deep-dive into running a modern, secure mail server on FreeBSD 15.0 using &lt;span class="caps"&gt;VNET&lt;/span&gt; jails, &lt;span class="caps"&gt;ZFS&lt;/span&gt; encryption, and GeoIP-aware&amp;nbsp;firewalling.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Self-hosting email is often described as a Sisyphean task. A constant battle against spam lists, deliverability issues, and security threats. While there&amp;#8217;s truth to that reputation, running your own mail infrastructure remains one of the best ways to reclaim privacy and truly understand how the internet&amp;#8217;s oldest federation protocol&amp;nbsp;works.&lt;/p&gt;
&lt;p&gt;In this article, I&amp;#8217;ll walk through my current mail server setup running on FreeBSD 15.0. The design philosophy prioritizes security through isolation (&lt;span class="caps"&gt;VNET&lt;/span&gt; jails), data protection (&lt;span class="caps"&gt;ZFS&lt;/span&gt; encryption), and attack surface reduction (GeoIP filtering). This isn&amp;#8217;t a beginner&amp;#8217;s tutorial - I&amp;#8217;ll assume familiarity with FreeBSD basics and focus on the architecture decisions and configuration details that make this setup&amp;nbsp;work.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Mailserver setup" src="https://blog.hofstede.it/images/2026-01-18-mailserver-architecture.png" title="Architecture: VNET Jails for separation of concerns"&gt;&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;p&gt;Before diving into configuration files, here&amp;#8217;s how the pieces fit&amp;nbsp;together:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                        Internet
                            |
                            v
                +-----------------------+
                |     PF Firewall       |
                |  GeoIP + NAT + RDR    |
                +-----------+-----------+
                            |
            +---------------+---------------+
            |                               |
     Port 25 (SMTP)              Ports 143/587/4190
     Open to all                 GeoIP restricted
            |                               |
            v                               v
    +-------+-------------------------------+-------+
    |              VNET Jail: mailstack             |
    |                                               |
    |  +----------+  +----------+  +-----------+    |
    |  | Postfix  |  | Dovecot  |  |  Rspamd   |    |
    |  |   MTA    |  |IMAP/LMTP |  |  Filter   |    |
    |  +----+-----+  +----+-----+  +-----+-----+    |
    |       |             |              |          |
    |       +------+------+------+-------+          |
    |              |                                |
    |              v                                |
    |     +------------------+                      |
    |     | /var/vmail       |  (nullfs mount)      |
    |     | ZFS encrypted    |                      |
    |     +------------------+                      |
    +-----------------------------------------------+
                            |
            +---------------+---------------+
            |                               |
    +-------+-------+               +-------+-------+
    | VNET Jail:    |               |    Host:      |
    |   webmail     |               | zroot/secure  |
    | (Roundcube)   |               | (encrypted)   |
    +---------------+               +---------------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key insight here is separation of concerns: the host handles networking and storage, while the jails run the actual services. Mail data lives on an encrypted &lt;span class="caps"&gt;ZFS&lt;/span&gt; dataset on the host and is mounted into the jail via nullfs. This means I can destroy and recreate the jail without touching any mail data, and backups of the encrypted dataset can be sent offsite without the backup server ever possessing the decryption&amp;nbsp;key.&lt;/p&gt;
&lt;h2 id="the-host-system-freebsd-150"&gt;The Host System: FreeBSD&amp;nbsp;15.0&lt;/h2&gt;
&lt;p&gt;The foundation is a hardened FreeBSD 15.0 system. Security starts at the kernel&amp;nbsp;level.&lt;/p&gt;
&lt;h3 id="security-hardening"&gt;Security&amp;nbsp;Hardening&lt;/h3&gt;
&lt;p&gt;I run&amp;nbsp;with &lt;code&gt;kern_securelevel="2"&lt;/code&gt;, which is a significant step up from the default. At this level, even the root user cannot write to immutable files, load kernel modules, or modify the firewall rules without a reboot. This means even if an attacker gains root access, they can&amp;#8217;t disable &lt;span class="caps"&gt;PF&lt;/span&gt; or load a&amp;nbsp;rootkit.&lt;/p&gt;
&lt;p&gt;Additional hardening via sysctl prevents users from seeing other processes and restricts access to kernel&amp;nbsp;internals:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/sysctl.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Prevent users from seeing processes of other UIDs
security.bsd.see_other_uids=0
security.bsd.see_other_gids=0
security.bsd.see_jail_proc=0

&lt;span class="gh"&gt;#&lt;/span&gt; Disable unprivileged access to kernel internals
security.bsd.unprivileged_read_msgbuf=0
security.bsd.unprivileged_proc_debug=0

&lt;span class="gh"&gt;#&lt;/span&gt; Randomize PIDs to make race conditions harder to exploit
kern.randompid=1

&lt;span class="gh"&gt;#&lt;/span&gt; ZFS tuning
vfs.zfs.vdev.min_auto_ashift=12

&lt;span class="gh"&gt;#&lt;/span&gt; Allow large PF tables for GeoIP filtering
net.pf.request_maxcount=500000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The last setting is crucial for GeoIP filtering - the default &lt;span class="caps"&gt;PF&lt;/span&gt; table size is too small to hold the ~273,000 &lt;span class="caps"&gt;CIDR&lt;/span&gt; blocks we&amp;#8217;ll be&amp;nbsp;loading.&lt;/p&gt;
&lt;h3 id="kernel-module-loading"&gt;Kernel Module&amp;nbsp;Loading&lt;/h3&gt;
&lt;p&gt;The necessary modules for jail networking are loaded at&amp;nbsp;boot:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/boot/loader.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;kern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disk_ident&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;kern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gptid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;zfs_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Required for VNET jails&lt;/span&gt;
&lt;span class="n"&gt;nullfs_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;if_bridge_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;if_epair_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;kern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;racct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="network-and-zfs-layout"&gt;Network and &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;nbsp;Layout&lt;/h3&gt;
&lt;p&gt;The host handles the physical connection and bridges traffic to the jails. The network configuration uses a bridge interface that provides connectivity to all &lt;span class="caps"&gt;VNET&lt;/span&gt;&amp;nbsp;jails:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/rc.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mail&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;keymap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;us.kbd&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Security hardening&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# External Interface (VPS provider network)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 192.0.2.50/32 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1c1c:4d2::1/65&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;172.31.1.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Bridge for VNET Jails&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.0.0.1 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1c1c:4d2:8000::1/65&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Enable routing for jails&lt;/span&gt;
&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;dumpdev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;jail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pflog_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;clear_tmp_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;moused_nondefault_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;ZFS&lt;/span&gt; layout separates sensitive data onto an encrypted dataset. Here&amp;#8217;s the relevant&amp;nbsp;portion:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;NAME                          USED  AVAIL  REFER  MOUNTPOINT
zroot/secure                 4.28G  29.2G   200K  /zroot/secure
zroot/secure/jails           4.04G  29.2G   264K  /jails
zroot/secure/jails/mailstack 1.40G  29.2G  1.00G  /jails/mailstack
zroot/secure/jails/webmail   1022M  29.2G   805M  /jails/webmail
zroot/secure/vmail            248M  29.2G   233M  /var/mail
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The encryption uses &lt;span class="caps"&gt;AES&lt;/span&gt;-256-&lt;span class="caps"&gt;GCM&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;encryption&lt;span class="w"&gt; &lt;/span&gt;zroot/secure
NAME&lt;span class="w"&gt;          &lt;/span&gt;PROPERTY&lt;span class="w"&gt;    &lt;/span&gt;VALUE&lt;span class="w"&gt;        &lt;/span&gt;SOURCE
zroot/secure&lt;span class="w"&gt;  &lt;/span&gt;encryption&lt;span class="w"&gt;  &lt;/span&gt;aes-256-gcm&lt;span class="w"&gt;  &lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This means all child datasets (jails and mail storage) inherit the encryption. After a reboot, the dataset must be manually unlocked before the jails can start - a deliberate trade-off between availability and&amp;nbsp;security.&lt;/p&gt;
&lt;h2 id="the-firewall-pf-with-geoip-filtering"&gt;The Firewall: &lt;span class="caps"&gt;PF&lt;/span&gt; with GeoIP&amp;nbsp;Filtering&lt;/h2&gt;
&lt;p&gt;I use &lt;span class="caps"&gt;PF&lt;/span&gt; (Packet Filter) to ruthlessly cut down on attack surface. The most effective measure has been GeoIP filtering. Since my users are all in Central Europe, I can block authentication attempts from everywhere else while keeping &lt;span class="caps"&gt;SMTP&lt;/span&gt; open for mail&amp;nbsp;delivery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/pf.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Macros ---&lt;/span&gt;
&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.0.0.0/24&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::/65&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1c1c:4d2::1&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;mail_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.0.0.3&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;mail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::3&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;webmail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::4&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# --- Tables ---&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;geoip_users&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;

&lt;span class="c1"&gt;# --- Options ---&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scrub ---&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;

&lt;span class="c1"&gt;# --- NAT (IPv4 only for legacy support) ---&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- RDR for services ---&lt;/span&gt;
&lt;span class="c1"&gt;# Client ports: GeoIP restricted&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;geoip_users&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4190&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_ipv4&lt;/span&gt;

&lt;span class="c1"&gt;# SMTP: Open to all (server-to-server)&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_ipv4&lt;/span&gt;

&lt;span class="c1"&gt;# --- Filtering ---&lt;/span&gt;
&lt;span class="c1"&gt;# Block known bad actors immediately&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Default deny&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;

&lt;span class="c1"&gt;# Allow all outbound (stateful)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Anti-spoofing&lt;/span&gt;
&lt;span class="n"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bridge0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# SSH only from GeoIP restricted IP ranges&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;geoip_users&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Block and log all other SSH attempts&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh_not_trusted&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# IPv6: Mail client ports with GeoIP restriction&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;geoip_users&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4190&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# IPv6: SMTP open to all&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# IPv6: Webmail with GeoIP restriction&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;geoip_users&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;webmail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Essential ICMPv6&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ICMP for IPv4 (ping, MTU discovery)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Jail egress&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bridge0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bridge0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;jails_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;c1c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;d2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The dual approach to traffic handling is visible in the&amp;nbsp;rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;SMTP&lt;/span&gt; (port 25)&lt;/strong&gt;: &lt;code&gt;from any&lt;/code&gt; - must be globally accessible for mail&amp;nbsp;delivery&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client ports (143, 587, 4190)&lt;/strong&gt;: &lt;code&gt;from &amp;lt;geoip_users&amp;gt;&lt;/code&gt; - restricted to allowed&amp;nbsp;countries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I covered the GeoIP update script in detail in my &lt;a href="https://blog.hofstede.it/geoip-aware-firewalling-with-pf-on-freebsd/"&gt;previous article on GeoIP-aware firewalling&lt;/a&gt;. The short version: I parse MaxMind&amp;#8217;s GeoLite2 &lt;span class="caps"&gt;CSV&lt;/span&gt; data for my target countries (&lt;span class="caps"&gt;DE&lt;/span&gt;, &lt;span class="caps"&gt;AT&lt;/span&gt;, &lt;span class="caps"&gt;CH&lt;/span&gt;, &lt;span class="caps"&gt;NL&lt;/span&gt;, &lt;span class="caps"&gt;LU&lt;/span&gt;, &lt;span class="caps"&gt;FR&lt;/span&gt;) and load the resulting ~273,000 &lt;span class="caps"&gt;CIDR&lt;/span&gt; blocks into &lt;span class="caps"&gt;PF&lt;/span&gt; in chunks to avoid memory&amp;nbsp;exhaustion.&lt;/p&gt;
&lt;h2 id="jail-architecture-vnet-for-true-isolation"&gt;Jail Architecture: &lt;span class="caps"&gt;VNET&lt;/span&gt; for True&amp;nbsp;Isolation&lt;/h2&gt;
&lt;p&gt;I use &lt;span class="caps"&gt;VNET&lt;/span&gt; jails rather than traditional &lt;span class="caps"&gt;IP&lt;/span&gt; alias jails. Unlike &lt;span class="caps"&gt;IP&lt;/span&gt; aliases, &lt;span class="caps"&gt;VNET&lt;/span&gt; gives each jail its own fully virtualized network stack with dedicated interfaces. This allows firewall rules inside the jail if needed and keeps the host&amp;#8217;s networking&amp;nbsp;clean.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/jail.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;mailstack&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;enforce_statfs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;devfs_ruleset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consolelog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jails&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mailstack_console&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/sh /etc/rc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/sh /etc/rc.shutdown&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mailstack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/jails/mailstack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Mount the mail storage from the host&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/mail /jails/mailstack/var/vmail nullfs rw 0 0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# VNET configuration&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;e0b_mailstack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;epair0=$(ifconfig epair create) &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        ifconfig ${epair0} up name e0a_mailstack &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        ifconfig ${epair0%a}b up name e0b_mailstack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig bridge0 addm e0a_mailstack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig e0a_mailstack description &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt;vnet0 host interface for Jail mailstack&lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poststop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig e0a_mailstack destroy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;webmail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;enforce_statfs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;devfs_ruleset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consolelog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jails&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webmail_console&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/sh /etc/rc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/sh /etc/rc.shutdown&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;webmail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/jails/webmail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;e0b_webmail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;epair0=$(ifconfig epair create) &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        ifconfig ${epair0} up name e0a_webmail &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        ifconfig ${epair0%a}b up name e0b_webmail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig bridge0 addm e0a_webmail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prestart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig e0a_webmail description &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt;vnet0 host interface for Jail webmail&lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poststop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ifconfig e0a_webmail destroy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To ensure the jails have only access to the /dev device nodes that they need, we need to have a devfs rule with the same id in /etc/devfs.rules and re-start the
devfs servive&amp;nbsp;with &lt;code&gt;service devfs restart&lt;/code&gt;. This is good practice for isolation of the&amp;nbsp;jails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/etc/devfs.rules&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[jail_vnet=4]&lt;/span&gt;
&lt;span class="na"&gt;add include \$devfsrules_hide_all&lt;/span&gt;
&lt;span class="na"&gt;add include \$devfsrules_unhide_basic&lt;/span&gt;
&lt;span class="na"&gt;add include \$devfsrules_unhide_login&lt;/span&gt;
&lt;span class="na"&gt;add include \$devfsrules_jail&lt;/span&gt;
&lt;span class="na"&gt;add include \$devfsrules_jail_vnet&lt;/span&gt;
&lt;span class="na"&gt;add path &amp;#39;bpf*&amp;#39; unhide&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The epair interfaces create a virtual cable - one end attached to the host&amp;#8217;s bridge, the other inside the jail. When the jail stops, the interface is automatically&amp;nbsp;destroyed.&lt;/p&gt;
&lt;p&gt;Inside the mailstack jail, the network is configured like any FreeBSD&amp;nbsp;system:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/jails/mailstack/etc/rc.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_e0b_mailstack_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.0.0.3 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1c1c:4d2:8000::3/65&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jail interface for bridge0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.0.0.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Disable sendmail (we use Postfix)&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_submit_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_outbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_msp_queue_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;syslogd_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-ss&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;cron_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-J 60&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;postfix_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;dovecot_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rspamd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;local_unbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that I run a local Unbound resolver inside the jail. This ensures &lt;span class="caps"&gt;DNS&lt;/span&gt; queries for &lt;span class="caps"&gt;DKIM&lt;/span&gt; verification and &lt;span class="caps"&gt;SPF&lt;/span&gt; checks don&amp;#8217;t leak to external resolvers and provides caching for the many lookups mail processing&amp;nbsp;requires.&lt;/p&gt;
&lt;h2 id="the-mail-stack-configuration"&gt;The Mail Stack&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Inside the mailstack jail, three components work together: Postfix handles &lt;span class="caps"&gt;SMTP&lt;/span&gt;, Dovecot manages storage and &lt;span class="caps"&gt;IMAP&lt;/span&gt; access, and Rspamd filters spam and signs outgoing mail with &lt;span class="caps"&gt;DKIM&lt;/span&gt;.&lt;/p&gt;
&lt;h3 id="postfix-the-mta"&gt;Postfix (The &lt;span class="caps"&gt;MTA&lt;/span&gt;)&lt;/h3&gt;
&lt;p&gt;Postfix handles both receiving mail from the internet and accepting submissions from authenticated users. The configuration enforces strict restrictions to reject spam early in the &lt;span class="caps"&gt;SMTP&lt;/span&gt;&amp;nbsp;conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/postfix/main.cf&lt;/strong&gt; (key&amp;nbsp;settings)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;compatibility_level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;3.10&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Identity&lt;/span&gt;
&lt;span class="nx"&gt;myhostname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;
&lt;span class="nx"&gt;myorigin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;myhostname&lt;/span&gt;
&lt;span class="nx"&gt;mydestination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;mydomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localhost&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Network&lt;/span&gt;
&lt;span class="nx"&gt;inet_interfaces&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;
&lt;span class="nx"&gt;inet_protocols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;
&lt;span class="nx"&gt;mynetworks_style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TLS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Incoming&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Outgoing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_tls_cert_file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;certs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pem&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_tls_key_file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;certs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pem&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_tls_security_level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;may&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_tls_auth_only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yes&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_tls_loglevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;smtp_tls_security_level&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;may&lt;/span&gt;
&lt;span class="nx"&gt;smtp_tls_loglevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;smtp_tls_CApath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ssl&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;certs&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SASL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Auth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Dovecot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_sasl_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dovecot&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_sasl_path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_sasl_auth_enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yes&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_sasl_security_options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;noanonymous&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Virtual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Domains&lt;/span&gt;
&lt;span class="nx"&gt;virtual_mailbox_domains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;
&lt;span class="nx"&gt;virtual_mailbox_maps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;postfix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;vmailbox&lt;/span&gt;
&lt;span class="nx"&gt;virtual_alias_maps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;postfix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kd"&gt;virtual&lt;/span&gt;
&lt;span class="nx"&gt;virtual_uid_maps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;static&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="nx"&gt;virtual_gid_maps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;static&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="nx"&gt;virtual_minimum_uid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Hand&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Dovecot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;
&lt;span class="nx"&gt;virtual_transport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lmtp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;unix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dovecot&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;lmtp&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Rspamd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;milter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_milters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;11332&lt;/span&gt;
&lt;span class="nx"&gt;non_smtpd_milters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;11332&lt;/span&gt;
&lt;span class="nx"&gt;milter_protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="nx"&gt;milter_default_action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accept&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Strict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HELO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;requirements&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_helo_required&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yes&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_helo_restrictions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit_mynetworks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit_sasl_authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_invalid_helo_hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_non_fqdn_helo_hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restrictions&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_sender_restrictions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit_mynetworks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit_sasl_authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_non_fqdn_sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_unknown_sender_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Recipient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restrictions&lt;/span&gt;
&lt;span class="nx"&gt;smtpd_recipient_restrictions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit_mynetworks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;permit_sasl_authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_unauth_destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_non_fqdn_recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;reject_unknown_reverse_client_hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The submission port (587) for authenticated users is configured&amp;nbsp;in &lt;code&gt;master.cf&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/postfix/master.cf&lt;/strong&gt; (submission&amp;nbsp;entry)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_milters=inet:localhost:11332
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The virtual mailbox and alias maps define which addresses are valid and where they&amp;#8217;re&amp;nbsp;delivered:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/postfix/vmailbox&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# example.org
admin@example.org        OK
backups@example.org      OK

# example.net
user@example.net         OK
accounts@example.net     OK
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;/usr/local/etc/postfix/virtual&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Aliases for example.org
postmaster@example.org   admin@example.org
abuse@example.org        admin@example.org
webmaster@example.org    admin@example.org

# Catch-all for specific services -&amp;gt; accounts
github@example.net       accounts@example.net
hetzner@example.net      accounts@example.net
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After modifying these files, rebuild the hash&amp;nbsp;databases:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;postmap&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/postfix/vmailbox
postmap&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/postfix/virtual
postfix&lt;span class="w"&gt; &lt;/span&gt;reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="dovecot-storage-authentication"&gt;Dovecot (Storage &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt;&amp;nbsp;Authentication)&lt;/h3&gt;
&lt;p&gt;Dovecot is the workhorse. It serves &lt;span class="caps"&gt;IMAP&lt;/span&gt; to clients, handles authentication for Postfix via &lt;span class="caps"&gt;SASL&lt;/span&gt;, receives mail from Postfix via &lt;span class="caps"&gt;LMTP&lt;/span&gt;, and processes Sieve filter rules.
The special_use attributes implement &lt;span class="caps"&gt;RFC&lt;/span&gt; 6154. This signals to modern clients (iOS, Outlook) which folders are for Sent, Trash, and Drafts, preventing the chaos of mixed
&amp;#8216;Sent&amp;#8217; and &amp;#8216;Sent Messages&amp;#8217;&amp;nbsp;folders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/dovecot/dovecot.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;protocols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;imap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lmtp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sieve&lt;/span&gt;
&lt;span class="n"&gt;auth_mechanisms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;login&lt;/span&gt;

&lt;span class="c1"&gt;# Security - require TLS&lt;/span&gt;
&lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;
&lt;span class="n"&gt;ssl_cert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;certs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;
&lt;span class="n"&gt;ssl_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;certs&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pem&lt;/span&gt;
&lt;span class="n"&gt;ssl_min_protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TLSv1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;# Mail storage location&lt;/span&gt;
&lt;span class="n"&gt;mail_location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maildir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vmail&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Maildir&lt;/span&gt;
&lt;span class="n"&gt;mail_uid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="n"&gt;mail_gid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="n"&gt;mmap_disable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;

&lt;span class="c1"&gt;# Sieve for server-side filtering&lt;/span&gt;
&lt;span class="n"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;sieve&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vmail&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;dovecot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sieve&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;sieve_dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vmail&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sieve&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Authentication (simple passwd-file for easy management)&lt;/span&gt;
&lt;span class="n"&gt;passdb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BLF&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;CRYPT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;username_format&lt;/span&gt;&lt;span class="o"&gt;=%&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dovecot&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;userdb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vmail&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;/%&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Socket for Postfix SASL authentication&lt;/span&gt;
&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;unix_listener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;spool&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postfix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0660&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postfix&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postfix&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Socket for Postfix LMTP delivery&lt;/span&gt;
&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lmtp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;unix_listener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;spool&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;postfix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dovecot&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lmtp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0660&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postfix&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;postfix&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lmtp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mail_plugins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;mail_plugins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sieve&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Standard IMAP folders (RFC 6154)&lt;/span&gt;
&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Drafts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;special_use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\&lt;span class="n"&gt;Drafts&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Junk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;special_use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\&lt;span class="n"&gt;Junk&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Trash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;special_use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\&lt;span class="n"&gt;Trash&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;special_use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\&lt;span class="n"&gt;Sent&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Sent Messages&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;special_use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\&lt;span class="n"&gt;Sent&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Archive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;special_use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\&lt;span class="n"&gt;Archive&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The users file contains password hashes (generated&amp;nbsp;with &lt;code&gt;doveadm pw -s BLF-CRYPT&lt;/code&gt;):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/dovecot/users&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;user@example.net:{BLF-CRYPT}$2y$05$...hash...
admin@example.org:{BLF-CRYPT}$2y$05$...hash...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="rspamd-the-gatekeeper"&gt;Rspamd (The&amp;nbsp;Gatekeeper)&lt;/h3&gt;
&lt;p&gt;Rspamd analyzes incoming messages and handles &lt;span class="caps"&gt;DKIM&lt;/span&gt; signing for outgoing mail. Without valid &lt;span class="caps"&gt;DKIM&lt;/span&gt; signatures, your mail will likely land in Gmail&amp;#8217;s spam&amp;nbsp;folder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/usr/local/etc/rspamd/local.d/dkim_signing.conf&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/var/lib/rspamd/dkim/$domain.key&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;mail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;allow_username_mismatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Generate &lt;span class="caps"&gt;DKIM&lt;/span&gt; keys for each&amp;nbsp;domain:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rspamadm&lt;span class="w"&gt; &lt;/span&gt;dkim_keygen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;mail&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;example.org&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-k&lt;span class="w"&gt; &lt;/span&gt;/var/lib/rspamd/dkim/example.org.key&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/var/lib/rspamd/dkim/example.org.pub
chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;640&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/var/lib/rspamd/dkim/example.org.key
chown&lt;span class="w"&gt; &lt;/span&gt;rspamd:rspamd&lt;span class="w"&gt; &lt;/span&gt;/var/lib/rspamd/dkim/example.org.key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The public key goes into your &lt;span class="caps"&gt;DNS&lt;/span&gt; as a &lt;span class="caps"&gt;TXT&lt;/span&gt; record&amp;nbsp;at &lt;code&gt;mail._domainkey.example.org&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="boot-and-recovery-procedure"&gt;Boot and Recovery&amp;nbsp;Procedure&lt;/h2&gt;
&lt;p&gt;Because the mail data lives on an encrypted &lt;span class="caps"&gt;ZFS&lt;/span&gt; dataset, the system requires manual intervention after a reboot. This is a deliberate security&amp;nbsp;trade-off.&lt;/p&gt;
&lt;p&gt;After the system&amp;nbsp;boots:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Unlock the encrypted dataset&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;load-key&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;zroot/secure

&lt;span class="c1"&gt;# Mount all ZFS filesystems&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;mount&lt;span class="w"&gt; &lt;/span&gt;-a

&lt;span class="c1"&gt;# Start the jails&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This sequence ensures that even if someone gains physical access to the server (or its disks), they cannot read the mail data without the encryption&amp;nbsp;passphrase.&lt;/p&gt;
&lt;h2 id="backups-encrypted-at-rest"&gt;Backups: Encrypted at&amp;nbsp;Rest&lt;/h2&gt;
&lt;p&gt;One of &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;#8217;s killer features for mail hosting is the ability to send encrypted raw streams. The backup server never sees unencrypted&amp;nbsp;data.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a recursive snapshot&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;zroot@backup_20260118

&lt;span class="c1"&gt;# Send encrypted stream to offsite backup&lt;/span&gt;
&lt;span class="c1"&gt;# The -w flag sends the raw encrypted blocks&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;@backup_20260117&lt;span class="w"&gt; &lt;/span&gt;zroot@backup_20260118&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;backup-user@backup.example.net&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;recv&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;mountpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;canmount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off&lt;span class="w"&gt; &lt;/span&gt;data1/mailserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The backup server stores the data in encrypted form. Even if it&amp;#8217;s compromised, the attacker only gets encrypted blocks without the&amp;nbsp;key.&lt;/p&gt;
&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;p&gt;After everything is configured, verify the running services inside the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;root@mailstack:/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# ps auxw | grep -E &amp;#39;postfix|dovecot|rspamd&amp;#39;&lt;/span&gt;
root&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;16095&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.3&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;130672&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;53916&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;SsJ&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.10&lt;span class="w"&gt; &lt;/span&gt;rspamd:&lt;span class="w"&gt; &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;process
rspamd&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;16324&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.3&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;131056&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;54692&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;SJ&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.12&lt;span class="w"&gt; &lt;/span&gt;rspamd:&lt;span class="w"&gt; &lt;/span&gt;rspamd_proxy&lt;span class="w"&gt; &lt;/span&gt;process
rspamd&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;16521&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;131964&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;56244&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;SJ&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:01.09&lt;span class="w"&gt; &lt;/span&gt;rspamd:&lt;span class="w"&gt; &lt;/span&gt;controller&lt;span class="w"&gt; &lt;/span&gt;process
rspamd&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;16619&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;132592&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;58544&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;SJ&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.58&lt;span class="w"&gt; &lt;/span&gt;rspamd:&lt;span class="w"&gt; &lt;/span&gt;normal&lt;span class="w"&gt; &lt;/span&gt;process
root&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;35467&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;15984&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;4104&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;IsJ&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.15&lt;span class="w"&gt; &lt;/span&gt;/usr/local/sbin/dovecot
dovecot&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;36239&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;15916&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3832&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;IJ&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.03&lt;span class="w"&gt; &lt;/span&gt;dovecot/anvil
root&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;69385&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.4&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;61164&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14668&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;IsJ&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.11&lt;span class="w"&gt; &lt;/span&gt;/usr/local/libexec/postfix/master
postfix&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;69918&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.4&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;61188&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14676&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;IJ&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.03&lt;span class="w"&gt; &lt;/span&gt;pickup&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;unix&lt;span class="w"&gt; &lt;/span&gt;-u
postfix&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;70681&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.4&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;61244&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14744&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;-&lt;span class="w"&gt;  &lt;/span&gt;IJ&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:21&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00.03&lt;span class="w"&gt; &lt;/span&gt;qmgr&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;unix&lt;span class="w"&gt; &lt;/span&gt;-u
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Test &lt;span class="caps"&gt;SMTP&lt;/span&gt;&amp;nbsp;connectivity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;telnet&lt;span class="w"&gt; &lt;/span&gt;mail.example.org&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;25&lt;/span&gt;
&lt;span class="m"&gt;220&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mail.example.org&lt;span class="w"&gt; &lt;/span&gt;ESMTP&lt;span class="w"&gt; &lt;/span&gt;Postfix
EHLO&lt;span class="w"&gt; &lt;/span&gt;test.example.com
&lt;span class="m"&gt;250&lt;/span&gt;-mail.example.org
&lt;span class="m"&gt;250&lt;/span&gt;-STARTTLS
&lt;span class="m"&gt;250&lt;/span&gt;-AUTH&lt;span class="w"&gt; &lt;/span&gt;PLAIN&lt;span class="w"&gt; &lt;/span&gt;LOGIN
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Test email deliverability using tools like &lt;a href="https://www.mail-tester.com/"&gt;mail-tester.com&lt;/a&gt; or &lt;a href="https://mxtoolbox.com/"&gt;MXToolbox&lt;/a&gt;. A properly configured server with &lt;span class="caps"&gt;SPF&lt;/span&gt;, &lt;span class="caps"&gt;DKIM&lt;/span&gt;, and &lt;span class="caps"&gt;DMARC&lt;/span&gt; should score&amp;nbsp;10/10.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Running a mail server in 2026 on FreeBSD 15.0 feels robust. The combination of &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s fine-grained control, &lt;span class="caps"&gt;VNET&lt;/span&gt; jails for network isolation, &lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;#8217;s native encryption, and GeoIP filtering creates a platform that is secure by&amp;nbsp;design.&lt;/p&gt;
&lt;p&gt;While it requires initial effort to configure correctly - especially getting &lt;span class="caps"&gt;DNS&lt;/span&gt; records (&lt;span class="caps"&gt;SPF&lt;/span&gt;, &lt;span class="caps"&gt;DKIM&lt;/span&gt;, &lt;span class="caps"&gt;DMARC&lt;/span&gt;) aligned - the result is a private, secure communication hub that you truly own. You aren&amp;#8217;t mining your own data for ads, and you aren&amp;#8217;t subject to the whims of a provider who might lock your account without&amp;nbsp;recourse.&lt;/p&gt;
&lt;p&gt;The architecture also makes maintenance straightforward: update the host and jails independently, restore from encrypted backups without exposing plaintext data, and adjust GeoIP rules as your travel patterns&amp;nbsp;change.&lt;/p&gt;
&lt;p&gt;Is it more work than using a hosted provider? Absolutely. Is it worth it? For those who value understanding their infrastructure and maintaining control over their communications, unquestionably&amp;nbsp;yes.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postfix.org/documentation.html"&gt;Postfix&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.dovecot.org/"&gt;Dovecot&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rspamd.com/doc/"&gt;Rspamd&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook:&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=pf.conf"&gt;FreeBSD pf.conf(5) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data"&gt;MaxMind GeoLite2 Free Geolocation&amp;nbsp;Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/Getting%20Started/Ubuntu/Ubuntu%2020.04%20Root%20on%20ZFS.html#encryption"&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; Encryption (OpenZFS&amp;nbsp;Documentation)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;The mail protocols we use today were designed in an era of mutual trust between operators. That trust has eroded, but the protocols remain remarkably resilient. With careful configuration and modern security layers, self-hosted email is not just viable - it&amp;#8217;s a statement of digital&amp;nbsp;autonomy.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="mail"/><category term="postfix"/><category term="dovecot"/><category term="rspamd"/><category term="jails"/><category term="zfs"/><category term="security"/></entry><entry><title>Card Wars: Hiding Smartcard Readers from Eager Rust Agents with LD_PRELOAD</title><link href="https://blog.hofstede.it/card-wars-hiding-smartcard-readers-from-eager-rust-agents-with-ld_preload/" rel="alternate"/><published>2026-01-17T00:00:00+01:00</published><updated>2026-01-17T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-17:/card-wars-hiding-smartcard-readers-from-eager-rust-agents-with-ld_preload/</id><summary type="html">&lt;p&gt;Using LD_PRELOAD to surgically hide a &lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; smartcard reader from a Rust-based OpenPGP &lt;span class="caps"&gt;SSH&lt;/span&gt; agent, allowing two incompatible hardware security stacks to coexist peacefully on the same&amp;nbsp;system.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Running multiple hardware security tokens simultaneously sounds straightforward until you try it. I recently built what I affectionately call a &amp;#8220;Workstation from Hell&amp;#8221;. A Fedora setup with two distinct smartcard ecosystems that absolutely refuse to get&amp;nbsp;along:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Enterprise Layer&lt;/strong&gt;: An &lt;strong&gt;Aventra MyEID&lt;/strong&gt; card (&lt;span class="caps"&gt;PKCS&lt;/span&gt;#15) in the internal laptop reader for Kerberos/&lt;span class="caps"&gt;PKINIT&lt;/span&gt;&amp;nbsp;authentication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;GPG&lt;/span&gt; Layer&lt;/strong&gt;: A &lt;strong&gt;Nitrokey 3&lt;/strong&gt; (OpenPGP) for &lt;span class="caps"&gt;SSH&lt;/span&gt; and Git signing, managed by the modern Rust-based &lt;a href="https://codeberg.org/openpgp-card/ssh-agent"&gt;openpgp-card-ssh-agent&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The MyEID handles enterprise authentication. The Nitrokey handles developer workflows. In theory, they should coexist without issue. In practice, one of them throws a tantrum every time it sees the&amp;nbsp;other.&lt;/p&gt;
&lt;h2 id="the-problem-aggressive-slot-scanning"&gt;The Problem: Aggressive Slot&amp;nbsp;Scanning&lt;/h2&gt;
&lt;p&gt;The Rust OpenPGP stack is modern, fast, and memory-safe. It&amp;#8217;s also a bit &lt;em&gt;eager&lt;/em&gt;. When the &lt;span class="caps"&gt;SSH&lt;/span&gt; agent starts, it scans all available &lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; slots looking for OpenPGP cards - a reasonable approach when you&amp;#8217;re the only smartcard on the&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;When it encounters my internal Alcor reader containing the MyEID card, things go sideways. The MyEID speaks strict &lt;span class="caps"&gt;PKCS&lt;/span&gt;#15, not OpenPGP. The Rust stack receives unexpected response codes, interprets them as a fatal protocol failure, and&amp;nbsp;crashes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Jan 17 19:42:43 neochristop openpgp-card-ssh-agent[44822]: [2026-01-17T18:42:43Z ERROR ssh_agent_lib::agent] Error handling message: Proto(UnsupportedCommand { command: 27 })
Jan 17 19:42:43 neochristop openpgp-card-ssh-agent[44822]: [2026-01-17T18:42:43Z ERROR ssh_agent_lib::agent] Error handling message: Failure
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The agent doesn&amp;#8217;t offer a configuration option to whitelist specific readers. I needed a way to make the internal reader invisible to the Rust agent without affecting the rest of the system - the Kerberos stack still needs full access to the MyEID&amp;nbsp;card.&lt;/p&gt;
&lt;h2 id="the-solution-ld_preload-interception"&gt;The Solution: LD_PRELOAD&amp;nbsp;Interception&lt;/h2&gt;
&lt;p&gt;If the application won&amp;#8217;t filter readers, we filter them at the system &lt;span class="caps"&gt;API&lt;/span&gt; level. The &lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt;&amp;nbsp;uses &lt;code&gt;SCardListReaders&lt;/code&gt; to enumerate available smartcard slots. By intercepting this function, we can surgically remove specific readers from the list before the application sees&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;The approach is&amp;nbsp;straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a shared library that&amp;nbsp;intercepts &lt;code&gt;SCardListReaders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Call the real function to get the complete reader&amp;nbsp;list&lt;/li&gt;
&lt;li&gt;Remove any entries matching our filter&amp;nbsp;criteria&lt;/li&gt;
&lt;li&gt;Return the filtered list to the&amp;nbsp;application&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This keeps the Kerberos stack happy (it sees both readers) while the OpenPGP agent only sees the reader it can actually work&amp;nbsp;with.&lt;/p&gt;
&lt;h2 id="the-shim-library"&gt;The Shim&amp;nbsp;Library&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s the C code that makes this&amp;nbsp;work:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;#define _GNU_SOURCE&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;dlfcn.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;winscard.h&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Function pointer matching the PC/SC prototype&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LONG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SCardListReaders_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;SCARDCONTEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LPCSTR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LPSTR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LPDWORD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;LONG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;SCardListReaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCARDCONTEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LPCSTR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszGroups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="n"&gt;LPSTR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszReaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LPDWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pcchReaders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Load and call the original function&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;SCardListReaders_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;original_fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCardListReaders_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RTLD_NEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SCardListReaders&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;LONG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;original_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszGroups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszReaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pcchReaders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// If the call failed or we&amp;#39;re just querying buffer size, return immediately&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SCARD_S_SUCCESS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszReaders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pcchReaders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Filter the multi-string list&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// PC/SC returns readers as &amp;quot;Reader A\0Reader B\0\0&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszReaders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszReaders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;new_len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;total_len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcchReaders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mszReaders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;total_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Double null indicates end of list&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// FILTER LOGIC: If &amp;quot;Alcor&amp;quot; is in the name, skip it&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Alcor&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Skip this reader (hide it from the application)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Keep this reader: shift bytes if necessary&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;memmove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;new_len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Terminate the new list properly&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sc"&gt;&amp;#39;\0&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;new_len&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Update the length returned to the caller&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcchReaders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;new_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key insight is that &lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; returns reader names as a multi-string buffer, a sequence of null-terminated strings followed by an extra null byte. The filter walks this structure, copying entries that don&amp;#8217;t match the filter pattern while skipping those that&amp;nbsp;do.&lt;/p&gt;
&lt;h2 id="compilation-and-installation"&gt;Compilation and&amp;nbsp;Installation&lt;/h2&gt;
&lt;p&gt;Building the shim requires gcc and the &lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; headers. On&amp;nbsp;Fedora:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pcsc-lite-devel&lt;span class="w"&gt; &lt;/span&gt;gcc
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;~/.local/lib

gcc&lt;span class="w"&gt; &lt;/span&gt;-shared&lt;span class="w"&gt; &lt;/span&gt;-fPIC&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;~/.local/lib/libhackyreaderfix.so&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;hacky_reader_fix.c&lt;span class="w"&gt; &lt;/span&gt;-ldl&lt;span class="w"&gt; &lt;/span&gt;-I/usr/include/PCSC
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-shared -fPIC&lt;/code&gt; flags create a position-independent shared library suitable&amp;nbsp;for &lt;code&gt;LD_PRELOAD&lt;/code&gt; injection.&amp;nbsp;The &lt;code&gt;-ldl&lt;/code&gt; links against the dynamic loader library for&amp;nbsp;the &lt;code&gt;dlsym&lt;/code&gt; call.&lt;/p&gt;
&lt;h2 id="injecting-the-shim"&gt;Injecting the&amp;nbsp;Shim&lt;/h2&gt;
&lt;p&gt;I&amp;nbsp;use &lt;code&gt;systemd --user&lt;/code&gt; to manage my &lt;span class="caps"&gt;SSH&lt;/span&gt; agent. To inject the library, create a drop-in&amp;nbsp;override:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;openpgp-card-ssh-agent.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add the following&amp;nbsp;content:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;LD_PRELOAD=%h/.local/lib/libhackyreaderfix.so&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;%h&lt;/code&gt; specifier expands to the user&amp;#8217;s home directory, keeping the configuration&amp;nbsp;portable.&lt;/p&gt;
&lt;p&gt;After saving, restart the&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;daemon-reload
systemctl&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;openpgp-card-ssh-agent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="the-result"&gt;The&amp;nbsp;Result&lt;/h2&gt;
&lt;p&gt;With the shim in place, the two security stacks are perfectly&amp;nbsp;isolated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System/Kerberos&lt;/strong&gt;: Sees both readers, including the internal Alcor with the MyEID card. &lt;span class="caps"&gt;PKINIT&lt;/span&gt; authentication works&amp;nbsp;normally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; Agent&lt;/strong&gt;: Completely unaware that the Alcor reader exists. Connects directly to the Nitrokey in the external Identiv reader. No more crashes, no more protocol&amp;nbsp;errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can verify the filtering is working using pcsc_scan (from the pcsc-tools package) to see exactly what the shim&amp;nbsp;exposes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Without LD_PRELOAD - shows both readers&lt;/span&gt;
pcsc_scan
&lt;span class="c1"&gt;# Reader 0: Identiv uTrust 3512 SAM slot Token [CCID Interface] (55512030603915) 00 00&lt;/span&gt;
&lt;span class="c1"&gt;# Reader 1: Alcor Link AK9563 01 00&lt;/span&gt;
&lt;span class="c1"&gt;# Reader 2: Nitrokey Nitrokey 3 [CCID/ICCD Interface] 02 00&lt;/span&gt;

&lt;span class="c1"&gt;# With LD_PRELOAD - only shows the allowed reader&lt;/span&gt;
&lt;span class="nv"&gt;LD_PRELOAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.local/lib/libhackyreaderfix.so&lt;span class="w"&gt; &lt;/span&gt;pcsc_scan
&lt;span class="c1"&gt;# Reader 0: Identiv uTrust 3512 SAM slot Token [CCID Interface] (55512030603915) 00 00&lt;/span&gt;
&lt;span class="c1"&gt;# Reader 1: Nitrokey Nitrokey 3 [CCID/ICCD Interface] 02 00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="extending-the-filter"&gt;Extending the&amp;nbsp;Filter&lt;/h2&gt;
&lt;p&gt;The current implementation hardcodes &amp;#8220;Alcor&amp;#8221; as the filter pattern. For more flexibility, you could read the pattern from an environment&amp;nbsp;variable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;PCSC_HIDE_READER&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;strstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Skip this reader&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then configure it via&amp;nbsp;systemd:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;LD_PRELOAD=%h/.local/lib/libhackyreaderfix.so&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;PCSC_HIDE_READER=Alcor&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This makes the shim reusable across different reader combinations without&amp;nbsp;recompilation.&lt;/p&gt;
&lt;h2 id="why-not-upstream-a-fix"&gt;Why Not Upstream a&amp;nbsp;Fix?&lt;/h2&gt;
&lt;p&gt;The right solution would be for the Rust OpenPGP stack to handle non-OpenPGP cards gracefully. Either by ignoring readers that return unexpected responses or by offering a configuration option to whitelist readers. Both would be reasonable feature&amp;nbsp;requests.&lt;/p&gt;
&lt;p&gt;However, the LD_PRELOAD approach solves the immediate problem without waiting for upstream changes, and it works for any application that misbehaves when confronted with unexpected smartcard readers. It&amp;#8217;s a general-purpose tool for a specific class of&amp;nbsp;problems.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;p&gt;A few things to keep in&amp;nbsp;mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security implications&lt;/strong&gt;: LD_PRELOAD modifies application behavior at runtime. Only use libraries you control and&amp;nbsp;trust.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fragility&lt;/strong&gt;: If the &lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt; changes significantly, the shim may need updates. In practice, this &lt;span class="caps"&gt;API&lt;/span&gt; has been stable for&amp;nbsp;decades.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugging&lt;/strong&gt;: If something goes wrong, remember the shim is in the path. Temporarily removing the LD_PRELOAD can help isolate&amp;nbsp;issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Sometimes the cleanest solution is a dirty hack. LD_PRELOAD interception isn&amp;#8217;t elegant, but it&amp;#8217;s effective. When two pieces of software refuse to cooperate and neither offers the configuration knobs you need, a small shim library can bridge the&amp;nbsp;gap.&lt;/p&gt;
&lt;p&gt;The MyEID handles enterprise authentication. The Nitrokey handles my development workflow. And a few dozen lines of C keep them from stepping on each other&amp;#8217;s&amp;nbsp;toes.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codeberg.org/openpgp-card/ssh-agent"&gt;openpgp-card-ssh-agent&lt;/a&gt; - Rust-based OpenPGP &lt;span class="caps"&gt;SSH&lt;/span&gt;&amp;nbsp;agent&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pcsclite.apdu.fr/"&gt;&lt;span class="caps"&gt;PC&lt;/span&gt;/&lt;span class="caps"&gt;SC&lt;/span&gt; Lite&lt;/a&gt; - Middleware for smartcard access on Unix&amp;nbsp;systems&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aventra.fi/myeid/"&gt;Aventra MyEID&lt;/a&gt; - &lt;span class="caps"&gt;PKCS&lt;/span&gt;#15 compliant&amp;nbsp;smartcards&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nitrokey.com/products/nitrokeys"&gt;Nitrokey 3&lt;/a&gt; - Open-source hardware security&amp;nbsp;keys&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;The best kind of interoperability hack is one you can forget about. Set it up once, and your hardware security layers peacefully ignore each other&amp;nbsp;forever.&lt;/p&gt;</content><category term="Linux"/><category term="linux"/><category term="smartcard"/><category term="security"/><category term="fedora"/></entry><entry><title>GeoIP-Aware Firewalling with PF on FreeBSD</title><link href="https://blog.hofstede.it/geoip-aware-firewalling-with-pf-on-freebsd/" rel="alternate"/><published>2026-01-13T00:00:00+01:00</published><updated>2026-01-13T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2026-01-13:/geoip-aware-firewalling-with-pf-on-freebsd/</id><summary type="html">&lt;p&gt;Using MaxMind&amp;#8217;s GeoLite2 database with FreeBSD&amp;#8217;s &lt;span class="caps"&gt;PF&lt;/span&gt; firewall to restrict client-facing services to specific countries, reducing brute-force attempts and log noise while keeping essential services globally&amp;nbsp;accessible.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Running a mail server on the public internet means dealing with a constant stream of brute-force attempts. Credential stuffers, password sprayers, and opportunistic bots hammer away at &lt;span class="caps"&gt;IMAP&lt;/span&gt;, submission ports, and webmail interfaces around the clock. While fail2ban-style rate limiting helps, the sheer volume of attempts still clutters logs and wastes resources on connection&amp;nbsp;handling.&lt;/p&gt;
&lt;p&gt;The solution I&amp;#8217;ve implemented takes a different approach: geographic restriction. My users are all located in Central Europe - Germany, Austria, Switzerland, and neighboring countries. There&amp;#8217;s no legitimate reason for someone in a botnet-heavy region to connect to my &lt;span class="caps"&gt;IMAP&lt;/span&gt; server. By restricting client-facing ports to &lt;span class="caps"&gt;IP&lt;/span&gt; ranges allocated to specific countries, I&amp;#8217;ve dramatically reduced both attack surface and log&amp;nbsp;noise.&lt;/p&gt;
&lt;p&gt;&lt;img alt="World map" src="https://blog.hofstede.it/images/2026-01-13-freebsd-geoip-firewall.jpg" title="Geographic filtering: allowing only what you need"&gt;&lt;/p&gt;
&lt;h2 id="the-strategy"&gt;The&amp;nbsp;Strategy&lt;/h2&gt;
&lt;p&gt;The key insight is distinguishing between server-to-server and client-to-server&amp;nbsp;traffic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;SMTP&lt;/span&gt; (port 25)&lt;/strong&gt; must remain globally accessible. Mail servers from anywhere in the world need to deliver messages to my server. Restricting this would break&amp;nbsp;email.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client ports&lt;/strong&gt; - &lt;span class="caps"&gt;IMAP&lt;/span&gt; (143), Submission (587), Sieve (4190), and webmail (80/443) - only need to be accessible from where my users actually&amp;nbsp;are.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates a two-tier firewall policy: &lt;span class="caps"&gt;SMTP&lt;/span&gt; open to all, everything else restricted by&amp;nbsp;geography.&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    Internet
                        |
                        v
            +-----------------------+
            |    PF Firewall        |
            |                       |
            |  &amp;lt;geoip_users&amp;gt; table  |
            |  ~273,000 CIDR blocks |
            +-----------+-----------+
                        |
        +---------------+---------------+
        |                               |
        v                               v
   +----------+                   +-----------+
   | SMTP:25  |                   | Client    |
   | Open to  |                   | Ports     |
   | everyone |                   | GeoIP     |
   +----------+                   | filtered  |
                                  +-----------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;geoip_users&amp;gt;&lt;/code&gt; table contains approximately 273,000 &lt;span class="caps"&gt;CIDR&lt;/span&gt; blocks covering the allowed countries. &lt;span class="caps"&gt;PF&lt;/span&gt; evaluates incoming connections against this table before allowing access to restricted&amp;nbsp;services.&lt;/p&gt;
&lt;h2 id="maxmind-geolite2-database"&gt;MaxMind GeoLite2&amp;nbsp;Database&lt;/h2&gt;
&lt;p&gt;The geographic data comes from &lt;a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data"&gt;MaxMind&amp;#8217;s GeoLite2&lt;/a&gt; database, available free with registration. The &lt;span class="caps"&gt;CSV&lt;/span&gt; format works well for our&amp;nbsp;purposes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ ls -lah /usr/local/share/GeoIP/GeoLite2-Country-CSV_20260106
total 11 MB
-rw-r--r--  1 root wheel   22M Jan  6 07:42 GeoLite2-Country-Blocks-IPv4.csv
-rw-r--r--  1 root wheel   27M Jan  6 07:42 GeoLite2-Country-Blocks-IPv6.csv
-rw-r--r--  1 root wheel  9.6K Jan  6 07:42 GeoLite2-Country-Locations-en.csv
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The database structure is&amp;nbsp;straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Locations files&lt;/strong&gt; map geoname IDs to country codes and&amp;nbsp;names&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blocks files&lt;/strong&gt; map &lt;span class="caps"&gt;CIDR&lt;/span&gt; ranges to geoname&amp;nbsp;IDs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find all &lt;span class="caps"&gt;IP&lt;/span&gt; ranges for Germany, you first look up Germany&amp;#8217;s geoname &lt;span class="caps"&gt;ID&lt;/span&gt; in the locations file, then find all &lt;span class="caps"&gt;CIDR&lt;/span&gt; blocks with that &lt;span class="caps"&gt;ID&lt;/span&gt; in the blocks&amp;nbsp;files.&lt;/p&gt;
&lt;h2 id="the-update-script"&gt;The Update&amp;nbsp;Script&lt;/h2&gt;
&lt;p&gt;Processing the GeoLite2 data and loading it into &lt;span class="caps"&gt;PF&lt;/span&gt; requires careful handling. With nearly 300,000 entries, you can&amp;#8217;t just dump everything at once - &lt;span class="caps"&gt;PF&lt;/span&gt; will reject the operation with a memory error. The solution is chunked&amp;nbsp;loading:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration&lt;/span&gt;
&lt;span class="nv"&gt;GEOIP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/usr/local/share/GeoIP/GeoLite2-Country-CSV_20260106&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;PF_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/db/pf/geoip_allowed&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;TEMP_OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/tmp/geoip_allowed.tmp&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;TEMP_IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/tmp/target_ids.txt&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;CHUNK_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/tmp/geoip_chunks&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Allowed countries (ISO Codes)&lt;/span&gt;
&lt;span class="nv"&gt;COUNTRIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DE|AT|CH|NL|LU|FR&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Cleanup&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_OUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CHUNK_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Processing GeoIP data for: &lt;/span&gt;&lt;span class="nv"&gt;$COUNTRIES&lt;/span&gt;&lt;span class="s2"&gt; ...&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 1: Extract geoname IDs for target countries&lt;/span&gt;
grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;,(&lt;/span&gt;&lt;span class="nv"&gt;$COUNTRIES&lt;/span&gt;&lt;span class="s2"&gt;),&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GEOIP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&amp;quot;&lt;/span&gt;*Locations-en.csv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-d,&lt;span class="w"&gt; &lt;/span&gt;-f1&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_IDS&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Extract CIDRs from block files&lt;/span&gt;
&lt;span class="c1"&gt;# IPv4&lt;/span&gt;
grep&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_IDS&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GEOIP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&amp;quot;&lt;/span&gt;*Blocks-IPv4.csv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-d,&lt;span class="w"&gt; &lt;/span&gt;-f1&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_OUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# IPv6&lt;/span&gt;
grep&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_IDS&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GEOIP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&amp;quot;&lt;/span&gt;*Blocks-IPv6.csv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-d,&lt;span class="w"&gt; &lt;/span&gt;-f1&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_OUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_OUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dirname&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PF_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_OUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PF_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Loading firewall table in 50k chunks...&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Flush existing entries&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;geoip_users&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;flush

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Split into manageable chunks&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CHUNK_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;split&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PF_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CHUNK_DIR&lt;/span&gt;&lt;span class="s2"&gt;/chunk_&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Load each chunk&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CHUNK_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;/chunk_*&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;geoip_users&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Verify&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;geoip_users&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Success: &lt;/span&gt;&lt;span class="nv"&gt;$COUNT&lt;/span&gt;&lt;span class="s2"&gt; networks now active in memory.&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Cleanup&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CHUNK_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_IDS&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: No data extracted.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_OUT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The script processes both IPv4 and IPv6 ranges, creating a unified list that &lt;span class="caps"&gt;PF&lt;/span&gt; can use for dual-stack&amp;nbsp;filtering.&lt;/p&gt;
&lt;h2 id="kernel-tuning"&gt;Kernel&amp;nbsp;Tuning&lt;/h2&gt;
&lt;p&gt;By default, &lt;span class="caps"&gt;PF&lt;/span&gt; has a relatively low limit on table entries. Loading 273,000 &lt;span class="caps"&gt;CIDR&lt;/span&gt; blocks requires increasing this limit&amp;nbsp;via &lt;code&gt;sysctl.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Allow large PF tables for GeoIP filtering
net.pf.request_maxcount=500000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This setting must be applied before loading the GeoIP data. After adding it&amp;nbsp;to &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;, either reboot or apply it&amp;nbsp;live:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;net.pf.request_maxcount&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;500000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="pf-configuration"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s the relevant portion&amp;nbsp;of &lt;code&gt;/etc/pf.conf&lt;/code&gt; showing how the GeoIP table integrates with the firewall&amp;nbsp;rules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macros&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.0.0.0/24&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::/65&amp;quot;&lt;/span&gt;

&lt;span class="nx"&gt;mail_ipv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.0.0.3&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;mail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::3&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;webmail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:1c1c:4d2:8000::4&amp;quot;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Tables&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;bruteforce&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;persist&lt;/span&gt;
&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;jails_v4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;jails_v6&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;geoip_users&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;persist&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;
&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lo0&lt;/span&gt;
&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;
&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reassemble&lt;/span&gt;
&lt;span class="nx"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;NAT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;only&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;jails_v4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;services&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GeoIP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restricted&lt;/span&gt;
&lt;span class="nx"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;geoip_users&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4190&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;mail_ipv4&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;mail_ipv4&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Filtering&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;bruteforce&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;
&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;

&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bridge0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GeoIP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restriction&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;geoip_users&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;mail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4190&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SMTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;mail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Webmail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GeoIP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restriction&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;geoip_users&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nx"&gt;webmail_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Essential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMPv6&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ICMP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Jail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;egress&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bridge0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;jails_v4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;10.0.0.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bridge0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;jails_v6&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;c1c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;d2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="o"&gt;::/&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note&amp;nbsp;the &lt;code&gt;set limit table-entries 1000000&lt;/code&gt; directive - this is the &lt;span class="caps"&gt;PF&lt;/span&gt;-level configuration that complements the sysctl&amp;nbsp;tuning.&lt;/p&gt;
&lt;p&gt;The dual approach to traffic handling is visible in the&amp;nbsp;rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;SMTP&lt;/span&gt; (port 25)&lt;/strong&gt;: &lt;code&gt;from any&lt;/code&gt; - no source&amp;nbsp;restriction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client ports (143, 587, 4190, 80, 443)&lt;/strong&gt;: &lt;code&gt;from &amp;lt;geoip_users&amp;gt;&lt;/code&gt; - restricted to allowed&amp;nbsp;countries&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="verifying-the-setup"&gt;Verifying the&amp;nbsp;Setup&lt;/h2&gt;
&lt;p&gt;After loading the GeoIP data, verify the table is&amp;nbsp;populated:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;geoip_users&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;273460&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;geoip_users&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;2a1b:3e0:500::/64
&lt;span class="w"&gt;   &lt;/span&gt;2a1d:3e0:500::/64
&lt;span class="w"&gt;   &lt;/span&gt;2a1f:3e0:500::/64
&lt;span class="w"&gt;   &lt;/span&gt;2c0f:2c40:a310::/48
&lt;span class="w"&gt;   &lt;/span&gt;2c0f:fc04:2000::/64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The loaded rules can be verified&amp;nbsp;with &lt;code&gt;pfctl -s r&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pass in quick on vtnet0 inet6 proto tcp from &amp;lt;geoip_users&amp;gt; \
    to 2001:db8:1c1c:4d2:8000::3 port = imap flags S/SA keep state
pass in quick on vtnet0 inet6 proto tcp from &amp;lt;geoip_users&amp;gt; \
    to 2001:db8:1c1c:4d2:8000::3 port = submission flags S/SA keep state
pass in quick on vtnet0 inet6 proto tcp from any \
    to 2001:db8:1c1c:4d2:8000::3 port = smtp flags S/SA keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The rules clearly show the differentiation: &lt;span class="caps"&gt;IMAP&lt;/span&gt; and submission require membership&amp;nbsp;in &lt;code&gt;&amp;lt;geoip_users&amp;gt;&lt;/code&gt;, while &lt;span class="caps"&gt;SMTP&lt;/span&gt;&amp;nbsp;allows &lt;code&gt;any&lt;/code&gt; source.&lt;/p&gt;
&lt;h2 id="automating-updates"&gt;Automating&amp;nbsp;Updates&lt;/h2&gt;
&lt;p&gt;MaxMind updates the GeoLite2 database weekly. A cron job keeps the firewall table&amp;nbsp;current:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Update GeoIP data weekly (Mondays at 3 AM)&lt;/span&gt;
&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;update_geoip_maxmind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;geoip_update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For the database download itself, MaxMind provides&amp;nbsp;a &lt;code&gt;geoipupdate&lt;/code&gt; tool that handles authentication and retrieval. The update script then processes the fresh&amp;nbsp;data.&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;The impact has been&amp;nbsp;significant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Log volume&lt;/strong&gt;: Brute-force attempt logs dropped by roughly&amp;nbsp;90%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection overhead&lt;/strong&gt;: Fewer &lt;span class="caps"&gt;TCP&lt;/span&gt; handshakes to reject means lower resource&amp;nbsp;usage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signal-to-noise ratio&lt;/strong&gt;: When something does appear in the logs, it&amp;#8217;s more likely to be worth&amp;nbsp;investigating&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The remaining attempts typically come from compromised hosts within allowed countries or &lt;span class="caps"&gt;VPN&lt;/span&gt; exit nodes. These still hit rate limiting, but at a manageable&amp;nbsp;volume.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;p&gt;This approach isn&amp;#8217;t without&amp;nbsp;trade-offs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Traveling users&lt;/strong&gt;: Anyone traveling outside the allowed countries will be blocked. Communicate this clearly to users, or consider adding &lt;span class="caps"&gt;VPN&lt;/span&gt; access as a&amp;nbsp;bypass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;VPN&lt;/span&gt; services&lt;/strong&gt;: Users connecting through &lt;span class="caps"&gt;VPN&lt;/span&gt; providers with exit nodes outside allowed countries will be&amp;nbsp;blocked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database accuracy&lt;/strong&gt;: GeoIP data isn&amp;#8217;t perfect. Some &lt;span class="caps"&gt;IP&lt;/span&gt; ranges are misattributed, and mobile carriers sometimes route traffic through unexpected&amp;nbsp;locations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Table size&lt;/strong&gt;: 273,000 entries consume kernel memory. On memory-constrained systems, consider restricting to fewer&amp;nbsp;countries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my use case - a small mail server with users who rarely travel - these trade-offs are&amp;nbsp;acceptable.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Geographic filtering is a powerful layer in a defense-in-depth strategy. It doesn&amp;#8217;t replace proper authentication, &lt;span class="caps"&gt;TLS&lt;/span&gt;, or rate limiting, but it dramatically reduces the attack surface by eliminating entire classes of attackers before they even reach the application&amp;nbsp;layer.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s table mechanism handles large GeoIP datasets efficiently, and the chunked loading approach works around memory limitations during updates. Combined with MaxMind&amp;#8217;s regularly updated databases, this provides a low-maintenance way to keep your servers visible only where they need to&amp;nbsp;be.&lt;/p&gt;
&lt;p&gt;The bots scanning from halfway around the world? They now see nothing but a silent drop. Your logs stay clean, your resources stay available for legitimate users, and your threat surface shrinks to a fraction of what it&amp;nbsp;was.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data"&gt;MaxMind GeoLite2 Free Geolocation&amp;nbsp;Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openbsd.org/faq/pf/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; - The OpenBSD Packet Filter&lt;/a&gt; (FreeBSD&amp;#8217;s &lt;span class="caps"&gt;PF&lt;/span&gt; derives from&amp;nbsp;OpenBSD)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/firewalls/"&gt;FreeBSD Handbook:&amp;nbsp;Firewalls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=pf.conf"&gt;FreeBSD pf.conf(5) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Sometimes the best security is simply not being there. If an attacker can&amp;#8217;t reach your service, they can&amp;#8217;t attack it. Geographic filtering makes your server invisible to most of the internet while remaining fully accessible to the people who actually need&amp;nbsp;it.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="security"/><category term="firewall"/><category term="pf"/><category term="geoip"/><category term="mail"/></entry><entry><title>Managing FreeBSD Jails with Ansible: The jailexec Connection Plugin</title><link href="https://blog.hofstede.it/managing-freebsd-jails-with-ansible-the-jailexec-connection-plugin/" rel="alternate"/><published>2025-12-31T00:00:00+01:00</published><updated>2025-12-31T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-12-31:/managing-freebsd-jails-with-ansible-the-jailexec-connection-plugin/</id><summary type="html">&lt;p&gt;A custom Ansible connection plugin that enables native management of FreeBSD jails via jexec, without requiring &lt;span class="caps"&gt;SSH&lt;/span&gt; inside each&amp;nbsp;jail.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;FreeBSD jails are elegant isolation containers, but managing them with Ansible has traditionally required either running &lt;span class="caps"&gt;SSH&lt;/span&gt; daemons inside each jail or using awkward workarounds. The &lt;strong&gt;jailexec&lt;/strong&gt; connection plugin solves this by connecting to the jail host via &lt;span class="caps"&gt;SSH&lt;/span&gt; and&amp;nbsp;using &lt;code&gt;jexec&lt;/code&gt; to execute commands inside jails - just like you would&amp;nbsp;manually.&lt;/p&gt;
&lt;p&gt;&lt;img alt="jailexec in action" src="https://blog.hofstede.it/images/2025-12-31-ansible-jailexec.png" title="The jailexec connection plugin executing Ansible ping against multiple FreeBSD jails"&gt;&lt;/p&gt;
&lt;h2 id="the-problem-with-jail-management"&gt;The Problem with Jail&amp;nbsp;Management&lt;/h2&gt;
&lt;p&gt;When automating FreeBSD jail infrastructure, you face a&amp;nbsp;choice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run &lt;span class="caps"&gt;SSH&lt;/span&gt; in every jail&lt;/strong&gt; - Works, but defeats much of the security benefit of jails and adds management&amp;nbsp;overhead&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use&amp;nbsp;the &lt;code&gt;jail&lt;/code&gt; connection plugin&lt;/strong&gt; - Requires Ansible to run directly on the jail host, limiting remote&amp;nbsp;management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual jexec wrapper scripts&lt;/strong&gt; - Brittle, hard to maintain, and doesn&amp;#8217;t integrate cleanly with Ansible&amp;#8217;s&amp;nbsp;ecosystem&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What I wanted was simple: &lt;span class="caps"&gt;SSH&lt;/span&gt; to my jail host, then&amp;nbsp;use &lt;code&gt;jexec&lt;/code&gt; to run commands inside any jail. That&amp;#8217;s exactly what the jailexec plugin&amp;nbsp;does.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How It&amp;nbsp;Works&lt;/h2&gt;
&lt;p&gt;The plugin extends Ansible&amp;#8217;s &lt;span class="caps"&gt;SSH&lt;/span&gt; connection to create a two-hop execution&amp;nbsp;model:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[ Control Machine ] --SSH--&amp;gt; [ Jail Host ] --jexec--&amp;gt; [ Jail ]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When you run an Ansible task against a&amp;nbsp;jail:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The plugin establishes an &lt;span class="caps"&gt;SSH&lt;/span&gt; connection to the jail&amp;nbsp;host&lt;/li&gt;
&lt;li&gt;Commands are wrapped with privilege escalation&amp;nbsp;(&lt;code&gt;doas&lt;/code&gt; or &lt;code&gt;sudo&lt;/code&gt;)&amp;nbsp;and &lt;code&gt;jexec&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Output is captured and returned to Ansible as if running directly in the&amp;nbsp;jail&lt;/li&gt;
&lt;li&gt;File transfers use a two-stage process: upload to the host, then move into the jail&amp;#8217;s&amp;nbsp;filesystem&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means your jails don&amp;#8217;t need &lt;span class="caps"&gt;SSH&lt;/span&gt; or any additional services - just a working FreeBSD&amp;nbsp;environment.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation&lt;/h2&gt;
&lt;p&gt;The plugin is a single Python file. Drop it into your Ansible plugins&amp;nbsp;directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# User-specific installation&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;~/.ansible/plugins/connection/
curl&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;jailexec.py&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/chofstede/ansible_jailexec/main/jailexec.py
mv&lt;span class="w"&gt; &lt;/span&gt;jailexec.py&lt;span class="w"&gt; &lt;/span&gt;~/.ansible/plugins/connection/

&lt;span class="c1"&gt;# Or project-specific&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;connection_plugins/
cp&lt;span class="w"&gt; &lt;/span&gt;jailexec.py&lt;span class="w"&gt; &lt;/span&gt;connection_plugins/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For project-specific installation, add to&amp;nbsp;your &lt;code&gt;ansible.cfg&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[defaults]&lt;/span&gt;
&lt;span class="na"&gt;connection_plugins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;./connection_plugins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="inventory-configuration"&gt;Inventory&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;The key difference from standard Ansible inventory is specifying the jail host separately from the jail&amp;nbsp;itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[freebsd_hosts]&lt;/span&gt;
&lt;span class="na"&gt;jail-host.example.com ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ssh ansible_user=ansible ansible_port=22&lt;/span&gt;

&lt;span class="k"&gt;[freebsd_jails]&lt;/span&gt;
&lt;span class="na"&gt;web-jail    ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=jail-host.example.com ansible_user=ansible&lt;/span&gt;
&lt;span class="na"&gt;db-jail     ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=jail-host.example.com ansible_user=ansible&lt;/span&gt;
&lt;span class="na"&gt;app-jail    ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=jail-host.example.com ansible_user=ansible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The inventory hostname becomes the jail name by default. &lt;span class="caps"&gt;SSH&lt;/span&gt; authentication settings are inherited from your &lt;span class="caps"&gt;SSH&lt;/span&gt; configuration or can be specified&amp;nbsp;per-host.&lt;/p&gt;
&lt;h3 id="configuration-variables"&gt;Configuration&amp;nbsp;Variables&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansible_jail_host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(required)&lt;/td&gt;
&lt;td&gt;FreeBSD host running the jails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansible_jail_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inventory_hostname&lt;/td&gt;
&lt;td&gt;Override jail name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansible_jail_user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User for command execution in jail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansible_jail_privilege_escalation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;doas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Privilege escalation method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansible_jail_remote_tmp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/tmp/.ansible/tmp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporary directory path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="jail-host-setup"&gt;Jail Host&amp;nbsp;Setup&lt;/h2&gt;
&lt;p&gt;The &lt;span class="caps"&gt;SSH&lt;/span&gt; user on your jail host needs privilege escalation configured for jail management commands. I&amp;nbsp;recommend &lt;code&gt;doas&lt;/code&gt; for its&amp;nbsp;simplicity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install doas&lt;/span&gt;
pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;doas

&lt;span class="c1"&gt;# Configure /usr/local/etc/doas.conf&lt;/span&gt;
permit&lt;span class="w"&gt; &lt;/span&gt;nopass&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;jls
permit&lt;span class="w"&gt; &lt;/span&gt;nopass&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;jexec
permit&lt;span class="w"&gt; &lt;/span&gt;nopass&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;mkdir
permit&lt;span class="w"&gt; &lt;/span&gt;nopass&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;mv
permit&lt;span class="w"&gt; &lt;/span&gt;nopass&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;rm
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For sudo, the equivalent&amp;nbsp;in &lt;code&gt;/usr/local/etc/sudoers&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;NOPASSWD:&lt;span class="w"&gt; &lt;/span&gt;/usr/sbin/jls,&lt;span class="w"&gt; &lt;/span&gt;/usr/sbin/jexec,&lt;span class="w"&gt; &lt;/span&gt;/bin/mkdir,&lt;span class="w"&gt; &lt;/span&gt;/bin/mv,&lt;span class="w"&gt; &lt;/span&gt;/bin/rm
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="basic-usage"&gt;Basic&amp;nbsp;Usage&lt;/h2&gt;
&lt;p&gt;Test&amp;nbsp;connectivity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;hosts.ini&lt;span class="w"&gt; &lt;/span&gt;freebsd_jails&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;ping
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Expected&amp;nbsp;output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;web&lt;/span&gt;&lt;span class="mi"&gt;-&lt;/span&gt;&lt;span class="err"&gt;jail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SUCCESS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;changed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;ping&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pong&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Run commands inside&amp;nbsp;jails:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check FreeBSD version in all jails&lt;/span&gt;
ansible&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;hosts.ini&lt;span class="w"&gt; &lt;/span&gt;freebsd_jails&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;freebsd-version&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Install packages&lt;/span&gt;
ansible&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;hosts.ini&lt;span class="w"&gt; &lt;/span&gt;freebsd_jails&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;community.general.pkgng&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name=nginx state=present&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="example-playbook"&gt;Example&amp;nbsp;Playbook&lt;/h2&gt;
&lt;p&gt;A typical playbook for configuring a web server&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Configure web server jail&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;web-jail&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;jailexec&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;become&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Install nginx&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;community.general.pkgng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;nginx&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;present&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Copy nginx configuration&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;files/nginx.conf&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/usr/local/etc/nginx/nginx.conf&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;root&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;wheel&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;0644&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;restart nginx&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Enable and start nginx&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;nginx&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;started&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;restart nginx&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;nginx&lt;/span&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 l-Scalar l-Scalar-Plain"&gt;restarted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="multi-environment-inventory"&gt;Multi-Environment&amp;nbsp;Inventory&lt;/h2&gt;
&lt;p&gt;For more complex setups with multiple jail&amp;nbsp;hosts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[production_jails]&lt;/span&gt;
&lt;span class="na"&gt;prod-web-01   ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=prod-host-01.example.com&lt;/span&gt;
&lt;span class="na"&gt;prod-web-02   ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=prod-host-02.example.com&lt;/span&gt;
&lt;span class="na"&gt;prod-db-01    ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=prod-host-01.example.com  ansible_jail_user=postgres&lt;/span&gt;

&lt;span class="k"&gt;[staging_jails]&lt;/span&gt;
&lt;span class="na"&gt;stage-web     ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=stage-host.example.com&lt;/span&gt;
&lt;span class="na"&gt;stage-db      ansible_connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jailexec  ansible_jail_host=stage-host.example.com&lt;/span&gt;

&lt;span class="k"&gt;[all_jails:children]&lt;/span&gt;
&lt;span class="na"&gt;production_jails&lt;/span&gt;
&lt;span class="na"&gt;staging_jails&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="security-design"&gt;Security&amp;nbsp;Design&lt;/h2&gt;
&lt;p&gt;The plugin implements several security&amp;nbsp;measures:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Input validation&lt;/strong&gt;: Jail names are validated against FreeBSD naming conventions. Path traversal attempts&amp;nbsp;(&lt;code&gt;..&lt;/code&gt;) and shell injection patterns are&amp;nbsp;blocked.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two-stage file transfers&lt;/strong&gt;: Files are first uploaded to a temporary location the &lt;span class="caps"&gt;SSH&lt;/span&gt; user can access, then moved into the jail using privilege escalation. This prevents direct writes to privileged&amp;nbsp;paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Secure temporary files&lt;/strong&gt;: Temporary files use mode 600 and are cleaned up on&amp;nbsp;failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connection reuse&lt;/strong&gt;: &lt;span class="caps"&gt;SSH&lt;/span&gt; connections are pooled, reducing authentication overhead and&amp;nbsp;exposure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Privilege escalation control&lt;/strong&gt;: You choose&amp;nbsp;between &lt;code&gt;doas&lt;/code&gt; or &lt;code&gt;sudo&lt;/code&gt;, and can lock down exactly which commands are&amp;nbsp;permitted.&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;Enable verbose output for&amp;nbsp;debugging:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Maximum verbosity&lt;/span&gt;
ansible&lt;span class="w"&gt; &lt;/span&gt;-vvv&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;hosts.ini&lt;span class="w"&gt; &lt;/span&gt;freebsd_jails&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;ping

&lt;span class="c1"&gt;# With Ansible debugging&lt;/span&gt;
&lt;span class="nv"&gt;ANSIBLE_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;-vvv&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;hosts.ini&lt;span class="w"&gt; &lt;/span&gt;freebsd_jails&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;ping
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="common-issues"&gt;Common&amp;nbsp;Issues&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;No jail host specified&amp;#8221;&lt;/strong&gt;&amp;nbsp;Add &lt;code&gt;ansible_jail_host=your-host.example.com&lt;/code&gt; to your&amp;nbsp;inventory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Permission denied accessing jail&amp;#8221;&lt;/strong&gt;
Verify doas/sudo configuration. Test manually on the jail&amp;nbsp;host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;doas&lt;span class="w"&gt; &lt;/span&gt;jls
doas&lt;span class="w"&gt; &lt;/span&gt;jexec&lt;span class="w"&gt; &lt;/span&gt;web-jail&lt;span class="w"&gt; &lt;/span&gt;/bin/sh&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Jail &amp;#8216;name&amp;#8217; not found&amp;#8221;&lt;/strong&gt;
Check that the jail is&amp;nbsp;running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;jls
service&lt;span class="w"&gt; &lt;/span&gt;jail&lt;span class="w"&gt; &lt;/span&gt;onestart&lt;span class="w"&gt; &lt;/span&gt;web-jail
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;File transfer failures&lt;/strong&gt;
Verify the &lt;span class="caps"&gt;SSH&lt;/span&gt; user can write&amp;nbsp;to &lt;code&gt;/tmp&lt;/code&gt; on the jail host and that disk space is&amp;nbsp;available.&lt;/p&gt;
&lt;h2 id="future-development"&gt;Future&amp;nbsp;Development&lt;/h2&gt;
&lt;p&gt;The plugin is actively maintained. Potential future&amp;nbsp;additions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ansible Collection packaging for easier&amp;nbsp;distribution&lt;/li&gt;
&lt;li&gt;Support for jail templates and cloning&amp;nbsp;operations&lt;/li&gt;
&lt;li&gt;Parallel execution&amp;nbsp;optimizations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Contributions are welcome on &lt;a href="https://github.com/chofstede/ansible_jailexec"&gt;GitHub&lt;/a&gt; or &lt;a href="https://codeberg.org/Larvitz/ansible_jailexec"&gt;Codeberg&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The jailexec plugin fills a gap in FreeBSD automation tooling. It brings proper Ansible support to jail management without compromising the security benefits of running minimal jail&amp;nbsp;environments.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re managing FreeBSD jails at any scale, this plugin lets you treat them as first-class Ansible targets - the same playbooks, roles, and patterns you use everywhere else, now working seamlessly with FreeBSD&amp;#8217;s native&amp;nbsp;containerization.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/chofstede/ansible_jailexec"&gt;jailexec on&amp;nbsp;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codeberg.org/Larvitz/ansible_jailexec"&gt;jailexec on&amp;nbsp;Codeberg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook -&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/plugins/connection.html"&gt;Ansible Connection Plugins&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/doas"&gt;doas - dedicated OpenBSD application&amp;nbsp;subexecutor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="ansible"/><category term="jails"/><category term="automation"/><category term="devops"/></entry><entry><title>FreeBSD 15 Cloud-Init on Proxmox: Working Around nuageinit’s Network-Config Gap</title><link href="https://blog.hofstede.it/freebsd-15-cloud-init-on-proxmox-working-around-nuageinits-network-config-gap/" rel="alternate"/><published>2025-12-28T00:00:00+01:00</published><updated>2025-12-28T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-12-28:/freebsd-15-cloud-init-on-proxmox-working-around-nuageinits-network-config-gap/</id><summary type="html">&lt;p&gt;Proxmox &lt;span class="caps"&gt;VE&lt;/span&gt; generates network-config v1, but FreeBSD 15&amp;#8217;s nuageinit only speaks v2. Here&amp;#8217;s a script that bridges the gap for static &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;configuration.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;FreeBSD 15.0-&lt;span class="caps"&gt;RELEASE&lt;/span&gt; shipped earlier this month with &lt;span class="caps"&gt;VM&lt;/span&gt; images that are &amp;#8220;cloud-init compatible&amp;#8221; with a catch. Instead of using the Python-based cloud-init that Linux distributions rely on, FreeBSD implemented their own solution: nuageinit(7). It&amp;#8217;s lighter, written in Lua, and fits FreeBSD&amp;#8217;s philosophy of minimal dependencies. The only problem? It doesn&amp;#8217;t understand network-config version&amp;nbsp;1.&lt;/p&gt;
&lt;p&gt;This matters because Proxmox &lt;span class="caps"&gt;VE&lt;/span&gt;-even in version 9.1 still generates network-config v1 format when you configure cloud-init through the web &lt;span class="caps"&gt;UI&lt;/span&gt;. The result: your carefully configured static &lt;span class="caps"&gt;IP&lt;/span&gt; settings are silently ignored, and the &lt;span class="caps"&gt;VM&lt;/span&gt; falls back to &lt;span class="caps"&gt;DHCP&lt;/span&gt;. For production environments where predictable addressing matters, this is a&amp;nbsp;deal-breaker.&lt;/p&gt;
&lt;h2 id="the-version-mismatch"&gt;The Version&amp;nbsp;Mismatch&lt;/h2&gt;
&lt;p&gt;Cloud-init&amp;#8217;s network configuration has evolved through several&amp;nbsp;versions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version 1&lt;/strong&gt;: The original format, with nested dictionaries and explicit type&amp;nbsp;fields&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version 2 (Netplan)&lt;/strong&gt;: A cleaner &lt;span class="caps"&gt;YAML&lt;/span&gt; structure that became the de facto&amp;nbsp;standard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;#8217;s what Proxmox generates&amp;nbsp;(v1):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;physical&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;vtnet0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;static&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.254.254.42/24&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.254.254.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And what nuageinit expects&amp;nbsp;(v2):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2&lt;/span&gt;
&lt;span class="nt"&gt;ethernets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;vtnet0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.254.254.42/24&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;gateway4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.254.254.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The structures are fundamentally different. nuageinit simply doesn&amp;#8217;t parse v1, leaving your &lt;span class="caps"&gt;VM&lt;/span&gt; with &lt;span class="caps"&gt;DHCP&lt;/span&gt;-assigned addresses instead of the static configuration you&amp;nbsp;specified.&lt;/p&gt;
&lt;h2 id="the-workaround"&gt;The&amp;nbsp;Workaround&lt;/h2&gt;
&lt;p&gt;Until Proxmox adds v2 support or nuageinit gains v1 compatibility, the solution is to generate your own cloud-init &lt;span class="caps"&gt;ISO&lt;/span&gt;. The script below runs on a Proxmox host and creates a properly formatted &lt;span class="caps"&gt;ISO&lt;/span&gt; that FreeBSD will actually&amp;nbsp;understand.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;Save the script&amp;nbsp;as &lt;code&gt;/usr/local/bin/freebsd-cloudinit-iso&lt;/code&gt; and make it&amp;nbsp;executable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/freebsd-cloudinit-iso
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Dependencies are&amp;nbsp;minimal-just &lt;code&gt;genisoimage&lt;/code&gt; or &lt;code&gt;mkisofs&lt;/code&gt;, which are typically already present on Proxmox&amp;nbsp;hosts.&lt;/p&gt;
&lt;h3 id="basic-usage"&gt;Basic&amp;nbsp;Usage&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;freebsd-cloudinit-iso&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--vmid&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;203&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--hostname&lt;span class="w"&gt; &lt;/span&gt;myfreebsd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--domain&lt;span class="w"&gt; &lt;/span&gt;example.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;admin&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--password&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;secretpassword&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ip4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.254.42/24&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--gw4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.254.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dns&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.1.1.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dns&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.0.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--storage&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This creates an &lt;span class="caps"&gt;ISO&lt;/span&gt;&amp;nbsp;with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hostname set&amp;nbsp;to &lt;code&gt;myfreebsd.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A&amp;nbsp;user &lt;code&gt;admin&lt;/code&gt; with sudo privileges and the specified&amp;nbsp;password&lt;/li&gt;
&lt;li&gt;Static IPv4&amp;nbsp;configuration&lt;/li&gt;
&lt;li&gt;Cloudflare &lt;span class="caps"&gt;DNS&lt;/span&gt;&amp;nbsp;servers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;span class="caps"&gt;ISO&lt;/span&gt; is automatically attached to &lt;span class="caps"&gt;VM&lt;/span&gt; 203 as a &lt;span class="caps"&gt;CD&lt;/span&gt;-&lt;span class="caps"&gt;ROM&lt;/span&gt;&amp;nbsp;drive.&lt;/p&gt;
&lt;h3 id="dual-stack-configuration"&gt;Dual-Stack&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;For IPv6-first&amp;nbsp;environments:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;freebsd-cloudinit-iso&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--vmid&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;204&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--hostname&lt;span class="w"&gt; &lt;/span&gt;webserver&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--domain&lt;span class="w"&gt; &lt;/span&gt;prod.example.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;deploy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ssh-key-file&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_ed25519.pub&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ip6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2001&lt;/span&gt;:db8::50/64&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--gw6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2001&lt;/span&gt;:db8::1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ip4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.0.0.50/24&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--gw4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.0.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dns&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2001&lt;/span&gt;:db8::1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dns&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.0.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--storage&lt;span class="w"&gt; &lt;/span&gt;ceph-nvme
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that IPv6 addresses are listed first in the generated config-nuageinit applies them in order, and IPv6-first makes sense for modern&amp;nbsp;networks.&lt;/p&gt;
&lt;h3 id="ssh-key-authentication"&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; Key&amp;nbsp;Authentication&lt;/h3&gt;
&lt;p&gt;For production deployments, prefer &lt;span class="caps"&gt;SSH&lt;/span&gt; keys over&amp;nbsp;passwords:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;freebsd-cloudinit-iso&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--vmid&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;205&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--hostname&lt;span class="w"&gt; &lt;/span&gt;secure&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--domain&lt;span class="w"&gt; &lt;/span&gt;internal.lan&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;ansible&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ssh-key-file&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/automation.pub&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--ip4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.100.0.10/24&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--gw4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.100.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--dns&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.100.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--storage&lt;span class="w"&gt; &lt;/span&gt;local-zfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also pass the key directly&amp;nbsp;with &lt;code&gt;--ssh-key "ssh-ed25519 AAAA..."&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-the-script-generates"&gt;What the Script&amp;nbsp;Generates&lt;/h2&gt;
&lt;p&gt;The script creates three files in the &lt;span class="caps"&gt;ISO&lt;/span&gt;:&lt;/p&gt;
&lt;h3 id="meta-data"&gt;meta-data&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;instance-id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;203-1735400000&lt;/span&gt;
&lt;span class="nt"&gt;local-hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;myfreebsd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The instance-id uses the &lt;span class="caps"&gt;VM&lt;/span&gt; &lt;span class="caps"&gt;ID&lt;/span&gt; and timestamp, ensuring cloud-init recognizes each boot as potentially needing&amp;nbsp;reconfiguration.&lt;/p&gt;
&lt;h3 id="user-data"&gt;user-data&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#cloud-config&lt;/span&gt;
&lt;span class="nt"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;myfreebsd&lt;/span&gt;
&lt;span class="nt"&gt;fqdn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;myfreebsd.example.com&lt;/span&gt;
&lt;span class="nt"&gt;manage_etc_hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="nt"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;admin&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;wheel&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/bin/sh&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;sudo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ALL=(ALL) NOPASSWD:ALL&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;lock_passwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;passwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;$6$rounds=5000$...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ssh_authorized_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ssh-ed25519 AAAA...&lt;/span&gt;

&lt;span class="nt"&gt;ssh_pwauth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;chpasswd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Passwords are hashed using &lt;span class="caps"&gt;SHA&lt;/span&gt;-512 before being written-plaintext credentials never touch the &lt;span class="caps"&gt;ISO&lt;/span&gt;.&lt;/p&gt;
&lt;h3 id="network-config"&gt;network-config&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2&lt;/span&gt;
&lt;span class="nt"&gt;ethernets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;vtnet0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2001:db8::50/64&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.0.0.50/24&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;gateway6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2001:db8::1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;gateway4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.0.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;nameservers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2001:db8::1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10.0.0.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;prod.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the crucial part-version 2 format that nuageinit actually&amp;nbsp;parses.&lt;/p&gt;
&lt;h2 id="full-option-reference"&gt;Full Option&amp;nbsp;Reference&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Required:
  --vmid &amp;lt;id&amp;gt;           VM ID in Proxmox
  --hostname &amp;lt;name&amp;gt;     Hostname (without domain)
  --user &amp;lt;username&amp;gt;     Username to create

Network (at least one required):
  --ip4 &amp;lt;addr/prefix&amp;gt;   IPv4 address with prefix (e.g., 10.0.0.50/24)
  --gw4 &amp;lt;addr&amp;gt;          IPv4 gateway
  --ip6 &amp;lt;addr/prefix&amp;gt;   IPv6 address with prefix
  --gw6 &amp;lt;addr&amp;gt;          IPv6 gateway

Authentication (at least one required):
  --password &amp;lt;pass&amp;gt;       Password for user (will be hashed)
  --root-password &amp;lt;pass&amp;gt;  Password for root (will be hashed)
  --ssh-key &amp;lt;key&amp;gt;         SSH public key string
  --ssh-key-file &amp;lt;file&amp;gt;   Path to SSH public key file

DNS:
  --dns &amp;lt;addr&amp;gt;          DNS server (repeatable)
  --search &amp;lt;domain&amp;gt;     Search domain (repeatable)
  --domain &amp;lt;domain&amp;gt;     Domain (also added to search)

Storage:
  --storage &amp;lt;name&amp;gt;      Proxmox storage for ISO (default: auto-detect)

Options:
  --iface &amp;lt;name&amp;gt;        Network interface (default: vtnet0)
  --groups &amp;lt;groups&amp;gt;     Comma-separated groups (default: wheel)
  --output-dir &amp;lt;dir&amp;gt;    Output directory instead of storage
  --no-attach           Create ISO but don&amp;#39;t attach to VM
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="workflow-integration"&gt;Workflow&amp;nbsp;Integration&lt;/h2&gt;
&lt;h3 id="with-terraformopentofu"&gt;With&amp;nbsp;Terraform/OpenTofu&lt;/h3&gt;
&lt;p&gt;If you&amp;#8217;re provisioning FreeBSD VMs with Terraform, call the script as a local-exec provisioner after creating the &lt;span class="caps"&gt;VM&lt;/span&gt; but before first&amp;nbsp;boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;&amp;quot;proxmox_vm_qemu&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;&amp;quot;freebsd&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;freebsd-web&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;vmid&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;210&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;target_node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pve1&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;  # ... other config ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;&amp;quot;null_resource&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;&amp;quot;cloudinit&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;proxmox_vm_qemu.freebsd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kr"&gt;provisioner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;&amp;quot;local-exec&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="dl"&gt;EOT&lt;/span&gt;
&lt;span class="sh"&gt;      ssh root@pve1 &amp;#39;freebsd-cloudinit-iso \&lt;/span&gt;
&lt;span class="sh"&gt;        --vmid 210 \&lt;/span&gt;
&lt;span class="sh"&gt;        --hostname freebsd-web \&lt;/span&gt;
&lt;span class="sh"&gt;        --domain example.com \&lt;/span&gt;
&lt;span class="sh"&gt;        --user terraform \&lt;/span&gt;
&lt;span class="sh"&gt;        --ssh-key-file /root/.ssh/terraform.pub \&lt;/span&gt;
&lt;span class="sh"&gt;        --ip4 10.0.0.210/24 \&lt;/span&gt;
&lt;span class="sh"&gt;        --gw4 10.0.0.1 \&lt;/span&gt;
&lt;span class="sh"&gt;        --dns 10.0.0.1 \&lt;/span&gt;
&lt;span class="sh"&gt;        --storage local&amp;#39;&lt;/span&gt;
&lt;span class="dl"&gt;    EOT&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="with-ansible"&gt;With&amp;nbsp;Ansible&lt;/h3&gt;
&lt;p&gt;For Ansible-driven&amp;nbsp;provisioning:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Generate FreeBSD cloud-init ISO&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;delegate_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;proxmox_host&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;freebsd-cloudinit-iso&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--vmid {{ vm_id }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--hostname {{ inventory_hostname_short }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--domain {{ domain }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--user {{ ansible_user }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--ssh-key-file /root/.ssh/ansible.pub&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--ip4 {{ ansible_host }}/24&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--gw4 {{ gateway }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--dns {{ dns_server }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;--storage {{ proxmox_storage }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="limitations-and-caveats"&gt;Limitations and&amp;nbsp;Caveats&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Single interface only&lt;/strong&gt;: The script currently supports one network interface. Multiple NICs would require extending the network-config&amp;nbsp;generation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No &lt;span class="caps"&gt;DHCP&lt;/span&gt; option&lt;/strong&gt;: This is intentionally for static configurations. If you want &lt;span class="caps"&gt;DHCP&lt;/span&gt;, the default Proxmox cloud-init works fine (nuageinit falls back to &lt;span class="caps"&gt;DHCP&lt;/span&gt; when it can&amp;#8217;t parse the network&amp;nbsp;config).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VirtIO interface name&lt;/strong&gt;: The&amp;nbsp;default &lt;code&gt;vtnet0&lt;/code&gt; assumes VirtIO network devices. If you&amp;#8217;re using emulated e1000 or another driver,&amp;nbsp;specify &lt;code&gt;--iface&lt;/code&gt; accordingly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storage detection&lt;/strong&gt;: Auto-detection looks for storage&amp;nbsp;with &lt;code&gt;iso&lt;/code&gt; content type. If your Proxmox setup is non-standard,&amp;nbsp;specify &lt;code&gt;--storage&lt;/code&gt; explicitly.&lt;/p&gt;
&lt;h2 id="why-not-just-fix-proxmox"&gt;Why Not Just Fix&amp;nbsp;Proxmox?&lt;/h2&gt;
&lt;p&gt;Fair question. There&amp;#8217;s an open feature request for network-config v2 support in Proxmox, but it&amp;#8217;s not trivial-the entire cloud-init subsystem assumes v1 internally. FreeBSD&amp;#8217;s nuageinit could also add v1 parsing, but the project explicitly chose v2 as the modern&amp;nbsp;format.&lt;/p&gt;
&lt;p&gt;In the meantime, this script fills the gap. It&amp;#8217;s not elegant, but it works reliably and integrates cleanly with existing Proxmox&amp;nbsp;workflows.&lt;/p&gt;
&lt;h2 id="the-script"&gt;The&amp;nbsp;Script&lt;/h2&gt;
&lt;p&gt;The full script is available below. It&amp;#8217;s a single Bash file with no external dependencies beyond standard Proxmox&amp;nbsp;tools:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# freebsd-cloudinit-iso - Generate cloud-init ISO for FreeBSD VMs on Proxmox&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# This works around FreeBSD 15&amp;#39;s nuageinit not supporting network-config v1&lt;/span&gt;
&lt;span class="c1"&gt;# by generating v2 format that nuageinit actually understands.&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="c1"&gt;# Defaults&lt;/span&gt;
&lt;span class="nv"&gt;VMID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;SSH_KEY_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;IP4&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;GW4&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;IP6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;GW6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;DNS&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="nv"&gt;STORAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;IFACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ATTACH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="nv"&gt;USER_GROUPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;wheel&amp;quot;&lt;/span&gt;

usage&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;Usage: $(basename &amp;quot;$0&amp;quot;) [options]&lt;/span&gt;

&lt;span class="s"&gt;Required:&lt;/span&gt;
&lt;span class="s"&gt;  --vmid &amp;lt;id&amp;gt;           VM ID&lt;/span&gt;
&lt;span class="s"&gt;  --hostname &amp;lt;name&amp;gt;     Hostname (without domain)&lt;/span&gt;
&lt;span class="s"&gt;  --user &amp;lt;username&amp;gt;     Username to create&lt;/span&gt;

&lt;span class="s"&gt;Network (at least one of --ip4 or --ip6 required):&lt;/span&gt;
&lt;span class="s"&gt;  --ip4 &amp;lt;addr/prefix&amp;gt;   IPv4 address with prefix (e.g., 10.0.0.50/24)&lt;/span&gt;
&lt;span class="s"&gt;  --gw4 &amp;lt;addr&amp;gt;          IPv4 gateway&lt;/span&gt;
&lt;span class="s"&gt;  --ip6 &amp;lt;addr/prefix&amp;gt;   IPv6 address with prefix (e.g., 2001:db8::50/64)&lt;/span&gt;
&lt;span class="s"&gt;  --gw6 &amp;lt;addr&amp;gt;          IPv6 gateway&lt;/span&gt;

&lt;span class="s"&gt;Authentication (at least one required):&lt;/span&gt;
&lt;span class="s"&gt;  --password &amp;lt;pass&amp;gt;     Password for user (plaintext, will be hashed)&lt;/span&gt;
&lt;span class="s"&gt;  --root-password &amp;lt;pass&amp;gt; Password for root (plaintext, will be hashed)&lt;/span&gt;
&lt;span class="s"&gt;  --ssh-key &amp;lt;key&amp;gt;       SSH public key string&lt;/span&gt;
&lt;span class="s"&gt;  --ssh-key-file &amp;lt;file&amp;gt; Path to SSH public key file&lt;/span&gt;

&lt;span class="s"&gt;DNS:&lt;/span&gt;
&lt;span class="s"&gt;  --dns &amp;lt;addr&amp;gt;          DNS server (can be specified multiple times)&lt;/span&gt;
&lt;span class="s"&gt;  --search &amp;lt;domain&amp;gt;     Search domain (can be specified multiple times)&lt;/span&gt;
&lt;span class="s"&gt;  --domain &amp;lt;domain&amp;gt;     Domain name (also added to search if not present)&lt;/span&gt;

&lt;span class="s"&gt;Storage:&lt;/span&gt;
&lt;span class="s"&gt;  --storage &amp;lt;name&amp;gt;      Proxmox storage name (default: auto-detect)&lt;/span&gt;

&lt;span class="s"&gt;Options:&lt;/span&gt;
&lt;span class="s"&gt;  --iface &amp;lt;name&amp;gt;        Network interface name (default: vtnet0)&lt;/span&gt;
&lt;span class="s"&gt;  --groups &amp;lt;groups&amp;gt;     Comma-separated groups (default: wheel)&lt;/span&gt;
&lt;span class="s"&gt;  --output-dir &amp;lt;dir&amp;gt;    Output directory for ISO (default: storage path)&lt;/span&gt;
&lt;span class="s"&gt;  --no-attach           Don&amp;#39;t attach ISO to VM, just create it&lt;/span&gt;
&lt;span class="s"&gt;  --help                Show this help&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

error&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

warn&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Warning: &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Parse arguments&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--vmid&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nv"&gt;VMID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--hostname&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--domain&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--user&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--password&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--root-password&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--ssh-key&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--ssh-key-file&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SSH_KEY_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--ip4&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;IP4&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--gw4&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;GW4&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--ip6&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;IP6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--gw6&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;GW6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--dns&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;DNS&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--search&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--storage&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;STORAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--iface&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;IFACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--groups&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nv"&gt;USER_GROUPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--output-dir&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--no-attach&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;ATTACH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;false&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--help&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;usage&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;*&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unknown option: &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c1"&gt;# Validation&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VMID&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--vmid is required&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--hostname is required&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--user is required&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$IP4&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$IP6&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;At least one of --ip4 or --ip6 is required&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;At least one of --password, --ssh-key, or --ssh-key-file is required&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Check for required tools&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;genisoimage&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;mkisofs&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;genisoimage or mkisofs required&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;qm&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;warn&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;qm not found - not running on Proxmox?&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Load SSH key from file if specified&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;SSH key file not found: &lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;SSH_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Find storage path&lt;/span&gt;
find_storage_path&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$OUTPUT_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$OUTPUT_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pvesm&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;--content&lt;span class="w"&gt; &lt;/span&gt;iso&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NR&amp;gt;1 {print $1; exit}&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;No storage with &amp;#39;iso&amp;#39; content type found. Specify --storage or --output-dir&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pvesm&lt;span class="w"&gt; &lt;/span&gt;path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;storage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:iso/dummy.iso&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s|/dummy\.iso$||&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Could not determine path for storage: &lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;STORAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;SNIPPETS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find_storage_path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$STORAGE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SNIPPETS_PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Create temp directory&lt;/span&gt;
&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rm -rf &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;EXIT

&lt;span class="c1"&gt;# Generate password hashes&lt;/span&gt;
&lt;span class="nv"&gt;PASSWORD_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;PASSWORD_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;passwd&lt;span class="w"&gt; &lt;/span&gt;-6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;PASSWORD_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import crypt; print(crypt.crypt(&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;, crypt.mksalt(crypt.METHOD_SHA512)))&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Could not hash password&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nv"&gt;ROOT_PASSWORD_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;ROOT_PASSWORD_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;passwd&lt;span class="w"&gt; &lt;/span&gt;-6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;ROOT_PASSWORD_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import crypt; print(crypt.crypt(&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;, crypt.mksalt(crypt.METHOD_SHA512)))&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;error&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Could not hash root password&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Build FQDN&lt;/span&gt;
&lt;span class="nv"&gt;FQDN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;FQDN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Add domain to search if not present&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;domain_in_search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;domain_in_search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;$domain_in_search&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Create meta-data&lt;/span&gt;
cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/meta-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;instance-id: $(uuidgen 2&amp;gt;/dev/null || echo &amp;quot;${VMID}-$(date +%s)&amp;quot;)&lt;/span&gt;
&lt;span class="s"&gt;local-hostname: ${HOSTNAME}&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

&lt;span class="c1"&gt;# Create user-data&lt;/span&gt;
cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/user-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;#cloud-config&lt;/span&gt;
&lt;span class="s"&gt;hostname: ${HOSTNAME}&lt;/span&gt;
&lt;span class="s"&gt;fqdn: ${FQDN}&lt;/span&gt;
&lt;span class="s"&gt;manage_etc_hosts: true&lt;/span&gt;

&lt;span class="s"&gt;users:&lt;/span&gt;
&lt;span class="s"&gt;  - name: ${USER}&lt;/span&gt;
&lt;span class="s"&gt;    groups: ${USER_GROUPS}&lt;/span&gt;
&lt;span class="s"&gt;    shell: /bin/sh&lt;/span&gt;
&lt;span class="s"&gt;    sudo: ALL=(ALL) NOPASSWD:ALL&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD_HASH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/user-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;    lock_passwd: false&lt;/span&gt;
&lt;span class="s"&gt;    passwd: ${PASSWORD_HASH}&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_KEY&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/user-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;    ssh_authorized_keys:&lt;/span&gt;
&lt;span class="s"&gt;      - ${SSH_KEY}&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD_HASH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/user-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;

&lt;span class="s"&gt;ssh_pwauth: true&lt;/span&gt;
&lt;span class="s"&gt;chpasswd:&lt;/span&gt;
&lt;span class="s"&gt;  expire: false&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_PASSWORD_HASH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/user-data&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;

&lt;span class="s"&gt;chpasswd:&lt;/span&gt;
&lt;span class="s"&gt;  expire: false&lt;/span&gt;
&lt;span class="s"&gt;  users:&lt;/span&gt;
&lt;span class="s"&gt;    - name: root&lt;/span&gt;
&lt;span class="s"&gt;      password: ${ROOT_PASSWORD_HASH}&lt;/span&gt;
&lt;span class="s"&gt;      type: HASH&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Create network-config (v2 format)&lt;/span&gt;
cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;version: 2&lt;/span&gt;
&lt;span class="s"&gt;ethernets:&lt;/span&gt;
&lt;span class="s"&gt;  ${IFACE}:&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;    addresses:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$IP6&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;      - &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$IP4&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;      - &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IP4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GW6&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;    gateway6: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GW6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GW4&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;    gateway4: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GW4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;DNS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;    nameservers:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;DNS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;      addresses:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dns&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DNS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;        - &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;dns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;      search:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;search&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SEARCH&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;        - &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;search&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;/network-config&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Generate ISO&lt;/span&gt;
&lt;span class="nv"&gt;ISO_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;freebsd-ci-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VMID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ISO_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SNIPPETS_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ISO_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;genisoimage&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;MKISO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;genisoimage&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;MKISO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mkisofs&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nv"&gt;$MKISO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-output&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ISO_PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-volid&lt;span class="w"&gt; &lt;/span&gt;cidata&lt;span class="w"&gt; &lt;/span&gt;-joliet&lt;span class="w"&gt; &lt;/span&gt;-rock&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TMPDIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Created: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ISO_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Attach to VM&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ATTACH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;qm&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;qm&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VMID&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;qm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VMID&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--delete&lt;span class="w"&gt; &lt;/span&gt;ide2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;qm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$VMID&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--ide2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STORAGE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:iso/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ISO_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,media=cdrom&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Attached to VM &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VMID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; as ide2&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;warn&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;VM &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VMID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not found, ISO created but not attached&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;FreeBSD 15&amp;#8217;s nuageinit and Proxmox&amp;#8217;s cloud-init generator speak different dialects of the same configuration language. Until upstream fixes arrive, this script provides a reliable workaround for anyone running FreeBSD VMs on Proxmox who needs static network&amp;nbsp;configuration.&lt;/p&gt;
&lt;p&gt;The approach is straightforward: generate the &lt;span class="caps"&gt;ISO&lt;/span&gt; yourself with the correct format. It integrates with existing automation tools, requires no modifications to either Proxmox or FreeBSD, and produces predictable&amp;nbsp;results.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.freebsd.org/releases/15.0R/announce/"&gt;FreeBSD 15.0-&lt;span class="caps"&gt;RELEASE&lt;/span&gt;&amp;nbsp;Announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=nuageinit&amp;amp;sektion=7"&gt;nuageinit(7) man&amp;nbsp;page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html"&gt;Cloud-init Network Config Version&amp;nbsp;2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pve.proxmox.com/wiki/Cloud-Init_Support"&gt;Proxmox Cloud-Init&amp;nbsp;Support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="proxmox"/><category term="cloud-init"/><category term="virtualization"/><category term="automation"/></entry><entry><title>Interactive System Troubleshooting with AI: The Linux MCP Server</title><link href="https://blog.hofstede.it/interactive-system-troubleshooting-with-ai-the-linux-mcp-server/" rel="alternate"/><published>2025-12-25T00:00:00+01:00</published><updated>2025-12-25T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-12-25:/interactive-system-troubleshooting-with-ai-the-linux-mcp-server/</id><summary type="html">&lt;p&gt;How the linux-mcp-server bridges &lt;span class="caps"&gt;AI&lt;/span&gt; assistants and Linux systems for interactive diagnostics, enabling natural language troubleshooting and system&amp;nbsp;analysis.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Troubleshooting Linux systems traditionally involves a familiar dance: run a command, copy the output, paste it into a chat with an &lt;span class="caps"&gt;AI&lt;/span&gt; assistant, wait for suggestions, repeat. The linux-mcp-server changes this paradigm entirely. Instead of being a passive advisor, your &lt;span class="caps"&gt;AI&lt;/span&gt; assistant becomes an active participant that can directly query your systems, correlate information across multiple data sources, and provide contextual analysis in&amp;nbsp;real-time.&lt;/p&gt;
&lt;p&gt;&lt;img alt="AI System Diagnostics" src="https://blog.hofstede.it/images/2025-12-25-linux-mcp-server.png" title="AI-powered system diagnostics"&gt;&lt;/p&gt;
&lt;h2 id="what-is-mcp"&gt;What is &lt;span class="caps"&gt;MCP&lt;/span&gt;?&lt;/h2&gt;
&lt;p&gt;The Model Context Protocol (&lt;span class="caps"&gt;MCP&lt;/span&gt;) is an open standard introduced by Anthropic in November 2024. It defines how &lt;span class="caps"&gt;AI&lt;/span&gt; models can interact with external tools and data sources in a structured, secure way. Think of it as a standardized &lt;span class="caps"&gt;API&lt;/span&gt; that lets &lt;span class="caps"&gt;AI&lt;/span&gt; assistants reach beyond their training data to access live&amp;nbsp;information.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;MCP&lt;/span&gt; servers expose specific capabilities - tools that the &lt;span class="caps"&gt;AI&lt;/span&gt; can invoke when needed. The &lt;span class="caps"&gt;AI&lt;/span&gt; decides when and how to use these tools based on the conversation context, making interactions feel natural rather than&amp;nbsp;scripted.&lt;/p&gt;
&lt;h2 id="enter-linux-mcp-server"&gt;Enter&amp;nbsp;linux-mcp-server&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/rhel-lightspeed/linux-mcp-server"&gt;linux-mcp-server&lt;/a&gt; is a Red Hat project that implements &lt;span class="caps"&gt;MCP&lt;/span&gt; for Linux system administration. It provides read-only diagnostic tools&amp;nbsp;covering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System information&lt;/strong&gt;: &lt;span class="caps"&gt;OS&lt;/span&gt; details, &lt;span class="caps"&gt;CPU&lt;/span&gt;, memory, disk usage, hardware&amp;nbsp;specs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service management&lt;/strong&gt;: systemd service status, logs, and&amp;nbsp;configuration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Process monitoring&lt;/strong&gt;: Running processes with resource&amp;nbsp;metrics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log analysis&lt;/strong&gt;: Journal queries, audit logs, application&amp;nbsp;logs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network diagnostics&lt;/strong&gt;: Interfaces, connections, listening&amp;nbsp;ports&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage analysis&lt;/strong&gt;: Block devices, mount points, directory&amp;nbsp;listings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key design principle is safety: all operations are read-only. The server can inspect your system but cannot modify it, making it safe to use even on production&amp;nbsp;systems.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation&lt;/h2&gt;
&lt;p&gt;The server requires Python 3.10+ and can be installed several&amp;nbsp;ways:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Via pip&lt;/span&gt;
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;linux-mcp-server

&lt;span class="c1"&gt;# Via uv (recommended for isolation)&lt;/span&gt;
uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;linux-mcp-server

&lt;span class="c1"&gt;# Or run directly without installing&lt;/span&gt;
uvx&lt;span class="w"&gt; &lt;/span&gt;linux-mcp-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For Claude Desktop or Claude Code integration, add it to your &lt;span class="caps"&gt;MCP&lt;/span&gt;&amp;nbsp;configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;mcpServers&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;linux-mcp-server&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;command&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;uvx&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;args&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;linux-mcp-server&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="local-and-remote-systems"&gt;Local and Remote&amp;nbsp;Systems&lt;/h2&gt;
&lt;p&gt;One of the most powerful features is transparent &lt;span class="caps"&gt;SSH&lt;/span&gt; support. Every tool accepts an&amp;nbsp;optional &lt;code&gt;host&lt;/code&gt; parameter. Without it, commands run locally. With it, they execute on the remote system via &lt;span class="caps"&gt;SSH&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Query local system
get_system_info()

# Query remote system
get_system_info(host=&amp;quot;webserver.example.com&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; authentication uses key-based auth exclusively (no passwords), reading keys&amp;nbsp;from &lt;code&gt;~/.ssh/&lt;/code&gt; or a path specified&amp;nbsp;via &lt;code&gt;LINUX_MCP_SSH_KEY_PATH&lt;/code&gt;. Your&amp;nbsp;existing &lt;code&gt;~/.ssh/config&lt;/code&gt; is respected, so per-host usernames, ports, and jump hosts work&amp;nbsp;automatically.&lt;/p&gt;
&lt;h2 id="real-world-example-diagnosing-a-remote-server"&gt;Real-World Example: Diagnosing a Remote&amp;nbsp;Server&lt;/h2&gt;
&lt;p&gt;Here&amp;#8217;s where things get interesting. Instead of explaining what the tools do, let me demonstrate by querying a real system. The following analysis was performed live on a server named&amp;nbsp;&amp;#8220;janeway&amp;#8221;:&lt;/p&gt;
&lt;h3 id="system-overview"&gt;System&amp;nbsp;Overview&lt;/h3&gt;
&lt;p&gt;When I ask the &lt;span class="caps"&gt;AI&lt;/span&gt; to check the system, it queries the server&amp;nbsp;directly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;What can you tell me about the janeway&amp;nbsp;server?&amp;#8221;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Hostname: janeway.home.hofstede.it
Operating System: Red Hat Enterprise Linux 10.1 (Coughlan)
Kernel Version: 6.12.0-124.21.1.el10_1.x86_64
Architecture: x86_64
Uptime: up 5 days, 3 hours, 59 minutes
Boot Time: 2025-12-20 12:54:29

CPU Model: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
CPU Cores: 2 logical (1 physical)
Load Average: 0.37, 0.22, 0.13
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a &lt;span class="caps"&gt;RHEL&lt;/span&gt; 10.1 &lt;span class="caps"&gt;VM&lt;/span&gt; running on a Proxmox host, with modest resources allocated for its workload. The &lt;span class="caps"&gt;AI&lt;/span&gt; can immediately see this is a stable system - low load averages and over 5 days of&amp;nbsp;uptime.&lt;/p&gt;
&lt;h3 id="service-health-check"&gt;Service Health&amp;nbsp;Check&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Is the Omada controller running&amp;nbsp;properly?&amp;#8221;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The server runs a &lt;span class="caps"&gt;TP&lt;/span&gt;-Link Omada network controller as a Podman container managed through systemd&amp;nbsp;Quadlets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;● omada-controller.service - TP-Link Omada Controller (Podman Quadlet)
     Loaded: loaded (/etc/containers/systemd/omada-controller.container; generated)
     Active: active (running) since Sat 2025-12-20 12:54:56 CET; 5 days ago
   Main PID: 1398 (conmon)
      Tasks: 317 (limit: 22891)
     Memory: 2.1G (peak: 2.1G)
        CPU: 8h 6min 41.871s
     CGroup: /system.slice/omada-controller.service
             ├─libpod-payload-6cbbd984675b...
             │ ├─1413 java -server -Xms128m -Xmx1024m ...
             │ ├─1811 mongod --port 27217 --dbpath ../data/db ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;AI&lt;/span&gt; correlates multiple data points: the service is active, has been running since boot (matching the system uptime), and is using about 2.&lt;span class="caps"&gt;1GB&lt;/span&gt; of memory for its Java process and embedded MongoDB. It can also see recent log entries showing normal operation - firmware checks running on schedule, topology updates proceeding without&amp;nbsp;errors.&lt;/p&gt;
&lt;h3 id="resource-analysis"&gt;Resource&amp;nbsp;Analysis&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;How are resources looking? Any&amp;nbsp;concerns?&amp;#8221;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;=== RAM Information ===
Total: 3.6GB
Available: 1.4GB
Used: 2.2GB (61.7%)
Free: 550.8MB

=== Swap Information ===
Total: 3.9GB
Used: 0.0B (0.0%)
Free: 3.9GB

=== Filesystem Usage ===
Filesystem             Size  Used Avail Use% Mounted on
/dev/mapper/rhel-root   35G  4.0G   31G  12% /
/dev/vda2              960M  448M  513M  47% /boot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;AI&lt;/span&gt; provides contextual analysis: &amp;#8220;Memory usage at 62% is healthy for a server running a Java application with MongoDB. The fact that swap is completely unused indicates no memory pressure - the system has headroom. Disk usage at 12% on root is excellent, though /boot at 47% might warrant attention during future kernel&amp;nbsp;updates.&amp;#8221;&lt;/p&gt;
&lt;p&gt;This kind of interpretation is where the conversational approach shines. The raw numbers are available, but the &lt;span class="caps"&gt;AI&lt;/span&gt; adds the &amp;#8220;so what&amp;#8221; that helps you understand whether action is&amp;nbsp;needed.&lt;/p&gt;
&lt;h3 id="network-visibility"&gt;Network&amp;nbsp;Visibility&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;What services are exposed on this&amp;nbsp;machine?&amp;#8221;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;=== Listening Ports ===
tcp  LISTEN  0.0.0.0:22     SSH
tcp  LISTEN  0.0.0.0:111    rpcbind
tcp  LISTEN  0.0.0.0:8043   Omada Controller (HTTPS management)
tcp  LISTEN  0.0.0.0:8088   Omada Controller (HTTP redirect)
tcp  LISTEN  0.0.0.0:8843   Omada Controller (HTTPS portal)
tcp  LISTEN  0.0.0.0:29810-29814  Omada device communication
udp  UNCONN  0.0.0.0:29810  Omada device discovery
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;span class="caps"&gt;AI&lt;/span&gt; maps the ports to their purposes: &amp;#8220;This is a dedicated Omada controller. &lt;span class="caps"&gt;SSH&lt;/span&gt; is available for administration. The Omada-specific ports (8043, 8088, 8843, 29810-29814) are for the controller&amp;#8217;s web interface and communication with managed network devices. I also notice port 111 (rpcbind) is listening - this is typically for &lt;span class="caps"&gt;NFS&lt;/span&gt; support. If &lt;span class="caps"&gt;NFS&lt;/span&gt; isn&amp;#8217;t actively used, consider whether this service needs to be&amp;nbsp;running.&amp;#8221;&lt;/p&gt;
&lt;h2 id="the-conversational-advantage"&gt;The Conversational&amp;nbsp;Advantage&lt;/h2&gt;
&lt;p&gt;The real power isn&amp;#8217;t in individual commands - it&amp;#8217;s in the conversation. Consider this&amp;nbsp;exchange:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: &amp;#8220;My web server seems slow. Can you check what&amp;#8217;s going&amp;nbsp;on?&amp;#8221;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;span class="caps"&gt;AI&lt;/span&gt; assistant can&amp;nbsp;now:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Query system load and memory&amp;nbsp;pressure&lt;/li&gt;
&lt;li&gt;Check if the web server service is running&amp;nbsp;properly&lt;/li&gt;
&lt;li&gt;Examine recent logs for&amp;nbsp;errors&lt;/li&gt;
&lt;li&gt;Look at network connections and listening&amp;nbsp;ports&lt;/li&gt;
&lt;li&gt;Correlate all this information and provide a coherent&amp;nbsp;diagnosis&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All without the user needing to know which specific commands to run or how to interpret the raw output. The assistant contextualizes numbers that might be meaningless to a casual user: &amp;#8220;Memory usage is at 89%, which is high but not critical. However, I notice significant swap activity which could explain the perceived&amp;nbsp;slowness.&amp;#8221;&lt;/p&gt;
&lt;h2 id="security-considerations"&gt;Security&amp;nbsp;Considerations&lt;/h2&gt;
&lt;p&gt;The linux-mcp-server takes a defense-in-depth&amp;nbsp;approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-only operations&lt;/strong&gt;: No tool can modify system&amp;nbsp;state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predefined tools only&lt;/strong&gt;: Arbitrary command execution is not&amp;nbsp;possible&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log path allowlisting&lt;/strong&gt;: Only explicitly permitted log files can be read (controlled&amp;nbsp;via &lt;code&gt;LINUX_MCP_ALLOWED_LOG_PATHS&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parameter validation&lt;/strong&gt;: Inputs are sanitized to prevent injection&amp;nbsp;attacks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit logging&lt;/strong&gt;: All operations are logged for&amp;nbsp;accountability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For remote access, standard &lt;span class="caps"&gt;SSH&lt;/span&gt; security applies. The server uses your existing &lt;span class="caps"&gt;SSH&lt;/span&gt; configuration and key-based authentication, inheriting whatever access controls you&amp;#8217;ve already&amp;nbsp;established.&lt;/p&gt;
&lt;h2 id="configuration-options"&gt;Configuration&amp;nbsp;Options&lt;/h2&gt;
&lt;p&gt;Environment variables control server&amp;nbsp;behavior:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LINUX_MCP_ALLOWED_LOG_PATHS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated list of accessible log file paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LINUX_MCP_LOG_LEVEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logging verbosity (&lt;span class="caps"&gt;DEBUG&lt;/span&gt;, &lt;span class="caps"&gt;INFO&lt;/span&gt;, &lt;span class="caps"&gt;WARNING&lt;/span&gt;, &lt;span class="caps"&gt;ERROR&lt;/span&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LINUX_MCP_SSH_KEY_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to &lt;span class="caps"&gt;SSH&lt;/span&gt; private key for remote connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LINUX_MCP_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default &lt;span class="caps"&gt;SSH&lt;/span&gt; username for remote connections&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="available-tools"&gt;Available&amp;nbsp;Tools&lt;/h2&gt;
&lt;p&gt;The server exposes these diagnostic&amp;nbsp;capabilities:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_system_information&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;OS&lt;/span&gt; version, kernel, hostname, uptime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_cpu_information&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;CPU&lt;/span&gt; model, cores, current load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_memory_information&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;RAM&lt;/span&gt; usage, swap, available memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_disk_usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filesystem usage, mount points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_hardware_information&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class="caps"&gt;PCI&lt;/span&gt;/&lt;span class="caps"&gt;USB&lt;/span&gt; devices, &lt;span class="caps"&gt;DMI&lt;/span&gt; data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All systemd services and their states&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_service_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detailed status of a specific service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_service_logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Journal entries for a service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_processes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Running processes with resource usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_process_info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detailed info about a specific &lt;span class="caps"&gt;PID&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_network_interfaces&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network interfaces and addresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_network_connections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Active network connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_listening_ports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Services listening on ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_journal_logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Systemd journal queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_audit_logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Security audit log entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read_log_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read from allowlisted log files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_block_devices&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disk and partition information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_files&lt;/code&gt; / &lt;code&gt;list_directories&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Directory contents with metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="use-cases"&gt;Use&amp;nbsp;Cases&lt;/h2&gt;
&lt;p&gt;Beyond ad-hoc troubleshooting, the linux-mcp-server&amp;nbsp;enables:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-upgrade assessment&lt;/strong&gt;: &amp;#8220;Check if this server is ready for a major &lt;span class="caps"&gt;OS&lt;/span&gt; upgrade&amp;#8221; - the &lt;span class="caps"&gt;AI&lt;/span&gt; can examine repositories, disk space, running services, and potential compatibility&amp;nbsp;issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Capacity planning&lt;/strong&gt;: &amp;#8220;Analyze resource usage trends&amp;#8221; - correlate &lt;span class="caps"&gt;CPU&lt;/span&gt;, memory, and disk metrics to identify bottlenecks before they become&amp;nbsp;problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security auditing&lt;/strong&gt;: &amp;#8220;Review what services are exposed to the network&amp;#8221; - examine listening ports, running services, and their&amp;nbsp;configurations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: &amp;#8220;Summarize this server&amp;#8217;s configuration&amp;#8221; - generate human-readable descriptions of system setup for documentation&amp;nbsp;purposes.&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;The current implementation has intentional&amp;nbsp;constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No write operations&lt;/strong&gt;: You can&amp;#8217;t restart services, edit files, or make changes through the &lt;span class="caps"&gt;MCP&lt;/span&gt;&amp;nbsp;server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt;/systemd focus&lt;/strong&gt;: Optimized for Red Hat-based distributions; some tools may not work on non-systemd&amp;nbsp;systems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller models need verification&lt;/strong&gt;: While Claude and other large models provide reliable guidance, smaller open-source models may produce suggestions that require&amp;nbsp;verification&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="getting-started"&gt;Getting&amp;nbsp;Started&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Install the&amp;nbsp;server: &lt;code&gt;pip install linux-mcp-server&lt;/code&gt; or &lt;code&gt;uv tool install linux-mcp-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Configure your &lt;span class="caps"&gt;AI&lt;/span&gt; client (Claude Desktop, Claude Code, or another &lt;span class="caps"&gt;MCP&lt;/span&gt;-compatible&amp;nbsp;client)&lt;/li&gt;
&lt;li&gt;Start asking questions about your&amp;nbsp;systems&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The barrier to entry is remarkably low. If you can &lt;span class="caps"&gt;SSH&lt;/span&gt; to a machine, the linux-mcp-server can query&amp;nbsp;it.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The linux-mcp-server represents a meaningful shift in how we interact with systems. Instead of the &lt;span class="caps"&gt;AI&lt;/span&gt; being a sophisticated search engine that helps you find the right commands, it becomes a collaborative partner that can actually see your system&amp;#8217;s&amp;nbsp;state.&lt;/p&gt;
&lt;p&gt;This isn&amp;#8217;t about replacing sysadmin knowledge - you still need to understand what the &lt;span class="caps"&gt;AI&lt;/span&gt; is telling you and make informed decisions. But it dramatically lowers the friction of system diagnostics, especially&amp;nbsp;for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers who occasionally need to debug production&amp;nbsp;issues&lt;/li&gt;
&lt;li&gt;Junior administrators learning the&amp;nbsp;ropes&lt;/li&gt;
&lt;li&gt;Anyone managing systems outside their primary&amp;nbsp;expertise&lt;/li&gt;
&lt;li&gt;Experienced admins who want faster initial&amp;nbsp;triage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project is open source and actively developed. Contributions for additional diagnostic tools, support for other distributions, and integration with documentation via &lt;span class="caps"&gt;RAG&lt;/span&gt; are&amp;nbsp;welcome.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/rhel-lightspeed/linux-mcp-server"&gt;linux-mcp-server on&amp;nbsp;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fedoramagazine.org/find-out-how-your-fedora-system-really-feels-with-the-linux-mcp-server/"&gt;Fedora Magazine: Find out how your Fedora system really&amp;nbsp;feels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/"&gt;Model Context Protocol&amp;nbsp;specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/news/model-context-protocol"&gt;Anthropic&amp;#8217;s &lt;span class="caps"&gt;MCP&lt;/span&gt;&amp;nbsp;announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Thanks to the Red Hat and Fedora teams for developing linux-mcp-server and making &lt;span class="caps"&gt;AI&lt;/span&gt;-assisted system administration a reality. The future of troubleshooting is&amp;nbsp;conversational.&lt;/p&gt;</content><category term="Linux"/><category term="linux"/><category term="ai"/><category term="mcp"/><category term="troubleshooting"/><category term="fedora"/><category term="rhel"/><category term="claude"/></entry><entry><title>Running a Factorio Headless Server on FreeBSD with the Linuxulator</title><link href="https://blog.hofstede.it/running-a-factorio-headless-server-on-freebsd-with-the-linuxulator/" rel="alternate"/><published>2025-12-20T00:00:00+01:00</published><updated>2025-12-20T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-12-20:/running-a-factorio-headless-server-on-freebsd-with-the-linuxulator/</id><summary type="html">&lt;p&gt;How to run a Factorio dedicated server inside a FreeBSD jail using Linux binary compatibility, with no native port&amp;nbsp;required.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Factorio doesn&amp;#8217;t have a native FreeBSD build, but that doesn&amp;#8217;t mean you can&amp;#8217;t run it on FreeBSD. The Linuxulator, FreeBSD&amp;#8217;s Linux binary compatibility layer, handles Linux &lt;span class="caps"&gt;ELF&lt;/span&gt; binaries seamlessly. This article walks through setting up a Factorio headless server inside a Bastille jail, complete with firewall rules for public&amp;nbsp;access.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Factorio" src="https://blog.hofstede.it/images/2025-12-20-factorio-freebsd-linuxulator.png" title="Factorio: The factory must grow"&gt;&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;This guide assumes you already&amp;nbsp;have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A FreeBSD host (14.x or later) with Bastille&amp;nbsp;installed&lt;/li&gt;
&lt;li&gt;A working bridge network for jails (see my &lt;a href="https://blog.hofstede.it/hosting-a-static-blog-on-freebsd-with-bastille-jails-and-automated-deployment/"&gt;previous article on FreeBSD jail infrastructure&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A Factorio account to download the headless&amp;nbsp;server&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="enabling-the-64-bit-linuxulator"&gt;Enabling the 64-bit&amp;nbsp;Linuxulator&lt;/h2&gt;
&lt;p&gt;The Factorio server is a 64-bit Linux binary. FreeBSD&amp;#8217;s Linuxulator needs to be loaded on the &lt;strong&gt;host&lt;/strong&gt; system before jails can use&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;Add&amp;nbsp;to &lt;code&gt;/boot/loader.conf&lt;/code&gt; on the&amp;nbsp;host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;linux64_load&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Reboot, or load it&amp;nbsp;immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;kldload&lt;span class="w"&gt; &lt;/span&gt;linux64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can verify it&amp;#8217;s&amp;nbsp;loaded:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;kldstat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;linux
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0xffffffff827fa000&lt;span class="w"&gt;    &lt;/span&gt;619f8&lt;span class="w"&gt; &lt;/span&gt;linux64.ko
&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0xffffffff8285c000&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;20970&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;linux_common.ko
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="creating-the-jail"&gt;Creating the&amp;nbsp;Jail&lt;/h2&gt;
&lt;p&gt;Create a standard &lt;span class="caps"&gt;VNET&lt;/span&gt; jail attached to your existing bridge. In this example, I&amp;#8217;m using&amp;nbsp;the &lt;code&gt;bastille0&lt;/code&gt; bridge with a private IPv4/IPv6 address&amp;nbsp;pair:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;.3-RELEASE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.252.98&lt;span class="w"&gt; &lt;/span&gt;bastille0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Configure the jail&amp;#8217;s network in&amp;nbsp;its &lt;code&gt;/etc/rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_e0b_factorio_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.254.252.98 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:8000::98/64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.254.252.1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# That&amp;#39;s the hosts IP address on the bridge bastille0&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:8000::1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# That&amp;#39;s the hosts IPv6 address on the bridge bastille0&lt;/span&gt;

&lt;span class="nv"&gt;syslogd_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-ss&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_submit_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_outbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_msp_queue_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;cron_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-J 60&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;linux_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key line&amp;nbsp;is &lt;code&gt;linux_enable="YES"&lt;/code&gt; - this enables the Linux compatibility layer inside the&amp;nbsp;jail.&lt;/p&gt;
&lt;p&gt;Start the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;factorio
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="installing-linux-userland"&gt;Installing Linux&amp;nbsp;Userland&lt;/h2&gt;
&lt;p&gt;Inside the jail, install the Rocky Linux 9 base package. This provides the necessary Linux shared&amp;nbsp;libraries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;linux_base-rl9
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This pulls in a minimal Linux userland including glibc, which the Factorio binary needs. The installed packages are&amp;nbsp;lightweight:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;linux_base-rl9-9.6_1           Base set of packages needed in Linux mode (Rocky Linux 9.6)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="creating-the-factorio-user"&gt;Creating the Factorio&amp;nbsp;User&lt;/h2&gt;
&lt;p&gt;Create a dedicated user to run the&amp;nbsp;server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;cmd&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;adduser
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Follow the prompts - the defaults are fine. I&amp;nbsp;used &lt;code&gt;factorio&lt;/code&gt; as the username&amp;nbsp;with &lt;code&gt;/home/factorio&lt;/code&gt; as the home directory. When prompted for the&amp;nbsp;shell, &lt;code&gt;/bin/sh&lt;/code&gt; is a safe&amp;nbsp;choice.&lt;/p&gt;
&lt;h2 id="downloading-and-extracting-factorio"&gt;Downloading and Extracting&amp;nbsp;Factorio&lt;/h2&gt;
&lt;p&gt;Enter the jail and switch to the factorio&amp;nbsp;user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;console&lt;span class="w"&gt; &lt;/span&gt;factorio
su&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;factorio
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Download and extract the server. You can use FreeBSD&amp;#8217;s&amp;nbsp;built-in &lt;code&gt;fetch&lt;/code&gt; or&amp;nbsp;install &lt;code&gt;wget&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;factorio-headless.tar.xz&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://factorio.com/get-download/stable/headless/linux64&amp;quot;&lt;/span&gt;
tar&lt;span class="w"&gt; &lt;/span&gt;xf&lt;span class="w"&gt; &lt;/span&gt;factorio-headless.tar.xz
rm&lt;span class="w"&gt; &lt;/span&gt;factorio-headless.tar.xz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This creates&amp;nbsp;a &lt;code&gt;factorio/&lt;/code&gt; directory with the server files. You can verify the binary is a Linux &lt;span class="caps"&gt;ELF&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;factorio/bin/x64/factorio
factorio/bin/x64/factorio:&lt;span class="w"&gt; &lt;/span&gt;ELF&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;-bit&lt;span class="w"&gt; &lt;/span&gt;LSB&lt;span class="w"&gt; &lt;/span&gt;pie&lt;span class="w"&gt; &lt;/span&gt;executable,&lt;span class="w"&gt; &lt;/span&gt;x86-64,&lt;span class="w"&gt; &lt;/span&gt;version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;SYSV&lt;span class="o"&gt;)&lt;/span&gt;,
dynamically&lt;span class="w"&gt; &lt;/span&gt;linked,&lt;span class="w"&gt; &lt;/span&gt;interpreter&lt;span class="w"&gt; &lt;/span&gt;/lib64/ld-linux-x86-64.so.2,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;GNU/Linux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.2.0,
with&lt;span class="w"&gt; &lt;/span&gt;debug_info,&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;stripped
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="configuring-the-server"&gt;Configuring the&amp;nbsp;Server&lt;/h2&gt;
&lt;p&gt;Factorio stores its configuration&amp;nbsp;in &lt;code&gt;factorio/data/server-settings.json&lt;/code&gt;. Copy the example and customize&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;factorio
cp&lt;span class="w"&gt; &lt;/span&gt;data/server-settings.example.json&lt;span class="w"&gt; &lt;/span&gt;data/server-settings.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Edit at&amp;nbsp;minimum:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: Your server&amp;#8217;s public&amp;nbsp;name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;: What players will see in the server&amp;nbsp;browser&lt;/li&gt;
&lt;li&gt;&lt;code&gt;visibility&lt;/code&gt;: Set&amp;nbsp;to &lt;code&gt;{"public": true, "lan": true}&lt;/code&gt; if you want it&amp;nbsp;listed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt;: Your Factorio account credentials (required for public&amp;nbsp;servers)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;game_password&lt;/code&gt;: Optional password for players to&amp;nbsp;join&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="running-the-server"&gt;Running the&amp;nbsp;Server&lt;/h2&gt;
&lt;p&gt;When running under the Linuxulator, Factorio can&amp;#8217;t automatically detect its installation directory and defaults to looking&amp;nbsp;in &lt;code&gt;/usr/share/factorio&lt;/code&gt;.&amp;nbsp;The &lt;code&gt;--executable-path&lt;/code&gt; flag tells it where to find the game&amp;nbsp;data.&lt;/p&gt;
&lt;h3 id="quick-start-with-tmux"&gt;Quick Start with&amp;nbsp;tmux&lt;/h3&gt;
&lt;p&gt;The simplest approach is running the server in a tmux session. This keeps the console accessible and survives disconnections. Note&amp;nbsp;that &lt;code&gt;~/factorio&lt;/code&gt; expands&amp;nbsp;to &lt;code&gt;/home/factorio/factorio&lt;/code&gt; since the home directory contains the&amp;nbsp;extracted &lt;code&gt;factorio/&lt;/code&gt; folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tmux&lt;span class="w"&gt; &lt;/span&gt;new-session&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;factorio
tmux&lt;span class="w"&gt; &lt;/span&gt;send-keys&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cd ~/factorio &amp;amp;&amp;amp; bin/x64/factorio --executable-path ~/factorio/bin/x64/ --start-server-load-latest --server-settings ./data/server-settings.json&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Enter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To attach to the console&amp;nbsp;later:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tmux&lt;span class="w"&gt; &lt;/span&gt;attach&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;factorio
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To create a new map (run from&amp;nbsp;within &lt;code&gt;~/factorio&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bin/x64/factorio&lt;span class="w"&gt; &lt;/span&gt;--executable-path&lt;span class="w"&gt; &lt;/span&gt;~/factorio/bin/x64/&lt;span class="w"&gt; &lt;/span&gt;--create&lt;span class="w"&gt; &lt;/span&gt;./saves/myworld.zip
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then start the server with that&amp;nbsp;save:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bin/x64/factorio&lt;span class="w"&gt; &lt;/span&gt;--executable-path&lt;span class="w"&gt; &lt;/span&gt;~/factorio/bin/x64/&lt;span class="w"&gt; &lt;/span&gt;--start-server&lt;span class="w"&gt; &lt;/span&gt;./saves/myworld.zip&lt;span class="w"&gt; &lt;/span&gt;--server-settings&lt;span class="w"&gt; &lt;/span&gt;./data/server-settings.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="proper-rcd-service-script"&gt;Proper rc.d Service&amp;nbsp;Script&lt;/h3&gt;
&lt;p&gt;For production use, an rc.d script integrates with FreeBSD&amp;#8217;s service management.&amp;nbsp;Create &lt;code&gt;/usr/local/etc/rc.d/factorio&lt;/code&gt; inside the&amp;nbsp;jail:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="c1"&gt;# PROVIDE: factorio&lt;/span&gt;
&lt;span class="c1"&gt;# REQUIRE: LOGIN&lt;/span&gt;
&lt;span class="c1"&gt;# KEYWORD: shutdown&lt;/span&gt;

.&lt;span class="w"&gt; &lt;/span&gt;/etc/rc.subr

&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;factorio&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rcvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;factorio_enable

load_rc_config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;

:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_enable&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_user&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;factorio&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_dir&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/home/factorio/factorio&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_savefile&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--start-server-load-latest&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_flags&lt;/span&gt;&lt;span class="p"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/run/factorio/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pid&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;start_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_start&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;stop_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_stop&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;status_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_status&amp;quot;&lt;/span&gt;

factorio_start&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;755&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;/var/run/factorio
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Starting &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# -S: don&amp;#39;t send SIGHUP to child on exit, -T: set process title for ps&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/usr/sbin/daemon&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-S&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/bin/x64/factorio&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--executable-path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/bin/x64/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_savefile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--server-settings&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/data/server-settings.json&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;factorio_flags&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

factorio_status&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is running as pid &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not running.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

factorio_stop&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Stopping &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-TERM&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null
&lt;span class="w"&gt;        &lt;/span&gt;sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not running.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_rc_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Make it executable and enable&amp;nbsp;it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;/usr/local/etc/rc.d/factorio
sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;factorio_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you can manage the server with standard&amp;nbsp;commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;service&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;start
service&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;stop
service&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;status
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To specify a particular save file instead of loading the&amp;nbsp;latest:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sysrc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;factorio_savefile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--start-server /home/factorio/factorio/saves/myworld.zip&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="firewall-configuration"&gt;Firewall&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Factorio uses &lt;span class="caps"&gt;UDP&lt;/span&gt; port 34197 by default. Add redirect and pass rules to your&amp;nbsp;host&amp;#8217;s &lt;code&gt;/etc/pf.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;# Macros&lt;/span&gt;
&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8::f3d1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;factorio_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;10.254.252.98&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;factorio_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2001:db8:8000::98&amp;quot;&lt;/span&gt;

&lt;span class="cp"&gt;# Redirect incoming Factorio traffic to the jail&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34197&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;factorio_v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34197&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;factorio_v6&lt;/span&gt;

&lt;span class="cp"&gt;# Allow the traffic through&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;factorio_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34197&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;factorio_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34197&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Test and reload &lt;span class="caps"&gt;PF&lt;/span&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-nf&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pfctl&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/pf.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="verifying-the-setup"&gt;Verifying the&amp;nbsp;Setup&lt;/h2&gt;
&lt;p&gt;If using the rc.d script, start the server and check the logs (inside the&amp;nbsp;jail):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;service&lt;span class="w"&gt; &lt;/span&gt;factorio&lt;span class="w"&gt; &lt;/span&gt;start
tail&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/home/factorio/factorio/factorio-current.log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A successful startup looks like&amp;nbsp;this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;   0.000 2025-12-20 12:02:46; Factorio 2.0.72 (build 84292, linux64, headless)
   0.000 Operating system: Linux
   0.000 Config path: /usr/home/factorio/factorio/config/config.ini
   0.000 Read data path: /usr/home/factorio/factorio/data
   0.000 Write data path: /usr/home/factorio/factorio [108461/109184MB]
   0.009 System info: [CPU: AMD EPYC-Rome Processor, 8 cores, RAM: 15957 MB]
   0.010 Running in headless mode
   ...
   2.404 Hosting game at IP ADDR:({0.0.0.0:34197})
   2.739 Info ServerMultiplayerManager.cpp:808: changing state to(InGame)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &amp;#8220;Operating system: Linux&amp;#8221; line is the key indicator - it proves the Linuxulator translation layer is active and the binary genuinely believes it&amp;#8217;s running on Linux. Players can now connect via your server&amp;#8217;s public &lt;span class="caps"&gt;IP&lt;/span&gt; on port&amp;nbsp;34197.&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;error while loading shared libraries&amp;#8221;&lt;/strong&gt;: The Linux userland isn&amp;#8217;t installed&amp;nbsp;or &lt;code&gt;linux_enable&lt;/code&gt; isn&amp;#8217;t set.&amp;nbsp;Install &lt;code&gt;linux_base-rl9&lt;/code&gt; and&amp;nbsp;ensure &lt;code&gt;/etc/rc.conf&lt;/code&gt; has &lt;code&gt;linux_enable="YES"&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server starts but players can&amp;#8217;t connect&lt;/strong&gt;: Check your &lt;span class="caps"&gt;PF&lt;/span&gt; rules. Ensure both&amp;nbsp;the &lt;code&gt;rdr&lt;/code&gt; and &lt;code&gt;pass&lt;/code&gt; rules are in place for &lt;span class="caps"&gt;UDP&lt;/span&gt; 34197.&amp;nbsp;Use &lt;code&gt;tcpdump -i vtnet0 udp port 34197&lt;/code&gt; on the host to verify traffic is&amp;nbsp;arriving.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Failed to find the system certificate authority file&amp;#8221;&lt;/strong&gt;: This warning is harmless. Factorio falls back to its bundled certificate file for &lt;span class="caps"&gt;HTTPS&lt;/span&gt; requests to the auth&amp;nbsp;server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Error configuring paths: There is no package core in /usr/share/factorio&amp;#8221;&lt;/strong&gt;:&amp;nbsp;The &lt;code&gt;--executable-path&lt;/code&gt; flag is missing. Under the Linuxulator, Factorio can&amp;#8217;t auto-detect its installation directory. Always&amp;nbsp;specify &lt;code&gt;--executable-path /path/to/factorio/bin/x64/&lt;/code&gt; when starting the&amp;nbsp;server.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;Running Factorio on FreeBSD is straightforward thanks to the&amp;nbsp;Linuxulator:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load &lt;code&gt;linux64.ko&lt;/code&gt; on the&amp;nbsp;host&lt;/li&gt;
&lt;li&gt;Create a jail&amp;nbsp;with &lt;code&gt;linux_enable="YES"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;linux_base-rl9&lt;/code&gt; for the Linux shared&amp;nbsp;libraries&lt;/li&gt;
&lt;li&gt;Download the headless server and run&amp;nbsp;it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Linuxulator handles the translation transparently - the Factorio binary thinks it&amp;#8217;s running on Linux. Combined with FreeBSD jails for isolation and &lt;span class="caps"&gt;PF&lt;/span&gt; for traffic control, you get a clean, manageable game server&amp;nbsp;setup.&lt;/p&gt;
&lt;p&gt;The factory must grow. Even on&amp;nbsp;FreeBSD.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://factorio.com/download"&gt;Factorio Headless Server&amp;nbsp;Download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.factorio.com/Multiplayer#Dedicated_Server"&gt;Factorio Dedicated Server&amp;nbsp;Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/linuxemu/"&gt;FreeBSD Handbook: Linux Binary&amp;nbsp;Compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bastillebsd.org"&gt;BastilleBSD&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openbsd.org/faq/pf/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; - The OpenBSD Packet&amp;nbsp;Filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Thanks to the FreeBSD team for maintaining the Linuxulator, making it possible to run Linux-only software without virtualization&amp;nbsp;overhead.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="gaming"/><category term="jails"/><category term="bastille"/><category term="factorio"/></entry><entry><title>Hosting a Static Blog on FreeBSD with Bastille Jails and Automated Deployment</title><link href="https://blog.hofstede.it/hosting-a-static-blog-on-freebsd-with-bastille-jails-and-automated-deployment/" rel="alternate"/><published>2025-12-14T00:00:00+01:00</published><updated>2025-12-14T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-12-14:/hosting-a-static-blog-on-freebsd-with-bastille-jails-and-automated-deployment/</id><summary type="html">&lt;p&gt;A full-stack overview of hosting a Pelican blog on FreeBSD 15.0 using Bastille jails, Caddy reverse proxy, and automated &lt;span class="caps"&gt;CI&lt;/span&gt;/&lt;span class="caps"&gt;CD&lt;/span&gt; deployment via Forgejo&amp;nbsp;Actions.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;p&gt;Self-hosting a blog might seem like overkill in the age of managed platforms, but there&amp;#8217;s something deeply satisfying about controlling your entire stack. This article walks through how this very blog is hosted: a FreeBSD 15.0 server running multiple Bastille jails, with automated deployments triggered by git pushes to a self-hosted Forgejo instance. The setup prioritizes security through isolation, simplicity through static site generation, and reliability through&amp;nbsp;automation.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Safety deposit boxes" src="https://blog.hofstede.it/images/2025-12-14-freebsd-blog-infrastructure.png" title="Jails: isolated compartments within a secure host"&gt;&lt;/p&gt;
&lt;h2 id="architecture-overview"&gt;Architecture&amp;nbsp;Overview&lt;/h2&gt;
&lt;p&gt;Before diving into the details, here&amp;#8217;s how the pieces fit&amp;nbsp;together:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    Internet
                        |
                        v
            +-----------------------+
            |    PF Firewall        |
            |    NAT + RDR          |
            +-----------+-----------+
                        |
           +------------+------------+
           |                         |
           v                         v
    +-------------+           +-------------+
    | Caddy Jail  |           | Transporter |
    | TLS + Proxy |           |    Jail     |
    +------+------+           | rsync only  |
           |                  +------+------+
           v                         ^
    +-------------+                  |
    |  Blog Jail  |    nullfs mount  |
    |   Nginx     +------------------+
    +-------------+                  |
                              (rsync over SSH)
                                     |
                            +--------+--------+
                            | Forgejo Runner  |
                            | (remote server) |
                            +-----------------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Traffic flows from the internet through &lt;span class="caps"&gt;PF&lt;/span&gt;&amp;#8217;s packet filter, gets redirected to the Caddy jail for &lt;span class="caps"&gt;TLS&lt;/span&gt; termination, then proxied to the blog jail where Nginx serves static files. Deployments come in separately: a Forgejo runner builds the site and rsyncs it to the transporter jail, which has the blog&amp;#8217;s webroot mounted via&amp;nbsp;nullfs.&lt;/p&gt;
&lt;p&gt;The Forgejo Git-Forge installation is not covered in this article. This might be a topic for a future post&amp;nbsp;:-)&lt;/p&gt;
&lt;h2 id="the-host-system"&gt;The Host&amp;nbsp;System&lt;/h2&gt;
&lt;p&gt;The server runs FreeBSD 15.0-&lt;span class="caps"&gt;RELEASE&lt;/span&gt; on a &lt;span class="caps"&gt;VPS&lt;/span&gt; with dual-stack networking. Here&amp;#8217;s the relevant portion&amp;nbsp;of &lt;code&gt;/etc/rc.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;server.example.com&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Security hardening&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;kern_securelevel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Network setup - dual stack&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 192.0.2.10 netmask 255.255.252.0 -lro -tso&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8::2 prefixlen 68&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;192.0.2.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Bastille jail network bridge&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.254.254.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:1000::1 prefixlen 68&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;bastille_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Setting &lt;code&gt;kern_securelevel="2"&lt;/code&gt; is a simple but effective hardening measure. At securelevel 2, the system prevents loading kernel modules, writing to mounted filesystems marked immutable, and modifying the firewall rules without a reboot. This means even if an attacker gains root access, they can&amp;#8217;t disable &lt;span class="caps"&gt;PF&lt;/span&gt; or load a rootkit&amp;nbsp;module.&lt;/p&gt;
&lt;p&gt;The Bastille bridge network&amp;nbsp;(&lt;code&gt;bastille0&lt;/code&gt;) provides a private network for all jails. Each jail gets an &lt;span class="caps"&gt;IP&lt;/span&gt; from the 10.254.254.0/24 range for IPv4 and a corresponding address in the 2001:db8:1000::/68 range for&amp;nbsp;IPv6.&lt;/p&gt;
&lt;h2 id="jail-architecture"&gt;Jail&amp;nbsp;Architecture&lt;/h2&gt;
&lt;p&gt;Bastille manages three jails relevant to this&amp;nbsp;setup:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;all
&lt;span class="w"&gt; &lt;/span&gt;JID&lt;span class="w"&gt;  &lt;/span&gt;State&lt;span class="w"&gt;  &lt;/span&gt;IP&lt;span class="w"&gt; &lt;/span&gt;Address&lt;span class="w"&gt;       &lt;/span&gt;Hostname&lt;span class="w"&gt;       &lt;/span&gt;Release
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;Up&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.254.20&lt;span class="w"&gt;    &lt;/span&gt;blog&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;.0-RELEASE
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="m"&gt;2001&lt;/span&gt;:db8:1000::20
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;Up&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.254.10&lt;span class="w"&gt;    &lt;/span&gt;caddy&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;.0-RELEASE
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="m"&gt;2001&lt;/span&gt;:db8:1000::10
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;Up&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.254.254.25&lt;span class="w"&gt;    &lt;/span&gt;transporter&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;.0-RELEASE
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="m"&gt;2001&lt;/span&gt;:db8:1000::25
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="the-caddy-jail"&gt;The Caddy&amp;nbsp;Jail&lt;/h3&gt;
&lt;p&gt;This jail handles all incoming &lt;span class="caps"&gt;HTTPS&lt;/span&gt; traffic. Caddy automatically obtains and renews Let&amp;#8217;s Encrypt certificates, terminates &lt;span class="caps"&gt;TLS&lt;/span&gt;, and proxies requests to the appropriate backend. It&amp;#8217;s the only jail that needs to be reachable from the internet on ports 80 and&amp;nbsp;443.&lt;/p&gt;
&lt;h3 id="the-blog-jail"&gt;The Blog&amp;nbsp;Jail&lt;/h3&gt;
&lt;p&gt;A minimal jail running Nginx to serve static files. The webroot&amp;nbsp;at &lt;code&gt;/usr/local/www/&lt;/code&gt; contains the generated Pelican output. The Nginx configuration is intentionally vanilla - just a&amp;nbsp;basic &lt;code&gt;server&lt;/code&gt; block pointing at the webroot with no special tuning required for static content. This jail has no public network exposure - all traffic comes through Caddy&amp;#8217;s reverse&amp;nbsp;proxy.&lt;/p&gt;
&lt;h3 id="the-transporter-jail"&gt;The Transporter&amp;nbsp;Jail&lt;/h3&gt;
&lt;p&gt;This is the clever part. The transporter jail exists solely to receive deployments. It runs &lt;span class="caps"&gt;SSH&lt;/span&gt; but with heavily restricted access: only rsync is allowed, only from the Forgejo runner&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt;, and only to a specific directory. The magic happens through nullfs&amp;nbsp;mounts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# /usr/local/bastille/jails/transporter/fstab
/usr/local/bastille/jails/blog/root/usr/local/www/blog.example.com \
    /usr/local/bastille/jails/transporter/root/usr/local/deploy/blog nullfs 0 0
/usr/local/bastille/jails/blog/root/usr/local/www/staging.example.com \
    /usr/local/bastille/jails/transporter/root/usr/local/deploy/stageblog nullfs 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When the &lt;span class="caps"&gt;CI&lt;/span&gt; runner rsyncs files&amp;nbsp;to &lt;code&gt;/usr/local/deploy/blog/&lt;/code&gt; inside the transporter jail, they&amp;#8217;re actually being written directly to the blog jail&amp;#8217;s webroot. The transporter never stores anything - it&amp;#8217;s just a secure&amp;nbsp;gateway.&lt;/p&gt;
&lt;h2 id="pf-firewall-configuration"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; Firewall&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; ties everything together with &lt;span class="caps"&gt;NAT&lt;/span&gt; for outbound jail traffic and redirects for inbound&amp;nbsp;services:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# --- Macros ---
ext_if = &amp;quot;vtnet0&amp;quot;
jail_net = &amp;quot;10.254.254.0/24&amp;quot;
jail_net6 = &amp;quot;2001:db8:1000::/68&amp;quot;
host_ipv6 = &amp;quot;2001:db8::2&amp;quot;

frontend_v4 = &amp;quot;10.254.254.10&amp;quot;
frontend_v6 = &amp;quot;2001:db8:1000::10&amp;quot;
transport_v4 = &amp;quot;10.254.254.25&amp;quot;
transport_v6 = &amp;quot;2001:db8:1000::25&amp;quot;
forgejo_runner_v4 = &amp;quot;198.51.100.50&amp;quot;

# --- Tables ---
table &amp;lt;bruteforce&amp;gt; persist
table &amp;lt;jails_v4&amp;gt; { $jail_net }
table &amp;lt;jails_v6&amp;gt; { $jail_net6 }

# --- Options ---
set skip on lo0
set block-policy drop

# --- NAT for jail egress ---
nat on $ext_if inet from &amp;lt;jails_v4&amp;gt; to any -&amp;gt; ($ext_if)
nat on $ext_if inet6 from &amp;lt;jails_v6&amp;gt; to any -&amp;gt; $host_ipv6

# --- RDR for services ---
rdr pass on $ext_if inet proto tcp to ($ext_if) port {80,443} -&amp;gt; $frontend_v4
rdr pass on $ext_if inet6 proto tcp to $host_ipv6 port {80,443} -&amp;gt; $frontend_v6

# Deployment SSH - only from Forgejo runner
rdr pass on $ext_if inet proto tcp from $forgejo_runner_v4 \
    to ($ext_if) port 2225 -&amp;gt; $transport_v4 port 22

# --- Filtering ---
block quick from &amp;lt;bruteforce&amp;gt;
block drop in log all
block drop out log all

pass out quick all keep state
antispoof quick for { $ext_if, bastille0 }

# Essential ICMP
pass in inet proto icmp icmp-type { echoreq, unreach }
pass in quick inet6 proto ipv6-icmp icmp6-type { echoreq, echorep, neighbrsol, neighbradv }

# Jail egress
pass in quick on bastille0 from &amp;lt;jails_v4&amp;gt; to ! 10.254.254.0/24 keep state
pass in quick on bastille0 inet6 from &amp;lt;jails_v6&amp;gt; to ! 2001:db8:1000::/68 keep state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A few things worth&amp;nbsp;noting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Default deny&lt;/strong&gt;: Everything is blocked unless explicitly&amp;nbsp;allowed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment &lt;span class="caps"&gt;SSH&lt;/span&gt; on port 2225&lt;/strong&gt;: The transporter jail&amp;#8217;s &lt;span class="caps"&gt;SSH&lt;/span&gt; is only reachable from the Forgejo runner&amp;#8217;s &lt;span class="caps"&gt;IP&lt;/span&gt; address. Anyone else hitting that port gets silently&amp;nbsp;dropped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brute-force protection&lt;/strong&gt;:&amp;nbsp;The &lt;code&gt;&amp;lt;bruteforce&amp;gt;&lt;/code&gt; table can be populated by rules elsewhere (e.g., for the host&amp;#8217;s &lt;span class="caps"&gt;SSH&lt;/span&gt;) to automatically block repeat&amp;nbsp;offenders&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt; for jails&lt;/strong&gt;: Jails can reach the internet for package updates and certificate validation, but they appear to come from the host&amp;#8217;s public &lt;span class="caps"&gt;IP&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-deployment-pipeline"&gt;The Deployment&amp;nbsp;Pipeline&lt;/h2&gt;
&lt;p&gt;The deployment relies on an &lt;span class="caps"&gt;SSH&lt;/span&gt; keypair generated beforehand: the public key lives in the transporter&amp;nbsp;jail&amp;#8217;s &lt;code&gt;authorized_keys&lt;/code&gt; (with the rrsync restriction shown later), while the private key is stored as a secret in Forgejo. When I push to the blog&amp;#8217;s git repository, a Forgejo Actions workflow kicks&amp;nbsp;off:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;**&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;docker&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;python:3.11-slim&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Install Dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;apt-get update&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;apt-get install -y rsync openssh-client git&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;pip install pelican pelican-sitemap markdown typogrify&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Checkout Code&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v3&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;submodules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;recursive&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Configure Target&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;meta&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;SHORT_SHA=$(git rev-parse --short HEAD)&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;if [[ &amp;quot;${{ github.ref }}&amp;quot; == &amp;quot;refs/heads/main&amp;quot; ]]; then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;echo &amp;quot;url=https://blog.example.com&amp;quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;echo &amp;quot;dest=blog/&amp;quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;echo &amp;quot;url=https://staging.example.com/$SHORT_SHA&amp;quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;echo &amp;quot;dest=stageblog/$SHORT_SHA/&amp;quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Build Site&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;SITEURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ steps.meta.outputs.url }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;pelican content -o output -s publishconf.py&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&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 l-Scalar l-Scalar-Plain"&gt;Deploy via Rsync&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;rsync -avz --delete -e &amp;quot;ssh -p 2225&amp;quot; \&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;output/ deploy@server.example.com:${{ steps.meta.outputs.dest }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The branch strategy is simple: pushes&amp;nbsp;to &lt;code&gt;main&lt;/code&gt; deploy to production, pushes to any other branch deploy to a staging &lt;span class="caps"&gt;URL&lt;/span&gt; with the commit hash in the path. This lets me preview changes before&amp;nbsp;merging.&lt;/p&gt;
&lt;p&gt;The transporter&amp;nbsp;jail&amp;#8217;s &lt;code&gt;authorized_keys&lt;/code&gt; file locks down what the deploy user can&amp;nbsp;do:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;command=&amp;quot;/usr/local/sbin/rrsync -wo /usr/local/deploy/&amp;quot;,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA... deploy@runner
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;rrsync&lt;/code&gt; wrapper (restricted rsync) ensures the &lt;span class="caps"&gt;SSH&lt;/span&gt; key can only write&amp;nbsp;to &lt;code&gt;/usr/local/deploy/&lt;/code&gt; and nothing else. Combined with the &lt;span class="caps"&gt;PF&lt;/span&gt; rules limiting source IPs, this creates a minimal attack surface for&amp;nbsp;deployments.&lt;/p&gt;
&lt;h2 id="caddy-configuration"&gt;Caddy&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Caddy&amp;#8217;s configuration is refreshingly simple. Here&amp;#8217;s the production site&amp;nbsp;block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;blog.example.com {
    reverse_proxy 10.254.254.20:80  # blog jail

    header {
        Strict-Transport-Security &amp;quot;max-age=31536000; includeSubDomains; preload&amp;quot;
        X-Content-Type-Options &amp;quot;nosniff&amp;quot;
        X-Frame-Options &amp;quot;DENY&amp;quot;
        Referrer-Policy &amp;quot;strict-origin-when-cross-origin&amp;quot;
    }

    log {
        output file /var/log/caddy/blog.access.log {
            roll_size 100mb
            roll_keep 10
        }
        format json
    }
}

staging.example.com {
    reverse_proxy 10.254.254.20:80

    header {
        Strict-Transport-Security &amp;quot;max-age=31536000; includeSubDomains; preload&amp;quot;
        X-Content-Type-Options &amp;quot;nosniff&amp;quot;
        X-Frame-Options &amp;quot;DENY&amp;quot;
        Referrer-Policy &amp;quot;strict-origin-when-cross-origin&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Caddy handles certificate management automatically via &lt;span class="caps"&gt;ACME&lt;/span&gt;. The security headers provide defense-in-depth: &lt;span class="caps"&gt;HSTS&lt;/span&gt; ensures browsers always use &lt;span class="caps"&gt;HTTPS&lt;/span&gt;, X-Frame-Options prevents clickjacking, and the other headers mitigate various web attacks. For a static blog these might seem excessive, but they cost nothing and establish good&amp;nbsp;habits.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This setup might look complex at first glance, but each piece serves a&amp;nbsp;purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bastille jails&lt;/strong&gt; provide lightweight isolation without the overhead of full&amp;nbsp;VMs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;PF&lt;/span&gt;&lt;/strong&gt; gives fine-grained control over traffic flow and enforces the principle of least&amp;nbsp;privilege&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The transporter pattern&lt;/strong&gt; allows secure deployments without exposing the blog jail&amp;nbsp;directly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forgejo Actions&lt;/strong&gt; automates the entire build-and-deploy&amp;nbsp;cycle&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caddy&lt;/strong&gt; simplifies &lt;span class="caps"&gt;TLS&lt;/span&gt; and reverse proxying to a few lines of&amp;nbsp;config&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a blog that deploys automatically on every git push, runs in isolated jails with minimal attack surface, and costs just a few euros per month on a small &lt;span class="caps"&gt;VPS&lt;/span&gt;. FreeBSD&amp;#8217;s jails remain one of the most elegant isolation mechanisms available - simpler than containers, more lightweight than VMs, and battle-tested over two&amp;nbsp;decades.&lt;/p&gt;
&lt;p&gt;If you&amp;#8217;re considering self-hosting, I&amp;#8217;d encourage giving FreeBSD and Bastille a try. The learning curve is worth&amp;nbsp;it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="further-reading"&gt;Further&amp;nbsp;Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook:&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bastillebsd.org"&gt;Bastille&amp;nbsp;documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/"&gt;Caddy web&amp;nbsp;server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.getpelican.com/"&gt;Pelican static site&amp;nbsp;generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forgejo.org/docs/latest/user/actions/"&gt;Forgejo&amp;nbsp;Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openbsd.org/faq/pf/"&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; - The OpenBSD Packet&amp;nbsp;Filter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Thanks to everyone behind FreeBSD, Bastille, Caddy, Pelican, Forgejo, and the countless other open source projects that make setups like this possible. Standing on the shoulders of giants has never been&amp;nbsp;easier.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="jails"/><category term="deployment"/><category term="caddy"/><category term="ci-cd"/><category term="bastille"/><category term="nginx"/><category term="infrastructure"/></entry><entry><title>Migrating burningboard.net Mastodon instance to a Multi-Jail FreeBSD Setup</title><link href="https://blog.hofstede.it/migrating-burningboardnet-mastodon-instance-to-a-multi-jail-freebsd-setup/" rel="alternate"/><published>2025-12-07T00:00:00+01:00</published><updated>2025-12-07T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-12-07:/migrating-burningboardnet-mastodon-instance-to-a-multi-jail-freebsd-setup/</id><summary type="html">&lt;p&gt;Migrating a Mastodon instance to FreeBSD with BastilleBSD - a multi-jail architecture with aggressive service separation, centralized &lt;span class="caps"&gt;PF&lt;/span&gt; firewalling, and a fully dual-stack network&amp;nbsp;design.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Over the last few weeks, I’ve been working on migrating our Mastodon instance &lt;strong&gt;burningboard.net&lt;/strong&gt; from its current Linux host to a modular FreeBSD jail-based setup powered by &lt;a href="https://bastillebsd.org"&gt;BastilleBSD&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post walks through the architecture and design rationale of my new multi-jail Mastodon system, with aggressive separation of concerns, centralized firewalling, and a fully dual-stack network&amp;nbsp;design.&lt;/p&gt;
&lt;h2 id="acknowledgements"&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;This work is based on the excellent post by &lt;strong&gt;Stefano Marinelli&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://it-notes.dragas.net/2022/11/23/installing-mastodon-on-a-freebsd-jail/"&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Installing Mastodon on a FreeBSD&amp;nbsp;jail&amp;#8221;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Stefano’s article inspired me to try Mastodon on FreeBSD. My implementation takes that foundation and extends it for a more maintainable, production-ready&amp;nbsp;architecture.&lt;/p&gt;
&lt;h2 id="design-goals"&gt;Design&amp;nbsp;Goals&lt;/h2&gt;
&lt;p&gt;The motivation behind this&amp;nbsp;move:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Central &lt;span class="caps"&gt;PF&lt;/span&gt; firewall&lt;/strong&gt; – all filtering, &lt;span class="caps"&gt;NAT&lt;/span&gt;, and routing are handled by the host only. Jails see a clean, local L2 view - no &lt;span class="caps"&gt;PF&lt;/span&gt; inside jails, no double &lt;span class="caps"&gt;NAT&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separation of concerns&lt;/strong&gt; – every jail runs exactly one functional service:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nginx&lt;/code&gt; — reverse proxy + &lt;span class="caps"&gt;TLS&lt;/span&gt;&amp;nbsp;termination  &lt;/li&gt;
&lt;li&gt;&lt;code&gt;mastodonweb&lt;/code&gt; — Puma / Rails web&amp;nbsp;backend  &lt;/li&gt;
&lt;li&gt;&lt;code&gt;mastodonsidekiq&lt;/code&gt; — background&amp;nbsp;jobs  &lt;/li&gt;
&lt;li&gt;&lt;code&gt;database&lt;/code&gt; — PostgreSQL and Valkey (Redis&amp;nbsp;fork)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host‑managed source&lt;/strong&gt; – Mastodon source tree shared&amp;nbsp;via &lt;code&gt;nullfs&lt;/code&gt; between web and sidekiq jails.&amp;nbsp;Common &lt;code&gt;.env.production&lt;/code&gt;, shared dependencies, single codebase to&amp;nbsp;maintain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean dual‑stack (IPv4 + IPv6)&lt;/strong&gt; – every component visible under both protocols; no &lt;span class="caps"&gt;NAT66&lt;/span&gt; or translation&amp;nbsp;hacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable networking&lt;/strong&gt; – each functional group lives on its own bridge with private address&amp;nbsp;space.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="jail-and-network-overview"&gt;Jail and Network&amp;nbsp;Overview&lt;/h2&gt;
&lt;p&gt;Example address plan (using &lt;span class="caps"&gt;RFC&lt;/span&gt; 5737 and 3849 documentation&amp;nbsp;spaces):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Jail&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;IPv4&lt;/th&gt;
&lt;th&gt;IPv6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nginx&lt;/td&gt;
&lt;td&gt;Reverse proxy&lt;/td&gt;
&lt;td&gt;192.0.2.13&lt;/td&gt;
&lt;td&gt;2001:db8:8000::13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mastodonweb&lt;/td&gt;
&lt;td&gt;Rails backend&lt;/td&gt;
&lt;td&gt;198.51.100.9&lt;/td&gt;
&lt;td&gt;2001:db8:9000::9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mastodonsidekiq&lt;/td&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;198.51.100.8&lt;/td&gt;
&lt;td&gt;2001:db8:b000::8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;database&lt;/td&gt;
&lt;td&gt;PostgreSQL + Valkey&lt;/td&gt;
&lt;td&gt;198.51.100.6&lt;/td&gt;
&lt;td&gt;2001:db8:a000::6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;&amp;#8220;burningboard.example.net&amp;#8221;&lt;/td&gt;
&lt;td&gt;203.0.113.1&lt;/td&gt;
&lt;td&gt;2001:db8::f3d1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each functional bucket gets its&amp;nbsp;own &lt;code&gt;bridge(4)&lt;/code&gt; interface on the host&amp;nbsp;(&lt;code&gt;bastille0&lt;/code&gt;..&lt;code&gt;bastille3&lt;/code&gt;) and its&amp;nbsp;own &lt;code&gt;/24&lt;/code&gt; and &lt;code&gt;/64&lt;/code&gt; subnet.&lt;br&gt;
Jails are created and attached to the corresponding&amp;nbsp;bridge.&lt;/p&gt;
&lt;h2 id="schematic-diagram"&gt;Schematic&amp;nbsp;diagram&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[ Internet ]
     |
     v
 [ PF Host ]
     ├── bridge0 — nginx (192.0.2.13 / 2001:db8:8000::13)
     ├── bridge1 — mastodonweb (198.51.100.9 / 2001:db8:9000::9)
     ├── bridge2 — database (198.51.100.6 / 2001:db8:a000::6)
     └── bridge3 — sidekiq (198.51.100.8 / 2001:db8:b000::8)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With the address plan established, the next step is creating the individual jails and assigning virtual network&amp;nbsp;interfaces.&lt;/p&gt;
&lt;h2 id="jail-creation-and-perjail-configuration"&gt;Jail Creation and Per‑Jail&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;Each jail was created directly through Bastille using &lt;span class="caps"&gt;VNET&lt;/span&gt; support, attaching it to its respective bridge.&lt;br&gt;
For example, creating&amp;nbsp;the &lt;code&gt;nginx&lt;/code&gt; frontend jail on&amp;nbsp;the &lt;code&gt;bastille0&lt;/code&gt; bridge:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;bastille&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;nginx&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;.3-RELEASE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.0.2.13&lt;span class="w"&gt; &lt;/span&gt;bastille0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Bastille automatically provisions a &lt;span class="caps"&gt;VNET&lt;/span&gt; interface inside the jail&amp;nbsp;(&lt;code&gt;vnet0&lt;/code&gt;) and associates it with the corresponding bridge on the host.&lt;br&gt;
Inside each jail,&amp;nbsp;the &lt;code&gt;/etc/rc.conf&lt;/code&gt; defines its own network interface, IPv4/IPv6 addresses, default routes, and any service daemons enabled for that&amp;nbsp;jail.&lt;/p&gt;
&lt;p&gt;Example configuration for&amp;nbsp;the &lt;code&gt;database&lt;/code&gt; jail (substituted with documentation&amp;nbsp;addresses):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_e0b_database_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.6 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:a000::6/64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_descr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;database jail interface on bastille2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:a000::1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;syslogd_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-ss&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_submit_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_outbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_msp_queue_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;cron_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-J 60&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;valkey_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;postgresql_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each jail is therefore a &lt;strong&gt;fully self‑contained FreeBSD environment&lt;/strong&gt; with native rc(8) configuration, its own routing table, and service definition. Bastille’s role ends at boot‑time network attachment - the rest is standard FreeBSD&amp;nbsp;administration.&lt;/p&gt;
&lt;h2 id="host-etcrcconf"&gt;Host &lt;code&gt;/etc/rc.conf&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Below is a simplified version of the host configuration that ties everything together.&lt;br&gt;
Each jail bridge subnet is assigned both IPv4 and IPv6 space; the host acts as&amp;nbsp;gateway.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Basic host config&lt;/span&gt;
&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;burningboard.example.net&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;keymap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;us.kbd&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Networking&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 203.0.113.1 netmask 255.255.255.255&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8::f3d1/64&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;203.0.113.254&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8::1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Bridges for jails&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0 bridge1 bridge2 bridge3&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge1_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge2_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille2&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge3_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille3&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Bridge interfaces for individual networks&lt;/span&gt;

&lt;span class="c1"&gt;# Frontend (nginx)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 192.0.2.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:8000::1/64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Mastodon Web (Rails / Puma)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille1_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:9000::1/64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Database&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.2/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille2_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:a000::1/64&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Sidekiq (workers)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille3&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 198.51.100.3/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille3_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:b000::1/64&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pflog_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;bastille_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ntpd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ntpd_sync_on_start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This provides proper L3 separation for each functional group.&lt;br&gt;
In this&amp;nbsp;layout, &lt;code&gt;bastille0&lt;/code&gt; →&amp;nbsp;frontend, &lt;code&gt;bastille1&lt;/code&gt; →&amp;nbsp;app, &lt;code&gt;bastille2&lt;/code&gt; → &lt;span class="caps"&gt;DB&lt;/span&gt;, &lt;code&gt;bastille3&lt;/code&gt; → worker&amp;nbsp;pool.&lt;/p&gt;
&lt;h2 id="etcpfconf"&gt;&lt;code&gt;/etc/pf.conf&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The host firewall serves the dual purpose of &lt;span class="caps"&gt;NAT&lt;/span&gt; gateway and service ingress&amp;nbsp;controller.&lt;/p&gt;
&lt;p&gt;Below is an anonymized but structurally identical&amp;nbsp;configuration.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Macros ---&lt;/span&gt;
&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;198.51.100.0/20&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:8000::/64&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8::f3d1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;frontend_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;192.0.2.13&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;frontend_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:8000::13&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Trusted management networks (example)&lt;/span&gt;
&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 203.0.113.42, 192.0.2.222 }&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ 2001:db8:beef::/64 }&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;

&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;

&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;

&lt;span class="c1"&gt;# --- NAT ---&lt;/span&gt;
&lt;span class="c1"&gt;# Jails -&amp;gt; egress internet (IPv4)&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Port redirection ---&lt;/span&gt;
&lt;span class="c1"&gt;# Incoming HTTP/HTTPS -&amp;gt; nginx jail&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v6&lt;/span&gt;

&lt;span class="c1"&gt;# --- Filtering policy ---&lt;/span&gt;

&lt;span class="c1"&gt;# Default deny (log for audit)&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;

&lt;span class="c1"&gt;# Allow existing stateful flows out&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Allow management SSH (example port 30822) only from trusted subnets&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30822&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30822&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Block all other SSH&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30822&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh_blocked&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# ICMP/ICMPv6 essentials&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Inter-jail traffic&lt;/span&gt;
&lt;span class="c1"&gt;# nginx -&amp;gt; mastodonweb&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;192.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;2.13&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# mastodonweb -&amp;gt; database (Postgres + Valkey)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;a000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# sidekiq -&amp;gt; database&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;b000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;a000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Optional: temporary egress blocking during testing&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;

&lt;span class="c1"&gt;# External access&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;frontend_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This &lt;span class="caps"&gt;PF&lt;/span&gt; configuration centralizes control at the host. The jails have no firewall logic - just clean &lt;span class="caps"&gt;IP&lt;/span&gt;&amp;nbsp;connectivity.&lt;/p&gt;
&lt;h2 id="shared-source-design"&gt;Shared Source&amp;nbsp;Design&lt;/h2&gt;
&lt;p&gt;Both &lt;code&gt;mastodonweb&lt;/code&gt; and &lt;code&gt;mastodonsidekiq&lt;/code&gt; jails&amp;nbsp;mount &lt;code&gt;/usr/local/mastodon&lt;/code&gt; from the&amp;nbsp;host:&lt;/p&gt;
&lt;p&gt;/usr/local/mastodon -&amp;gt;&amp;nbsp;/usr/local/bastille/jails/mastodonweb/root/usr/home/mastodon&lt;/p&gt;
&lt;p&gt;/usr/local/mastodon -&amp;gt;&amp;nbsp;/usr/local/bastille/jails/mastodonsidekiq/root/usr/home/mastodon&lt;/p&gt;
&lt;p&gt;Example &lt;code&gt;fstab&lt;/code&gt; entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/usr/local/mastodon /usr/local/bastille/jails/mastodonweb/root/usr/home/mastodon nullfs rw 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That way, only one source tree needs updates after a git pull or bundle/yarn operation. The jails simply see the current state of that&amp;nbsp;directory.&lt;/p&gt;
&lt;p&gt;Logs and tmp directories are symlinked to /var/log/mastodon and /var/tmp/mastodon inside each jail for persistence and&amp;nbsp;cleanup.&lt;/p&gt;
&lt;h2 id="service-boot-integration"&gt;Service Boot&amp;nbsp;Integration&lt;/h2&gt;
&lt;p&gt;Each Mastodon jail defines lightweight /usr/local/etc/rc.d&amp;nbsp;scripts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c1"&gt;# PROVIDE: mastodon_web&lt;/span&gt;
&lt;span class="c1"&gt;# KEYWORD: shutdown&lt;/span&gt;

.&lt;span class="w"&gt; &lt;/span&gt;/etc/rc.subr

&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mastodon_web&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;rcvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mastodon_web_enable
&lt;span class="nv"&gt;pidfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/run/mastodon/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pid&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;start_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mastodon_web_start&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;stop_cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mastodon_web_stop&amp;quot;&lt;/span&gt;

mastodon_web_start&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/var/run/mastodon
&lt;span class="w"&gt;    &lt;/span&gt;chown&lt;span class="w"&gt; &lt;/span&gt;mastodon:mastodon&lt;span class="w"&gt; &lt;/span&gt;/var/run/mastodon
&lt;span class="w"&gt;    &lt;/span&gt;su&lt;span class="w"&gt; &lt;/span&gt;mastodon&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;export PATH=/usr/local/bin:/usr/bin:/bin; \&lt;/span&gt;
&lt;span class="s2"&gt;        export RAILS_ENV=production; export PORT=3000; \&lt;/span&gt;
&lt;span class="s2"&gt;        cd /home/mastodon/live &amp;amp;&amp;amp; \&lt;/span&gt;
&lt;span class="s2"&gt;        /usr/sbin/daemon -T &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -P /var/run/mastodon/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_supervisor.pid \&lt;/span&gt;
&lt;span class="s2"&gt;        -p /var/run/mastodon/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pid -f -S -r \&lt;/span&gt;
&lt;span class="s2"&gt;        /usr/local/bin/bundle exec puma -C config/puma.rb&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

mastodon_web_stop&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-9&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/var/run/mastodon/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;_supervisor.pid&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-15&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/var/run/mastodon/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.pid&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null
&lt;span class="o"&gt;}&lt;/span&gt;

load_rc_config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;
run_rc_command&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Equivalent scripts exist for mastodon_streaming and the Sidekiq&amp;nbsp;worker.&lt;/p&gt;
&lt;p&gt;Everything integrates seamlessly with FreeBSD’s native service&amp;nbsp;management:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;service&lt;span class="w"&gt; &lt;/span&gt;mastodon_web&lt;span class="w"&gt; &lt;/span&gt;start
service&lt;span class="w"&gt; &lt;/span&gt;mastodon_streaming&lt;span class="w"&gt; &lt;/span&gt;restart
service&lt;span class="w"&gt; &lt;/span&gt;mastodonsidekiq&lt;span class="w"&gt; &lt;/span&gt;status
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No Docker, no systemd, no exotic process&amp;nbsp;supervisors.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-it-matters"&gt;Why It&amp;nbsp;Matters&lt;/h2&gt;
&lt;p&gt;The resulting system is simple, observable, and&amp;nbsp;robust:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firewall rules are centralized and&amp;nbsp;auditable.&lt;/li&gt;
&lt;li&gt;Each jail is a clean service container (pure FreeBSD primitives, no overlay&amp;nbsp;complexity).&lt;/li&gt;
&lt;li&gt;IPv4/IPv6 connectivity is symmetrical and&amp;nbsp;clear.&lt;/li&gt;
&lt;li&gt;Source and configs are under full administrator control, not hidden in&amp;nbsp;containers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s also easy to snapshot with &lt;span class="caps"&gt;ZFS&lt;/span&gt; or promote new releases jail-by-jail using Bastille’s clone/deploy&amp;nbsp;model.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;In&amp;nbsp;short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host does &lt;span class="caps"&gt;PF&lt;/span&gt;, routing, &lt;span class="caps"&gt;NAT&lt;/span&gt;,&amp;nbsp;bridges&lt;/li&gt;
&lt;li&gt;Each jail has exactly one&amp;nbsp;purpose&lt;/li&gt;
&lt;li&gt;Source code lives once on the&amp;nbsp;host&lt;/li&gt;
&lt;li&gt;Dual-stack networking, no&amp;nbsp;translation&lt;/li&gt;
&lt;li&gt;Everything&amp;nbsp;FreeBSD-native&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This structure makes it easy to reason about - each moving part has one&amp;nbsp;job. &lt;/p&gt;
&lt;p&gt;That’s how I like my infrastructure: boringly&amp;nbsp;reliable.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://it-notes.dragas.net/2022/11/23/installing-mastodon-on-a-freebsd-jail/"&gt;Stefano Marinelli — Installing Mastodon on a FreeBSD&amp;nbsp;jail&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bastillebsd.org/"&gt;The BastilleBSD&amp;nbsp;Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook –&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc3849"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 3849 – IPv6 Documentation&amp;nbsp;Prefix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5737"&gt;&lt;span class="caps"&gt;RFC&lt;/span&gt; 5737 – IPv4 Documentation&amp;nbsp;Space&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="networking"/><category term="mastodon"/><category term="ipv6"/></entry><entry><title>Production-Grade Container Deployment with Podman Quadlets</title><link href="https://blog.hofstede.it/production-grade-container-deployment-with-podman-quadlets/" rel="alternate"/><published>2025-11-16T00:00:00+01:00</published><updated>2025-11-16T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-11-16:/production-grade-container-deployment-with-podman-quadlets/</id><summary type="html">&lt;p&gt;A Practical Guide to build secure, maintainable multi-container applications with&amp;nbsp;Podman&lt;/p&gt;</summary><content type="html">&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Containers have become the de facto standard for application deployment, but the conversation often jumps straight to Kubernetes when discussing production workloads. While K8s excels at large-scale orchestration, many production services don&amp;#8217;t require that level of complexity. For single-host or small-scale deployments, a well-architected Podman setup with systemd integration can provide robust, secure, and maintainable&amp;nbsp;infrastructure.&lt;/p&gt;
&lt;p&gt;This article demonstrates a production-grade container deployment using &lt;strong&gt;Red Hat Enterprise Linux 10&lt;/strong&gt;, &lt;strong&gt;Podman Quadlets&lt;/strong&gt;, and &lt;strong&gt;Traefik&lt;/strong&gt; as a reverse proxy. We&amp;#8217;ll walk through deploying Forgejo (a self-hosted Git service) as a practical example, covering the technical implementation and the architectural decisions behind&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Some shipping containers" src="https://blog.hofstede.it/images/2025-11-16-container_forgejo_podman_quadlet.webp" title="Container Image"&gt;&lt;/p&gt;
&lt;h2 id="why-this-approach"&gt;Why This&amp;nbsp;Approach?&lt;/h2&gt;
&lt;p&gt;Before diving into the implementation, let&amp;#8217;s address the fundamental design&amp;nbsp;decisions:&lt;/p&gt;
&lt;h3 id="podman-over-docker"&gt;Podman over&amp;nbsp;Docker&lt;/h3&gt;
&lt;p&gt;Red Hat has made Podman the standard container runtime in &lt;span class="caps"&gt;RHEL&lt;/span&gt; for compelling technical and security reasons. This isn&amp;#8217;t just vendor preference - it represents fundamental architectural&amp;nbsp;improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Daemonless architecture&lt;/strong&gt;: No privileged daemon running as root, reducing the attack surface significantly. Each container runs as a direct child of systemd or the user&amp;nbsp;session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rootless containers&lt;/strong&gt;: Native support for running containers as unprivileged users - a first-class feature, not a&amp;nbsp;bolt-on&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;systemd integration&lt;/strong&gt;: First-class integration with the init system that already manages your services. This is particularly powerful in &lt;span class="caps"&gt;RHEL&lt;/span&gt; environments where systemd&amp;#8217;s maturity and tooling are&amp;nbsp;well-established.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;OCI&lt;/span&gt; compliance&lt;/strong&gt;: Full compatibility with Docker images and registries - your existing container images work without&amp;nbsp;modification&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod support&lt;/strong&gt;: Kubernetes-style pod concepts for grouping containers, making the transition to K8s smoother if&amp;nbsp;needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fork/exec model&lt;/strong&gt;: Unlike Docker&amp;#8217;s client-server architecture, Podman uses traditional Unix fork/exec, making it more auditable and debuggable with standard&amp;nbsp;tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From an enterprise perspective, Podman aligns with &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;#8217;s security-first philosophy. SELinux, user namespaces, and cgroups v2 integration are not afterthoughts but core design&amp;nbsp;elements.&lt;/p&gt;
&lt;h3 id="quadlets-over-compose"&gt;Quadlets over&amp;nbsp;Compose&lt;/h3&gt;
&lt;p&gt;Traditional Docker Compose files are imperative and require a separate daemon. Podman Quadlets leverage systemd&amp;#8217;s unit file format,&amp;nbsp;providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Declarative configuration&lt;/strong&gt;: Define the desired state, let systemd handle the&amp;nbsp;lifecycle&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native service management&lt;/strong&gt;: Use&amp;nbsp;familiar &lt;code&gt;systemctl&lt;/code&gt; commands - the same tooling administrators already&amp;nbsp;know&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency management&lt;/strong&gt;: Leverage systemd&amp;#8217;s robust dependency graph&amp;nbsp;(&lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt;,&amp;nbsp;etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic updates&lt;/strong&gt;: Built-in support for container image updates&amp;nbsp;via &lt;code&gt;podman-auto-update.timer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource control&lt;/strong&gt;: Direct access to systemd&amp;#8217;s cgroup integration for &lt;span class="caps"&gt;CPU&lt;/span&gt;, memory, and I/O&amp;nbsp;limits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Journal integration&lt;/strong&gt;: All container logs automatically flow to journald, integrating with existing log&amp;nbsp;infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="network-segmentation-by-design"&gt;Network Segmentation by&amp;nbsp;Design&lt;/h3&gt;
&lt;p&gt;Proper network isolation is crucial for&amp;nbsp;security:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend network&lt;/strong&gt;: IPv6-enabled network for Traefik and application&amp;nbsp;containers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend networks&lt;/strong&gt;: Isolated networks for database&amp;nbsp;communication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No unnecessary exposure&lt;/strong&gt;: Database containers never touch the frontend&amp;nbsp;network&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-architecture"&gt;The&amp;nbsp;Architecture&lt;/h2&gt;
&lt;p&gt;Our example deployment consists of three&amp;nbsp;components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Forgejo&lt;/strong&gt; - The Git service&amp;nbsp;application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; - The database&amp;nbsp;backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traefik&lt;/strong&gt; - The reverse proxy handling &lt;span class="caps"&gt;TLS&lt;/span&gt; and&amp;nbsp;routing&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    Internet
                       │
                       ▼
                   Traefik (Port 443)
                       │
          ┌────────────┴────────────┐
          │    frontend network     │
          │    (10.89.0.0/24)       │
          └────────────┬────────────┘
                       │
                  Forgejo Container
                       │
          ┌────────────┴────────────┐
          │ forgejo-backend.network │
          │   (isolated)            │
          └────────────┬────────────┘
                       │
                PostgreSQL Container
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="practical-implementation-deploying-forgejo"&gt;Practical Implementation: Deploying&amp;nbsp;Forgejo&lt;/h2&gt;
&lt;p&gt;Let&amp;#8217;s walk through deploying a complete Git hosting service with database backend and &lt;span class="caps"&gt;TLS&lt;/span&gt;&amp;nbsp;termination.&lt;/p&gt;
&lt;h3 id="step-1-enable-podman-socket-for-traefik"&gt;Step 1: Enable Podman Socket for&amp;nbsp;Traefik&lt;/h3&gt;
&lt;p&gt;Traefik&amp;#8217;s Docker provider expects a Docker-compatible &lt;span class="caps"&gt;API&lt;/span&gt; socket. Podman provides this through a systemd-managed&amp;nbsp;socket:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enable and start the Podman socket&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;podman.socket

&lt;span class="c1"&gt;# Verify the socket is active&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;podman.socket

&lt;span class="c1"&gt;# Check socket location&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;/run/podman/podman.sock
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Technical detail&lt;/strong&gt;: The Podman socket&amp;nbsp;(&lt;code&gt;/run/podman/podman.sock&lt;/code&gt;) provides a Docker-compatible &lt;span class="caps"&gt;REST&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt;. Traefik connects to this socket to discover containers and read their labels for dynamic configuration. This is mounted into the Traefik container&amp;nbsp;as &lt;code&gt;/var/run/docker.sock&lt;/code&gt;, maintaining compatibility with Traefik&amp;#8217;s Docker provider&amp;nbsp;configuration.&lt;/p&gt;
&lt;p&gt;Without this socket enabled, Traefik cannot discover containers or process their routing labels - the declarative configuration simply won&amp;#8217;t&amp;nbsp;work.&lt;/p&gt;
&lt;h3 id="step-2-network-configuration"&gt;Step 2: Network&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;First, create the networks. The frontend network enables IPv6 and connects to&amp;nbsp;Traefik:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create the IPv6-enabled frontend network&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;network&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--ipv6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--subnet&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.89.0.0/24&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--gateway&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.89.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;ipv6

&lt;span class="c1"&gt;# Create the isolated backend network for database communication&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;network&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;forgejo-backend.network
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Design Decision&lt;/strong&gt;: Why two networks? The frontend network (despite its name &amp;#8220;ipv6&amp;#8221;) handles all external-facing traffic. The backend network ensures PostgreSQL is never directly accessible from the frontend, implementing&amp;nbsp;defense-in-depth.&lt;/p&gt;
&lt;h3 id="step-3-secrets-management"&gt;Step 3: Secrets&amp;nbsp;Management&lt;/h3&gt;
&lt;p&gt;Never hardcode passwords in configuration files. Podman secrets integrate with systemd and provide secure credential&amp;nbsp;storage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generate a strong database password&lt;/span&gt;
pwgen&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;podman&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;forgejo_db_password&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Design Decision&lt;/strong&gt;:&amp;nbsp;Using &lt;code&gt;podman secret&lt;/code&gt; over environment variables in files provides:
- Encrypted storage
- Proper access control
- No secrets in process lists or logs
- Easy rotation without rebuilding&amp;nbsp;containers&lt;/p&gt;
&lt;h3 id="step-4-database-container-quadlet"&gt;Step 4: Database Container&amp;nbsp;(Quadlet)&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;/etc/containers/systemd/forgejo-db.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-db&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/postgres:16-alpine&lt;/span&gt;

&lt;span class="c1"&gt;# Network isolation - only on backend network&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-backend.network&lt;/span&gt;

&lt;span class="c1"&gt;# PostgreSQL configuration&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_USER=forgejo&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_DB=forgejo&lt;/span&gt;

&lt;span class="c1"&gt;# Secret injection as environment variable&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo_db_password,type=env,target=POSTGRES_PASSWORD&lt;/span&gt;

&lt;span class="c1"&gt;# Persistent storage with SELinux context&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/postgres:/var/lib/postgresql/data:z&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key technical points&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AutoUpdate=registry&lt;/code&gt;: Enables automatic image updates&amp;nbsp;via &lt;code&gt;podman auto-update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Volume&lt;/code&gt; flag &lt;code&gt;:z&lt;/code&gt;: Automatically relabels SELinux contexts for container&amp;nbsp;access&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Secret&lt;/code&gt; directive: Injects the secret as an environment variable at&amp;nbsp;runtime&lt;/li&gt;
&lt;li&gt;No frontend network: Database is completely isolated from external&amp;nbsp;access&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-5-application-container-quadlet"&gt;Step 5: Application Container&amp;nbsp;(Quadlet)&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;/etc/containers/systemd/forgejo-server.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-server&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;codeberg.org/forgejo/forgejo:13&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="c1"&gt;# Dual network attachment&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-backend.network&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ipv6&lt;/span&gt;

&lt;span class="c1"&gt;# Application configuration&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;USER_UID=1000&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;USER_GID=1000&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__DB_TYPE=postgres&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__HOST=forgejo-db:5432&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__NAME=forgejo&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;FORGEJO__database__USER=forgejo&lt;/span&gt;

&lt;span class="c1"&gt;# Database password from secret&lt;/span&gt;
&lt;span class="na"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo_db_password,type=env,target=FORGEJO__database__PASSWD&lt;/span&gt;

&lt;span class="c1"&gt;# Persistent storage&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/forgejo:/data:z&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;

&lt;span class="c1"&gt;# Traefik labels for routing&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.enable=true&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.docker.network=ipv6&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.rule=Host(`git.example.com`)&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.entrypoints=https&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.service=forgejo-http&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.tls.certresolver=traefiktls&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.routers.forgejo.middlewares=secure-headers@file&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.http.services.forgejo-http.loadbalancer.server.port=3000&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# SSH Git access via Traefik TCP routing&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.routers.forgejo-ssh.rule=HostSNI(`*`)&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.routers.forgejo-ssh.entrypoints=ssh&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.routers.forgejo-ssh.service=forgejo-ssh&amp;quot;&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;traefik.tcp.services.forgejo-ssh.loadbalancer.server.port=22&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;

&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo-db.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Design decisions explained&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dual network attachment&lt;/strong&gt;: The container needs backend network for PostgreSQL and frontend for&amp;nbsp;Traefik&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traefik labels&lt;/strong&gt;: Declarative routing configuration - no manual Traefik config files&amp;nbsp;needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;SSH&lt;/span&gt; routing&lt;/strong&gt;: Traefik handles both &lt;span class="caps"&gt;HTTP&lt;/span&gt; and &lt;span class="caps"&gt;TCP&lt;/span&gt; (Git &lt;span class="caps"&gt;SSH&lt;/span&gt;) on different&amp;nbsp;ports&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Systemd dependency&lt;/strong&gt;: &lt;code&gt;After=forgejo-db.service&lt;/code&gt; ensures database starts&amp;nbsp;first&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="step-6-reverse-proxy-configuration"&gt;Step 6: Reverse Proxy&amp;nbsp;Configuration&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;/etc/containers/systemd/traefik.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik&lt;/span&gt;
&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik:latest&lt;/span&gt;
&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;

&lt;span class="c1"&gt;# Required for binding to privileged ports&lt;/span&gt;
&lt;span class="na"&gt;AddCapability&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CAP_NET_BIND_SERVICE&lt;/span&gt;

&lt;span class="c1"&gt;# Frontend network only&lt;/span&gt;
&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ipv6&lt;/span&gt;

&lt;span class="c1"&gt;# Port exposure&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;80:80&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;443:443&lt;/span&gt;
&lt;span class="na"&gt;PublishPort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2222:2222&lt;/span&gt;

&lt;span class="c1"&gt;# Security hardening&lt;/span&gt;
&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;SecurityLabelType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;container_runtime_t&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration and state&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/run/podman/podman.sock:/var/run/docker.sock:ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik/traefik.yml:/etc/traefik/traefik.yml:z,ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik/config.yml:/etc/traefik/config.yml:z,ro&lt;/span&gt;
&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik/letsencrypt:/letsencrypt:z&lt;/span&gt;

&lt;span class="c1"&gt;# Self-configuration for dashboard&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.entrypoints=https&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.service=api@internal&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.tls=true&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.tls.certresolver=traefiktls&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.dashboard.middlewares=dashboard-auth,secure-headers@file&lt;/span&gt;
&lt;span class="na"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$...&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create &lt;code&gt;/opt/traefik/traefik.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;checkNewVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;sendAnonymousUsage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;insecure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;:80&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;redirections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;entryPoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;:443&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;:2222&amp;quot;&lt;/span&gt;

&lt;span class="nt"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;unix:///var/run/docker.sock&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;exposedByDefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ipv6&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/traefik/config.yml&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="nt"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;traefiktls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;acme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;admin@example.com&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/letsencrypt/acme.json&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;httpChallenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;entryPoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;http&lt;/span&gt;

&lt;span class="nt"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;INFO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create &lt;code&gt;/opt/traefik/config.yml&lt;/code&gt; for shared&amp;nbsp;middlewares:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;secure-headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;stsSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;31536000&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;stsIncludeSubdomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;stsPreload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;forceSTSHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;customFrameOptionsValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SAMEORIGIN&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;contentTypeNosniff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;browserXssFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;referrerPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;strict-origin-when-cross-origin&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;permissionsPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;geolocation=(),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;microphone=(),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;camera=()&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Security highlights&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NoNewPrivileges&lt;/code&gt;: Prevents privilege&amp;nbsp;escalation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SecurityLabelType&lt;/code&gt;: SELinux type&amp;nbsp;enforcement&lt;/li&gt;
&lt;li&gt;Automatic &lt;span class="caps"&gt;HTTP&lt;/span&gt; to &lt;span class="caps"&gt;HTTPS&lt;/span&gt;&amp;nbsp;redirect&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;HSTS&lt;/span&gt; headers for &lt;span class="caps"&gt;HTTPS&lt;/span&gt;&amp;nbsp;enforcement&lt;/li&gt;
&lt;li&gt;Let&amp;#8217;s Encrypt automation for &lt;span class="caps"&gt;TLS&lt;/span&gt;&amp;nbsp;certificates&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-7-deployment"&gt;Step 7:&amp;nbsp;Deployment&lt;/h3&gt;
&lt;p&gt;Reload systemd to recognize the new&amp;nbsp;units:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;daemon-reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Enable and start the&amp;nbsp;services:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enable automatic startup&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;forgejo-db.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;traefik.service

&lt;span class="c1"&gt;# Start the stack&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;forgejo-db.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service
systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;traefik.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The beauty of systemd integration&lt;/strong&gt;: You can now manage containers like any other&amp;nbsp;service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check status&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service

&lt;span class="c1"&gt;# View logs&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service&lt;span class="w"&gt; &lt;/span&gt;-f

&lt;span class="c1"&gt;# Restart&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service

&lt;span class="c1"&gt;# View dependency tree&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;list-dependencies&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="step-8-automated-updates"&gt;Step 8: Automated&amp;nbsp;Updates&lt;/h3&gt;
&lt;p&gt;Enable automatic container image&amp;nbsp;updates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enable the timer&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;podman-auto-update.timer

&lt;span class="c1"&gt;# Check update status&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;auto-update&lt;span class="w"&gt; &lt;/span&gt;--dry-run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;AutoUpdate=registry&lt;/code&gt; in the Quadlet files, Podman&amp;nbsp;will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check for new images&amp;nbsp;daily&lt;/li&gt;
&lt;li&gt;Pull updates if&amp;nbsp;available&lt;/li&gt;
&lt;li&gt;Recreate containers with new&amp;nbsp;images&lt;/li&gt;
&lt;li&gt;Preserve all volumes and&amp;nbsp;configuration&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="advanced-topics"&gt;Advanced&amp;nbsp;Topics&lt;/h2&gt;
&lt;h3 id="working-with-red-hat-registries"&gt;Working with Red Hat&amp;nbsp;Registries&lt;/h3&gt;
&lt;p&gt;While this example uses public container registries, production &lt;span class="caps"&gt;RHEL&lt;/span&gt; deployments often leverage Red Hat&amp;#8217;s container&amp;nbsp;catalog:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Authenticate to Red Hat registry (requires active subscription)&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;registry.redhat.io

&lt;span class="c1"&gt;# Pull RHEL-based images&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;pull&lt;span class="w"&gt; &lt;/span&gt;registry.redhat.io/rhel9/postgresql-15

&lt;span class="c1"&gt;# Use in Quadlet&lt;/span&gt;
&lt;span class="nv"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;registry.redhat.io/rhel9/postgresql-15
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Red Hat certified container images include:
- Support lifecycle matching &lt;span class="caps"&gt;RHEL&lt;/span&gt; versions
- Security errata and &lt;span class="caps"&gt;CVE&lt;/span&gt; fixes
- Compliance with enterprise requirements
- Verified compatibility with &lt;span class="caps"&gt;RHEL&lt;/span&gt; container&amp;nbsp;hosts&lt;/p&gt;
&lt;h3 id="selinux-integration"&gt;SELinux&amp;nbsp;Integration&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;#8217;s mandatory access control is a feature, not a bug. While many Docker tutorials suggest disabling SELinux, Podman embraces it as a critical security layer.&amp;nbsp;The &lt;code&gt;:z&lt;/code&gt; flag in volume mounts automatically handles SELinux&amp;nbsp;labeling:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/forgejo/postgres:/var/lib/postgresql/data:z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This relabels the host directory with the correct SELinux context&amp;nbsp;(&lt;code&gt;container_file_t&lt;/code&gt;) for container access. For read-only mounts,&amp;nbsp;use &lt;code&gt;:ro&lt;/code&gt; without &lt;code&gt;:z&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;span class="caps"&gt;RHEL&lt;/span&gt; best practice&lt;/strong&gt;: Never disable SELinux. If you encounter permission issues, investigate the context&amp;nbsp;(&lt;code&gt;ls -Z&lt;/code&gt;) rather than setting permissive mode. Podman&amp;#8217;s integration makes this dramatically easier than it was with&amp;nbsp;Docker.&lt;/p&gt;
&lt;p&gt;For advanced scenarios, you can use&amp;nbsp;uppercase &lt;code&gt;:Z&lt;/code&gt; to make the volume private to that specific container, or manually manage&amp;nbsp;contexts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check SELinux context&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;-Z&lt;span class="w"&gt; &lt;/span&gt;/opt/forgejo/

&lt;span class="c1"&gt;# Manually set context if needed&lt;/span&gt;
semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;container_file_t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/opt/forgejo(/.*)?&amp;quot;&lt;/span&gt;
restorecon&lt;span class="w"&gt; &lt;/span&gt;-Rv&lt;span class="w"&gt; &lt;/span&gt;/opt/forgejo/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="resource-limits"&gt;Resource&amp;nbsp;Limits&lt;/h3&gt;
&lt;p&gt;Systemd provides granular resource control through&amp;nbsp;cgroups:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2G&lt;/span&gt;
&lt;span class="na"&gt;CPUQuota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;200%&lt;/span&gt;
&lt;span class="na"&gt;TasksMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These limits are enforced by the kernel and prevent resource&amp;nbsp;exhaustion.&lt;/p&gt;
&lt;h3 id="health-checks"&gt;Health&amp;nbsp;Checks&lt;/h3&gt;
&lt;p&gt;Podman supports container health&amp;nbsp;checks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;span class="na"&gt;HealthCmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/curl -f http://localhost:3000/ || exit 1&lt;/span&gt;
&lt;span class="na"&gt;HealthInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;
&lt;span class="na"&gt;HealthTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;HealthRetries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Systemd can react to failed health checks and restart containers&amp;nbsp;automatically.&lt;/p&gt;
&lt;h2 id="monitoring-and-observability"&gt;Monitoring and&amp;nbsp;Observability&lt;/h2&gt;
&lt;h3 id="viewing-logs"&gt;Viewing&amp;nbsp;Logs&lt;/h3&gt;
&lt;p&gt;All container output goes to&amp;nbsp;journald:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Real-time logs&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service&lt;span class="w"&gt; &lt;/span&gt;-f

&lt;span class="c1"&gt;# Last 100 lines&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;

&lt;span class="c1"&gt;# Logs from specific time&lt;/span&gt;
journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service&lt;span class="w"&gt; &lt;/span&gt;--since&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2024-01-01 12:00:00&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="container-inspection"&gt;Container&amp;nbsp;Inspection&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Container details&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;inspect&lt;span class="w"&gt; &lt;/span&gt;forgejo-server

&lt;span class="c1"&gt;# Resource usage&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;stats&lt;span class="w"&gt; &lt;/span&gt;forgejo-server

&lt;span class="c1"&gt;# Network information&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;network&lt;span class="w"&gt; &lt;/span&gt;inspect&lt;span class="w"&gt; &lt;/span&gt;ipv6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="comparison-with-kubernetes"&gt;Comparison with&amp;nbsp;Kubernetes&lt;/h2&gt;
&lt;p&gt;This approach is not meant to replace Kubernetes for large-scale deployments, but it offers distinct advantages for single-host or small-scale&amp;nbsp;scenarios:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Significantly lower resource&amp;nbsp;overhead&lt;/li&gt;
&lt;li&gt;Simpler mental model and&amp;nbsp;troubleshooting&lt;/li&gt;
&lt;li&gt;Direct integration with &lt;span class="caps"&gt;OS&lt;/span&gt;-level&amp;nbsp;tools&lt;/li&gt;
&lt;li&gt;No additional control plane&amp;nbsp;components&lt;/li&gt;
&lt;li&gt;Easier to audit and&amp;nbsp;secure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to choose K8s instead&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multi-host orchestration&amp;nbsp;requirements&lt;/li&gt;
&lt;li&gt;Advanced scheduling&amp;nbsp;needs&lt;/li&gt;
&lt;li&gt;Built-in service mesh&amp;nbsp;requirements&lt;/li&gt;
&lt;li&gt;Teams already invested in K8s&amp;nbsp;ecosystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="security-considerations"&gt;Security&amp;nbsp;Considerations&lt;/h2&gt;
&lt;p&gt;This setup implements several security&amp;nbsp;layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Network segmentation&lt;/strong&gt;: Databases isolated from&amp;nbsp;frontend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rootless option&lt;/strong&gt;: All containers can run as unprivileged&amp;nbsp;users&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SELinux enforcement&lt;/strong&gt;: Mandatory access&amp;nbsp;control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret management&lt;/strong&gt;: No credentials in configuration&amp;nbsp;files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic updates&lt;/strong&gt;: Regular security&amp;nbsp;patches&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;TLS&lt;/span&gt; termination&lt;/strong&gt;: Encrypted transport with Let&amp;#8217;s&amp;nbsp;Encrypt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security headers&lt;/strong&gt;: &lt;span class="caps"&gt;HSTS&lt;/span&gt;, &lt;span class="caps"&gt;CSP&lt;/span&gt;, and other&amp;nbsp;protections&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For even stricter security, run containers in rootless&amp;nbsp;mode:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# As regular user (not root)&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;forgejo-server.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Modern container deployment doesn&amp;#8217;t require Kubernetes for every use case. With &lt;span class="caps"&gt;RHEL&lt;/span&gt; Quadlets, Podman, and proper architectural patterns, you can build production-grade container infrastructure that&amp;nbsp;is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Secure&lt;/strong&gt;: Multiple layers of isolation and access control, leveraging &lt;span class="caps"&gt;RHEL&lt;/span&gt;&amp;#8217;s security-first&amp;nbsp;design&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintainable&lt;/strong&gt;: Declarative configuration with systemd&amp;nbsp;integration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observable&lt;/strong&gt;: Native integration with journald and systemd&amp;nbsp;tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated&lt;/strong&gt;: Built-in update mechanisms via systemd&amp;nbsp;timers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilient&lt;/strong&gt;: Systemd&amp;#8217;s proven service&amp;nbsp;management&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise-ready&lt;/strong&gt;: Backed by Red Hat&amp;#8217;s support lifecycle and security&amp;nbsp;practices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach proves particularly valuable&amp;nbsp;for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Self-hosted services and edge&amp;nbsp;deployments&lt;/li&gt;
&lt;li&gt;Development environments matching&amp;nbsp;production&lt;/li&gt;
&lt;li&gt;Organizations preferring simpler operational&amp;nbsp;models&lt;/li&gt;
&lt;li&gt;Hybrid scenarios where some services don&amp;#8217;t warrant K8s&amp;nbsp;overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="next-steps-for-red-hat-practitioners"&gt;Next Steps for Red Hat&amp;nbsp;Practitioners&lt;/h3&gt;
&lt;p&gt;This foundation scales naturally into the broader Red Hat container&amp;nbsp;ecosystem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fedora CoreOS&lt;/strong&gt;: Apply these Quadlet patterns to immutable, auto-updating&amp;nbsp;infrastructure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenShift&lt;/strong&gt;: Recognize how systemd-managed containers relate to K8s&amp;nbsp;pods&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ansible automation&lt;/strong&gt;: Codify Quadlet deployment&amp;nbsp;with &lt;code&gt;containers.podman&lt;/code&gt; collection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image building&lt;/strong&gt;: Explore Buildah and Skopeo for &lt;span class="caps"&gt;OCI&lt;/span&gt; image&amp;nbsp;workflows&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The combination of Podman&amp;#8217;s security-first design and systemd&amp;#8217;s battle-tested service management creates a robust foundation for containerized applications without the operational overhead of full orchestration platforms - and provides essential knowledge for working across Red Hat&amp;#8217;s entire container&amp;nbsp;portfolio.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further&amp;nbsp;Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"&gt;Podman Quadlet&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/10/html/building_running_and_managing_containers/"&gt;Red Hat Enterprise Linux Container&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doc.traefik.io/traefik/providers/docker/"&gt;Traefik Podman&amp;nbsp;Provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html"&gt;systemd.resource-control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://connect.redhat.com/en/partner-with-us/red-hat-container-certification"&gt;Red Hat Container&amp;nbsp;Certification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.fedoraproject.org/en-US/fedora-coreos/"&gt;Fedora CoreOS&amp;nbsp;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</content><category term="Linux"/><category term="linux"/><category term="rhel"/><category term="podman"/><category term="containers"/><category term="forgejo"/></entry><entry><title>FreeBSD Dual-Stack Jails on Hetzner Cloud</title><link href="https://blog.hofstede.it/freebsd-dual-stack-jails-on-hetzner-cloud/" rel="alternate"/><published>2025-11-12T00:00:00+01:00</published><updated>2025-11-12T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-11-12:/freebsd-dual-stack-jails-on-hetzner-cloud/</id><summary type="html">&lt;p&gt;A reproducible dual-stack configuration for FreeBSD on Hetzner Cloud with &lt;span class="caps"&gt;VNET&lt;/span&gt; jails, &lt;span class="caps"&gt;PF&lt;/span&gt; &lt;span class="caps"&gt;NAT&lt;/span&gt; for IPv4, and a /65 split trick to give jails native globally-routable IPv6 from a single&amp;nbsp;/64.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hetzner Cloud networking has a few quirks that complicate otherwise standard FreeBSD&amp;nbsp;setups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IPv4 arrives as a /32 with a pseudo on-link next-hop at&amp;nbsp;172.31.1.1.&lt;/li&gt;
&lt;li&gt;You typically get exactly one routed IPv6 /64 per&amp;nbsp;server.&lt;/li&gt;
&lt;li&gt;There’s no shared L2 domain; your &lt;span class="caps"&gt;VM&lt;/span&gt; doesn’t &lt;span class="caps"&gt;ARP&lt;/span&gt;/&lt;span class="caps"&gt;NDP&lt;/span&gt; for other&amp;nbsp;tenants.&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;NIC&lt;/span&gt; offload features can misbehave with &lt;span class="caps"&gt;VNET&lt;/span&gt; + bridges on&amp;nbsp;FreeBSD.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This guide shows a reproducible dual-stack configuration for a FreeBSD 14 host with &lt;span class="caps"&gt;VNET&lt;/span&gt; jails managed by Bastille. The&amp;nbsp;pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IPv4: &lt;span class="caps"&gt;RFC1918&lt;/span&gt; on the jail bridge; &lt;span class="caps"&gt;NAT&lt;/span&gt; to host’s public&amp;nbsp;IPv4.&lt;/li&gt;
&lt;li&gt;IPv6: Split the single routed /64 into two /65s: one for the host, one routed to the jails (no &lt;span class="caps"&gt;ULA&lt;/span&gt;, no &lt;span class="caps"&gt;NAT66&lt;/span&gt;).&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;PF&lt;/span&gt; for IPv4 &lt;span class="caps"&gt;NAT&lt;/span&gt; and port forwards; pure routing for&amp;nbsp;IPv6.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All addresses below use documentation&amp;nbsp;ranges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host IPv4:&amp;nbsp;203.0.113.10/32&lt;/li&gt;
&lt;li&gt;Hetzner pseudo-gateway (IPv4):&amp;nbsp;172.31.1.1&lt;/li&gt;
&lt;li&gt;Assigned IPv6 /64:&amp;nbsp;2001:db8:abcd:1000::/64&lt;/li&gt;
&lt;li&gt;Host half (/65):&amp;nbsp;2001:db8:abcd:1000::/65&lt;/li&gt;
&lt;li&gt;Jails half (/65):&amp;nbsp;2001:db8:abcd:1000:8000::/65&lt;/li&gt;
&lt;li&gt;Bastille bridge IPv4:&amp;nbsp;10.100.0.1/24&lt;/li&gt;
&lt;li&gt;Jail example IPv4:&amp;nbsp;10.100.0.100/24&lt;/li&gt;
&lt;li&gt;Host IPv6 (example):&amp;nbsp;2001:db8:abcd:1000::10/65&lt;/li&gt;
&lt;li&gt;Bridge IPv6 (gateway for jails):&amp;nbsp;2001:db8:abcd:1000:8000::1/65&lt;/li&gt;
&lt;li&gt;Jail example IPv6:&amp;nbsp;2001:db8:abcd:1000:8000::100/65&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Why split the /64 into two&amp;nbsp;/65s?&lt;/p&gt;
&lt;p&gt;Hetzner Cloud gives you a single routed /64. To put global IPv6 addresses in jails without &lt;span class="caps"&gt;NAT66&lt;/span&gt;/&lt;span class="caps"&gt;ULA&lt;/span&gt;, split that /64:
- Keep the lower /65 on the host interface.
- Route the upper /65 to your jail bridge and use it for jails.
This mirrors the “host + routed downstream” model and keeps IPv6 fully&amp;nbsp;end-to-end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="0-design-philosophy"&gt;0) Design&amp;nbsp;Philosophy&lt;/h2&gt;
&lt;p&gt;This guide follows modern networking&amp;nbsp;principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IPv6 is &lt;span class="caps"&gt;IP&lt;/span&gt;.&lt;/strong&gt; IPv4 is the legacy compatibility&amp;nbsp;layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No &lt;span class="caps"&gt;NAT66&lt;/span&gt;.&lt;/strong&gt; &lt;span class="caps"&gt;NAT&lt;/span&gt; was an IPv4 workaround for address exhaustion. 
  IPv6 has 340 undecillion addresses - use&amp;nbsp;them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-end connectivity.&lt;/strong&gt; Jails get real, globally routable 
  IPv6 addresses. Firewalling provides security, not address&amp;nbsp;translation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv4 best-effort.&lt;/strong&gt; &lt;span class="caps"&gt;NAT&lt;/span&gt; is acceptable for IPv4 because we have 
  no choice. It&amp;#8217;s temporary&amp;nbsp;infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If this offends you, you&amp;#8217;re reading the wrong guide.&amp;nbsp;🙂&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="1-host-etcrcconf"&gt;1) Host&amp;nbsp;/etc/rc.conf&lt;/h2&gt;
&lt;p&gt;Configure the /32 IPv4 with the on-link host route to the pseudo gateway, enable forwarding, define the Bastille bridge, and assign both v4 and v6 (using the /65&amp;nbsp;split).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;freebsd-hcloud&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;keymap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;us.kbd&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Forwarding for jails&lt;/span&gt;
&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Hetzner Cloud NIC: /32 IPv4 and routed IPv6&lt;/span&gt;
&lt;span class="c1"&gt;# Disable LRO/TSO to avoid VNET/bridge issues&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 203.0.113.10 netmask 255.255.255.255 -tso -lro&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:abcd:1000::10/65&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# IPv4 pseudo-gateway and default route&lt;/span&gt;
&lt;span class="nv"&gt;static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hcloud default&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_hcloud&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 172.31.1.1 -interface vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;default 172.31.1.1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# IPv6 default router (Hetzner uses link-local on your NIC)&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Bastille VNET bridge&lt;/span&gt;
&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.100.0.1/24&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# Routed upper /65 goes to the bridge (acts as gateway for jails)&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:abcd:1000:8000::1/65&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Services&lt;/span&gt;
&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pflog_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ntpd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ntpd_sync_on_start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notes:&amp;nbsp;- &lt;code&gt;-tso -lro&lt;/code&gt; on vtnet0 avoids checksum/segmentation oddities with bridged epairs.
- The host’s IPv6 address lives in the lower /65; the bridge gets a gateway address in the upper&amp;nbsp;/65.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="2-etcsysctlconf"&gt;2)&amp;nbsp;/etc/sysctl.conf&lt;/h2&gt;
&lt;p&gt;Ensure forwarding is on and (optionally) disable&amp;nbsp;redirects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;net.inet.ip.forwarding&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
net.inet6.ip6.forwarding&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
net.inet.ip.redirect&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
net.inet6.ip6.redirect&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id="3-pf-etcpfconf"&gt;3) &lt;span class="caps"&gt;PF&lt;/span&gt;:&amp;nbsp;/etc/pf.conf&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt; IPv4 from the jail network to the host’s public IPv4. IPv6 is routed (no &lt;span class="caps"&gt;NAT&lt;/span&gt;). Forward &lt;span class="caps"&gt;HTTP&lt;/span&gt;/&lt;span class="caps"&gt;HTTPS&lt;/span&gt; to a specific jail as an example. Lock down &lt;span class="caps"&gt;SSH&lt;/span&gt; to trusted&amp;nbsp;sources.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# --- Macros ---&lt;/span&gt;
&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Jail networks&lt;/span&gt;
&lt;span class="n"&gt;jail_net_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.0/24&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:abcd:1000:8000::/65&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Host IPv6 on the external interface (lower /65)&lt;/span&gt;
&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:abcd:1000::10&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Example trusted admin sources&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;203.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;113.50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ffff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Brute-force tracking&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;

&lt;span class="c1"&gt;# --- Options ---&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scrub ---&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;

&lt;span class="c1"&gt;# --- NAT (IPv4 only) ---&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- RDR for public services to a &amp;quot;web&amp;quot; jail ---&lt;/span&gt;
&lt;span class="c1"&gt;# v4: to host IPv4, forward 80/443 to the jail v4&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.100&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.100&lt;/span&gt;
&lt;span class="c1"&gt;# v6: to the host&amp;#39;s IPv6, forward 80/443 to the jail v6&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;abcd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="c1"&gt;# --- Policy ---&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;

&lt;span class="c1"&gt;# Allow established and all outbound by default&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Anti-spoof&lt;/span&gt;
&lt;span class="n"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# SSH allowlist (example nonstandard port 22222)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Log and drop other SSH attempts&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh_blocked&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Essential ICMPv6 (ND, pMTU, echo)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Useful ICMPv4 (echo, unreach for pMTU)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Allow from jails bridge (host-facing side)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="c1"&gt;# Public HTTP/HTTPS to forwarded jail&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id="4-bastillejail-networking"&gt;4) Bastille/Jail&amp;nbsp;networking&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;VNET&lt;/span&gt; jails get an epair; the host end attaches&amp;nbsp;to &lt;code&gt;bastille0&lt;/code&gt;. Inside the jail, rename the epair&amp;nbsp;to &lt;code&gt;vnet0&lt;/code&gt; and assign addresses. The bridge addresses act as the default&amp;nbsp;gateways.&lt;/p&gt;
&lt;p&gt;Example&amp;nbsp;jail &lt;code&gt;rc.conf&lt;/code&gt; (inside the&amp;nbsp;jail):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_e0b_webjail_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.100.0.100 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:abcd:1000:8000::100/65&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:abcd:1000:8000::1&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Optional: quiet base daemons&lt;/span&gt;
&lt;span class="nv"&gt;syslogd_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-ss&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_submit_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_outbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_msp_queue_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If templating with Bastille, a minimal jail.conf&amp;nbsp;fragment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;vnet=&amp;quot;on&amp;quot;;
vnet.interface=&amp;quot;e0a_${name}&amp;quot;;

exec.prestart=&amp;quot;ifconfig e0a_${name} create up&amp;quot;;
exec.prestart=&amp;quot;ifconfig bastille0 addm e0a_${name}&amp;quot;;

exec.start=&amp;quot;/bin/sh /etc/rc&amp;quot;;
exec.stop=&amp;quot;/bin/sh /etc/rc.shutdown&amp;quot;;

mount.devfs;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Bastille will hand the jail end&amp;nbsp;as &lt;code&gt;e0b_${name}&lt;/code&gt;, which you rename&amp;nbsp;to &lt;code&gt;vnet0&lt;/code&gt; in the jail’s&amp;nbsp;rc.conf.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="5-bring-up-and-verification"&gt;5) Bring-up and&amp;nbsp;verification&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Reload &lt;span class="caps"&gt;PF&lt;/span&gt; and confirm&amp;nbsp;rules:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;service pf restart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pfctl -sr -v&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify routes on the&amp;nbsp;host:&lt;/li&gt;
&lt;li&gt;IPv4 default&amp;nbsp;via &lt;code&gt;172.31.1.1&lt;/code&gt; on &lt;code&gt;vtnet0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IPv6 default&amp;nbsp;via &lt;code&gt;fe80::1%vtnet0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Local routes&amp;nbsp;to &lt;code&gt;10.100.0.0/24&lt;/code&gt; and &lt;code&gt;2001:db8:abcd:1000:8000::/65&lt;/code&gt; on &lt;code&gt;bastille0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Start Bastille and your&amp;nbsp;jail:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;service bastille start&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bastille start webjail&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Inside the&amp;nbsp;jail:&lt;/li&gt;
&lt;li&gt;IPv4: &lt;code&gt;ping -c 3 1.1.1.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IPv6: &lt;code&gt;ping -c 3 2001:4860:4860::8888&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From the internet:
- &lt;span class="caps"&gt;HTTP&lt;/span&gt;/&lt;span class="caps"&gt;HTTPS&lt;/span&gt; should land on the jail via rdr.
- &lt;span class="caps"&gt;SSH&lt;/span&gt; should only accept from your trusted&amp;nbsp;sources.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="6-hetzner-cloud-quirks-and-fixes"&gt;6) Hetzner Cloud quirks and&amp;nbsp;fixes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;IPv4 /32 with pseudo&amp;nbsp;next-hop:&lt;/li&gt;
&lt;li&gt;You must add a host route&amp;nbsp;to &lt;code&gt;172.31.1.1&lt;/code&gt; on &lt;code&gt;vtnet0&lt;/code&gt; and set it as&amp;nbsp;default.&lt;/li&gt;
&lt;li&gt;Offload&amp;nbsp;features:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-tso -lro&lt;/code&gt; on &lt;code&gt;vtnet0&lt;/code&gt; prevents oddities with bridged epairs and IPv6&amp;nbsp;checksums.&lt;/li&gt;
&lt;li&gt;IPv6 routing&amp;nbsp;model:&lt;/li&gt;
&lt;li&gt;IPv6 is routed to your &lt;span class="caps"&gt;VM&lt;/span&gt;. Don’t expect &lt;span class="caps"&gt;SLAAC&lt;/span&gt; for jails. Assign static v6 from your routed half (/65) and route via the&amp;nbsp;host.&lt;/li&gt;
&lt;li&gt;If IPv6 works on the host but not in&amp;nbsp;jails:&lt;/li&gt;
&lt;li&gt;Ensure &lt;code&gt;net.inet6.ip6.forwarding=1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make sure jails&amp;nbsp;use &lt;code&gt;2001:db8:abcd:1000:8000::1&lt;/code&gt; as their default&amp;nbsp;router&lt;/li&gt;
&lt;li&gt;Confirm &lt;span class="caps"&gt;PF&lt;/span&gt; isn’t blocking &lt;span class="caps"&gt;NDP&lt;/span&gt; or pMTU&amp;nbsp;(&lt;code&gt;ipv6-icmp&lt;/code&gt; types are allowed&amp;nbsp;above)&lt;/li&gt;
&lt;li&gt;If IPv4 &lt;span class="caps"&gt;NAT&lt;/span&gt; seems&amp;nbsp;flaky:&lt;/li&gt;
&lt;li&gt;Check that no later &lt;span class="caps"&gt;PF&lt;/span&gt; rules&amp;nbsp;override &lt;code&gt;pass out&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify &lt;span class="caps"&gt;MTU&lt;/span&gt;/&lt;span class="caps"&gt;MSS&lt;/span&gt;&amp;nbsp;(scrub &lt;code&gt;max-mss 1500&lt;/code&gt; is&amp;nbsp;set)&lt;/li&gt;
&lt;li&gt;Inspect&amp;nbsp;states: &lt;code&gt;pfctl -ss | grep 10.100.0.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="7-security-notes"&gt;7) Security&amp;nbsp;notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Restrict &lt;span class="caps"&gt;SSH&lt;/span&gt; with allowlists and non-default ports; consider WireGuard for&amp;nbsp;admin.&lt;/li&gt;
&lt;li&gt;Enable pflog and inspect with&amp;nbsp;tcpdump:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpdump -n -e -ttt -r /var/log/pflog&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Rate-limit public services at your reverse proxy jail if&amp;nbsp;needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="8-quick-reference-full-files"&gt;8) Quick reference: full&amp;nbsp;files&lt;/h2&gt;
&lt;h3 id="etcrcconf"&gt;/etc/rc.conf&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;freebsd-hcloud&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;keymap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;us.kbd&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_gateway_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;ifconfig_vtnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 203.0.113.10 netmask 255.255.255.255 -tso -lro&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vtnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:abcd:1000::10/65&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;static_routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hcloud default&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_hcloud&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-host 172.31.1.1 -interface vtnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;route_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;default 172.31.1.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fe80::1%vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;cloned_interfaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bridge0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bridge0_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bastille0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.100.0.1/24&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_bastille0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:abcd:1000:8000::1/65&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;pf_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;pflog_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sshd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;zfs_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ntpd_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ntpd_sync_on_start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="etcsysctlconf"&gt;/etc/sysctl.conf&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;net.inet.ip.forwarding&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
net.inet6.ip6.forwarding&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
net.inet.ip.redirect&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
net.inet6.ip6.redirect&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="etcpfconf"&gt;/etc/pf.conf&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vtnet0&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;jail_net_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.0/24&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;jail_net_v6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:abcd:1000:8000::/65&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:abcd:1000::10&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;198.51&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;203.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;113.50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ffff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;persist&lt;/span&gt;

&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lo0&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;
&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loginterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;

&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reassemble&lt;/span&gt;
&lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;

&lt;span class="n"&gt;nat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;jail_net_v4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.100&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.100&lt;/span&gt;
&lt;span class="n"&gt;rdr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;host_ipv6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;db8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;abcd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="n"&gt;antispoof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v4&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;trusted_v6&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;\
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;SA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;bruteforce&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssh_blocked&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echorep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbrsol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;neighbradv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toobig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paramprob&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;icmp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echoreq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bastille0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;

&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ext_if&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="jail-rcconf-inside-the-jail"&gt;Jail rc.conf (inside the&amp;nbsp;jail)&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ifconfig_e0b_webjail_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vnet0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet 10.100.0.100 netmask 255.255.255.0&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ifconfig_vnet0_ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inet6 2001:db8:abcd:1000:8000::100/65&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10.100.0.1&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;ipv6_defaultrouter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2001:db8:abcd:1000:8000::1&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;syslogd_flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-ss&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_submit_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_outbound_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;sendmail_msp_queue_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;NO&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id="9-troubleshooting-checklist"&gt;9) Troubleshooting&amp;nbsp;checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Host&amp;nbsp;routing:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netstat -rn&lt;/code&gt; shows default v4 via 172.31.1.1; default v6 via&amp;nbsp;fe80::1%vtnet0&lt;/li&gt;
&lt;li&gt;Bridge&amp;nbsp;membership:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ifconfig bastille0&lt;/code&gt; lists e0a_ interfaces for running&amp;nbsp;jails&lt;/li&gt;
&lt;li&gt;IPv6 neighbors and&amp;nbsp;pMTU:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ndp -an&lt;/code&gt; on host shows upstream neighbors; ensure ipv6-icmp is&amp;nbsp;allowed&lt;/li&gt;
&lt;li&gt;&lt;span class="caps"&gt;NAT&lt;/span&gt;&amp;nbsp;state:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pfctl -ss | grep 10.100.0.&lt;/code&gt; confirms IPv4 &lt;span class="caps"&gt;NAT&lt;/span&gt;&amp;nbsp;states&lt;/li&gt;
&lt;li&gt;Throughput/stalls:&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;-tso -lro&lt;/code&gt; on&amp;nbsp;vtnet0; &lt;code&gt;scrub out max-mss 1500&lt;/code&gt; is&amp;nbsp;present&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="wrap-up"&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;This pattern—IPv4 NATed jails, IPv6 routed using a /64 split into two /65s—fits Hetzner Cloud’s model and avoids &lt;span class="caps"&gt;NAT66&lt;/span&gt;/&lt;span class="caps"&gt;ULA&lt;/span&gt; while keeping end-to-end IPv6. It’s simple, robust, and friendly to &lt;span class="caps"&gt;PF&lt;/span&gt;.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further&amp;nbsp;Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.freebsd.org/en/books/handbook/jails/"&gt;FreeBSD Handbook:&amp;nbsp;Jails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bastillebsd.org"&gt;Bastille&amp;nbsp;documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hetzner.com/cloud/servers/primary-ips/primary-ip-configuration#ipv6"&gt;IPv6 at Hetzner&amp;nbsp;Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="networking"/></entry><entry><title>FreeBSD 15.0 on the ThinkPad T480 — Efficient, Stable, and 8 Hours on Battery</title><link href="https://blog.hofstede.it/freebsd-150-on-the-thinkpad-t480-efficient-stable-and-8-hours-on-battery/" rel="alternate"/><published>2025-11-09T00:00:00+01:00</published><updated>2025-11-09T00:00:00+01:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-11-09:/freebsd-150-on-the-thinkpad-t480-efficient-stable-and-8-hours-on-battery/</id><summary type="html">&lt;p&gt;Achieving near-Linux battery life on the T480 while staying purely &lt;span class="caps"&gt;BSD&lt;/span&gt;.&lt;/p&gt;</summary><content type="html">&lt;hr&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The ThinkPad T480 has long had a reputation for being one of the most practical and robust laptops for engineers and developers. It’s built like a tank, easy to maintain, and works brilliantly with open-source operating&amp;nbsp;systems.&lt;/p&gt;
&lt;p&gt;Over the last few months, I’ve been tuning and optimizing &lt;strong&gt;FreeBSD&lt;/strong&gt; on this machine with a very clear goal in mind:&lt;br&gt;
&lt;strong&gt;reach the best possible power efficiency without sacrificing performance or&amp;nbsp;usability.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After a good amount of trial and error, firmware testing, and sysctl tuning, I finally reached a sweet spot. A setup that reliably gives me &lt;strong&gt;6–8 hours of real battery life&lt;/strong&gt; under normal&amp;nbsp;workloads.&lt;/p&gt;
&lt;p&gt;This post documents that configuration for anyone in the &lt;span class="caps"&gt;BSD&lt;/span&gt; community who wants to make their ThinkPad a genuinely mobile FreeBSD&amp;nbsp;workstation.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of FreeBSD desktop" src="https://blog.hofstede.it/images/2025-11-09-t480-freebsd-efficiency.webp" title="FreeBSD 15.0 Desktop"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="hardware-overview"&gt;Hardware&amp;nbsp;Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model:&lt;/strong&gt; Lenovo ThinkPad&amp;nbsp;T480&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;CPU&lt;/span&gt;:&lt;/strong&gt; Intel Core i7-8550U (4C/8T, Kaby&amp;nbsp;Lake-R)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;GPU&lt;/span&gt;:&lt;/strong&gt; Intel &lt;span class="caps"&gt;UHD&lt;/span&gt; Graphics 620&amp;nbsp;(integrated)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display:&lt;/strong&gt; 1920×1080 (&lt;span class="caps"&gt;IPS&lt;/span&gt;,&amp;nbsp;14&amp;#8221;)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wireless:&lt;/strong&gt; Intel &lt;span class="caps"&gt;AX210&lt;/span&gt;&amp;nbsp;(&lt;code&gt;iwlwifi&lt;/code&gt;)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; NVMe &lt;span class="caps"&gt;SSD&lt;/span&gt; (&lt;span class="caps"&gt;ZFS&lt;/span&gt;&amp;nbsp;root)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory:&lt;/strong&gt; 32 GiB &lt;span class="caps"&gt;DDR4&lt;/span&gt;  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Battery:&lt;/strong&gt; 72 Wh&amp;nbsp;extended&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The combination of efficient hardware and modern drivers from the FreeBSD 15 branch provides an excellent base to work&amp;nbsp;with.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="drivers-and-firmware"&gt;Drivers and&amp;nbsp;Firmware&lt;/h2&gt;
&lt;p&gt;I’m using the latest driver sets available from the ports&amp;nbsp;tree:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;drm-latest-kmod-6.9.1500068
gpu-firmware-intel-kmod-kabylake-20230625.1500501
wifi-firmware-iwlwifi-kmod-ax210-20241017.1500501_2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These make the Intel &lt;span class="caps"&gt;GPU&lt;/span&gt; and Wi-Fi adapters fully functional with modern capabilities: &lt;span class="caps"&gt;RC6&lt;/span&gt;, &lt;span class="caps"&gt;PSR&lt;/span&gt;, &lt;span class="caps"&gt;FBC&lt;/span&gt;, and full 802.11ac/ax&amp;nbsp;performance.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="system-configuration"&gt;System&amp;nbsp;Configuration&lt;/h2&gt;
&lt;p&gt;FreeBSD’s configuration model is simple and transparent, which makes power tuning straightforward once you understand the&amp;nbsp;fundamentals.&lt;/p&gt;
&lt;h3 id="etcrcconf"&gt;/etc/rc.conf&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;hostname=&amp;quot;christop-bsd&amp;quot;

# Wireless configuration
wlans_iwlwifi0=&amp;quot;wlan0&amp;quot;
ifconfig_wlan0=&amp;quot;WPA DHCP&amp;quot;
create_args_wlan0=&amp;quot;wlanmode sta regdomain ETSI country DE mode 11na&amp;quot;
ifconfig_wlan0_ipv6=&amp;quot;inet6 accept_rtadv&amp;quot;

# Networking bridge (for bhyve)
cloned_interfaces=&amp;quot;bridge0&amp;quot;
ifconfig_bridge0=&amp;quot;inet 192.168.87.1 netmask 255.255.255.0&amp;quot;

# Power management
powerd_enable=&amp;quot;YES&amp;quot;
powerd_flags=&amp;quot;-a hiadaptive -b adaptive -m 400 -M 4000&amp;quot;
performance_cx_lowest=&amp;quot;Cmax&amp;quot;
economy_cx_lowest=&amp;quot;Cmax&amp;quot;

# Driver autoloads
kld_list=&amp;quot;i915kms&amp;quot;

# Services
sshd_enable=&amp;quot;YES&amp;quot;
zfs_enable=&amp;quot;YES&amp;quot;
seatd_enable=&amp;quot;YES&amp;quot;
dbus_enable=&amp;quot;YES&amp;quot;
sddm_enable=&amp;quot;YES&amp;quot;
webcamd_enable=&amp;quot;YES&amp;quot;
pf_enable=&amp;quot;YES&amp;quot;
pflog_enable=&amp;quot;YES&amp;quot;
dnsmasq_enable=&amp;quot;YES&amp;quot;
pcscd_enable=&amp;quot;YES&amp;quot;
gateway_enable=&amp;quot;YES&amp;quot;
clear_tmp_enable=&amp;quot;YES&amp;quot;

# bhyve
vm_enable=&amp;quot;YES&amp;quot;
vm_dir=&amp;quot;zfs:zroot/bhyve&amp;quot;

# Misc
moused_nondefault_enable=&amp;quot;NO&amp;quot;
dumpdev=&amp;quot;NO&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key piece here&amp;nbsp;is &lt;code&gt;powerd&lt;/code&gt;, with different adaptive modes for &lt;span class="caps"&gt;AC&lt;/span&gt; and battery, letting the &lt;span class="caps"&gt;CPU&lt;/span&gt; drop down to deep C-states efficiently.&lt;br&gt;
&lt;span class="caps"&gt;XFCE&lt;/span&gt; and lightweight services complete the power-slim system&amp;nbsp;profile.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="bootloaderconf"&gt;/boot/loader.conf&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Kernel features
kern.geom.label.disk_ident.enable=&amp;quot;0&amp;quot;
kern.geom.label.gptid.enable=&amp;quot;0&amp;quot;

# Essential modules
zfs_load=&amp;quot;YES&amp;quot;
cryptodev_load=&amp;quot;YES&amp;quot;
cuse_load=&amp;quot;YES&amp;quot;
cpuctl_load=&amp;quot;YES&amp;quot;
coretemp_load=&amp;quot;YES&amp;quot;
nvme_load=&amp;quot;YES&amp;quot;

# Graphics and power tuning
i915kms_load=&amp;quot;YES&amp;quot;
drm.i915.enable_rc6=7
drm.i915.enable_fbc=1
drm.i915.lvds_downclock=1
drm.i915.enable_psr=1
compat.linuxkpi.i915_disable_power_well=&amp;quot;0&amp;quot;

# NVMe and PCI power tweaks
hw.nvme.use_nvd=&amp;quot;0&amp;quot;
hw.pci.do_power_nodriver=3
hw.pci.do_power_suspend=1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These settings allow the &lt;span class="caps"&gt;GPU&lt;/span&gt; and &lt;span class="caps"&gt;PCI&lt;/span&gt; devices to enter the lowest power states reliably.&lt;br&gt;
Together&amp;nbsp;with &lt;code&gt;powerd&lt;/code&gt;, they make a significant difference in idle and light load power&amp;nbsp;draw.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="desktop-environment"&gt;Desktop&amp;nbsp;Environment&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Desktop:&lt;/strong&gt; &lt;span class="caps"&gt;XFCE&lt;/span&gt;&amp;nbsp;4.20  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Window Manager:&lt;/strong&gt; Xfwm4&amp;nbsp;(X11)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Theme:&lt;/strong&gt;&amp;nbsp;Greybird-Dark  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Icons:&lt;/strong&gt;&amp;nbsp;elementary-xfce  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fonts:&lt;/strong&gt; Noto Sans (11&amp;nbsp;pt)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Terminal font:&lt;/strong&gt; UbuntuMono Nerd Font (11&amp;nbsp;pt)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span class="caps"&gt;XFCE&lt;/span&gt; is lightweight, fast, and power-friendly - no unnecessary daemons, no heavy compositing. It feels modern and stays responsive even under&amp;nbsp;load.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="real-world-results"&gt;Real-World&amp;nbsp;Results&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Power Draw&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle (Wi-Fi on, 40 % brightness)&lt;/td&gt;
&lt;td&gt;4–5 W&lt;/td&gt;
&lt;td&gt;ultra-low power idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video playback (720p)&lt;/td&gt;
&lt;td&gt;6–7 W&lt;/td&gt;
&lt;td&gt;stable playback via Intel &lt;span class="caps"&gt;GPU&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moderate development workload&lt;/td&gt;
&lt;td&gt;10–12 W&lt;/td&gt;
&lt;td&gt;perfect balance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;make buildkernel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18–22 W&lt;/td&gt;
&lt;td&gt;all cores fully utilized&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With the 72 Wh extended battery, real-world battery time ranges from &lt;strong&gt;6 to 8 hours&lt;/strong&gt; depending on activity, brightness, and Wi-Fi&amp;nbsp;intensity.&lt;/p&gt;
&lt;p&gt;Suspend and resume are fully functional and reliable, one of the last major milestones that used to be tricky on FreeBSD&amp;nbsp;laptops.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="expert-notes-additional-tweaks"&gt;Expert Notes &lt;span class="amp"&gt;&amp;amp;&lt;/span&gt; Additional&amp;nbsp;Tweaks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Enable &lt;span class="caps"&gt;ZFS&lt;/span&gt; compression&amp;nbsp;(&lt;code&gt;lz4&lt;/code&gt;) and&amp;nbsp;disable &lt;code&gt;atime&lt;/code&gt; to cut down write&amp;nbsp;I/O.  &lt;/li&gt;
&lt;li&gt;Keep firmware packages current&amp;nbsp;using &lt;code&gt;pkg upgrade&lt;/code&gt;.  &lt;/li&gt;
&lt;li&gt;Consider&amp;nbsp;running &lt;code&gt;powerdxx&lt;/code&gt; if you want even finer &lt;span class="caps"&gt;CPU&lt;/span&gt; scaling — though&amp;nbsp;base &lt;code&gt;powerd&lt;/code&gt; does a great job&amp;nbsp;here.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;FreeBSD 15.0 marks a huge step forward for laptop support.&lt;br&gt;
On the ThinkPad T480, everything from suspend to Wi-Fi to graphics just works, and with the right configuration, you can enjoy &lt;strong&gt;Linux-level power efficiency&lt;/strong&gt; combined with &lt;strong&gt;FreeBSD’s rock-solid stability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It’s a joy to use this machine daily now: clean, simple, and efficient. True to FreeBSD’s&amp;nbsp;spirit.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Thanks to everyone in the &lt;span class="caps"&gt;BSD&lt;/span&gt; community pushing kernel and driver work - this level of polish on a laptop would’ve been unthinkable just a few releases&amp;nbsp;ago.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Comments and feedback welcome via Mastodon: &lt;a href="https://mastodon.bsd.cafe/@larvitz"&gt;@larvitz@bsd.cafe&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="thinkpad"/><category term="power"/><category term="xfce"/><category term="zfs"/></entry><entry><title>Simple Temperature Monitoring on FreeBSD</title><link href="https://blog.hofstede.it/simple-temperature-monitoring-on-freebsd/" rel="alternate"/><published>2025-10-09T00:00:00+02:00</published><updated>2025-10-09T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-10-09:/simple-temperature-monitoring-on-freebsd/</id><summary type="html">&lt;p&gt;Using sysctl, coretemp, and shell scripting to monitor &lt;span class="caps"&gt;CPU&lt;/span&gt; and disk temperatures on FreeBSD - a lightweight approach without heavy monitoring&amp;nbsp;frameworks.&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you&amp;#8217;re running FreeBSD 14.x and want to keep an eye on your system temperatures without installing third-party packages, you&amp;#8217;re in luck.&amp;nbsp;FreeBSD&amp;#8217;s &lt;code&gt;sysctl&lt;/code&gt; interface provides everything you&amp;nbsp;need.&lt;/p&gt;
&lt;h2 id="the-built-in-approach"&gt;The Built-in&amp;nbsp;Approach&lt;/h2&gt;
&lt;p&gt;Modern FreeBSD systems expose temperature data through the sysctl tree. You can query all temperature sensors with a simple&amp;nbsp;command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;temperature
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will show &lt;span class="caps"&gt;CPU&lt;/span&gt; core temperatures, &lt;span class="caps"&gt;ACPI&lt;/span&gt; thermal zones, and &lt;span class="caps"&gt;PCH&lt;/span&gt; (Platform Controller Hub) temperatures if&amp;nbsp;available.&lt;/p&gt;
&lt;h2 id="a-practical-monitoring-script"&gt;A Practical Monitoring&amp;nbsp;Script&lt;/h2&gt;
&lt;p&gt;Rather than repeatedly typing sysctl commands, here&amp;#8217;s a lightweight monitoring script that refreshes every two&amp;nbsp;seconds:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
kldload&lt;span class="w"&gt; &lt;/span&gt;coretemp
&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;true&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
clear
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;   Temperature Monitor - &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;+%H:%M:%S&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;CPU Cores:&amp;quot;&lt;/span&gt;
sysctl&lt;span class="w"&gt; &lt;/span&gt;dev.cpu&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;temperature&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-V
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;System:&amp;quot;&lt;/span&gt;
sysctl&lt;span class="w"&gt; &lt;/span&gt;hw.acpi.thermal.tz0.temperature&lt;span class="w"&gt; &lt;/span&gt;hw.acpi.thermal.tz1.temperature&lt;span class="w"&gt; &lt;/span&gt;dev.pchtherm.0.temperature&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Press Ctrl+C to exit&amp;quot;&lt;/span&gt;
sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Save this&amp;nbsp;as &lt;code&gt;tempmon.sh&lt;/code&gt;, make it executable&amp;nbsp;with &lt;code&gt;chmod +x tempmon.sh&lt;/code&gt;, and run it whenever you need real-time temperature&amp;nbsp;monitoring.&lt;/p&gt;
&lt;h2 id="quick-access-with-an-alias"&gt;Quick Access with an&amp;nbsp;Alias&lt;/h2&gt;
&lt;p&gt;For instant temperature checks, add this alias to your shell&amp;nbsp;configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# For sh/bash (~/.shrc or ~/.bashrc)&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;temps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sysctl dev.cpu | grep temperature | sort -V; echo &amp;quot;&amp;quot;; sysctl hw.acpi.thermal.tz0.temperature dev.pchtherm.0.temperature&amp;#39;&lt;/span&gt;
&lt;span class="c1"&gt;# For csh/tcsh (~/.cshrc)&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;temps&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sysctl dev.cpu | grep temperature | sort -V; echo &amp;quot;&amp;quot;; sysctl hw.acpi.thermal.tz0.temperature dev.pchtherm.0.temperature&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now &lt;code&gt;temps&lt;/code&gt; gives you an instant snapshot of your system&amp;#8217;s thermal&amp;nbsp;state.&lt;/p&gt;
&lt;h2 id="understanding-the-output"&gt;Understanding the&amp;nbsp;Output&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;dev.cpu.X.temperature&lt;/code&gt; values represent individual &lt;span class="caps"&gt;CPU&lt;/span&gt; core temperatures,&amp;nbsp;while &lt;code&gt;hw.acpi.thermal.tzX.temperature&lt;/code&gt; shows &lt;span class="caps"&gt;ACPI&lt;/span&gt; thermal zone readings (typically overall system/motherboard temperatures).&amp;nbsp;The &lt;code&gt;dev.pchtherm.0.temperature&lt;/code&gt; reports the Platform Controller Hub temperature when available.
For most systems under normal load, &lt;span class="caps"&gt;CPU&lt;/span&gt; temperatures between 40-60°C are typical. Sustained temperatures above 80°C warrant&amp;nbsp;investigation.&lt;/p&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="sysadmin"/></entry><entry><title>FreeBSD Cheat Sheet for Linux Admins</title><link href="https://blog.hofstede.it/freebsd-cheat-sheet-for-linux-admins/" rel="alternate"/><published>2025-08-29T00:00:00+02:00</published><updated>2025-08-29T00:00:00+02:00</updated><author><name>Larvitz</name></author><id>tag:blog.hofstede.it,2025-08-29:/freebsd-cheat-sheet-for-linux-admins/</id><summary type="html">&lt;p&gt;A side-by-side reference of FreeBSD and Linux commands covering hardware, disks, networking, packages, services, users, and system management - for Linux admins getting started with&amp;nbsp;FreeBSD.&lt;/p&gt;</summary><content type="html">&lt;h2 id="hardware-information"&gt;Hardware&amp;nbsp;Information&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List &lt;span class="caps"&gt;PCI&lt;/span&gt; devices&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pciconf -lv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lspci -v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List &lt;span class="caps"&gt;USB&lt;/span&gt; devices&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usbconfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsusb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show &lt;span class="caps"&gt;CPU&lt;/span&gt; info&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysctl hw.model&lt;/code&gt; or &lt;code&gt;dmesg \| grep CPU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cat /proc/cpuinfo&lt;/code&gt; or &lt;code&gt;lscpu&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show memory info&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysctl hw.physmem&lt;/code&gt; or &lt;code&gt;top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;free -h&lt;/code&gt; or &lt;code&gt;cat /proc/meminfo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show kernel modules&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kldstat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsmod&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load kernel module&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kldload module_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;modprobe module_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unload kernel module&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kldunload module_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;modprobe -r module_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="disk-and-storage-management"&gt;Disk and Storage&amp;nbsp;Management&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List all disks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;geom disk list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsblk&lt;/code&gt; or &lt;code&gt;fdisk -l&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show disk partitions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpart show&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fdisk -l&lt;/code&gt; or &lt;code&gt;parted -l&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show disk usage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;df -h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;df -h&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show mounted filesystems&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mount&lt;/code&gt; or &lt;code&gt;findmnt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check filesystem&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fsck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fsck&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show &lt;span class="caps"&gt;SMART&lt;/span&gt; data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smartctl -a /dev/ada0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smartctl -a /dev/sda&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List &lt;span class="caps"&gt;ZFS&lt;/span&gt; pools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool list&lt;/code&gt; (if &lt;span class="caps"&gt;ZFS&lt;/span&gt; installed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show &lt;span class="caps"&gt;ZFS&lt;/span&gt; datasets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list&lt;/code&gt; (if &lt;span class="caps"&gt;ZFS&lt;/span&gt; installed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="network-commands"&gt;Network&amp;nbsp;Commands&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show network interfaces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ifconfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip addr&lt;/code&gt; or &lt;code&gt;ifconfig&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ifconfig em0 inet 192.168.1.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip addr add 192.168.1.10/24 dev eth0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show routing table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;netstat -rn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip route&lt;/code&gt; or &lt;code&gt;route -n&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add static route&lt;/td&gt;
&lt;td&gt;&lt;code&gt;route add default 192.168.1.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip route add default via 192.168.1.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show &lt;span class="caps"&gt;ARP&lt;/span&gt; table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arp -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arp -a&lt;/code&gt; or &lt;code&gt;ip neigh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear &lt;span class="caps"&gt;ARP&lt;/span&gt; entry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arp -d hostname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arp -d hostname&lt;/code&gt; or &lt;code&gt;ip neigh del&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show network statistics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;netstat -s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;netstat -s&lt;/code&gt; or &lt;code&gt;ss -s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show listening ports&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sockstat -l&lt;/code&gt; or &lt;code&gt;netstat -an&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ss -tuln&lt;/code&gt; or &lt;code&gt;netstat -tuln&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show all open ports/connections&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sockstat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ss -tuan&lt;/code&gt; or &lt;code&gt;netstat -tuan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packet capture&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tcpdump -i em0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tcpdump -i eth0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="process-management"&gt;Process&amp;nbsp;Management&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List all processes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ps aux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ps aux&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process tree&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pstree&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pstree&lt;/code&gt; or &lt;code&gt;ps axjf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time process view&lt;/td&gt;
&lt;td&gt;&lt;code&gt;top&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;top&lt;/code&gt; or &lt;code&gt;htop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kill process&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kill PID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kill PID&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show open files by process&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fstat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsof&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show process using port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sockstat -p 80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsof -i :80&lt;/code&gt; or &lt;code&gt;ss -tulpn \| grep :80&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="package-management"&gt;Package&amp;nbsp;Management&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux (varies by distro)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Install package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg install package_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt install&lt;/code&gt; / &lt;code&gt;yum install&lt;/code&gt; / &lt;code&gt;dnf install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg delete package_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt remove&lt;/code&gt; / &lt;code&gt;yum remove&lt;/code&gt; / &lt;code&gt;dnf remove&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update package list&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt update&lt;/code&gt; / &lt;code&gt;yum check-update&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade packages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg upgrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt upgrade&lt;/code&gt; / &lt;code&gt;yum upgrade&lt;/code&gt; / &lt;code&gt;dnf upgrade&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search packages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg search keyword&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt search&lt;/code&gt; / &lt;code&gt;yum search&lt;/code&gt; / &lt;code&gt;dnf search&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show package info&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg info package_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt show&lt;/code&gt; / &lt;code&gt;yum info&lt;/code&gt; / &lt;code&gt;dnf info&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List installed packages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dpkg -l&lt;/code&gt; / &lt;code&gt;rpm -qa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="system-services"&gt;System&amp;nbsp;Services&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux (systemd)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service servicename start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start servicename&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service servicename stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl stop servicename&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service servicename restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl restart servicename&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service servicename status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl status servicename&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable at boot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysrc servicename_enable="YES"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl enable servicename&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disable at boot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysrc servicename_enable="NO"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl disable servicename&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List all services&lt;/td&gt;
&lt;td&gt;&lt;code&gt;service -e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl list-units --type=service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="firewall"&gt;Firewall&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD (pf/ipfw)&lt;/th&gt;
&lt;th&gt;Linux (iptables/nftables)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show rules (pf)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pfctl -sr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iptables -L -n -v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show rules (ipfw)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ipfw list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nft list ruleset&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable firewall (pf)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pfctl -e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start firewalld&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disable firewall (pf)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pfctl -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl stop firewalld&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reload rules (pf)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pfctl -f /etc/pf.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iptables-restore &amp;lt; /etc/iptables/rules.v4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="system-information"&gt;System&amp;nbsp;Information&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System uptime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uptime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uptime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel version&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uname -a&lt;/code&gt; or &lt;code&gt;freebsd-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uname -a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show all sysctl variables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysctl -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysctl -a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show system messages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dmesg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dmesg&lt;/code&gt; or &lt;code&gt;journalctl -k&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show system logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tail /var/log/messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl&lt;/code&gt; or &lt;code&gt;tail /var/log/syslog&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="user-management"&gt;User&amp;nbsp;Management&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adduser&lt;/code&gt; or &lt;code&gt;pw useradd username&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useradd username&lt;/code&gt; or &lt;code&gt;adduser username&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rmuser&lt;/code&gt; or &lt;code&gt;pw userdel username&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;userdel username&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modify user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pw usermod username&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usermod username&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change password&lt;/td&gt;
&lt;td&gt;&lt;code&gt;passwd username&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;passwd username&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show logged-in users&lt;/td&gt;
&lt;td&gt;&lt;code&gt;who&lt;/code&gt; or &lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;who&lt;/code&gt; or &lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="file-systems"&gt;File&amp;nbsp;Systems&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FreeBSD&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mount filesystem&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mount /dev/ada0p2 /mnt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mount /dev/sda2 /mnt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmount filesystem&lt;/td&gt;
&lt;td&gt;&lt;code&gt;umount /mnt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;umount /mnt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create &lt;span class="caps"&gt;UFS&lt;/span&gt; filesystem&lt;/td&gt;
&lt;td&gt;&lt;code&gt;newfs /dev/ada0p2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N/A (&lt;span class="caps"&gt;UFS&lt;/span&gt; not common)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create ext4 filesystem&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mkfs.ext4 /dev/sda2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check disk space&lt;/td&gt;
&lt;td&gt;&lt;code&gt;du -sh /path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;du -sh /path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="zfs-commands-same-on-freebsd-and-linux-with-zfs"&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt; Commands (Same on FreeBSD and Linux with &lt;span class="caps"&gt;ZFS&lt;/span&gt;)&lt;/h2&gt;
&lt;h3 id="pool-operations"&gt;Pool&amp;nbsp;Operations&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List all pools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show pool status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool status tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show pool history&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool history tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show pool I/O statistics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool iostat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool iostat -v 2&lt;/code&gt; (every 2 seconds)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create simple pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool create tank /dev/ada1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create mirror pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool create tank mirror /dev/ada1 /dev/ada2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create &lt;span class="caps"&gt;RAIDZ&lt;/span&gt; pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool create tank raidz /dev/ada1 /dev/ada2 /dev/ada3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add disk to pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool add tank /dev/ada4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace disk in pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool replace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool replace tank /dev/ada1 /dev/ada4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove device from pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool remove&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool remove tank /dev/ada4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scrub pool (check integrity)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool scrub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool scrub tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop scrub&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool scrub -s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool scrub -s tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear pool errors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool clear tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool export tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool import tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List importable pools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool import&lt;/code&gt; (without pool name)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade pool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool upgrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool upgrade tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set pool property&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool set autoreplace=on tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get pool properties&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool get&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool get all tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="dataset-filesystem-operations"&gt;Dataset (Filesystem)&amp;nbsp;Operations&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List all datasets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List with specific properties&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -o name,used,avail,mountpoint&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List snapshots&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -t snapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -t snapshot&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create dataset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs create tank/home/user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destroy dataset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs destroy tank/old_data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destroy dataset and children&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs destroy -r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs destroy -r tank/old_data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set dataset property&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set compression=lz4 tank/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get dataset properties&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs get&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs get all tank/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set quota&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set quota=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set quota=10G tank/home/user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set reservation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set reservation=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set reservation=5G tank/database&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mount dataset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs mount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs mount tank/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmount dataset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs unmount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs unmount tank/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show mounted &lt;span class="caps"&gt;ZFS&lt;/span&gt; filesystems&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs mount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs mount&lt;/code&gt; (without arguments)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="snapshot-operations"&gt;Snapshot&amp;nbsp;Operations&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs snapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs snapshot tank/home@backup-2024&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create recursive snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs snapshot -r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs snapshot -r tank/home@daily-2024&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List snapshots&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -t snapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -t snapshot -r tank/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback to snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs rollback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs rollback tank/home@backup-2024&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destroy snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs destroy tank/home@old-backup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs rename&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs rename tank/home@old tank/home@archived&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clone snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs clone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs clone tank/home@backup tank/home_clone&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show snapshot disk usage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -o space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -r -o space tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="sendreceive-replication"&gt;Send/Receive&amp;nbsp;(Replication)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Send snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send tank/home@backup &amp;gt; /backup/home.zfs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send incremental&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send -i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send -i @snap1 tank/home@snap2 &amp;gt; incremental.zfs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs receive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs receive tank/home_restore &amp;lt; /backup/home.zfs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send over &lt;span class="caps"&gt;SSH&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send \| ssh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send tank/home@backup \| ssh user@host zfs receive tank/backup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dry-run receive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs receive -n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs receive -n tank/test &amp;lt; backup.zfs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send with progress&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs send -v tank/home@backup &amp;gt; backup.zfs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="common-property-settings"&gt;Common Property&amp;nbsp;Settings&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Enable compression&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set compression=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set compression=lz4 tank/data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable deduplication&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set dedup=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set dedup=on tank/backup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set record size&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set recordsize=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set recordsize=1M tank/media&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable encryption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs create -o encryption=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs create -o encryption=on -o keyformat=passphrase tank/secure&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set access time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set atime=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set atime=off tank/database&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set sync behavior&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set sync=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set sync=disabled tank/temp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable case insensitivity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs set casesensitivity=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs create -o casesensitivity=insensitive tank/windows&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="monitoring-and-maintenance"&gt;Monitoring and&amp;nbsp;Maintenance&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show pool I/O stats&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool iostat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool iostat -v tank 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show &lt;span class="caps"&gt;ARC&lt;/span&gt; stats (FreeBSD)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysctl kstat.zfs.misc.arcstats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sysctl kstat.zfs.misc.arcstats.size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show &lt;span class="caps"&gt;ARC&lt;/span&gt; stats (Linux)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arc_summary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arc_summary&lt;/code&gt; or &lt;code&gt;cat /proc/spl/kstat/zfs/arcstats&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check pool health&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool status -x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool status -x&lt;/code&gt; (only shows problems)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show pool events&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool events -v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated scrub time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zpool status&lt;/code&gt; (during scrub)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show compression ratio&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs get compressratio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs get compressratio tank/data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show space usage by type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -o space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zfs list -r -o space tank&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="useful-one-liners"&gt;Useful&amp;nbsp;One-Liners&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Find largest datasets&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;name,used&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;used

&lt;span class="c1"&gt;# Show all snapshots sorted by creation&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;name,creation&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;creation

&lt;span class="c1"&gt;# Calculate total snapshot space&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;used&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{sum+=$1} END {print sum/1024/1024/1024 &amp;quot; GB&amp;quot;}&amp;#39;&lt;/span&gt;

&lt;span class="c1"&gt;# Show datasets with compression disabled&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;compression&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lz4\|gzip\|zle&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Monitor pool I/O in real-time&lt;/span&gt;
zpool&lt;span class="w"&gt; &lt;/span&gt;iostat&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# Show properties that differ from defaults&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;all&lt;span class="w"&gt; &lt;/span&gt;tank&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;default

&lt;span class="c1"&gt;# Quick backup to remote system&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;snapshot&lt;span class="w"&gt; &lt;/span&gt;tank/important@&lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="w"&gt; &lt;/span&gt;+%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
zfs&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;tank/important@&lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="w"&gt; &lt;/span&gt;+%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;backup-server&lt;span class="w"&gt; &lt;/span&gt;zfs&lt;span class="w"&gt; &lt;/span&gt;receive&lt;span class="w"&gt; &lt;/span&gt;tank/backup-2024
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="quick-tips"&gt;Quick&amp;nbsp;Tips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Device naming&lt;/strong&gt;: FreeBSD uses different naming conventions (ada0 for &lt;span class="caps"&gt;SATA&lt;/span&gt;, da0 for &lt;span class="caps"&gt;SCSI&lt;/span&gt;/&lt;span class="caps"&gt;USB&lt;/span&gt;) vs Linux (sda, sdb,&amp;nbsp;etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network interfaces&lt;/strong&gt;: FreeBSD names interfaces by driver (em0, re0, bge0) while Linux traditionally used eth0, eth1 (now often uses predictable names like&amp;nbsp;enp0s3)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration files&lt;/strong&gt;: FreeBSD centralizes many configs&amp;nbsp;in &lt;code&gt;/etc/rc.conf&lt;/code&gt; while Linux distributes them across various&amp;nbsp;files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual pages&lt;/strong&gt;: Both&amp;nbsp;use &lt;code&gt;man command&lt;/code&gt;, but FreeBSD&amp;#8217;s man pages are often more&amp;nbsp;comprehensive&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ports vs Packages&lt;/strong&gt;: FreeBSD has both ports (source) and packages (binary), while Linux typically uses one package manager per&amp;nbsp;distribution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span class="caps"&gt;ZFS&lt;/span&gt;&lt;/strong&gt;: While native to FreeBSD, &lt;span class="caps"&gt;ZFS&lt;/span&gt; on Linux (ZoL) has reached feature parity. Commands are identical, but kernel module loading differs&amp;nbsp;(&lt;code&gt;kldload zfs&lt;/code&gt; on FreeBSD&amp;nbsp;vs &lt;code&gt;modprobe zfs&lt;/code&gt; on&amp;nbsp;Linux)&lt;/li&gt;
&lt;/ul&gt;</content><category term="FreeBSD"/><category term="freebsd"/><category term="linux"/><category term="sysadmin"/></entry></feed>