Wingsmith
Plan

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.

ConnectionWho dialsWhere
Operator to hubthe clusterwss:// to the hub
Agent to gatewaythe clusterthe gateway tunnel listener

Exposure summary

PathWho connectsHow to expose it
Plane api, :8000Browsers, hgrHTTPS ingress, or keep it private and use kubectl port-forward
Plane hub, :8001, /v1/hub/syncCluster operatorsHTTPS ingress with WebSocket upgrades and long idle timeouts
Gateway tunnel, :8443Cluster agentsTCP passthrough, TLS terminated by the gateway
Gateway internal, :8080Your own edgeClusterIP only, NetworkPolicy, no ingress
Gateway public, HANGAR_GATEWAY_PUBLIC_LISTENTenants calling directlyHTTPS ingress on a wildcard host. Off unless set
Metrics, api :8000 /metrics and gateway :9090Your PrometheusScrape 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.

SettingEffect
Too lowAgents 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 highAgents redial and land on replicas they already hold. Costs some dials, nothing else
UnsetEvery 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:

  1. 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_HOSTS replaces that rule with an explicit host list. This check is by name, so a public name that resolves to a private address gets through.
  2. The chart’s egress NetworkPolicy for serving pods.
  3. The engine’s own control, for vLLM allowed-media-domains in 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.