BunkerWeb auf FreeBSD

🇬🇧 Read this article in English.

Vom Linux-Projekt zum nativen Port, eine Entwicklungschronik


Manchmal beginnt alles mit einer simplen Frage: Welche WAF kann ich auf FreeBSD produktiv einsetzen?

Die Antwort, die ich fand: Keine, die mir wirklich gefiel. Also habe ich das Problem selbst gelöst, und BunkerWeb in die FreeBSD Ports Collection gebracht. Dieser Artikel beschreibt den Weg dorthin: Architekturentscheidungen, Rückschläge, Lösungen und alles, was zwischen dem ersten make und dem finalen Commit lag.


1. Motivation: Warum BunkerWeb?

Auf der Suche nach einer modernen Open-Source-WAF für FreeBSD hatte ich eine klare Anforderungsliste:

  • Reverse Proxy
  • OWASP Core Rule Set mit ModSecurity
  • Automatisches Let’s Encrypt
  • Security Header
  • Rate Limiting & Bot Protection
  • Zentrale, übersichtliche Konfiguration, idealerweise mit Web-UI

BunkerWeb erfüllte all das, auf Linux. Für FreeBSD existierte kein offizieller Port. Die Entscheidung war schnell getroffen: Ich erstelle den Port selbst und pflege ihn langfristig.


2. Analyse des Upstream-Projekts: Was erwartet einen?

Bevor man einen Port erstellt, muss man verstehen, womit man es zu tun hat. Ich habe mir angeschaut:

  • BUILD.md und Dockerfiles
  • Linux-Pakete und deren Inhalte
  • Das Buildsystem
  • Python-Komponenten und deren Abhängigkeiten
  • Lua-Komponenten
  • Die gesamte Verzeichnisstruktur

Das Ergebnis war ernüchternd, und gleichzeitig eine klare Aufgabenliste:

  • Sehr Linux-lastig: viele Annahmen über das Dateisystem
  • Fest kodierte Pfade wie /usr/share/bunkerweb und /etc/bunkerweb überall
  • Zahlreiche Python-Abhängigkeiten, von denen nicht alle als FreeBSD-Port existierten
  • Mehrere eigenständige Dienste: OpenResty, Scheduler, API, UI, jeder mit eigenen Anforderungen

Klar war: Das wird kein schnelles Wochenendprojekt.


3. Die erste Architekturentscheidung: nginx oder OpenResty?

Mein erster Ansatz war naheliegend, BunkerWeb basiert auf NGINX, also baut man auf www/nginx auf.

Das funktionierte nicht.

BunkerWeb nutzt umfangreiche Lua-Komponenten für seine Sicherheitslogik. Standard-NGINX bringt diese Lua-Umgebung nicht mit. Man könnte Lua-Module einzeln nachziehen, aber das wäre eine fragile Konstruktion mit viel Pflegeaufwand.

Die Lösung: Wechsel auf OpenResty.

OpenResty ist im Grunde NGINX mit vollständig integrierter LuaJIT-Umgebung und allen nötigen lua-nginx-module-Erweiterungen. Damit bringt BunkerWeb seine gesamte Lua-Logik sauber zum Laufen, ohne Sonderlösungen. Die Entscheidung hat den Port langfristig wartbarer gemacht, auch wenn sie bedeutete, dass ich von vorn anfangen musste.


4. Fehlende Ports: Baustellen vor der eigentlichen Baustelle

Beim Portieren stellte sich schnell heraus: Nicht alle Abhängigkeiten existierten bereits im FreeBSD Ports Tree.

Bevor www/bunkerweb überhaupt fertig werden konnte, mussten weitere Ports erstellt oder angepasst werden, unter anderem:

  • Anpassungen rund um nginx/OpenResty-Integration
  • Diverse Python-Abhängigkeiten als eigene Ports
  • py-defusedcsv: aus dem BunkerWeb-Umfeld heraus entstanden, aber inzwischen eigenständig nützlich

Das ist eine typische Situation beim Portieren komplexer Software: Man trägt nebenbei zum Ports Tree bei, ohne dass das ursprünglich geplant war.


5. Der eigentliche Port: Was alles dazugehört

Ein vollständiger FreeBSD-Port ist mehr als ein Makefile. Für www/bunkerweb entstanden:

www/bunkerweb/
├── Makefile          # Build-Logik, Abhängigkeiten, Optionen
├── distinfo          # Checksummen der Quelldateien
├── pkg-plist         # Alle installierten Dateien
├── pkg-message       # Post-Install-Hinweise
└── files/            # rc.d-Skript, Patches (patch-*), Konfig-Templates

