L7 proxies on FreeBSD

Cilium’s L7 policy story on Linux leans on Envoy. Envoy does not build cleanly on FreeBSD. The question this page answers: of the actually- shippable L7 proxies in the 2026 FreeBSD ports tree, which one belongs in front of a Coppice sandbox, and is the current pick (haproxy) defensible as more than a default.

Why Coppice needs one

pf is L3/L4 only. The policy shapes Coppice has to express to reach Cube parity include method POST → 403, path_beg /admin/ → 403, hdr(X-Agent-Role) auditor → 403. None of those live in the packet headers pf looks at. The architecture we landed on in /appendix/ebpf-to-pf is a userspace proxy sidecar per sandbox, 127.0.0.1:8080 inside the VNET jail, with the workload configured to speak HTTP to it — the Istio/Envoy placement, minus the bpf_sk_assign redirector. The sidecar has to inspect HTTP, apply the deny rules, pass the rest. That’s the job.

The non-negotiables from T3-B’s spec:

“License compatible with sidecar bundling” is the one worth being precise about. A sidecar is a separate process reached over a local socket; it is not linked into the workload’s address space. GPLv2 on the sidecar binary does not reach the workload — that’s not controversial, it’s how grep and ssh have worked for thirty years. What we actually need is a license that lets us ship the binary in an image without downstream redistribution obligations biting the workload code. GPL-with-standard-exceptions on a sidecar is fine; AGPL-on-a-service-that-ingests-user-traffic is not.

What’s in the FreeBSD 15.0 ports tree

pkg search on honor (FreeBSD 15.0-RELEASE-p4, 2026-04-22) for the ten candidates T3-B asked us to evaluate:

candidatepkg on 15.0versionports license
haproxyyes3.2.15 (also 2.4/2.6/2.8/3.0/3.3)LGPL2.1 + GPLv2
nginxyes1.28.3BSD-2
caddyyes2.11.2_1Apache 2.0
traefikyes3.6.13MIT
relaydyes7.4.2024.01.15_p3ISC
varnishyes7.7.3BSD-2
hitchyes1.8.0_1BSD-2
pomeriumstale0.8.4_23 (upstream: 0.32.5)Apache 2.0
envoynoApache 2.0
pingoranoApache 2.0
sozunoAGPLv3 / LGPLv3

pkg search -x ’^(haproxy|nginx|envoy|caddy|traefik|pingora|relayd|sozu|pomerium|hitch|varnish)’ on honor, 2026-04-22. Seven of the eleven ship as first-class packages. Pomerium’s port is five major versions behind and effectively abandoned. Three (envoy, pingora, sozu) are not packaged at all.

The survivors, in order

haproxy 3.2.15 (current pick)

The haproxy port ships 3.2.15 stable, with haproxy24, haproxy26, haproxy28, haproxy30, and haproxy33 as parallel slots. Upstream tagged 3.3.0 four months ago; the master branch had a commit six hours before this page was written (BUG/MAJOR: net_helper — the project is alive). Cirrus CI runs FreeBSD jobs on every push — the FreeBSD badge in the README points at cirrus-ci.com/github/haproxy/haproxy, and FreeBSD is tier-1 in practice, not a “should work.”

ACL language expresses all four shapes we need declaratively: acl admin path_beg /admin/, acl post method POST, acl host_match hdr(host) -i internal.example, acl auditor hdr(X-Agent-Role) -i auditor, then http-request deny if admin || post || auditor. No Lua required for the policies we’ve enumerated. The Lua hook exists if a sandbox later needs dynamic allow-list lookup against a local Unix-socket service, but it’s not on the shipped path.

haproxy -sf graceful reload: the old process stops accepting new connections, finishes in-flight ones, exits; the new process takes the listener. Measured on sbx-a: 12 ms swap, zero dropped requests across the probe window. Startup is 10 ms fork-to-listening. Per-request overhead (direct-loopback baseline ~460 µs, with-sidecar ~540 µs) is ~80 µs — 58-106 µs run-to-run. Receipts: benchmarks/results/l7-policy-envoy-2026-04-22.txt.

