Files
homelab-frontend/docs/adr/ADR-0001-retire-kong-for-go-gateway.md
T
Story Crater BotandClaude Opus 5 058f11cf2b
CI / Test (push) Canceled after 0s
CI / Vet (push) Canceled after 0s
CI / Build (push) Canceled after 0s
CI / Security (govulncheck) (push) Canceled after 0s
chore: initial commit of Go API gateway
Baseline for the Kong replacement on api.riotpiao.com. Brings the working
tree under version control for the first time: gateway source, the task
board that drives the agent runs, test fixtures, and K8s manifests.

Anchor the gateway ignore rule to the repo root. Unanchored, "gateway"
also matched the cmd/gateway/ source directory, so the program entrypoint
was excluded from every commit.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-19 20:54:34 -07:00

6.8 KiB

ADR-0001 — Retire Kong OSS in favour of a Go API gateway

Status: Accepted Date: 2026-08-19 Deciders: rock

Context

api.riotpiao.com is currently served by Kong OSS 3.4.1 (Helm, DB-less, namespace api, Argo wave 7), sitting behind ingress-nginx which owns TLS. Kong routes to the KServe model predictors in llm-serving via seven ingressClassName: kong Ingresses and six KongPlugin CRs.

Three separate capabilities were attempted on Kong OSS. All three failed, and each failure is already documented in-repo by the person who hit it:

1. Body-based model dispatch is not expressible. From k8s/apps/api/llm-routes.yaml:

a single /v1/chat/completions endpoint that dispatches on the body's model field is not expressible in Kong OSS (ai-proxy-advanced, which does multi-target model routing, is Enterprise-only).

The workaround is a path-per-model surface (/v1/reasoning/chat/completions, /v1/ornith/..., /v1/qwen/...) with a request-transformer force-overwriting the body's model field. This is not OpenAI-standard, so every client needs bespoke configuration — visible today in ~/.pi/agent/models.json, which carries three separate provider entries for what should be one endpoint.

2. OIDC is Enterprise-only. k8s/apps/api/AUTH-PLAN.md routes around the missing openid-connect plugin using the built-in jwt plugin, which requires pinning Authentik's RSA public key onto a KongConsumer. That plan lists its own consequence:

Pinning rsa_public_key: Authentik key rotation would break it — document a rotation runbook, or have the provision script re-export the cert PEM into the Kong credential on each run.

A rotation runbook is a standing operational liability accepted only because the gateway cannot fetch JWKS itself.

3. key-auth cannot read Authorization: Bearer. From k8s/apps/api/model-auth.yaml:

a raw apikey: <key> header succeeds (200), the same request with only Authorization: Bearer <key> fails (401). No OpenAI-SDK-compatible client (pi included) sends a raw apikey header or lets you customize the header name, so every such client was hard-blocked.

Consequence: authentication on the model routes is currently disabled. Verified live 2026-08-19 — api.riotpiao.com/v1/reasoning/chat/completions answers unauthenticated.

Separately, the intended surface has grown beyond LLM routing. The target is a capability-per-subdomain API over cluster services — sqs.riotpiao.com for queue operations, workflow.riotpiao.com for Temporal, cluster.riotpiao.com for atlas — each needing request shaping, per-caller budgets and streaming semantics that are application concerns, not gateway-plugin concerns.

Decision

Retire Kong OSS entirely. Replace it with a purpose-built Go service, homelab-frontend, which owns north-south routing, authentication, and request shaping for every public capability on *.riotpiao.com.

ingress-nginx keeps the edge and TLS. It forwards to the gateway instead of kong-proxy.

Authentication is Authentik OIDC, validated by fetching JWKS from https://authentik.riotpiao.com at runtime.

Options considered

A. Stay on Kong OSS, accept the workarounds. Keeps a battle-tested proxy and its Prometheus plugin. But the path-per-model surface stays non-standard, the RSA pinning runbook stays, and auth stays off until someone writes a request-transformer shim to copy Bearer into an apikey header. Every new capability (sqs, workflow) inherits the same constraints.

B. Buy Kong Enterprise. ai-proxy-advanced and openid-connect solve 1 and 2. Does not solve the genuinely application-level requirements at all — signed session cookies, per-session daily message budgets, a 6-of-8 GPU sequence-slot semaphore with a bounded queue, and disconnect-cancels-upstream are not gateway features in any tier. Cost for a homelab is not justifiable.

C. Go gateway, Kong retained for LLM paths only. Gradual migration, lower risk. But it means running two gateways indefinitely, splitting the routing table across Kong CRDs and Go code, and keeping the Kong Helm release and its CRDs. The split is the thing most likely to drift.

D. Go gateway, Kong retired entirely. — chosen One routing table, one auth implementation, one place to reason about timeouts. The logic being replaced is small: four request-transformer plugins that set a body field and rewrite a URI, one request-termination serving a static JSON model list, and one prometheus plugin. That is on the order of a hundred lines of Go, against roughly 480 lines of YAML it retires.

Consequences

Gained

  • Standard OpenAI surface. One POST /v1/chat/completions, model selected from the request body. Any OpenAI SDK works unmodified. The three pi provider entries collapse to one.
  • Working authentication. Bearer tokens are read from the header, because it is our code. JWKS is fetched and cached with automatic rotation handling, so the AUTH-PLAN.md rotation runbook is deleted rather than written.
  • Application-level policy becomes possible. GPU slot semaphore, per-session budgets, disconnect propagation and SSE handling live where the state is.
  • One timeout story. Kong currently sets read-timeout: 3600000 (1 hour) on chat routes, which silently defeats any shorter server-side cap. Retiring Kong removes the conflicting layer.
  • ~480 lines of gateway YAML deleted, plus the Kong CRDs, the Helm release, and its ServerSideApply workaround for oversized CRD annotations.

Lost / assumed

  • We now own proxy correctness. Connection pooling, retries, timeout propagation, streaming passthrough, header hygiene, graceful shutdown. net/http/httputil.ReverseProxy covers most of it, but it is our bug surface now.
  • Kong's Prometheus plugin goes away. The gateway must expose equivalent metrics itself (bandwidth, latency, status codes, upstream health) or observability regresses.
  • Migration touches live traffic. pi depends on api.riotpiao.com today. Cutover must be reversible — see docs/MIGRATION-kong.md.
  • agent-pod/console is a kong-class Ingress exposing /console (WebSocket), /run and /sessions. It must migrate too, and it is currently unauthenticated and publicly routed while accepting free-form prompts into a shell-capable container. Putting it behind the gateway's Authentik auth is a security improvement, not just a port.

Risks

  • Enabling Authentik auth will break any client currently relying on the unauthenticated surface — including pi, until its models.json is updated. Auth must ship behind a flag and be enabled deliberately.
  • Kong's request-termination for /v1/models returns a static list that can drift from what the engines actually serve. Porting it verbatim ports the bug; the gateway should derive the list from configured upstreams instead.