mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
zap_listener.go terminates TLS on a PUBLIC interface. zaphttp_listener.go serves
plaintext ZAP and is the thing the terminator forwards to. They defaulted
independently and both landed on 9999 — which is also what the zap-ingress
LoadBalancer (zap.hanzo.ai) targets, and what k8s/hanzo/deployment.yaml gave
BOTH ZAP_LISTENER_PORT and ZAP_INTERNAL_ADDR. Three spellings of one number
across two files and a manifest, so nothing held the relationship between them,
and the relationship is the entire safety property.
One process, one port, so each boot resolved it by a race:
the terminator binds first — it proxies every accepted connection to itself.
crypto/tls defers the handshake to the first Read, so Accept returns before any
certificate is exchanged: one bare unauthenticated TCP SYN starts an unbounded
accept -> self-dial -> accept loop, two fds and a goroutine a turn, on the pod
that also serves api.hanzo.ai.
ZAP-HTTP binds first — the terminator gets "address already in use", which
InitZapListenerFromEnv only slog.Error()s, and PLAINTEXT ZAP is what answers
the public LoadBalancer. ZAP has no session crypto in any language — not one
crypto/tls import in the zap-proto tree, and zaphttp listens and dials with
bare net.Listen/net.DialTimeout — so that is bearer tokens and prompts in the
clear, whatever the manifest comment says about "TLS 1.3+PQ".
Neither path is fatal. The pod reports Ready in both.
zaplisten.go the topology, decided once: zapPublicPort (9999, the terminator's
and nothing else's) and zapInternalAddr (127.0.0.1:9998), plus isLoopbackHost,
the one predicate that separates public from loopback. Untagged, unlike the
listeners, because zap_listener.go is in both builds and a constant that
exists in one cannot constrain a file that exists in both.
StartZapListener refuses, at startup, both shapes it used to accept: an
InternalAddr on its own port (it would forward to itself) and an InternalAddr
that is not loopback (the inward leg is plaintext, so a routable host puts
the bytes it just decrypted back on the network). A misconfiguration that can
be named at boot should not be discovered one accept at a time.
zapHTTPListenAddr a non-loopback GATEWAY_ZAP_LISTEN override now DISABLES the
plaintext server instead of being honored. Failing closed loses ZAP, which is
recoverable; failing open publishes cleartext, which is not. TestZapHTTPListenAddr
asserted the old permissive contract — ":12345" round-tripping is a wildcard
bind on every interface — and now asserts this one.
the manifest ZAP_INTERNAL_ADDR 127.0.0.1:9999 -> 127.0.0.1:9998. Nothing else
in the fleet dials the gateway's ZAP port: zap.hanzo.ai appears only in this
repo, universe declares no gateway values file, and no in-cluster caller
names gateway:9999. The public port and both Services are unchanged.
Proven, not asserted: with the guards commented out, all three of the new tests
go red with the exact production config; restored, `make test` is green on BOTH
passes it runs (tags=legacy and untagged), 0 failures.
Co-authored-by: Hanzo Dev <[email protected]>
125 lines
4.6 KiB
YAML
125 lines
4.6 KiB
YAML
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: gateway
|
|
namespace: hanzo
|
|
labels:
|
|
app: gateway
|
|
app.kubernetes.io/name: gateway
|
|
app.kubernetes.io/part-of: hanzo-platform
|
|
spec:
|
|
replicas: 2
|
|
selector:
|
|
matchLabels:
|
|
app: gateway
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: gateway
|
|
spec:
|
|
enableServiceLinks: false
|
|
imagePullSecrets:
|
|
- name: registry-credentials
|
|
containers:
|
|
- name: gateway
|
|
image: ghcr.io/hanzoai/gateway:v2.14.10
|
|
imagePullPolicy: Always
|
|
command: ["/usr/bin/gateway"]
|
|
args: ["run", "-c", "/etc/gateway/gateway.json"]
|
|
env:
|
|
- name: GATEWAY_PORT
|
|
value: "8080"
|
|
# -- Unified Auth Middleware --
|
|
- name: AUTH_JWKS_URL
|
|
value: "https://hanzo.id/v1/iam/.well-known/jwks"
|
|
- name: AUTH_ISSUER
|
|
value: "https://hanzo.id"
|
|
- name: AUTH_PUBLIC_PATHS
|
|
value: "/healthz,/__stats,/.well-known/,/favicon"
|
|
- name: AUTH_PUBLIC_HOSTS
|
|
value: "hanzo.id,lux.id,pars.id,id.bootno.de,id.zoo.network,iam.hanzo.ai"
|
|
# Set to "true" to require auth on all non-public endpoints
|
|
- name: AUTH_REQUIRE
|
|
value: "false"
|
|
# -- ZAP native binary listener (TLS 1.3+PQ) --
|
|
#
|
|
# TWO listeners, and they are not peers. ZAP_LISTENER_PORT is the
|
|
# PUBLIC one: the TLS terminator, which is what zap-ingress
|
|
# (zap.hanzo.ai) forwards to and the only ZAP thing on an interface
|
|
# this pod shares with anyone. ZAP_INTERNAL_ADDR is where it forwards
|
|
# AFTER decrypting — the plaintext ZAP server, on LOOPBACK, on a
|
|
# DIFFERENT port.
|
|
#
|
|
# These both said 9999. Same process, same port, so one of two things
|
|
# happened on every boot depending on which bound first: the
|
|
# terminator proxied every accepted connection back into itself
|
|
# (unbounded accept loop off one unauthenticated SYN), or it failed
|
|
# with "address already in use" — logged, never fatal — and PLAINTEXT
|
|
# ZAP served the public LoadBalancer. ZAP has no session crypto in any
|
|
# language, so that second one is bearer tokens and prompts in
|
|
# cleartext. The pod reports Ready either way.
|
|
#
|
|
# The binary now refuses both shapes at startup rather than picking
|
|
# one, so this file and zaplisten.go have to agree; they do.
|
|
- name: ZAP_LISTENER_ENABLED
|
|
value: "true"
|
|
- name: ZAP_LISTENER_PORT
|
|
value: "9999"
|
|
- name: ZAP_TLS_CERT
|
|
value: "/etc/tls/tls.crt"
|
|
- name: ZAP_TLS_KEY
|
|
value: "/etc/tls/tls.key"
|
|
- name: ZAP_INTERNAL_ADDR
|
|
value: "127.0.0.1:9998"
|
|
# -- Billing enforcement via Commerce API --
|
|
- name: AUTH_BILLING_URL
|
|
value: "http://commerce.hanzo.svc.cluster.local:8001"
|
|
# Admin-scoped S2S token the balance gate uses to call Commerce.
|
|
# REQUIRED from KMS (gateway-secrets) whenever BILLING_ENABLED=true:
|
|
# the gateway refuses to start without it (AuthConfig.Validate). It
|
|
# stays optional here only because billing is OFF (BILLING_ENABLED
|
|
# unset); populate it from KMS before flipping billing on.
|
|
- name: COMMERCE_SERVICE_TOKEN
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gateway-secrets
|
|
key: COMMERCE_SERVICE_TOKEN
|
|
optional: true
|
|
ports:
|
|
- containerPort: 8080
|
|
name: http
|
|
- containerPort: 9999
|
|
name: zap
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 128Mi
|
|
limits:
|
|
cpu: "1"
|
|
memory: 512Mi
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /healthz
|
|
port: 8080
|
|
initialDelaySeconds: 10
|
|
periodSeconds: 15
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /healthz
|
|
port: 8080
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /etc/gateway
|
|
- name: tls
|
|
mountPath: /etc/tls
|
|
readOnly: true
|
|
volumes:
|
|
- name: config
|
|
configMap:
|
|
name: gateway-config
|
|
- name: tls
|
|
secret:
|
|
secretName: api-hanzo-ai-tls
|