Octopus
1.x
Docs/Octopus/Virtual Gateways
Open

Reading5 min
Updated31 Jul 2026
Sourcev1/kubernetes/virtual-gateways.mdx

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, where customer-a.example.com/api serves that customer's API and customer-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.

Note

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: 600

Any 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 cors also answers OPTIONS preflight 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: 3000

Now 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

shared (default)
An in-process partition of the edge Octopus: own route partition, upstream namespacing (isolated load-balancer / circuit-breaker / pool state), and rate-limit buckets. One process, one hop.
dedicated
Rendered to its own Octopus Deployment + Service, reached directly (LoadBalancer / DNS), serving only its own gateway's routes. Strong blast-radius isolation — still one hop.

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.

Warning

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_id dimension — 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.