Skip to content
roxy

HTTP gateway

An http listener lets roxy stand in for an origin. Clients send ordinary requests to a name that resolves to roxy, and rules decide where each one goes and what to add. The example here is an LLM gateway: clients hold placeholder keys, and roxy adds the real ones on the way out.

An example gateway. Clients send HTTPS requests to a load balancer that terminates TLS and forwards plain HTTP to roxy's http listener on port 8080. Inside roxy the request streams through an auth service addon, which asks an auth service over a WebSocket who the client is and gets back allow, user=alice, or answers 401 if the caller is unknown. Then the rules route by host and inject roxy's own secrets: requests for anthropic.gw.example.com go on over HTTPS to api.anthropic.com with x-api-key added from roxy's secrets, and requests for openai.gw.example.com to api.openai.com with a bearer key added the same way. Responses return by the same path.

The pattern

*.gw.example.com points at a load balancer, which terminates TLS and forwards plain HTTP to roxy. One rule per subdomain redirects the request to its provider over HTTPS and sets the real key. Anything no rule matches is denied.

version: 1
 
listeners:
  - name: gateway
    mode: http
    bind: 0.0.0.0:8080
    forwarded_for: { trusted_hops: 1 }   # one load balancer in front
 
secrets:
  anthropic: { env: ANTHROPIC_API_KEY }
  openai:    { env: OPENAI_API_KEY }
  aws_akid:  { env: AWS_ACCESS_KEY_ID }
  aws_sk:    { env: AWS_SECRET_ACCESS_KEY }
 
rules:
  - id: anthropic
    when: listener.name == "gateway" and host == "anthropic.gw.example.com" and path starts_with "/v1/"
    then:
      - redirect: { host: api.anthropic.com, port: 443, scheme: https, rewrite_host: true }
      - set_header: { x-api-key: "${secret:anthropic}" }
      - allow
 
  - id: openai
    when: listener.name == "gateway" and host == "openai.gw.example.com" and path starts_with "/v1/"
    then:
      - redirect: { host: api.openai.com, port: 443, scheme: https, rewrite_host: true }
      - set_header: { authorization: "Bearer ${secret:openai}" }
      - allow
 
  - id: bedrock
    when: listener.name == "gateway" and host == "bedrock.gw.example.com" and path starts_with "/model/"
    then:
      - redirect: { host: bedrock-runtime.eu-west-2.amazonaws.com, port: 443, scheme: https, rewrite_host: true }
      - sign:
          aws_sigv4:
            service: bedrock
            region: eu-west-2
            access_key_id: "${secret:aws_akid}"
            secret_access_key: "${secret:aws_sk}"
      - allow

rewrite_host: true sends the provider's name in Host, which the provider expects (redirect). Bedrock needs a signature rather than a key. sign signs the request as forwarded, after the redirect (signing AWS requests).

Authenticating clients

The listener does no authentication: rules see only listener.name and client.ip. Authenticate clients at the load balancer, or with an addon that checks a credential and tags the exchange with the user, as the quickstart's auth check does (what just happened). A per-user quota is another addon.

What to know

  • This is not containment. The listener serves whoever reaches it, and clients may have other routes out (threat model).
  • Say how many proxies are in front. Set forwarded_for.trusted_hops to the number of proxies between clients and roxy, so client.ip is the real client rather than the load balancer (HTTP). Only the load balancer may be able to reach the listener: a client that connects directly can choose its own client.ip.
  • Leave the per-client connection cap alone. It counts the load balancer, not the client, so lowering it caps the whole gateway (limits).
  • TLS stays at the load balancer. roxy does not terminate TLS on an http listener, so keep the hop from the load balancer to roxy on a network you trust.
  • In-cluster upstreams have private addresses, which roxy refuses by default. A rule that forwards to one needs then: { allow: { private_ok: true } } (address floor).
  • Do not use quarantine. A quarantine shuts the whole process, so one client's bad requests would lock out every other client.