BunkerWeb on FreeBSD

🇩🇪 Diesen Artikel gibt es auch auf Deutsch.

From Linux project to native port, a development chronicle

Sometimes it all starts with a simple question: Which WAF can I run on FreeBSD in production?

The answer I found: none that I really liked. So I solved the problem myself, and brought BunkerWeb into the FreeBSD Ports Collection. This article describes the road there: architecture decisions, setbacks, solutions, and everything that happened between the first make and the final commit.


1. Motivation: Why BunkerWeb?

While looking for a modern open-source WAF for FreeBSD, I had a clear list of requirements:

  • Reverse proxy
  • OWASP Core Rule Set with ModSecurity
  • Automatic Let’s Encrypt
  • Security headers
  • Rate limiting & bot protection
  • Central, clear configuration, ideally with a web UI

BunkerWeb ticked all those boxes, on Linux. There was no official port for FreeBSD. The decision was made quickly: I would create the port myself and maintain it long-term.


2. Analyzing the upstream project: what are you in for?

Before you create a port, you have to understand what you are dealing with. I looked at:

  • BUILD.md and the Dockerfiles
  • The Linux packages and their contents
  • The build system
  • The Python components and their dependencies
  • The Lua components
  • The entire directory layout

The result was sobering, and at the same time a clear to-do list:

  • Very Linux-centric, lots of assumptions about the file system
  • Hard-coded paths like /usr/share/bunkerweb and /etc/bunkerweb everywhere
  • Numerous Python dependencies, not all of which existed as FreeBSD ports
  • Several independent services: OpenResty, scheduler, API, UI, each with its own requirements

One thing was clear: this was not going to be a quick weekend project.


3. The first architecture decision: nginx or OpenResty?

My first approach was the obvious one, BunkerWeb is based on NGINX, so build on top of www/nginx.

That did not work.

BunkerWeb relies on extensive Lua components for its security logic. Stock NGINX does not ship that Lua environment. You could bolt on the Lua modules individually, but that would be a fragile construction with a lot of maintenance overhead.

The solution: switching to OpenResty.

OpenResty is essentially NGINX with a fully integrated LuaJIT environment and all the required lua-nginx-module extensions. With it, BunkerWeb runs its entire Lua logic cleanly, without workarounds. That decision made the port more maintainable in the long run, even though it meant starting over from scratch.


4. Missing ports: construction sites before the actual construction site

While porting, it quickly became apparent that not all dependencies existed in the FreeBSD ports tree yet.

Before www/bunkerweb could be finished at all, additional ports had to be created or adapted, among others:

  • Adjustments around the nginx/OpenResty integration
  • Various Python dependencies as ports of their own
  • py-defusedcsv: born out of the BunkerWeb effort, but useful on its own by now

This is a typical situation when porting complex software: you end up contributing to the ports tree on the side, without ever having planned to.


5. The actual port: everything that belongs to it

A complete FreeBSD port is more than a Makefile. For www/bunkerweb, the following came into being:

www/bunkerweb/
├── Makefile          # Build logic, dependencies, options
├── distinfo          # Checksums of the source files
├── pkg-plist         # All installed files
├── pkg-message       # Post-install notes
└── files/            # rc.d script, patches (patch-*), config templates

Each of these files has a story of its own.


6. Linux paths: the biggest recurring annoyance

BunkerWeb expected its files exactly where Linux expects them:

  • /usr/share/bunkerweb/ → core, plugins, UI files
  • /etc/bunkerweb/ → configuration
  • /var/lib/bunkerweb/ → runtime data

On FreeBSD the convention is unambiguous: third-party software belongs under /usr/local/: i.e. PREFIX in ports jargon. Practically every path reference in the code had to be replaced:

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

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

That sounds mechanical, but it was tedious, because these paths were not only in configuration files, but buried deep in the Python code, in Lua scripts, and in startup scripts. For every occurrence: write a patch, test, repeat.


7. Python: several construction sites at once

The Python side of the project was a chapter of its own:

Dependencies: All runtime dependencies had to be wired up as py3xx-* ports, no pip install inside a port.

pkg_resources and setuptools: BunkerWeb uses pkg_resources internally for path discovery. On a cleanly installed FreeBSD port, that behaves differently than in a Linux virtualenv. This required adjustments.