Jede dieser Dateien hat ihre eigene Geschichte.


6. Linux-Pfade: Das größte Dauerärgernis

BunkerWeb erwartete seine Dateien genau dort, wo Linux sie erwartet:

/usr/share/bunkerweb/   → Core, Plugins, UI-Dateien
/etc/bunkerweb/         → Konfiguration
/var/lib/bunkerweb/     → Runtime-Daten

Auf FreeBSD ist die Konvention eindeutig: Drittanbieter-Software gehört unter /usr/local/: also ${PREFIX} im Ports-Jargon. Praktisch jede Pfadreferenz im Code musste ersetzt werden:

- /usr/share/bunkerweb/core
+ ${PREFIX}/share/bunkerweb/core

- /etc/bunkerweb/variables.env
+ ${PREFIX}/etc/bunkerweb/variables.env

Das klingt mechanisch, war aber aufwendig, weil diese Pfade nicht nur in Konfigurationsdateien, sondern tief im Python-Code, in Lua-Skripten und in Startskripten steckten. Für jeden Fundort: Patch schreiben, testen, wiederholen.


7. Python: Mehrere Baustellen auf einmal

Die Python-Seite des Projekts war ein eigenes Kapitel:

Abhängigkeiten: Alle Runtime-Abhängigkeiten mussten als py3xx-*-Ports eingebunden werden, kein pip install im Port.

pkg_resources und setuptools: BunkerWeb nutzt intern pkg_resources zur Pfaderkennung. Auf einem sauber installierten FreeBSD-Port verhält sich das anders als in einem Linux-Virtualenv. Das erforderte Anpassungen.

Keine virtuelle Umgebung: In Docker-Deployments läuft BunkerWeb typischerweise in einem isolierten Container mit eigenem Python. Für einen nativen Port gilt: System-Python, Ports-Module, keine venv. Das erforderte sorgfältige Abstimmung der Abhängigkeiten.

Dateirechte: Einige durch den Build generierte Python-Dateien hatten kein Execute-Bit gesetzt. Scheduler und Konfigurations-Generator liefen deshalb nicht, ein Fehler, der sich erst zur Laufzeit zeigte, nicht beim Build.


8. rc.d statt systemd

BunkerWeb kommt mit systemd-Units für Linux:

bunkerweb.service
bunkerweb-scheduler.service
bunkerweb-ui.service

FreeBSD kennt kein systemd. Stattdessen: rc(8). Für jeden Dienst entstand ein eigenes rc.d-Skript, das die FreeBSD-Konventionen für PID-Dateien, Runtime-Verzeichnisse und Logging berücksichtigt:

#!/bin/sh
# PROVIDE: bunkerweb
# REQUIRE: NETWORKING DAEMON
# KEYWORD: shutdown

. /etc/rc.subr

name="bunkerweb"
rcvar="bunkerweb_enable"
pidfile="/var/run/${name}/${name}.pid"
logfile="/var/log/${name}/${name}.log"

command="/usr/local/bin/bunkerweb"
command_args="--pid ${pidfile}"

load_rc_config $name
run_rc_command "$1"

Aktiviert werden die Dienste dann mit sysrc:

sysrc bunkerweb_enable="YES"
sysrc bunkerweb_scheduler_enable="YES"
sysrc bunkerweb_ui_enable="YES"   # optional

9. OpenResty: Mehr als nur NGINX umbenennen

Der Wechsel auf OpenResty war die richtige Architekturentscheidung, aber auch OpenResty hatte seine eigenen Tücken:

  • nginx.conf und Includes: BunkerWeb generiert seine NGINX-Konfiguration dynamisch. Die generierten Pfade für Includes, Log-Dateien und den Socket mussten auf FreeBSD-Konventionen angepasst werden.
  • HTTP/2: Erforderte spezifische OpenResty-Kompilierungsoptionen.
  • Prefix und Startparameter: OpenResty muss mit dem korrekten --prefix gestartet werden, damit es seine Konfiguration und Module findet.

10. Die Chronik der Probleme

Hier eine ehrliche Übersicht der wichtigsten Hürden, von Anfang bis Ende:

