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-proxyis 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 throughai-sse-sub2api-relay. APISIX and Sub2API share no network. Sub2API trusts forwarded IP headers only from outgoing relay target172.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
413before reaching Sub2API. - Public final
401and all final404responses become the same zero-byte404; other statuses and successful, SSE, and WebSocket responses remain transparent. - APISIX strips Sub2API's private
X-Client-Request-IDand preserves standardX-Request-IDon 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-adminnetwork. The separate server-rendered/statuspage 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
9083may 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-versionalpine/socatrelay between disjoint*-sourceand*-targetinternal 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 bindaisix-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-modelshas no CPA edge, credential or host port. It reaches only model-catalog and AISIX;ops/scripts/check-apisix-models-no-cpa.pyfails 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-keepershares the keepalive namespace, listens only on127.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 as65534:65534with a read-only root, no capabilities andno-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 orapisix-models. Admin and metrics remain on the private status network. Squid alone joinsproxy-egress; cloudflared alone joins its egress network and reaches APISIX only through its own relay. APISIX has no Internet route. - An optional
provider-sidecarstays 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.
bumpdestinations additionally require exact decrypted Host-to-SNI matching plus method/path ACLs.splicedestinations 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;
latestis 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.