L4 ztunnel-native egress
Enforce L4 egress policy directly in ztunnel without deploying dedicated proxy pods.
Set up L4 ztunnel-native egress for your ambient mesh. This approach enforces identity-based egress policy directly in the ztunnel DaemonSet, with no dedicated proxy pods deployed. This egress method is well-suited for TCP and TLS services where HTTP-level inspection is not required. For a comparison of egress approaches, see the egress overview.
Considerations
- L4 enforcement only: Authorization rules match on source identity (namespace and service account), and destination port and address. HTTP-level matching, such as paths, methods, and headers, is not supported. For HTTP-level enforcement, use L7 waypoint egress instead.
- No transport-level access restriction: All ambient-captured workloads in the cluster can reach the ztunnel-egress gateway at the transport level. There is no network-level restriction before policy evaluation. Access control depends entirely on AuthorizationPolicy rules that target the Gateway or ServiceEntry.
- Ambient workloads only: Sidecar-injected workloads cannot use ztunnel-native egress. The feature relies on ztunnel’s internal in-process handoff; there is no listener for sidecar proxies to route to. In clusters with both ambient and sidecar workloads, sidecars bypass the ztunnel-egress gateway and continue to route external traffic using registry-based routing.
- No GatewayClass-level policy binding: The AuthorizationPolicy resource does not support targeting the
solo-ztunnel-egressGatewayClass directly. Unlikeistio-waypoint, where a policy targeting the GatewayClass applies to all Gateways of that class, policies for ztunnel-native egress must target the specific Gateway resource. To apply a default-deny policy, target theztunnel-egressGateway directly, as shown in Step 2.
Step 1: Install an ambient mesh
- Use the Solo distribution of Istio version 1.30 or later to set up an ambient mesh using Helm. Note that the ztunnel egress feature requires updated Helm charts and is not yet supported with the Gloo Operator.
- Add services to the ambient mesh. This guide uses the Bookinfo sample app as an example.
- L4 ztunnel-native egress requires an Enterprise level license for Solo Enterprise for Istio, which is validated through istiod. If you do not have one, contact an account representative.
Step 2: Set up the ztunnel-egress gateway
Enable the egress gateway data path in ztunnel. The feature is disabled by default and must be explicitly enabled.
helm upgrade ztunnel oci://us-docker.pkg.dev/soloio-img/istio-helm/ztunnel \ --version 1.30.3-solo \ --namespace istio-system \ --reuse-values \ --set enableEgressGateway=trueCreate the cluster-wide egress Gateway in
istio-system.- The
solo-ztunnel-egressGatewayClass is created automatically by istiod. - The Gateway is backed by the existing ztunnel DaemonSet and does not deploy any new pods.
- The
allowedRoutes.namespaces.from: Allsetting allows ServiceEntries in any namespace to bind to this Gateway. To restrict access to specific namespaces, replaceAllwith aSelector.
kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: ztunnel-egress namespace: istio-system labels: istio.io/waypoint-for: service spec: gatewayClassName: solo-ztunnel-egress listeners: - name: mesh port: 15008 protocol: HBONE allowedRoutes: namespaces: from: All EOF- The
Apply a default-deny Istio AuthorizationPolicy that targets the egress Gateway. An
ALLOWpolicy with no rules denies all traffic through the Gateway unless a more specificALLOWpolicy explicitly permits it.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: default-deny-ztunnel-egress namespace: istio-system spec: targetRefs: - kind: Gateway group: gateway.networking.k8s.io name: ztunnel-egress action: ALLOW rules: [] EOF
Step 3: Configure egress for an external service
Register the external destination and grant access using a two-layer AuthorizationPolicy model. Both layers must permit the source namespace for traffic to reach the external service:
- Gateway layer: Policies targeting the
ztunnel-egressGateway control which source namespaces can transit the gateway and what destination ports they can reach. These policies live inistio-systemalongside the Gateway. - ServiceEntry layer: Policies targeting a ServiceEntry control which source namespaces can access a specific external host. These policies live in the same namespace as the ServiceEntry.
Repeat these steps for each external service that your workloads must access.
Create a ServiceEntry for the external destination.
- The
istio.io/use-waypointandistio.io/use-waypoint-namespacelabels bind the ServiceEntry to the ztunnel-egress Gateway. - Set the port number and protocol to match what the application actually sends to the external service.
- In clusters with sidecar-injected workloads, add the
solo.io/sidecar-skip-waypoint: "true"annotation so that sidecars ignore theuse-waypointbinding on this ServiceEntry and continue using their existing registry-based routing. Ambient workloads still honor the binding and route through the egress gateway.
kubectl apply -f - <<EOF apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: name: httpbin-org namespace: bookinfo labels: istio.io/use-waypoint: ztunnel-egress istio.io/use-waypoint-namespace: istio-system annotations: solo.io/sidecar-skip-waypoint: "true" spec: hosts: - httpbin.org ports: - number: 443 name: https protocol: TLS resolution: DNS location: MESH_EXTERNAL EOFWarning
In the Solo distribution of Istio 1.30, the
solo.io/sidecar-skip-waypointannotation has no effect onServiceEntryresources withresolution: DNS. The annotation is processed by the EDS endpoint builder, which does not run for DNS-type service entries. In a mixed-mode cluster where sidecar-injected workloads coexist with ambient workloads, setting this annotation on a DNSServiceEntrydoes not prevent sidecars from attempting to route through the waypoint. If you require sidecar bypass for a DNS external service, setresolution: STATICorresolution: NONEand provide explicit endpoint addresses instead. This issue is resolved in 1.31.- The
Verify that the ServiceEntry is bound to the Gateway.
kubectl get serviceentry httpbin-org -n bookinfo -o yamlExample output:
... status: addresses: - host: httpbin.org value: 240.240.0.2 conditions: - lastTransitionTime: "2025-05-15T10:00:00Z" message: Successfully attached to waypoint istio-system/ztunnel-egress reason: WaypointAccepted status: "True" type: istio.io/WaypointBoundWarning
In the Solo distribution of Istio 1.30, sidecar-injected clients may get 404 errors after the
ServiceEntrybecomesWaypointBound. When a sidecar client is already connected to istiod at the time theServiceEntry’sWaypointBoundstatus is set, istiod does not push the corrected route to the already-connected proxy. The sidecar keeps a stale config and 404s until its proxy reconnects. Roll the sidecar client Deployments after you confirmWaypointBoundstatus so that Envoy reconnects and pulls the updated configuration. This issue is resolved in 1.31.Apply a Gateway-layer AuthorizationPolicy to allow traffic from the source namespace through the egress gateway. Without a matching
ALLOWpolicy, the default-deny policy from Step 2 blocks all traffic. These policies live inistio-systemand target theztunnel-egressGateway.Allow all workloads in a namespace on any port: The following policy allows all workloads in the
bookinfonamespace to transit theztunnel-egressgateway on any destination port. Any service account inbookinfois permitted; workloads in other namespaces are still blocked by the default-deny policy.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: allow-bookinfo-egress namespace: istio-system spec: targetRefs: - kind: Gateway group: gateway.networking.k8s.io name: ztunnel-egress action: ALLOW rules: - from: - source: namespaces: ["bookinfo"] EOFAllow a namespace on a specific destination port only: The following policy allows workloads in the
bookinfonamespace to transit the gateway, but only to destination port 443. Attempts frombookinfoto reach external services on any other port are denied at the gateway layer.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: allow-bookinfo-https-only namespace: istio-system spec: targetRefs: - kind: Gateway group: gateway.networking.k8s.io name: ztunnel-egress action: ALLOW rules: - from: - source: namespaces: ["bookinfo"] to: - operation: ports: ["443"] EOFExplicitly deny a namespace: The following policy explicitly blocks all workloads in the
defaultnamespace from transiting the gateway. ADENYpolicy takes precedence over any matchingALLOWpolicy, so this blocksdefaulteven if a broaderALLOWpolicy would otherwise permit it.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: deny-default-egress namespace: istio-system spec: targetRefs: - kind: Gateway group: gateway.networking.k8s.io name: ztunnel-egress action: DENY rules: - from: - source: namespaces: ["default"] EOFApply a ServiceEntry-layer AuthorizationPolicy to allow access to the specific external host. A namespace must be permitted at both the gateway layer and the ServiceEntry layer to reach the external service.
Allow all workloads in a namespace: The following policy allows all workloads in the
bookinfonamespace to reachhttpbin.orgvia thehttpbin-orgServiceEntry. Thenamespacesfield matches the source namespace of the calling workload, so any service account withinbookinfois permitted; workloads in other namespaces are denied.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: allow-bookinfo-to-httpbin namespace: bookinfo spec: targetRefs: - kind: ServiceEntry group: networking.istio.io name: httpbin-org action: ALLOW rules: - from: - source: namespaces: ["bookinfo"] EOFAllow a specific service account only: The following policy allows only the
bookinfo-productpageservice account in thebookinfonamespace to reachhttpbin.org. All other identities, including other service accounts inbookinfo, are denied. Theprincipalsfield matches the full SPIFFE identity of the caller, which encodes both namespace and service account, so this policy implicitly prevents workloads in other namespaces from using this ServiceEntry even if those namespaces have no explicit deny policy.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: allow-productpage-to-httpbin namespace: bookinfo spec: targetRefs: - kind: ServiceEntry group: networking.istio.io name: httpbin-org action: ALLOW rules: - from: - source: principals: - "cluster.local/ns/bookinfo/sa/bookinfo-productpage" EOF
Step 4: Verify traffic enforcement
Confirm that traffic is allowed for authorized workloads and denied for all others.
Send a request from the
productpage-v1workload inbookinfotohttpbin.org. The request succeeds with a200response.kubectl -n bookinfo exec deploy/productpage-v1 -- curl -sk "https://httpbin.org/get" -o /dev/null -w "%{http_code}"Confirm that traffic from an unauthorized workload is denied. Run a temporary pod in the
defaultnamespace, which has no AuthorizationPolicy targeting the ServiceEntry. The connection fails.kubectl run curl-test --image=curlimages/curl --rm -it -n default -- curl -sk "https://httpbin.org/get" -o /dev/null -w "%{http_code}"Review the ztunnel logs to confirm that ztunnel handled the connection.
Get the ztunnel instance on the same node as your workload.
kubectl get pods -n istio-system -o wide | grep ztunnelCheck the logs for a connection entry showing the source SPIFFE identity and destination service. The presence of the source SPIFFE identity confirms that ztunnel intercepted and enforced policy on the connection.
kubectl logs -n istio-system <ztunnel-pod>Example output:
2025-05-15T10:01:23.456Z info access connection complete src.addr=10.0.2.5:54321 src.workload="productpage-v1-abc123" src.namespace="bookinfo" src.identity="spiffe://cluster.local/ns/bookinfo/sa/bookinfo-productpage" dst.addr=93.184.216.34:443 dst.service="httpbin.org" dst.cluster="cluster1" direction="outbound" bytes_sent=128 bytes_recv=512 duration="45ms"
Optional: Harden with ztunnel egress policies
Egress policies configure how ztunnel handles outbound traffic that does not match any known destination in the service registry (populated from both Kubernetes Services and Istio ServiceEntries). They complement the Gateway and ServiceEntry controls in this guide: while the Gateway and ServiceEntry handle registered destinations, egress policies handle everything else.
Configure egressPolicies in your ztunnel Helm values. Because the policies are passed to ztunnel as an environment variable (EGRESS_CONFIGS), helm upgrade automatically triggers a rolling restart of the ztunnel DaemonSet. No manual restart is required.
The following modes are available:
| Mode | Behavior |
|---|---|
Forward | Traffic is forwarded as-is without modification. |
Deny | Traffic is blocked. |
Gateway | Traffic is forwarded to the specified gateway over HBONE. |
Rules are evaluated in order. The first matching rule applies. If no rule matches, traffic is forwarded as-is.
Example: Deny all unregistered outbound traffic.
egressPolicies:
- matchCidrs:
- 0.0.0.0/0
- ::/0
policy: DenyExample: Exempt the egress gateway’s own namespace to avoid routing loops, then route all other unregistered traffic through the gateway.
egressPolicies:
# Exempt the egress gateway namespace to avoid routing loops
- namespaces: [istio-system]
policy: Forward
# Route all other unregistered external traffic through the egress gateway
- gateway: ztunnel-egress.istio-system.svc.cluster.local
policy: Gateway
matchCidrs:
- 0.0.0.0/0
- ::/0Each rule supports the following fields:
| Field | Required | Description |
|---|---|---|
policy | Yes | One of Forward, Deny, or Gateway. |
namespaces | No | Matches the source namespace of the client workload. |
matchCidrs | No | Matches the destination address. Specify both IPv4 and IPv6 ranges if relevant. |
gateway | Required when policy: Gateway | Hostname of the gateway. Traffic is tunneled over HBONE, so the gateway must be enrolled in the mesh. |
Warning
Egress policies cannot strictly guarantee enforcement. A workload that explicitly opts out of ambient capture bypasses ztunnel and thus bypasses egress policies. Use egress policies as a defense-in-depth measure alongside Kubernetes NetworkPolicy.
Optional: Prevent mesh bypass with network policy
Because traffic exits from the ztunnel pod’s network namespace, you can use a Kubernetes NetworkPolicy to block application pods from reaching external IPs directly. For example, you can apply the following policy to each namespace where you want to ensure all external traffic routes through ztunnel.
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: workload-egress
namespace: bookinfo
spec:
podSelector:
matchExpressions:
- key: gateway.networking.k8s.io/gateway-name
operator: DoesNotExist
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
- to:
- podSelector: {}
EOFThis policy allows workload pods to resolve DNS and reach other pods in the cluster. Direct external IP access from application pods is blocked at the CNI level. The ztunnel pod is not selected by this policy, so it can continue to forward egress traffic on behalf of workloads.
Cleanup
You can optionally remove the resources that you created in this guide.
kubectl delete authorizationpolicy allow-bookinfo-to-httpbin -n bookinfo
kubectl delete authorizationpolicy allow-bookinfo-egress -n istio-system
kubectl delete serviceentry httpbin-org -n bookinfo
kubectl delete authorizationpolicy default-deny-ztunnel-egress -n istio-system
kubectl delete gateway ztunnel-egress -n istio-system