ProblemUrsacheLösung
Architektur unklarUpstream nur auf Linux ausgelegtFreeBSD-Port-Konzept erarbeitet
nginx reicht nichtFehlende Lua-IntegrationWechsel auf OpenResty
Fehlende PortsAbhängigkeiten nicht im Ports TreeNeue Ports erstellt und committed
Runtime-Pfade falschLinux-Pfade fest kodiert${PREFIX}-Patches für alle Komponenten
Scheduler startet nichtFehlende Execute-Bits auf generierten DateienRechte im Port-Install korrigiert
API startet nichtPort 8888 bereits durch OpenResty belegtKonfiguration angepasst
Logs/PIDs fehlenVerzeichnisse wurden nicht angelegtIm rc.d-Skript automatisch erstellt

Der API-Port-Konflikt war dabei besonders tricky: Es sah aus wie ein Absturz der API, war aber keiner. OpenResty hatte USE_API=yes und API_HTTP_PORT=8888 gesetzt und belegte damit denselben Port, den die BunkerWeb-API ebenfalls verwenden wollte. Kein Crash, sondern eine Konfigurationsfrage.


11. Tests: Kein Merge ohne frische Jail

Ein erfolgreicher Build ist noch kein funktionierender Port. Das hat sich durch das gesamte Projekt gezogen.

Der Testprozess war entsprechend gründlich:

# Iterativer Build-Zyklus
make clean
make extract
make patch
make stage
make package

# Validierung
poudriere testport www/bunkerweb

Entscheidend waren vor allem Tests in frischen Jails: nur dort zeigt sich, ob wirklich alle Abhängigkeiten deklariert sind und der Port ohne vorhandene Reste aus früheren Installationen funktioniert. Runtime-Tests (Scheduler läuft? API erreichbar? UI zeigt sich?) kamen immer als letzter Schritt.


12. Installation: So nutzt ihr den Port

Via pkg (empfohlen)

pkg install bunkerweb

Aus dem Ports Tree

cd /usr/ports/www/bunkerweb
make install clean

Dienste aktivieren und starten

sysrc bunkerweb_enable="YES"
sysrc bunkerweb_scheduler_enable="YES"

service bunkerweb start
service bunkerweb-scheduler start

Erste Konfiguration

Die zentrale Konfiguration liegt in /usr/local/etc/bunkerweb/variables.env:

SERVER_NAME=www.example.com
USE_REVERSE_PROXY=yes
REVERSE_PROXY_URL=/
REVERSE_PROXY_HOST=http://127.0.0.1:3000

USE_LETS_ENCRYPT=yes
EMAIL_LETS_ENCRYPT=admin@example.com

Web-UI aufrufen

Das UI ist standardmäßig unter http://127.0.0.1:7000 erreichbar. Für Remote-Zugriff empfiehlt sich ein SSH-Tunnel:

ssh -L 7000:127.0.0.1:7000 user@mein-server

13. Lessons Learned

Das ist das Kapitel, das ich jedem empfehle, der selbst einen komplexen Port angehen will:

Nicht jedes Linux-Projekt lässt sich 1:1 portieren. BunkerWeb trägt Linux als Grundannahme tief im Code. Das ist kein Vorwurf ans Upstream-Projekt, aber man muss es verstehen, bevor man anfängt.

Architekturentscheidungen dürfen sich ändern. Der Wechsel von nginx zu OpenResty kam nicht am ersten Tag. Manchmal braucht man Zeit und Scheitern, um die richtige Lösung zu erkennen. Das ist kein Rückschritt.

Ein erfolgreicher Build bedeutet noch keinen funktionierenden Port. Die härtesten Probleme, falsche Pfade, fehlende Rechte, Port-Konflikte, zeigen sich erst zur Laufzeit. Build und Runtime sind zwei verschiedene Welten.

Poudriere und frische Jails sind unverzichtbar. Kein Port sollte ohne Tests in einer sauberen Umgebung in den Tree. Punkt.


Fazit

Monate Arbeit, einige Architekturwechsel, dutzende Patches und ein paar neue Ports später: BunkerWeb ist jetzt ein nativer FreeBSD-Port. Kein Docker-Wrapper, kein Linux-Compatibility-Layer, sondern www/bunkerweb, sauber integriert ins Ökosystem.

Der Port ist frisch. Feedback ist ausdrücklich willkommen, ob Bug Reports, Erfolgsberichte oder Verbesserungsvorschläge. Am besten direkt im FreeBSD-Forum-Thread oder über bugs.freebsd.org.


Wer meine Arbeit an diesem und über 40 weiteren Ports unterstützen möchte, findet in der Seitenleiste die Möglichkeit dazu.

Habt ihr BunkerWeb auf FreeBSD bereits getestet? Schreibt es in die Kommentare!

Schreibe einen Kommentar