License is GPLv2 with LGPL headers and an OpenSSL exemption. The sidecar placement defuses the viral concern: we ship the binary unchanged, we don’t link against it, we don’t modify it. This is the same footing as shipping pf itself in a FreeBSD image — nobody has ever been GPL-contaminated by exec’ing a GPLv2 program.

nginx 1.28.3

nginx ports tree has nginx at 1.28.3 stable plus nginx-devel at 1.29.7 and the usual constellation of modules. Upstream tagged 1.30.0 on 2026-04-14; the project is very much maintained. FreeBSD is a documented install target on nginx.org (“installing on FreeBSD”), long-standing.

The ACL story is weaker and worth naming precisely. Core nginx expresses method/path/host with location blocks and if directives plus return 403. Header-value matching in core config is map $http_x_agent_role $deny … plumbing — it works, it’s documented, but the ergonomics are worse than haproxy’s ACL vocabulary. For anything richer (compound conditions, early deny before the upstream connect) you reach for njs (nginx’s JavaScript) or OpenResty (ngx_lua). Both are packaged; both add a config format we’d otherwise not need.

Graceful reload (nginx -s reload) is long-proven, same semantics as haproxy’s -sf: old workers drain, new workers own the listener. License is BSD-2, a tick cleaner than haproxy’s GPLv2. Per-request overhead at loopback is in the same band as haproxy for the matched-and-forwarded path; we have not independently measured it for Coppice. Flagged unverified.

Caddy 2.11.2

caddy ports at 2.11.2_1, upstream 2.11.2 released 2026-03-06. Apache 2.0. Go build, repo contains listen_unix_setopt_freebsd.go — FreeBSD is a first-class target. Caddyfile matchers express method, path, host, and header directly, and abort / respond 403 deny. The expressiveness is roughly on par with haproxy for our shapes.

Two gotchas worth pinning. First, Caddy’s headline feature is automatic TLS — on first listen it will try to obtain certificates via ACME. For a loopback sidecar this is unwanted overhead and an egress dependency we do not need; you defeat it with auto_https off at the global block, which is one line but is a line you have to remember. Second, Caddy’s graceful reload is caddy reload via its admin API, documented as drain-and- swap. We have not benchmarked its dropped-connection behavior on FreeBSD; upstream describes it as zero-downtime.

Footprint is heavier than haproxy: a statically-linked Go binary in the ~35 MB range, vs haproxy’s ~3 MB stripped. For a one-sidecar-per-sandbox density target, that’s 32 MB × N that we pay in RSS or page cache, depending. Not prohibitive; not free either.

Traefik 3.6.13

traefik ports at 3.6.13, MIT licensed, 6159 commits on master, upstream active. Go, cross-platform, FreeBSD shipped as a normal build target. The problem is shape, not maintenance: Traefik’s policy model is built around routing (pick a backend) and middlewares (transform or authenticate). Denying a request by method or header requires either (a) a ForwardAuth middleware pointing at an external authorizer we’d have to write, (b) a Headers middleware with custom deny logic, or (c) a plugin. None of these are as direct as haproxy’s http-request deny if …. For a Kubernetes ingress with service discovery, Traefik is great; for a local sidecar whose only job is pattern-match and forward, it is more apparatus than we need.

relayd 7.4

