4. JUNI 2026 / 4 min read

Firewall-Audit per API — wenn die GUI lügt und pfctl die Wahrheit sagt

Ein KI-gestütztes Audit meiner OPNsense-Zero-Trust-Regeln. Wie ein vermeintlich kritischer DNS-Bug sich als Darstellungsartefakt entpuppte — und warum man Firewall-Regeln immer im Live-Kernel gegenprüfen muss.

opnsensehomelabsecurityfirewalldns

Im Post zum Netzwerk-Upgrade hatte ich es angeteasert: Aus der VLAN-Segmentierung wurde eine echte Zero-Trust-Firewall mit Default-Deny und DNS-Hijack-Block. Ein paar Wochen später wollte ich wissen, ob das alles noch so läuft wie gedacht — und habe meinen KI-Assistenten ein vollständiges Audit der OPNsense fahren lassen. Über die API, nicht über die GUI. Das Ergebnis war lehrreicher als erwartet.

Der Aufbau: API statt Klickstrecke

Die OPNsense lässt sich komplett über eine REST-API steuern. Credentials liegen in einer .env, der Rest ist curl:

curl -sk -u "$KEY:$SEC" https://10.17.1.1/api/firewall/filter/searchRule | jq

So konnte der Assistent alle ~70 Filter-Regeln, die DNAT-Redirects und die NAT-Konfiguration auslesen und gegen meine Dokumentation abgleichen. Erste Erkenntnis: Die Doku war an mehreren Stellen veraltet — eine Regel verwies noch auf einen alten Domain-Namen, eine WireGuard-Regel hing tot auf einem längst deaktivierten Interface herum.

Der vermeintliche Super-GAU

Dann der Schock: Die DNAT-Regeln, die meinen DNS-Hijack-Schutz tragen, zeigten in der API destination_port: null und enabled: null. Diese Regeln sollen erzwingen, dass jedes Gerät — auch ein Smart-TV mit hartkodiertem 8.8.8.8 — transparent auf meinen AdGuard-Resolver umgeleitet wird. Wenn die kaputt sind, läuft mein halbes Sicherheitskonzept ins Leere.

Erst die Gegenprüfung über einen anderen API-Endpoint (getRule statt searchRule) und ein Blick ins rohe Config-XML brachten Entwarnung: Die Regeln sind aktiv und korrekt. Die Such-API gibt schlicht bestimmte Felder nicht aus — Negationen (!) und Ports tauchen dort nicht auf.

Lektion 1: Einer einzelnen API-Sicht nie blind vertrauen. Verschiedene Endpoints zeigen verschiedene Teilwahrheiten.

Die einzige Quelle der Wahrheit: pfctl

Um endgültig sicherzugehen, brauchte ich den Blick in den laufenden Paketfilter-Kernel. Und hier wurde es interessant: Der SSH-Zugang zur OPNsense landet normalerweise im interaktiven Menü (das berühmte „Punkt 8 für Shell”). Für Automatisierung unbrauchbar — dachte ich.

Stellt sich heraus: Das Menü erscheint nur bei interaktivem Login. Ein SSH-Aufruf mit Kommando läuft direkt durch:

ssh root@10.17.1.1 '/bin/sh -c "pfctl -sr | grep vlan04"'

Eine kleine Stolperfalle: Die root-Shell ist csh, die bash-typische Umleitungen wie 2>/dev/null nicht versteht („Ambiguous output redirect”). Lösung: alles in sh -c '...' wrappen.

Mit pfctl -vvsr (Regeln samt Trefferzählern), pfctl -sn (NAT/Redirects) und pfctl -ss (State-Table) hatte ich endlich die echte Wahrheit. Und die sah anders aus als die Regelnamen vermuten ließen.

Die echte Überraschung: ein Regel-Reihenfolge-Rätsel

Im Live-Ruleset sah ich für das IOT-VLAN:

@141 block drop in quick on vlan04 inet from 10.17.30.0/24 to 10.0.0.0/8   [Packets: 122]
@163 pass  in       quick on vlan04 inet ... to 10.17.1.254 port = domain  [Packets: 0]

Mein AdGuard-Resolver liegt auf 10.17.1.254 — also innerhalb von 10.0.0.0/8. Die RFC1918-Isolationsregel (block … to 10/8) ist quick und steht vor der DNS-Erlaubnis. Nach pf-Logik („first match wins”) müsste sie jede DNS-Anfrage ans AdGuard verschlucken. Der Zähler bestätigte: Die DNS-Erlaubnis-Regel ließ null Pakete durch.

Panik? Kurz. Denn mein Netz funktionierte ja nachweislich. Die Auflösung lag im DHCP: Kea verteilt als DNS-Server nicht direkt den AdGuard, sondern die Gateway-IP des jeweiligen VLANs (z.B. 10.17.20.1 für Clients). Die Geräte fragen ihr Gateway — also die Firewall selbst — und der DNAT-Redirect lenkt das :53-Paket transparent auf den AdGuard um. Deshalb laufen die expliziten Erlaubnis-Regeln faktisch leer, und das System funktioniert trotzdem.

Die 122 geblockten Pakete? Kein DNS, sondern korrekt isolierte IoT-Broadcasts und Cross-VLAN-Versuche eines Smart-Home-Geräts. Genau das, was Zero-Trust wegfiltern soll.

Lektion 2: Eine Regel kann den richtigen Effekt haben, aber aus einem ganz anderen Grund als ihr Name suggeriert. Ohne den Blick in die State-Table und die Trefferzähler hätte ich das nie verstanden — und im schlimmsten Fall „repariert”, was gar nicht kaputt war.

Was tatsächlich verbessert wurde

Nach der ganzen Detektivarbeit blieben echte, saubere Änderungen übrig:

  • Vier tote/redundante Regeln entfernt — darunter zwei, die DNS auf any:53 erlaubten und damit (in der Eval-Reihenfolge vor dem Hijack-Block) den eigentlichen Schutz aushebelten. Jetzt ist der DNS-Hijack-Block ein echter Enforcer, nicht nur dekorativ.
  • DMZ-Isolation nachgezogen: Das (noch leere) DMZ-VLAN hatte keine RFC1918-Blocks. Proaktiv ergänzt, damit ein künftiger Host dort von Anfang an isoliert ist.
  • Alles dokumentiert — inklusive des Gateway-DNS-Pfades, damit ich beim nächsten Audit nicht wieder über dasselbe Rätsel stolpere.

Jede Änderung lief über die API, wurde angewendet und anschließend im Live-pf-Kernel gegenverifiziert. Backup vorher, versteht sich.

Fazit

Drei Dinge nehme ich mit. Erstens: Management-APIs sind bequem, aber sie zeigen eine kuratierte Sicht — der Paketfilter-Kernel ist die einzige Quelle der Wahrheit. Zweitens: Regelnamen sind Absichtserklärungen, kein Beweis für Verhalten; Trefferzähler lügen nicht. Und drittens: Ein KI-Assistent mit API- und Shell-Zugang ist für so ein Audit erstaunlich stark — vorausgesetzt, er prüft seine eigenen Schlüsse empirisch nach, statt dem ersten Befund zu glauben. Genau das „🔴 kritisch → doch nur ein Darstellungsartefakt” war der wertvollste Moment der Session.