Virtual Gateways
A virtual gateway is a gateway-in-a-gateway: a named scope that binds a set of domains, groups the routes and upstreams under them, and pushes inherited policy defaults (auth, CORS, rate-limit, timeout) down to its child routes. It lets you organise one Octopus into clean logical surfaces — for example:
api.example.com— all platform APIs (one federated surface, FARP-bound)app.example.com— all frontend apps*.example.com— per-customer subdomains, wherecustomer-a.example.com/apiserves that customer's API andcustomer-a.example.com/serves its frontend
Virtual gateways are just the OctopusGateway CRD —
no new resource kind. Routes attach to a gateway by host (a route whose host falls within the
gateway's hostnames attaches automatically), so adopting them is additive: with no OctopusGateway
declared, everything behaves exactly as before.
Single-hop invariant. Every tier is one HTTP proxy hop. A shared gateway is an in-process
partition of the edge Octopus; a dedicated gateway is reached directly (its own LoadBalancer /
DNS) — never proxied through the edge. There is no gateway-in-front-of-gateway.
Defining a gateway01
apiVersion: gateway.octopus.io/v1alpha1
kind: OctopusGateway
metadata:
name: platform-api
namespace: octopus-system
spec:
hostnames: ["api.example.com"] # exact host or wildcard (*.example.com)
isolation: shared
defaultPolicy:
authProvider: jwt # inherited by routes that don't set their own
timeoutSeconds: 30
rateLimit: { requests: 1000, windowSeconds: 60 }
cors:
allowedOrigins: ["https://app.example.com"]
allowedMethods: ["GET", "POST"]
allowCredentials: true
maxAge: 600Any HTTPRoute / OctopusRoute whose host is api.example.com now attaches to platform-api and
inherits its defaultPolicy. Exact hostnames win over wildcards, so api.example.com (this gateway)
and *.example.com (a tenant gateway) coexist cleanly.
Policy inheritance
defaultPolicy fills any field a route leaves unset; an explicit value on the route always wins.
Inherited today: authProvider, timeoutSeconds, rateLimit, and cors.
CORS preflight — a virtual gateway's
corsalso answersOPTIONSpreflight for its hostnames even when no specific route matches, so a gateway's whole domain has consistent CORS.Per-gateway rate limits — rate-limit buckets are namespaced by gateway, so two gateways that share a path get independent buckets.
Convention path-split (per-tenant /api vs /)02
A wildcard gateway combined with convention routing
collapses a per-tenant proxy hop into the edge. The convention derives the tenant namespace from the
host; routeRules then split by path prefix:
apiVersion: gateway.octopus.io/v1alpha1
kind: OctopusGateway
metadata: { name: tenants, namespace: octopus-system }
spec:
hostnames: ["*.example.com"]
defaultPolicy:
authProvider: tenant-convention-auth
---
apiVersion: gateway.octopus.io/v1alpha1
kind: OctopusRoute
metadata: { name: tenant-wildcard, namespace: octopus-system }
spec:
path: "/*"
upstream: "__convention__"
convention:
baseDomain: example.com
layout: ["namespace"] # customer-a.example.com → namespace "customer-a"
routeRules:
- pathPrefix: "/api" # /api/* → the tenant API service, /api stripped
stripPrefix: true
serviceOverride: api
portOverride: 7900
- pathPrefix: "/" # everything else → the tenant frontend
serviceOverride: studio
portOverride: 3000Now customer-a.example.com/api/orders goes straight to api.customer-a.svc:7900 (with /api
stripped) and customer-a.example.com/dashboard goes to studio.customer-a.svc:3000 — one hop, no
per-tenant gateway pod.
Convention-aware auth
The tenant-convention-auth provider above validates each tenant's token against an endpoint derived
from the resolved namespace (e.g. http://authsome.{namespace}.svc/v1/introspect), so the edge does
per-tenant token validation without a per-tenant gateway. Configure it as a convention_auth provider.
Isolation tiers03
Promote a gateway with isolation: dedicated. The operator (on the leader replica) renders an
octopus-vgw-<name> Deployment + ConfigMap + LoadBalancer Service; the child runs the same
Octopus binary scoped to that gateway (kubernetes.serveOnlyGateway) and serves its hostnames
directly. The edge stops serving a dedicated gateway's hosts (the child does). Deleting or downgrading
the gateway cleans the child up.
Dedicated rendering requires kubernetes.dedicatedGatewayImage (the Octopus image to run) and RBAC
to create Deployments/Services/ConfigMaps — both are in the Helm chart. When the image is unset,
dedicated gateways fall back to shared-edge behavior.
FARP federation surface04
Set farpBinding: true on a gateway to make it the FARP federation surface: every
discovered service is served under that gateway's hostnames (with service-scoped prefixes) and
inherits its policy — a single source of truth instead of also configuring farp.gateway.
spec:
hostnames: ["api.example.com"]
farpBinding: true
defaultPolicy: { authProvider: jwt }How it works05
One global route trie with a
gateway_iddimension — exact and wildcard gateways coexist and the most specific host wins. No per-gateway router; the lock-free read path is untouched.Routes attach to a gateway by host at reconcile time and inherit its policy when applied.
Upstreams are namespaced
{gateway}:{name}so per-gateway routes get isolated backend state.