No virtual environment: In Docker deployments, BunkerWeb typically runs in an isolated container with its own Python. For a native port, the rules are: system Python, modules from ports, no venv. That required carefully aligning the dependencies.

File permissions: Some Python files generated during the build were missing the execute bit. As a result, the scheduler and the configuration generator would not run, a bug that only showed up at runtime, not at build time.


8. rc.d instead of systemd

BunkerWeb ships with systemd units for Linux:

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

FreeBSD has no systemd. Instead: rc(8). Each service got its own rc.d script that follows the FreeBSD conventions for PID files, runtime directories and logging:

#!/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"

The services are then enabled with sysrc:

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

9. OpenResty: more than just renaming NGINX

Switching to OpenResty was the right architecture decision, but OpenResty had its own quirks:

  • nginx.conf and includes: BunkerWeb generates its NGINX configuration dynamically. The generated paths for includes, log files and the socket had to be adapted to FreeBSD conventions.
  • HTTP/2: Required specific OpenResty compile options.
  • Prefix and startup parameters: OpenResty has to be started with the correct --prefix so it can find its configuration and modules.

10. A chronicle of the problems

An honest overview of the most important hurdles, from start to finish:

ProblemCauseSolution
Architecture unclearUpstream designed for Linux onlyWorked out a FreeBSD port concept
nginx is not enoughMissing Lua integrationSwitched to OpenResty
Missing portsDependencies not in the ports treeCreated and committed new ports
Wrong runtime pathsHard-coded Linux paths${PREFIX} patches for all components
Scheduler will not startMissing execute bits on generated filesFixed permissions in the port install
API will not startPort 8888 already taken by OpenRestyAdjusted the configuration
Logs/PIDs missingDirectories were never createdCreated automatically in the rc.d script

The API port conflict was particularly tricky: it looked like the API was crashing, but it was not. OpenResty had USE_API=yes and API_HTTP_PORT=8888 set and was occupying the very port the BunkerWeb API wanted to use as well. Not a crash, but a configuration question.


11. Testing: no merge without a fresh jail

A successful build is not yet a working port. That theme ran through the entire project.

The testing process was accordingly thorough:

# Iterative build cycle
make clean
make extract
make patch
make stage
make package

# Validation
poudriere testport www/bunkerweb

Tests in fresh jails were the crucial part, only there does it become clear whether all dependencies are really declared and whether the port works without leftovers from earlier installations. Runtime tests (scheduler running? API reachable? UI showing up?) always came as the last step.


12. Installation: how to use the port

Via pkg (recommended)

pkg install bunkerweb

From the ports tree

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

Enable and start the services

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

service bunkerweb start
service bunkerweb-scheduler start

Initial configuration

The central configuration lives 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

Accessing the web UI

The UI is reachable at http://127.0.0.1:7000 by default. For remote access, an SSH tunnel is recommended:

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

13. Lessons learned

This is the chapter I recommend to anyone who wants to tackle a complex port themselves:

Not every Linux project can be ported 1:1. BunkerWeb carries Linux as a base assumption deep in its code. That is not a criticism of the upstream project, but you have to understand it before you start.

Architecture decisions are allowed to change. The switch from nginx to OpenResty did not happen on day one. Sometimes you need time and failure to recognize the right solution. That is not a step backwards.

A successful build does not mean a working port. The hardest problems, wrong paths, missing permissions, port conflicts, only show up at runtime. Build and runtime are two different worlds.

Poudriere and fresh jails are indispensable. No port should go into the tree without tests in a clean environment. Period.


Conclusion

Months of work, a few architecture changes, dozens of patches and a couple of new ports later: BunkerWeb is now a native FreeBSD port. No Docker wrapper, no Linux compatibility layer, but www/bunkerweb, cleanly integrated into the ecosystem.

The port is fresh. Feedback is very welcome, bug reports, success stories, or suggestions for improvement. The best places are the FreeBSD forum thread or bugs.freebsd.org.

If you would like to support my work on this and more than 40 other ports, you will find the option to do so in the sidebar.

Have you already tried BunkerWeb on FreeBSD? Let me know in the comments!

Schreibe einen Kommentar