

Overview
Cloudflint is an edge reverse proxy written from scratch in Rust — the layer that sits in front of origin servers and decides what reaches them. It terminates TLS, filters requests through a WAF with its own expression language, caches responses, rate-limits clients, fails over between origins and challenges suspected bots. It is split into a data plane (cloudflint-edge), a control plane (cloudflint-api) and a Next.js dashboard, so a broken dashboard can never take traffic down.
The project is public on GitHub with CI, and lives at cloudflint.org.
Technologies
- - Rust
- - tokio + hyper + rustls
- - axum + SQLite
- - Next.js + Tailwind CSS
- - Prometheus
- - Docker

Dashboard for a zone: requests, WAF blocks, rate limits, cache and latency from live local traffic

Live logs filtered to blocked: SQLi, XSS, path traversal, scanner and log4shell probes

WAF rules with the built-in tester showing a SQL injection being blocked

Fleet: edge health, config revision rollout, origins and certificates

Year
2026
Type
Systems / Network Security Project
Role
Solo Developer
Highlights
Built the full request path in Rust — TLS with ACME certificates, WAF, cache, rate limiting, compression and origin failover — with no nginx or Envoy underneath.
Designed fleet-wide rate limiting that keeps the control plane out of the request path, plus multi-node fleet management verified live with two edges in two regions.
Backed it with 485 Rust tests and clippy run with warnings as errors, verified live on Windows and Linux.

Data plane
cloudflint-edge
Control plane
cloudflint-api
Technical Implementation
Config without the database in the path: The edge never queries the control plane per request. It pulls a config snapshot, compiles it, and swaps it in atomically. Config is stored as revisions with one-click rollback.
Pipeline of stages: Each request runs through an ordered list of stages that can continue, respond or fail. Responding early is how both a WAF block and a cache hit short-circuit the rest of the path.
WAF: Rules are written in a filter expression language, parsed to an AST at config load and lowered to a tree of closures, with all regexes collected into one RegexSet. Body inspection is capped at 128 KB, with a per-zone choice of failing open or closed. Rules can match on country, continent, ASN, AS organisation, and hosting, VPN and Tor signals.
Cache: Request coalescing, stale-while-revalidate, stale-if-error, and purge by URL, tag or zone. Brotli and gzip run before the cache, so each variant is compressed once rather than on every hit.
TLS and origins: Per-zone certificates chosen by SNI, HTTP/2 via ALPN, hot certificate reload, and ACME issuance and renewal without dropping a connection. Origins can be a pool with passive health from real traffic, optional active probes and automatic failover, over validated HTTPS with optional mTLS.
Bot score and challenge: A 1–99 bot score rather than a yes/no flag, and a stateless proof-of-work challenge whose clearance cookie is signed with HMAC-SHA256 — a forged cookie is simply challenged again.

Tests
485 green
Verified on
Windows + Linux
Design Decisions & Challenges
Fleet-wide rate limiting had to work without asking a shared store on every request, because that would put the control plane back in the request path. Instead, the control plane answers one question on a timer — what share of this key's fleet-wide traffic is yours? — and each edge multiplies its configured limit by that share. Keys are hashed on the edge, so no client identifier reaches the control plane. If the control plane is unreachable, each edge falls back to its own per-node limits: never "no limits" and never "block everything".
Two guards make that safe. A node that stops reporting drops out of the calculation after three missed intervals, so a decommissioned edge cannot shrink everyone's share forever. A minimum share floor stops a node that sees very little traffic from refusing its own few users.
The worst bug was a silent one: the cache's body tee waited for an end-of-stream signal that hyper never sends when it already knows the Content-Length, so nothing was ever stored while every test still passed. It now completes on byte count, and that invariant is guarded.
A live smoke test caught another — the origin's HTTP version leaking through to the client. The proxy now answers each client in the client's own HTTP version, with a test to keep it that way.
