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:
- FreeBSD 15.0 pkg or clean source build (no Linux-only compile dances)
- method / path / host / header match, expressed declaratively
- graceful reload that does not drop in-flight connections
- sub-100 µs per-request overhead at loopback
- commits in the last 12 months (not vendored and abandoned)
- license compatible with per-sandbox sidecar bundling
“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:
| candidate | pkg on 15.0 | version | ports license |
|---|---|---|---|
| haproxy | yes | 3.2.15 (also 2.4/2.6/2.8/3.0/3.3) | LGPL2.1 + GPLv2 |
| nginx | yes | 1.28.3 | BSD-2 |
| caddy | yes | 2.11.2_1 | Apache 2.0 |
| traefik | yes | 3.6.13 | MIT |
| relayd | yes | 7.4.2024.01.15_p3 | ISC |
| varnish | yes | 7.7.3 | BSD-2 |
| hitch | yes | 1.8.0_1 | BSD-2 |
| pomerium | stale | 0.8.4_23 (upstream: 0.32.5) | Apache 2.0 |
| envoy | no | — | Apache 2.0 |
| pingora | no | — | Apache 2.0 |
| sozu | no | — | AGPLv3 / 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
| property | haproxy 3.2 | nginx 1.28 | Caddy 2.11 | relayd 7.4 |
|---|---|---|---|---|
| FreeBSD pkg | yes | yes | yes | yes |
| source build on 15.0 | clean | clean | clean (Go) | clean |
| method/path/host/header ACL | native, declarative | native + if/map or njs/Lua | native matchers | native match blocks |
| graceful reload | -sf, 12 ms measured | -s reload, long-proven | admin-API reload, unmeasured here | SIGHUP, unmeasured here |
| per-request overhead (loopback) | ~80 µs measured | similar band, unverified | heavier (Go GC), unverified | unverified |
| binary size (stripped) | ~3 MB | ~1.5 MB core | ~35 MB (Go static) | ~1 MB |
| last upstream commit | 2026-04-22 | 2026-04-14 (1.30.0) | 2026-03-06 (2.11.2) | 2024-01-15 snapshot; ports 2025-09-03 |
| FreeBSD in CI | yes (Cirrus) | documented install target | FreeBSD-specific code paths in tree | ports-maintained |
| license | GPLv2 + LGPL headers (sidecar-safe) | BSD-2 | Apache 2.0 | ISC |
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:
- Envoy is the thing we wanted and can’t have. The 2022-era FreeBSD issues on envoyproxy/envoy are closed-as-dead, not closed-as-fixed. No port attempt has landed; Cilium-on-FreeBSD is not a 2026 option.
- haproxy is the closest functional substitute. Native ACL language expresses the four policy shapes we need directly; graceful reload is 12 ms; per-request overhead is 80 µs; the project commits multiple times a week and treats FreeBSD as a tier-1 CI target.
- nginx is the runner-up. If the license mattered — if we were statically linking the proxy into a closed-source distribution rather than exec’ing it in its own process — nginx’s BSD-2 would win. Since we are exec’ing, it doesn’t. nginx’s weaker native ACL vocabulary is the tiebreaker against it.
- Caddy is the ergonomic pick we’d choose if we were starting
from scratch and binary size didn’t matter. Matchers are
beautiful;
auto_https offis a one-line papercut; 35 MB per sidecar × N sandboxes is real memory we don’t have to spend. - relayd is the fallback. If a CVE forced us off haproxy tomorrow, relayd is where we’d go: ISC license, OpenBSD pedigree, native L7 match/block vocabulary, in-ports-tree. We would lose the measured 80-µs-overhead claim until we re-ran the rig, and we would lose the Lua escape hatch, and we’d accept both.
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
- Caddy’s graceful-reload drop behavior on FreeBSD 15.0. Upstream claims zero-drop; we did not benchmark it for this page.
- nginx per-request overhead at loopback under the same rig as haproxy’s 80 µs. Expected band is similar; not measured.
- relayd’s equivalent latency numbers. Expected to be competitive given the C codebase and minimal feature surface; not measured.
- Pingora’s buildability from source on FreeBSD 15.0. We did not attempt it. The README’s Linux-first stance is the reason for not attempting.
See also
- /appendix/ebpf-to-pf — the architectural placement of the L7 sidecar and the measured 80 µs overhead / 12 ms reload numbers for the haproxy deployment.
- /appendix/ebpf-on-freebsd — the broader context for why a userspace sidecar is the right shape given the absence of a Cilium-style in-kernel L7 redirector.
- /appendix/parity-gaps — the “L7 policy” row in the gaps table, which this page is the why for. Status remains “partial” — the enforcement surface matches Cilium’s, the in-kernel redirection does not.