Skip to content

Security: xz-dev/AI-gateway

Security

docs/security.md

Security model

Security boundaries of the AI gateway stack: client authority, public filtering, one-way network edges, egress and image/runtime hardening.

Warning

An external firewall or cloud security group is a deployment prerequisite for every intentionally public host port. The tracked defaults bind host-published ports to 127.0.0.1. If that external boundary is unavailable, keep every *_BIND_ADDRESS on loopback or a specific trusted interface and never change it to 0.0.0.0. Container-internal listeners do not make a host port public; Compose ports bindings do.

  • Sub2API is the only client API-key authority. APISIX never validates client keys.
  • APISIX exposes an AI-method/path allowlist. Management, login, health, and unknown routes are not public.
  • ai-sse-keepalive-proxy is AI SSE protocol-specific, not a generic arbitrary-SSE transformer. It recognizes streaming OpenAI Responses, OpenAI Chat Completions, and Anthropic Messages framing. Placement after APISIX lets APISIX remain sole public security boundary while proxy owns parser-visible startup/idle writes during silent upstream periods.
  • APISIX reaches middleware only through apisix-ai-sse-relay; middleware reaches Sub2API only through ai-sse-sub2api-relay. APISIX and Sub2API share no network. Sub2API trusts forwarded IP headers only from outgoing relay target 172.30.26.3/32, preserving APISIX-sanitized forwarding headers through middleware.
  • APISIX keys limits on each resolved real client IP: 10 requests/second with a 40-request burst, at most 50 concurrent requests, and a hard 300 requests per 60-second local window. Excess traffic receives an immediate neutral 429; the long-window limit cannot degrade open and emits no quota headers.
  • Request bodies are capped at 16 MiB and rejected with a neutral 413 before reaching Sub2API.
  • Public final 401 and all final 404 responses become the same zero-byte 404; other statuses and successful, SSE, and WebSocket responses remain transparent.
  • APISIX strips Sub2API's private X-Client-Request-ID and preserves standard X-Request-ID on non-opaque responses.
  • Sub2API accepts forwarded client IPs only through AI SSE middleware's outgoing relay, after APISIX sanitizes them. Its URL allowlist stays disabled because AISIX and CPA use internal HTTP URLs; Docker pairwise networks provide the service-reachability boundary instead.
  • AISIX exclusively owns client-visible logical model IDs, ordered fallback, bounded retries, and per-concrete-target cooldown. CPA retains provider credentials and pool execution; Sub2API retains client authentication, entitlement and quota; it receives only HTTP/SSE because WebSocket ends at the front keeper and stateless bridge.
  • CPA, Sub2API admin access, and APISIX bind to loopback by default. AISIX Admin binds only to the dedicated unpublished aisix-status-admin network. The separate server-rendered /status page binds to loopback by default and may additionally bind to one explicit Tailscale address, never a wildcard.
  • The unauthenticated model diagnostics page on host port 9083 may publish only on loopback or an explicit Tailscale address. Validation checks the effective merged Compose configuration and rejects omitted host addresses, IPv4/IPv6 wildcards, and public IP bindings; failed public-IP probes are not evidence of a private binding.
  • Every directed TCP edge, including all four AISIX edges (sub2api-aisix, catalog-aisix, apisix-models-aisix, aisix-cpa), uses an independent explicit-version alpine/socat relay between disjoint *-source and *-target internal networks. A Docker network itself has no direction; direction comes from who listens where. Every relay therefore binds only its own source-side address (bind=). AISIX proxy binds its three relay-target addresses, AISIX Admin and metrics bind aisix-status-admin, both APISIX data planes bind their relay targets plus loopback with the default TLS listener disabled, and the catalog bridge binds its source address. Other applications, including Sub2API and model-catalog, still have wildcard listeners inside their route-stripped namespaces. The separated networks and relay bindings block the target from initiating a new connection across the relay toward the source application; established TCP connections remain bidirectional.
  • apisix-models has no CPA edge, credential or host port. It reaches only model-catalog and AISIX; ops/scripts/check-apisix-models-no-cpa.py fails validation if any network/relay walk from its namespace reaches CPA.
  • Residual trust: a relay can reach listeners on its target network and any listener its source-side peer exposes on their shared source network. Relays run as 65534, read-only and without capabilities; the CPA host relay uses a fixed shell supervision loop, so absence of a shell is not a blanket guarantee.
  • openai-api-cache-keeper shares the keepalive namespace, listens only on 127.0.0.1:8085, publishes no port, and reaches Sub2API through the stateless loopback WS bridge (127.0.0.1:8086) and existing one-way relay. The bridge has its own bounded disk spool volume, no published port, and the same non-root/read-only capability restrictions. It runs as 65534:65534 with a read-only root, no capabilities and no-new-privileges; only its disposable disk volume is writable. Local cached reads/cancellation revalidate the caller through Sub2API's billing endpoint and fail closed. Operator bypasses public filtering, never API authentication.
  • Every internal network has an explicit bounded membership; no service uses Compose's default network. CPA, Sub2API, APISIX, AISIX, and AI SSE middleware share networking with minimal Alpine namespace owners that delete all default routes and drop privilege. Separate host-ingress namespace owners hold published ports and the source/target sides of socat-relayed edges. This remains portable across rootless Podman and rootful Docker without host firewall changes or engine-specific bridge options.
  • CPA and Sub2API have no direct Internet route. Each reaches Squid only through its own source/target socat relay pair. AISIX has no Internet or Squid edge at all. Its configured application egress reaches CPA through aisix-cpa-relay; its three inbound relay edges do not provide reverse initiation into Sub2API, model-catalog or apisix-models. Admin and metrics remain on the private status network. Squid alone joins proxy-egress; cloudflared alone joins its egress network and reaches APISIX only through its own relay. APISIX has no Internet route.
  • An optional provider-sidecar stays out of the reusable base Compose file. This is a generic role for an explicit-version OpenAI-compatible service whose runtime-specific image, environment, mounts, command, healthcheck, domains, models, and credentials exist only in ignored production state. The generated override supplies reusable TLS, shared-TUN, virtual-DNS, relay, and Squid boundaries without naming or embedding a concrete provider.
  • Egress policy is fail-closed. Every HTTPS tunnel must pass service/source, CONNECT domain, exact ClientHello SNI-to-CONNECT matching, and resolved private/reserved-address denial. Squid resolves only through an in-container Unbound instance that forwards over DNS-over-TLS to Cloudflare and Google (so those resolvers see egress hostnames) and strips unsafe answers before caching, including mixed and rebinding responses. bump destinations additionally require exact decrypted Host-to-SNI matching plus method/path ACLs. splice destinations retain end-to-end TLS fingerprints but cannot expose encrypted Host/path to Squid.
  • TLS inspection uses a locally generated CA. Its private key is mounted only into Squid; CPA, Sub2API, and optional provider-sidecar receive only a public trust bundle. Upstream certificate validation remains enabled.
  • Every image uses an explicit non-floating version tag, optionally with a SHA-256 digest pin; latest is rejected by validation. Every container has PID, memory, CPU, capability, and log-size bounds. Root filesystems are read-only where runtime evidence showed no required overlay writes; APISIX and Sub2API retain writable roots for generated configuration and request handling.
  • Cloudflare Tunnel receives its token from the ignored mode-0600 .env, never from tracked Compose or config files.

Production rollout and bounded live checks: 2026-10-20 one-way edges deployment.

There aren't any published security advisories