Networking
Ingress paths, TLS, gateway listeners and NetworkPolicies, replica count, and egress for serving pods.
The topology is hub and spoke with outbound connections only. Clusters dial out to the plane and the gateway; nothing dials into a GPU cluster, so a cluster needs no inbound firewall rules.
| Connection | Who dials | Where |
|---|---|---|
| Operator to hub | the cluster | wss:// to the hub |
| Agent to gateway | the cluster | the gateway tunnel listener |
Exposure summary
| Path | Who connects | How to expose it |
|---|---|---|
Plane api, :8000 | Browsers, hgr | HTTPS ingress, or keep it private and use kubectl port-forward |
Plane hub, :8001, /v1/hub/sync | Cluster operators | HTTPS ingress with WebSocket upgrades and long idle timeouts |
Gateway tunnel, :8443 | Cluster agents | TCP passthrough, TLS terminated by the gateway |
Gateway internal, :8080 | Your own edge | ClusterIP only, NetworkPolicy, no ingress |
Gateway public, HANGAR_GATEWAY_PUBLIC_LISTEN | Tenants calling directly | HTTPS ingress on a wildcard host. Off unless set |
Metrics, api :8000 /metrics and gateway :9090 | Your Prometheus | Scrape pods on the cluster network. Never expose publicly |
Plane ingress
The api serves /health, /metrics, the API at /v1/* and the console bundle at /.
The hub holds long-lived WebSocket connections. Configure the ingress or load balancer for WebSocket upgrades and an idle timeout well above one minute. For ingress-nginx:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
The hub can run several replicas without sticky sessions. A reconnect that lands on a different pod costs that cluster a full resync instead of a resume.
Gateway listeners
The gateway decides trust by which listener accepted the connection. What each listener trusts is on Security.
Internal listener
:8080, no authentication. A caller here reaches every enabled tenant’s models.
- Expose it as a ClusterIP Service. Never give it an ingress.
- Apply a NetworkPolicy that admits only your edge. Without one, any pod in the cluster can send unauthenticated inference requests to every tenant’s models.
The gateway source ships an example policy that also admits the other listeners and denies the rest. Adjust the selectors to your install:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: hangar-gateway
namespace: hangar
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: hangar-gateway
policyTypes:
- Ingress
ingress:
# Your edge: authenticates and bills its users, then calls the internal listener.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: inference-edge
podSelector:
matchLabels:
app.kubernetes.io/name: inference-edge
ports:
- port: 8080
protocol: TCP
# Public listener, only if HANGAR_GATEWAY_PUBLIC_LISTEN is set. Match the port you chose.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 8081
protocol: TCP
# Tunnel listener: agents arrive from wherever your clusters are. Narrow the source if you can.
- ports:
- port: 8443
protocol: TCP
# Prometheus scrape.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 9090
protocol: TCP
A pod selected by any Ingress policy accepts only what some policy admits, so list every listener you use.
Tunnel listener
:8443. Each agent presents its cluster’s enrolment token, and the gateway verifies it with the plane before carrying traffic.
- Expose it with an L4 load balancer or an ingress TCP route. The tunnel is HTTP/2 after a custom handshake, so an HTTP ingress cannot proxy it.
- The gateway terminates TLS itself. See TLS.
apiVersion: v1
kind: Service
metadata:
name: hangar-gateway-tunnel
spec:
type: LoadBalancer
selector:
app.kubernetes.io/name: hangar-gateway
ports:
- name: tunnel
port: 8443
targetPort: 8443
protocol: TCP
Whatever address agents dial goes into the plane as HANGAR_TUNNEL_GATEWAY (host:port), which the plane writes into each cluster’s setup values.
Public listener
Optional. Off unless HANGAR_GATEWAY_PUBLIC_LISTEN is set to an address. It identifies the tenant from the Host header, so put it behind a wildcard host with one subdomain per tenant and an HTTPS ingress.
It serves only tenants with ingress: public and only endpoints with visibility: public. Whether it requires a key depends on the tenant’s auth setting; see Security.
Example Traefik route. Use the port from your HANGAR_GATEWAY_PUBLIC_LISTEN:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: hangar-public
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^[a-z0-9-]+\.example\.com$`)
kind: Rule
services:
- name: hangar-gateway-public
port: 8081
Gateway replicas
Every gateway replica needs a tunnel from every cluster it routes to. Run the gateway as a StatefulSet and set HANGAR_GATEWAY_REPLICAS to its replica count:
env:
- name: HANGAR_GATEWAY_REPLICAS
value: "3"
An agent learns the count from the handshake and redials until it holds a tunnel to that many distinct gateway ids.
| Setting | Effect |
|---|---|
| Too low | Agents stop dialling before they cover every replica. The missed replicas answer 503 no_capacity for those clusters while reporting healthy. This is the failure to avoid |
| Too high | Agents redial and land on replicas they already hold. Costs some dials, nothing else |
| Unset | Every replica reports one, which is only right for a single replica. The gateway logs a warning at startup |
Change the StatefulSet’s replicas and HANGAR_GATEWAY_REPLICAS in the same rollout. The pods restart, every agent redials and each reads the new count. hangar_gateway_tunnel_coverage shows which replica lacks a tunnel to which cluster. See Metrics.
Serving egress
Egress from the workload namespace is open by default, because the hub, the gateway, the API server and the model registries differ per cluster. Requests can name media by URL (image, video, audio), and the serving pod fetches it from inside the cluster.
Layers that limit this, which you should combine when tenants can send media URLs:
- The gateway refuses a media URL that names a private, link-local (including the cloud metadata address), loopback or single-label host.
HANGAR_GATEWAY_MEDIA_URL_HOSTSreplaces that rule with an explicit host list. This check is by name, so a public name that resolves to a private address gets through. - The chart’s egress NetworkPolicy for serving pods.
- The engine’s own control, for vLLM
allowed-media-domainsin the deployment’s engine arguments.
Enable the egress policy in the cluster chart values:
networkPolicy:
servingEgress:
enabled: true
apiServer:
- 10.0.0.10/32 # required when enabled; see `kubectl get endpoints kubernetes`
to: [] # additional peers
Serving pods may then reach only their own namespace, the release namespace (NATS), DNS on port 53, the API server CIDRs in apiServer, and peers in to. Prefetch and benchmark jobs are not selected, so weight downloads keep their egress.
Check what your models fetch at startup before enabling it. A deployment whose model is a bare Hugging Face repo id, or a multimodal model that fetches public image URLs, needs those hosts in to.
Metrics endpoints
The api serves /metrics on :8000 and the gateway on :9090 (HANGAR_GATEWAY_METRICS_LISTEN). Neither needs a credential. Scrape each pod on the cluster network and refuse /metrics at any public ingress. See Monitoring.
TLS
You need certificates for:
- the plane host (api and hub ingress), terminated by your ingress
- the gateway tunnel host, terminated by the gateway itself
- the public listener’s wildcard host, if you use it, terminated by your ingress
The tunnel listener reads a PEM pair. Without one it serves plaintext and expects TLS to be terminated in front of it.
env:
- name: HANGAR_GATEWAY_TUNNEL_TLS_CERT
value: /etc/tls/tls.crt
- name: HANGAR_GATEWAY_TUNNEL_TLS_KEY
value: /etc/tls/tls.key
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: gateway-tunnel-tls
Issue the certificate with cert-manager or your own PKI. The gateway re-reads the pair every 10 minutes, so renewal does not need a restart.