Skip to content

TLS and reverse proxy

The Kneo Agent Platform service speaks plain HTTP. It does not terminate TLS, parse X-Forwarded-* headers itself, or rate-limit by IP. Any deployment that faces a network beyond 127.0.0.1 must run behind a reverse proxy that terminates TLS and shields the service.

For deployment shapes (Container, Compose, Embedded) and the choice of persistence backend, see deployment.md. For the hardening checklist that includes TLS, see security_hardening.md.

Topology

client ──HTTPS──► reverse proxy ──HTTP──► kneo-serv
                  (TLS termination,
                   request size limits,
                   rate limiting,
                   client-IP injection)

The proxy is responsible for:

  • TLS termination and certificate management
  • Request body size limits at the edge (defense in depth above KNEO_SERV_MAX_BODY_BYTES)
  • IP-based rate limiting if you need it (the service has no built-in per-IP limiter)
  • Forwarding the client IP for the service's structured logs

Run the proxy and kneo-serv on the same host (or in the same private network) so the unencrypted hop is not exposed.

Bind address

Topology --host value Rationale
Proxy + service on the same host 127.0.0.1 Service is unreachable except through the proxy.
Proxy + service in a shared private network 0.0.0.0 The network boundary is the proxy; firewall the service port.
Compose (compose.yaml) 0.0.0.0 inside the container; only the proxy's port is published on the host. The Compose stack's internal network already isolates the API service.

The Dockerfile defaults to --host 0.0.0.0 --port 8000. Override with KNEO_SERV_PORT for the published host-side port; the container port is fixed at 8000.

Trusted-proxy headers

The service logs the immediate TCP peer as client_ip in its structured request logs (observability.md). When a proxy fronts the service, the immediate peer is the proxy, not the original client. To capture the real client IP in logs and traces, configure the proxy to write X-Forwarded-For upstream and ingest it at your log aggregator — the service itself does not rewrite client_ip from X-Forwarded-For (no implicit trust).

The service does honor X-Request-ID and echoes it back on the response. Proxies that already inject a request ID should pass it through; the service generates a UUID per request otherwise.

Reverse-proxy snippets

These are minimal examples. Production configurations should add timeouts, buffer sizing, and rate-limit zones; consult your proxy's docs.

nginx

server {
  listen 443 ssl http2;
  server_name kneo.example.com;

  ssl_certificate     /etc/letsencrypt/live/kneo.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/kneo.example.com/privkey.pem;

  client_max_body_size 2m;   # match or exceed KNEO_SERV_MAX_BODY_BYTES

  location / {
    proxy_pass         http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Request-ID      $request_id;
    proxy_read_timeout 130s;             # exceed KNEO_SERV_CLIENT_TIMEOUT
  }
}

Caddy

kneo.example.com {
  reverse_proxy 127.0.0.1:8000 {
    header_up X-Forwarded-For {remote_host}
    header_up X-Request-ID    {http.request.uuid}
  }
  request_body {
    max_size 2MB
  }
}

AWS ALB / generic L7 load balancer

  • Listener: HTTPS 443 with an ACM certificate; redirect 80 → 443.
  • Target group: HTTP, port 8000, healthcheck GET /readyz (interval 30s, unhealthy threshold 3, success codes 200). /readyz is unauthenticated by design.
  • Idle timeout: ≥ KNEO_SERV_CLIENT_TIMEOUT (default 120s); set 130s for a safety margin.
  • Body size: ALBs cap at 1 MiB by default — if you accept larger inline specs (KNEO_SERV_MAX_INLINE_SPEC_BYTES is 256 KiB by default), use a CloudFront or nginx tier in front and bypass the ALB cap accordingly.

Health-check endpoints behind the proxy

Expose /livez and /readyz directly to the proxy or load balancer. Both are unauthenticated to keep probe integration simple. Do not expose /readyz to the public internet — its 503 not_ready payload includes internal check names and registry contents that should stay inside the operational perimeter.

For most setups: bind the proxy's probe routes to internal listeners only, or restrict the source IP range to your load-balancer subnet.

Verifying TLS is actually in front

# TLS terminates at the proxy, service is unreachable directly.
curl -sf https://kneo.example.com/readyz | jq '.metadata.ready'   # → true
curl -sf http://kneo.example.com/readyz                            # → connection refused / 301
curl -sf http://127.0.0.1:8000/readyz                              # → only succeeds from the proxy host

If the third command succeeds from outside the proxy host, the service port is reachable from the public network and the bind address or firewall is misconfigured.

What kneo-serv does not provide

  • No built-in TLS. Terminate at the proxy.
  • No X-Forwarded-For rewriting. Capture client IPs at the proxy or in your log aggregator.
  • No per-IP rate limiting. Use the proxy's rate-limit zone.
  • No mTLS to upstream providers. Provider connections go out from the service host; lock down egress at the network layer.

See security_hardening.md for the full pre-launch checklist.