relayd ports at 7.4.2024.01.15_p3 — a rebadged snapshot of OpenBSD’s relayd(8). ISC license (cleanest of the set). Maintained on the FreeBSD side (last ports commit 2025-09-03; Mark Johnston). Natively expresses L7 matches via the http protocol block: match request method POST, match request path “/admin/*”, match request header “X-Agent-Role” value “auditor”, with block / pass verbs.

The real question on relayd is velocity. OpenBSD cadences are OpenBSD cadences; the project is alive, but it is not landing features at the rate Cloudflare lands Pingora features. For Coppice’s enumerated policies, that’s fine — those policies aren’t changing. For the “we want to match on this year’s obscure HTTP/3 thing” future, relayd is the wrong bet. As a fallback if haproxy ever became problematic, relayd is the pick we’d reach for first — same per-request config surface, ISC license, one ports-tree apart.

The ones that don’t make it

Envoy. Not packaged. Upstream Bazel build carries Linux-only sanitizer paths, a BoringSSL assumption that collides with LLVM-libunwind, and tcmalloc/kqueue shim gaps. Searching envoyproxy/ envoy for “freebsd” surfaces closed-in-2022 issues (#20130, #20229) and nothing open in 2024-2026 — the upstream has moved on from caring. Unless and until someone lands a sustained port series (nobody has), Envoy-on-FreeBSD is unshippable.

Pingora. Not packaged. README tier-1 target is Linux; “Unix environments” (macOS) are mentioned aspirationally. Rust builds-from-source on FreeBSD are possible but not CI’d, and the project explicitly does not promise it. A 12-FTE-week port effort is not the right spend when haproxy already measures 80 µs. Revisit if Cloudflare flips FreeBSD to tier-2.

sozu. Not packaged. License is AGPLv3 on the daemon with LGPLv3 on the command library. The project’s README explicitly states “traffic passing through Sōzu is not considered covered work,” which defangs the runtime-workload concern, but shipping AGPL in a distribution still carries compliance machinery we don’t want for a sidecar. Upstream: last commit 2026-03-17, FreeBSD compatibility commits landed 2025-11-21 (#1187) — surprising to see, but it’s for TCP socket options, not a full port. Not first-choice; flagged as a possibility if we ever need a Rust-in-sidecar memory-safety story.

Pomerium. Packaged, but at 0.8.4 from five years ago while upstream is at 0.32.5 (2025-04-08). The gap is not cosmetic: modern Pomerium bundles github.com/pomerium/envoy-custom, which is a patched Envoy binary. It inherits Envoy’s FreeBSD build problem. The port is effectively dead. Do not use.

varnish, hitch. Packaged and maintained. Varnish is a caching HTTP accelerator whose VCL could express method/ path/header denies via vcl_recv — a one-liner that synthesizes a 403 on req.method == “POST” — but Varnish’s model is caching + backend selection, not policy enforcement; you’d be using 80% of Varnish to ignore its point and 20% for the deny story. Hitch is TLS termination only; it doesn’t inspect HTTP. Neither is a reasonable Envoy substitute for Coppice’s shape.

The scoring matrix

propertyhaproxy 3.2nginx 1.28Caddy 2.11relayd 7.4
FreeBSD pkgyesyesyesyes
source build on 15.0cleancleanclean (Go)clean
method/path/host/header ACLnative, declarativenative + if/map or njs/Luanative matchersnative match blocks
graceful reload-sf, 12 ms measured-s reload, long-provenadmin-API reload, unmeasured hereSIGHUP, unmeasured here
per-request overhead (loopback)~80 µs measuredsimilar band, unverifiedheavier (Go GC), unverifiedunverified
binary size (stripped)~3 MB~1.5 MB core~35 MB (Go static)~1 MB
last upstream commit2026-04-222026-04-14 (1.30.0)2026-03-06 (2.11.2)2024-01-15 snapshot; ports 2025-09-03
FreeBSD in CIyes (Cirrus)documented install targetFreeBSD-specific code paths in treeports-maintained
licenseGPLv2 + LGPL headers (sidecar-safe)BSD-2Apache 2.0ISC

Four survivors on the axes that matter for a Coppice sidecar. haproxy wins on measured overhead and reload time; nginx wins on license cleanliness and binary size; Caddy wins on configuration ergonomics at a memory cost; relayd wins on license and on being the smallest footprint. Rows marked “unverified” are not measured by us for this page and carry that caveat.

Verdict

Keep haproxy. The current pick is the right pick. T3-B’s measurements stand up under a second look, the license concern evaporates once you’re precise about what sidecar deployment means, and no alternative in the set improves on haproxy’s measured numbers meaningfully enough to justify a swap.

The reasoning, stated bluntly:

What a swap would cost, for the record. The current rig (benchmarks/rigs/net/l7-policy-envoy.sh) and the two sidecar configs (tools/envoy/coppice-sidecar.yaml — the Envoy reference, intent-documenting; tools/envoy/coppice- sidecar-haproxy.cfg — the running config) are haproxy-shaped. A swap to relayd means rewriting the .cfg into relayd’s http protocol block syntax (~30 lines), updating the rig to spawn relayd -f instead of haproxy -f, and re-running the latency/reload measurements. Call it a half-day of rig work plus a re-measurement pass. Not free; not a project.

Unverified

See also