Skip to content
roxy

How roxy works

One exchange through roxy, top to bottom. The client's request enters roxy and passes through four stages: decrypt and parse (CONNECT, TLS, strict parse into the canonical request, and the lease check), the addon stack (enforce layers in order, with observers beside them on a copy), the rules (lease checked again; deny wins; effects: secrets, headers, redirect, sign) and connect (DNS, address floor, upstream TLS), then reaches the upstream. The response returns up the same stages: re-framed by the connector, re-checked by the rules, seen by the addons, and canonically re-serialised to the client. Each stage has an exit branching to the right: a parse failure is refused with parse_error, an expired policy denies with _expired at either lease check, an addon can answer the client directly or deny with layer:name, the rules deny with the rule id, and the address floor denies with _address_policy. A dotted line between the rules and connect marks where capture records what the rules allow. A flow-log strip down the right edge is fed by every stage and every exit: one record per exchange, never dropped.

An exchange is one request and its response. Every exchange takes the same path, however roxy is deployed.

Decrypt and parse

On a proxy listener the client sends CONNECT, and roxy terminates the TLS inside the tunnel with a certificate from its own CA. On a gateway listener, a load balancer in front has already terminated TLS (HTTP).

roxy parses each request into one canonical form and sends that form upstream. Ambiguous input is rejected rather than guessed at, so what the addons and rules see is exactly what is forwarded. Bodies stay streams.

Addons

An addon is a WASM component that runs inside roxy or an external service the traffic streams through. Addons run in the order they are configured and can rewrite, hold, answer or block the exchange. Whatever an addon passes on is checked again as strictly as a client request (addon model).

Rules

The rules are a list of allows, and denies that restrict them. A matching deny always wins, and anything no rule allows is denied. The decision to forward is made on the request head. Rules that read bodies or the response are checked again as the bytes stream, and can still stop the exchange (policy evaluation).

secrets:
  openai: { env: OPENAI_API_KEY }
 
rules:
  - id: github-reads
    when: host under "github.com" and method in [GET, HEAD]
    then: allow
 
  - id: openai
    when: host == "api.openai.com" and path starts_with "/v1/" and method == POST
    then:
      - set_header: { authorization: "Bearer ${secret:openai}" }   # the client never sees the key
      - allow
 
  - id: upload-cap                         # checked as the body streams
    when: host == "api.openai.com" and body.bytes > 10mb
    then: { deny: { status: 413 } }

Connect

roxy resolves the host itself and checks every address against the address floor before it connects. A name that resolves to a private address is refused unless a rule allows it (upstream, address floor).

Respond

The response comes back the same way: rules that read it are checked, the addons see it, and roxy re-frames it for the client.

Log

Every step writes to the flow log: one JSON line per exchange, naming the rule that decided, the addons that ran and what changed. The log never drops a record. When it falls behind, traffic waits (flow log).

Reloads

The compiled config is swapped in whole when it changes. An exchange finishes under the config it started with (reload). In node mode the config comes from a control plane instead of a file.