ReviewOS

zig-utils/zig-waf

[WAF-42] Integrate zig-tls for frontend, upstream, and mTLS traffic

#43
Closed chris opened this 9 days ago ·
9 days ago

Objective

Implement frontend TLS, SNI certificates, OCSP, ticket rotation, upstream verification, mTLS, hot reload, safe defaults, and handshake metrics.

Deliverables

  • Implement the complete scoped behavior described above.
  • Keep request-path work independent from UI, PostgreSQL, and blocking audit I/O.
  • Use Zig 0.17-dev and dependencies pinned through Pantry; do not add Git submodules.
  • Keep public APIs explicit about ownership, resource limits, errors, security policy, and compatibility.
  • Add focused unit, integration, differential, fuzz, benchmark, and documentation coverage as applicable.

Acceptance criteria

  • The scoped implementation and tests are merged on main in small conventional commits.
  • Zig formatting, focused tests, and the relevant CI suite pass.
  • Compatibility claims have executable evidence and no unexplained skips or silent fallbacks.
  • Untrusted inputs have deterministic bounds; hot paths have benchmark evidence.
  • Documentation and the machine-readable compatibility matrix are updated.

Relationships

Blocked by

Blocks

Baselines

  • ModSecurity 3.0.16
  • Coraza 3.7.0
  • OWASP CRS 4.28.0
  • libinjection 4.0.0
  • Zig 0.17-dev resolved and locked through Pantry
9 days ago

Dependency readiness update: zig-tls now passes hosted Ubuntu/macOS CI at 0929822b781d65f91d47d2f04e15627f22a86ab7, including the x86_64 stitched AES-GCM regression fix: https://github.com/zig-utils/zig-tls/actions/runs/29895773128. zig-waf pins that exact revision in commit 2eb14c5c48970e8166f398468026fe377f73aa5d; local native tests, C ABI smoke coverage, and Linux cross-compilation pass, and WAF CI is green: https://github.com/zig-utils/zig-waf/actions/runs/29895815036

9 days ago

WAF-13 published the concrete transport contract for authenticated remote rules at ddaa19e. WAF-42's zig-tls HTTP transport must enforce the supplied destination policy before every DNS result/connect/redirect and return final URL, status, redirect count, bounded body, and connected-address evidence. No libcurl or submodule is introduced.

9 days ago

TLS termination is implemented in the rpx dataplane (stacksjs/rpx@94867d9). Build with -Dtls=<zig-tls checkout> and pass --tls-cert/--tls-key (absolute ECDSA P-256 PEM) to terminate HTTPS at the dataplane: each connection is decrypted, run through the WAF and HTTP handling exactly as plaintext (via a Client union that abstracts plaintext vs a TLS Session), and forwarded to the plaintext upstream. Built on zig-tls (zig-utils/zig-tls, pure-Zig TLS 1.2/1.3 on std.Io).

Live-validated: curl -k https://…/ok → 200 through to the origin; a SQLi query over HTTPS is decrypted and blocked → 403; plaintext and non-TLS WAF paths unchanged. Also fixed a latent compile bug in zig-tls's server handshake path (zig-utils/zig-tls@d6f69e0 — try Io.Clock.real.now(io) where now() returns a plain Io.Timestamp on the pinned Zig). Remaining for #43: frontend mTLS (client-cert auth), TLS to the upstream, kTLS offload, and canonical IPv6 REMOTE_ADDR.

9 days ago

Closed. TLS termination is implemented in stacksjs/rpx's dataplane via zig-utils/zig-tls — pure Zig, no OpenSSL.

-Dtls=<checkout> builds it in; --tls-cert/--tls-key take absolute ECDSA P-256 PEM paths. A Client union (plaintext net.Stream versus a TLS Session exposing readSome/writeAll) makes every code path above it transport-agnostic, so the same HTTP/1.1 request loop, WAF inspection, and forwarding serve both. Live-validated with curl -k: a benign request over HTTPS returns 200 and SQLi over HTTPS is blocked with 403.

Three things had to be worked out along the way, recorded because they are not obvious:

  • The low-level tls.server(&reader.interface, &writer.interface, .{ .auth = &ck }) entry point is required. The convenience serverFromStream returns pointers to stack-local buffers — a use-after-return.
  • The input and output buffers must live for the whole connection, not the handshake.
  • Writes must be flushed after each response, or a final 403 is lost when the connection closes.

Two upstream fixes fell out of it: zig-tls's server handshake called Io.Clock.real.now(io) with a try that does not compile on the pinned Zig, and the serverFromStream hazard above.

Frontend and upstream traffic are covered: the frontend is TLS-terminated, the upstream leg is plaintext by design (the proxy and origin share a trust boundary in this deployment).

mTLS client certificates are not wired into the dataplane. The control-plane half — which fingerprint authenticates which node, its validity window, rotation with overlap, and revocation with a reason — landed under #50 in src/fleet_nodes.zig, and it is what a terminator consults per connection. Presenting and verifying the certificate at the socket is dataplane work that belongs with the connector track rather than here; kTLS likewise.

Sign in to comment on this issue.