ztunnel Only Sees Up to L4
In one line
Ambient mode divides the work between a ztunnel, one per node (L4: mTLS, identity, connection-level policy), and a waypoint placed only where needed (L7: HTTP routing, HTTP policy), instead of sidecars. If you write policies without knowing who enforces what, the rules you wrote quietly drop out or traffic that was fine gets cut.
Why this was needed
Sidecar mode attaches one Envoy to every Pod. With a thousand Pods there are a thousand Envoys, each uses memory and CPU, and to change a sidecar you have to recreate the Pod. And what most services actually want is only mTLS and identity-based L4 policy, yet everyone carries a heavy proxy that interprets HTTP.
Ambient separated these two layers. mTLS and L4 are handled by the node's ztunnel, and a waypoint (Envoy) is placed only for services that need HTTP. If you attach a single label to a namespace (istio.io/dataplane-mode=ambient), istio-cni plants interception rules inside the Pod's network namespace and sends the traffic to the ztunnel. You do not need to recreate the Pods.
How it works
ztunnel — L4. A connection leaving a Pod is received by that node's ztunnel, which establishes an HBONE tunnel (mTLS over HTTP CONNECT, port 15008) with the destination node's ztunnel. The ztunnel does not know the request path or method, but it knows the SPIFFE identities of both sides and leaves them in the access log as src.identity and dst.identity. An AuthorizationPolicy attached with a selector is enforced by the destination-side ztunnel when it receives the connection, so a rejection is not an HTTP 403 but a dropped connection.
If you write HTTP rules in a selector policy — istiod sends down the rules the ztunnel cannot enforce (method, path, and so on) with them left out, and notes that fact in the policy status with the ZtunnelAccepted condition. An ALLOW policy allows if even one matches, so if another ALLOW policy already allows that source, what you wrote as 'allow only GET' does nothing.
waypoint — L7. istioctl waypoint apply creates a Gateway API Gateway (gatewayClassName istio-waypoint), and --enroll-namespace attaches the istio.io/use-waypoint label to the namespace so that those services use the waypoint. After that, the route of a request going to a service is like this.
client ─(ztunnel)─ HBONE ─▶ waypoint(Envoy: HTTP 라우팅·HTTP 정책) ─ HBONE ─▶ (ztunnel) web
Two things change here.
| Before the waypoint | After the waypoint | |
|---|---|---|
| The source web's ztunnel sees | client | waypoint |
| Where HTTP rules are enforced | Nowhere | The waypoint (policies attached with targetRefs) |
So an L4 policy that allowed only the client cuts the service traffic the moment you attach a waypoint. And since a waypoint by default handles only requests going to a service (istio.io/waypoint-for: service), a request that comes directly to a Pod IP does not go through the waypoint — a way around the L7 policy. If you allow only the waypoint identity in the L4 policy, both problems are solved together.
The waypoint also does routing. You write in-mesh routing as an HTTPRoute whose parent is a Service (the Gateway API's GAMMA). The place that handles it is the waypoint, so without a waypoint it is not applied.
What it looks like in the field
"I attached a waypoint and the services are all 503." This is the case where the destination's L4 policy originally allowed only the client. tunnel_response:401 is printed in the waypoint log. Introducing a waypoint has to be done together with a policy review.
"I allowed only GET but POST works." You wrote an HTTP rule in a selector policy. The status of kubectl get authorizationpolicy -o yaml is already telling you.
"I put on an L7 policy but some calls pass." This is the case where a client that calls directly by Pod IP (a headless service, a StatefulSet, and so on) did not go through the waypoint.
Official docs: Ambient mode overview · Platform prerequisites — K3s · Configure waypoint proxies · Layer 4 security policy · Layer 7 features
What you will do in the next lab
You put a namespace into the real ztunnel installed with the ambient profile on k3s with a single label, and see it enroll without a restart and see the two identities the ztunnel left. You apply an L4 policy, confirm from the policy status how an HTTP rule drops out without a waypoint, and then attach a waypoint and go through, and fix, a request that was fine turning into a 503. Finally you put an HTTP policy and an HTTPRoute on the waypoint.