I use Istio Gateway API to route public hostnames to Kubernetes Services. An external reverse proxy terminates HTTPS and forwards HTTP to a shared Gateway in the cluster:
external TLS reverse proxy -> Istio Gateway Service -> Gateway API HTTPRoute -> Kubernetes Service -> Pod
The examples use the same fictional applications as the rest of the series:
example-api: public API atapi.example.comexample-worker: public worker UI atworker.example.comexample-admin: internal admin app, no public route
Only example-api and example-worker get public HTTPRoute objects here.
Why Gateway API
The older Ingress model is simple, but it becomes awkward when the cluster has service mesh concerns, shared gateways, cross-namespace routing, and route ownership.
Gateway API gives me clearer objects:
GatewayClass: provided by Istio.Gateway: the shared entry point.HTTPRoute: host/path routing.ReferenceGrant: explicit permission for cross-namespace backend routing.
This makes the boundary easier to reason about. Shared infrastructure owns the Gateway. Applications own or receive their own routes.
Gateway service as NodePort
For this setup, I keep a fixed NodePort for the Istio Gateway service. The external reverse proxy can send traffic to that one port.
apiVersion: v1
kind: ConfigMap
metadata:
name: example-istio-gateway-options
namespace: istio-ingress
data:
service: |
spec:
type: NodePort
ports:
- name: status-port
port: 15021
protocol: TCP
targetPort: 15021
- name: http
port: 80
protocol: TCP
targetPort: 80
nodePort: 32080
Then the Gateway references that infrastructure config:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: example-istio-gateway
namespace: istio-ingress
annotations:
networking.istio.io/service-type: NodePort
spec:
gatewayClassName: istio
infrastructure:
parametersRef:
group: ""
kind: ConfigMap
name: example-istio-gateway-options
listeners:
- name: http
hostname: "*.example.com"
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
The public TLS layer stays outside this Gateway in my design. That means the external reverse proxy terminates HTTPS, then forwards HTTP to the cluster Gateway service.
Example route:
client HTTPS -> reverse proxy TLS -> Gateway NodePort -> HTTPRoute -> Service
HTTPRoute to a service
The route object maps a hostname to a backend Service.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-api
namespace: istio-ingress
spec:
parentRefs:
- name: example-istio-gateway
hostnames:
- api.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: example-api
namespace: example-api
port: 3000
Because the route lives in istio-ingress and the Service lives in
example-api, I also need a ReferenceGrant in the backend namespace.
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-istio-ingress-to-example-api
namespace: example-api
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: istio-ingress
to:
- group: ""
kind: Service
name: example-api
The grant authorizes this route namespace to reference the named backend
Service. Route attachment to a Gateway is controlled separately by the
listener’s allowedRoutes; see the Gateway API security model.
Add another hostname
Adding another public service is another HTTPRoute plus ReferenceGrant.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-worker
namespace: istio-ingress
spec:
parentRefs:
- name: example-istio-gateway
hostnames:
- worker.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: example-worker
namespace: example-worker
port: 8000
The worker namespace also grants the route permission to reference its Service:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-istio-ingress-to-example-worker
namespace: example-worker
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: istio-ingress
to:
- group: ""
kind: Service
name: example-worker
This keeps the shared Gateway stable while applications can be added gradually.
I do not add a public route for example-admin in this example set. It can
still run in the cluster and use Vault/External Secrets, but keeping it internal
is the safer default.
How this fits with ambient mode
For services enrolled in Istio ambient mode, the namespace has:
istio.io/dataplane-mode: ambient
If I want L7 processing for service traffic, I add a waypoint:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: example-api-waypoint
namespace: example-api
labels:
istio.io/waypoint-for: service
spec:
gatewayClassName: istio-waypoint
listeners:
- name: mesh
port: 15008
protocol: HBONE
Creating the waypoint does not select its traffic. Label the target Service
with istio.io/use-waypoint: example-api-waypoint, as shown in the
ambient waypoint article.
The ingress Gateway brings traffic into the cluster. A Service’s
istio.io/use-waypoint label selects its waypoint for mesh traffic; ingress
traffic bypasses that waypoint by default. To include ingress traffic, Istio
1.25+ supports istio.io/ingress-use-waypoint: "true" on the destination
Service or Namespace, together with ENABLE_INGRESS_WAYPOINT_ROUTING=true on
istiod. Verify both settings before relying on waypoint policy or telemetry
for public requests. See ingress gateways and waypoints.
External service visibility
For external HTTPS destinations used by pods, I add ServiceEntry resources.
This helps Istio and Kiali classify egress traffic instead of grouping it as
unknown external traffic.
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: example-external-api
namespace: example-api
spec:
hosts:
- api.vendor.example
location: MESH_EXTERNAL
ports:
- number: 443
name: tls
protocol: TLS
resolution: DNS
I use explicit FQDNs. Wildcards are tempting, but explicit hosts are easier to audit and easier to explain when traffic appears in observability tools.
Validate
First check the Gateway and generated service:
kubectl -n istio-ingress get gateway,svc,deploy
Then check routes and grants:
kubectl -n istio-ingress get httproute
kubectl -n example-api get referencegrant
kubectl -n example-worker get referencegrant
Check from outside the cluster through the public domain:
curl https://api.example.com/health
curl https://worker.example.com/
If this fails, I check in layers:
- DNS points to the external reverse proxy.
- The reverse proxy forwards to the Gateway service.
- The Gateway listener accepts the hostname.
- The HTTPRoute is accepted.
- The ReferenceGrant allows the backend.
- The backend Service has ready endpoints.