Cluster Ingress Routing
Status: Revised — standard Kubernetes Ingress replaces Traefik IngressRoute CRD; object-based REST routing,
Oracle REST gateway, and Oracle dynamic Ingress management added. Revised 2026-06-03 — Postern is reached via a
/public-api/ path prefix on the shared host, not a dedicated public hostname.
Depends on: platform.axonis-core, platform.service-contract (service contract), component.oracle.gateway (Oracle)
Milestone: P2
Purpose
Defines how Axonis services are accessed within and outside the K8s cluster. Covers:
agentspacevirtual hostname — MCP/chat routing- REST path routing — both legacy (
/userspace/,/dataspace/) and new (/api/v1/) formats - Oracle REST gateway — optional; routes all REST through Oracle for centralized auth, guardrails, metering
- Oracle dynamic Ingress management — Oracle maintains the live routing table via the Kubernetes API
All routing uses standard networking.k8s.io/v1 Ingress resources. No Traefik IngressRoute CRD.
Ingress Controller
Traefik is the configured ingress controller (ingressClassName: traefik). Standard Kubernetes Ingress resources
are used throughout. Where priority ordering is required (agentspace fallback, Oracle REST gateway), the annotation
traefik.ingress.kubernetes.io/router.priority is used. Per Kubernetes Ingress path matching, longer (Prefix)
paths are more specific and take precedence over shorter ones without needing explicit priority.
The agentspace Virtual Hostname
agentspace.cluster.local routes to the best available AI gateway — Oracle when deployed, Cortex as fallback.
Two Ingress resources share the same hostname with differing priority annotations. Traefik activates the
highest-priority rule whose backend is healthy.
| Service | Priority | Target |
|---|---|---|
| Oracle | 100 | oracle:8001 |
| Cortex | 10 | cortex:8002 |
# oracle/charts/oracle/templates/ingress-agentspace.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: { { include "oracle.fullname" . } }-agentspace
annotations:
traefik.ingress.kubernetes.io/router.priority: "100"
spec:
ingressClassName: traefik
rules:
- host: agentspace.cluster.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: { { include "oracle.fullname" . } }
port:
number: { { .Values.service.port } }
# cortex/charts/cortex/templates/ingress-agentspace.yaml
# (identical structure, priority: "10", targets cortex)
When Oracle's deployment is absent, its Ingress does not exist and Cortex's priority-10 rule is the sole active rule.
REST Routing
Path scheme
Every domain object is reachable at two URL formats. No path rewriting occurs at the Ingress layer. Services mount their routes at both prefixes internally (see platform.service-contract).
| Format | Example | Notes |
|---|---|---|
| New | /api/v1/insight/abc123 |
Standard; use for all new integrations |
| Legacy | /userspace/insight/abc123 |
Preserved; existing clients require no changes |
/dataspace/... continues to route to fedai-rest unchanged.
Object ownership
Each service owns a set of domain objects and installs Ingress rules for every path format it handles. Ownership
is declared in the service's values.yaml and in the objects field of its /service-info response (see
platform.service-contract).
| Objects | Owning service | Port |
|---|---|---|
| insight, signal, block, profile, report, task, edition | cortex | 8002 |
| lens, lensbinding, entitymatch, entitycluster, fusionrun | parallax | 8003 |
| costlens, route, isochrone, risksurface, floodextent, actionwindow | prism | 8004 |
| sensor, threshold, alert | sentinel | 8005 |
| datapipeline, datapipelineconnection | conduit (fedai-rest when conduit absent) | 8008 / 8009 |
| all others | fedai-rest | 8009 |
Per-service Ingress rules
Each service's Helm chart generates Ingress path entries from a values.yaml object list:
# cortex/charts/cortex/values.yaml
ingress:
rest:
enabled: true
priority: "100"
objects:
- insight
- signal
- block
- profile
- report
- task
- edition
# cortex/charts/cortex/templates/ingress-rest.yaml
{ { - if .Values.ingress.rest.enabled } }
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: { { include "cortex.fullname" . } }-rest
annotations:
traefik.ingress.kubernetes.io/router.priority: "{{ .Values.ingress.rest.priority }}"
spec:
ingressClassName: traefik
rules:
- http:
paths:
{ { - range .Values.ingress.rest.objects } }
- path: /api/v1/{{ . }}
pathType: Prefix
backend:
service:
name: { { include "cortex.fullname" $ } }
port:
number: { { $.Values.service.port } }
- path: /userspace/{{ . }}
pathType: Prefix
backend:
service:
name: { { include "cortex.fullname" $ } }
port:
number: { { $.Values.service.port } }
{ { - end } }
{ { - end } }
fedai-rest catch-all
fedai-rest installs catch-all rules at priority 10. Any object not claimed by a higher-priority rule falls through to fedai-rest for generic UDS CRUD:
# fedai-rest/charts/fedai-rest/templates/ingress-rest.yaml
metadata:
annotations:
traefik.ingress.kubernetes.io/router.priority: "10"
spec:
rules:
- http:
paths:
- path: /userspace
pathType: Prefix
backend: { service: { name: fedai-rest, port: { number: 8009 } } }
- path: /dataspace
pathType: Prefix
backend: { service: { name: fedai-rest, port: { number: 8009 } } }
- path: /api/v1
pathType: Prefix
backend: { service: { name: fedai-rest, port: { number: 8009 } } }
Priority resolution:
GET /userspace/insight/abc123
→ /userspace/insight (priority 100, cortex) ✓ more specific path wins
GET /userspace/dataset/xyz
→ /userspace (priority 10, fedai-rest) ✓ no priority-100 rule for /userspace/dataset
GET /api/v1/lens/xyz789
→ /api/v1/lens (priority 100, parallax) ✓
GET /dataspace/entity?q=...
→ /dataspace (priority 10, fedai-rest) ✓ always fedai-rest
GET /public-api/dataspace/query
→ /public-api (priority 100, postern) ✓ prefix routes only to Postern; Postern strips it
and forwards /dataspace/query (authenticated, minted token) onward
The Postern Public Path Prefix
Postern (component.postern.proxy) is the platform's unauthenticated public proxy edge. Rather than a
dedicated hostname, it is reached on the same host(s) as every other service, under a
reserved /public-api/ path prefix — the entry point an external integrator (e.g. Hebronsoft)
codes against. The prefix routes exclusively to Postern; Postern strips it and replays the request
onto the normal authenticated routes under a minted limited service token.
| Path prefix | Auth | Backend | Priority |
|---|---|---|---|
/public-api (Prefix) |
none (Postern injects a limited service token) | postern:8012 |
100 |
# postern/charts/postern/templates/ingress-public.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: { { include "postern.fullname" . } }-public
annotations:
traefik.ingress.kubernetes.io/router.priority: "100"
spec:
ingressClassName: traefik
rules:
- http: # host-less: matches the shared platform host(s)
paths:
- path: { { .Values.ingress.public.pathPrefix } } # /public-api
pathType: Prefix
backend:
service:
name: { { include "postern.fullname" . } }
port:
number: { { .Values.service.port } } # 8012
The /public-api prefix is a catch-all to Postern; Postern's own default-deny allowlist (component.postern.proxy
Invariant 1) — not the Ingress — decides what is actually reachable. Per Invariant 2 the Ingress
does no path rewriting: Postern receives the full /public-api/... path and strips the prefix
itself before matching its (backend-path) allowlist and forwarding. Because the forwarded path no
longer carries the prefix, it re-enters at the owning service's normal authenticated rule (e.g.
/dataspace → fedai-rest), never back to Postern. No authenticated object route is installed under
/public-api, and Postern installs no rule outside its prefix.
Oracle REST Gateway (Optional)
When ingress.rest_gateway.enabled: true in Oracle's chart, Oracle intercepts all REST traffic and proxies it
to the correct backend service, applying authentication, guardrails, and metering uniformly. Default:
disabled. When disabled, REST calls route directly to backend services — Oracle is not in the REST call path.
# oracle/charts/oracle/values.yaml
ingress:
agentspace:
enabled: true
priority: "100"
rest_gateway:
enabled: false # set true to route all REST through Oracle
priority: "200" # outranks direct service rules (100) when active
Disabled (default):
GET /api/v1/insight/abc123
→ cortex Ingress (priority 100) — direct, no Oracle hop
Enabled:
GET /api/v1/insight/abc123
→ Oracle Ingress (priority 200) ✓
→ Oracle validates token, checks guardrails, meters call
→ Oracle proxies to cortex:8002 /api/v1/insight/abc123
→ one additional network hop; full middleware stack applied
# oracle/charts/oracle/templates/ingress-rest-gateway.yaml
{ { - if .Values.ingress.rest_gateway.enabled } }
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: { { include "oracle.fullname" . } }-rest-gateway
annotations:
traefik.ingress.kubernetes.io/router.priority: "{{ .Values.ingress.rest_gateway.priority }}"
spec:
ingressClassName: traefik
rules:
- http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: { { include "oracle.fullname" . } }
port:
number: { { .Values.service.port } }
- path: /userspace
pathType: Prefix
backend:
service:
name: { { include "oracle.fullname" . } }
port:
number: { { .Values.service.port } }
- path: /dataspace
pathType: Prefix
backend:
service:
name: { { include "oracle.fullname" . } }
port:
number: { { .Values.service.port } }
{ { - end } }
Oracle's REST proxy routing and guardrails are described in component.oracle.gateway. The /public-api
prefix is not intercepted by the Oracle REST gateway — anonymous public traffic always
terminates at Postern, which mints the token; routing it through Oracle's token-validating gateway
would defeat the unauthenticated edge.
Oracle Dynamic Ingress Management
Oracle maintains a live Ingress routing table via the Kubernetes API using axonis.k8s.IngressManager (see
platform.axonis-core). This supplements the static Ingress resources installed by each service's Helm chart.
Purpose
- Services deployed without a Helm chart (e.g., Conduit in some environments) automatically receive Ingress rules upon registration with Oracle
- When a service deregisters or misses its TTL heartbeat, Oracle removes its managed Ingress rules
- Oracle's internal routing table and the cluster's Ingress state stay in sync without manual intervention
Behaviour
On startup, Oracle discovers services via ORACLE_SERVICES (pull model) and POST /api/v1/register (push
model). For each discovered or registered service:
- Oracle reads the
objectsfield from the service's/service-inforesponse - Oracle creates or updates an Ingress resource for that service's owned objects, at the same priority (100) as static chart-installed rules
- On heartbeat re-registration, Oracle refreshes the Ingress resource TTL
- On TTL expiry or explicit deregistration, Oracle deletes the managed Ingress resource
Static chart-installed Ingress resources are never removed by Oracle. They serve as the floor: present when Oracle is absent, and co-existing (at equal priority) when Oracle is active.
Oracle-managed Ingress resources are named with the prefix oracle-managed- to distinguish them from static
resources:
oracle-managed-cortex-rest
oracle-managed-conduit-rest
...
REST gateway interaction
rest_gateway.enabled: false— Oracle creates Ingress rules that route directly to each service (same priority and target as static chart rules)rest_gateway.enabled: true— Oracle creates Ingress rules that route through Oracle (priority 200), superseding both static and Oracle-managed direct rules
RBAC
Oracle's Helm chart installs a namespaced Role granting Ingress management permissions:
# oracle/charts/oracle/templates/clusterrolebinding.yaml
rules:
- apiGroups: [ "networking.k8s.io" ]
resources: [ "ingresses" ]
verbs: [ "get", "list", "watch", "create", "update", "patch", "delete" ]
Direct Service Hostnames
Services inside the cluster bypass the REST routing layer by calling specific backends directly via their Kubernetes DNS names. Use direct hostnames for service-to-service calls where the target service is known.
| Hostname | Target | Use case |
|---|---|---|
cortex.cluster.local |
Cortex | Direct intelligence service access |
oracle.cluster.local |
Oracle | Direct Oracle access |
parallax.cluster.local |
Parallax | Direct entity resolution |
prism.cluster.local |
Prism | Direct cost lens |
fedai-rest.cluster.local |
fedai-rest | Direct data platform |
sentinel.cluster.local |
Sentinel | Direct alerting |
Use direct hostnames only when a service-to-service call has a known, fixed target. Client applications
(Beacon, external consumers) must use AGENTSPACE_URL and PLATFORM_API_URL.
Client Configuration
# AI gateway (MCP + chat)
AGENTSPACE_URL=http://agentspace.cluster.local # internal K8s — no external hop
AGENTSPACE_URL=https://<domain>/agentspace # external
# REST API
PLATFORM_API_URL=http://<traefik-service> # internal K8s — Traefik ClusterIP service name
PLATFORM_API_URL=https://<domain> # external
# Public unauthenticated edge (Postern) — same host, reserved path prefix
PUBLIC_API_URL=https://<domain>/public-api # external integrators (e.g. Hebronsoft)
Internal clients (e.g., Beacon running inside K8s) set PLATFORM_API_URL to the cluster-internal Traefik
service hostname. Traffic is resolved directly to the ingress controller without leaving the cluster — no
hairpin via the external domain.
Services configure direct upstream targets via individual env vars (for service-to-service calls only):
CORTEX_URL=http://cortex.cluster.local:8002
PARALLAX_URL=http://parallax.cluster.local:8003
ORACLE_URL=http://oracle.cluster.local:8001
FEDAI_REST_URL=http://fedai-rest.cluster.local:8009
Deployment Topologies
Full stack — Oracle deployed, REST gateway disabled (default)
PLATFORM_API_URL → Traefik (ClusterIP)
/api/v1/insight/... → cortex (priority 100, direct)
/api/v1/lens/... → parallax (priority 100, direct)
/userspace/dataset/ → fedai-rest (priority 10, catch-all)
/dataspace/... → fedai-rest (priority 10)
/public-api/... → postern (priority 100) → strips prefix → authenticated route + minted token
AGENTSPACE_URL → agentspace.cluster.local
→ Oracle (priority 100) — MCP tool aggregation + chat
Full stack — Oracle deployed, REST gateway enabled
PLATFORM_API_URL → Traefik
/api/v1/... → Oracle (priority 200) → proxies to correct service
/userspace/... → Oracle (priority 200) → proxies to correct service
/public-api/... → postern (priority 100) — never routed through Oracle (anonymous edge)
AGENTSPACE_URL → agentspace.cluster.local → Oracle (priority 100)
Cortex standalone (no Oracle)
PLATFORM_API_URL → Traefik
/api/v1/insight/... → cortex (priority 100, static chart rule)
/api/v1/... → fedai-rest (priority 10, catch-all)
AGENTSPACE_URL → agentspace.cluster.local → Cortex (priority 10, only active rule)
Local development (no K8s)
PLATFORM_API_URL=http://localhost:8009 # whichever service is relevant
AGENTSPACE_URL=http://localhost:8002 # Cortex running locally
No Ingress or path routing applies. Services are reached at their port directly.
Invariants
- All routing uses standard
networking.k8s.io/v1 Ingress. No Traefik IngressRoute CRD. - No path rewriting at the Ingress layer. Services handle all path formats for their owned objects internally.
(Postern likewise receives the full
/public-api/...path and strips its prefix in-app, not at the Ingress.) - Owned-object Ingress rules install at priority 100. fedai-rest catch-all rules install at priority 10. Oracle REST gateway, when enabled, installs at priority 200.
agentspace.cluster.localalways resolves to exactly one active backend. Oracle at priority 100, Cortex at priority 10. No other service may install an Ingress for this hostname.- Internal clients must use the Traefik ClusterIP service hostname for
PLATFORM_API_URL. Traffic must not leave the cluster to reach backend services (no hairpin via external domain). - Services must not hard-code service hostnames. All upstream URLs are environment-variable-configurable.
- Oracle's dynamic Ingress resources are prefixed
oracle-managed-. Static chart resources are never removed by Oracle. - Oracle REST gateway adds exactly one additional network hop compared to direct routing. When disabled, Oracle is not in the REST call path.
- When
rest_gateway.enabledchanges, only Oracle's chart needs updating. No other service chart changes. - The
/public-api/path prefix is the platform's only unauthenticated Ingress surface; it routes exclusively to Postern (priority 100) and carries no inbound auth at the Ingress layer. No authenticated object route is installed under/public-api, and Postern installs no Ingress rule outside its prefix. Reachability under the prefix is governed by Postern's default-deny allowlist (component.postern.proxy), not by Ingress path rules; Postern strips the prefix and forwards the remainder onto the normal authenticated routes. The/public-apiprefix is never routed through the Oracle REST gateway.
Depends on: component.oracle.gateway, platform.axonis-core, platform.service-contract
Required by: component.beacon.ticketing, component.beacon.workbench, component.oracle.gateway, component.postern.proxy, platform.devops-cicd