Skip to content
You are viewing the documentation for Solo Enterprise for Istio, formerly known as Gloo Mesh (OSS APIs).

L4 ztunnel-native egress

EnterpriseAlpha
Page as Markdown

Enforce L4 egress policy directly in ztunnel without deploying dedicated proxy pods.

This feature requires your mesh to be installed with the Solo distribution of Istio and an Enterprise-level license for Solo Enterprise for Istio. Contact your account representative to obtain a valid license.

L4 ztunnel-native egress is in the alpha state. Alpha features are likely to change, are not fully tested, and are not supported for production. For more information, see Solo feature maturity.

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-egress GatewayClass directly. Unlike istio-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 the ztunnel-egress Gateway directly, as shown in Step 2.

Step 1: Install an ambient mesh

  1. 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.
  2. Add services to the ambient mesh. This guide uses the Bookinfo sample app as an example.
  3. 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

  1. 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=true
  2. Create the cluster-wide egress Gateway in istio-system.

    • The solo-ztunnel-egress GatewayClass 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: All setting allows ServiceEntries in any namespace to bind to this Gateway. To restrict access to specific namespaces, replace All with a Selector.
    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
  3. Apply a default-deny Istio AuthorizationPolicy that targets the egress Gateway. An ALLOW policy with no rules denies all traffic through the Gateway unless a more specific ALLOW policy 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-egress Gateway control which source namespaces can transit the gateway and what destination ports they can reach. These policies live in istio-system alongside 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.
Because ztunnel enforces at Layer 4, authorization rules can match on source identity (namespace and service account) and destination port. HTTP-level matching, such as on paths, methods, and headers, is not supported. For those use cases, use L7 waypoint egress instead.

Repeat these steps for each external service that your workloads must access.

  1. Create a ServiceEntry for the external destination.

    • The istio.io/use-waypoint and istio.io/use-waypoint-namespace labels 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 the use-waypoint binding 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
    EOF

    Warning

    In the Solo distribution of Istio 1.30, the solo.io/sidecar-skip-waypoint annotation has no effect on ServiceEntry resources with resolution: 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 DNS ServiceEntry does not prevent sidecars from attempting to route through the waypoint. If you require sidecar bypass for a DNS external service, set resolution: STATIC or resolution: NONE and provide explicit endpoint addresses instead. This issue is resolved in 1.31.

  2. Verify that the ServiceEntry is bound to the Gateway.

    kubectl get serviceentry httpbin-org -n bookinfo -o yaml

    Example 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/WaypointBound

    Warning

    In the Solo distribution of Istio 1.30, sidecar-injected clients may get 404 errors after the ServiceEntry becomes WaypointBound. When a sidecar client is already connected to istiod at the time the ServiceEntry’s WaypointBound status 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 confirm WaypointBound status so that Envoy reconnects and pulls the updated configuration. This issue is resolved in 1.31.

  3. Apply a Gateway-layer AuthorizationPolicy to allow traffic from the source namespace through the egress gateway. Without a matching ALLOW policy, the default-deny policy from Step 2 blocks all traffic. These policies live in istio-system and target the ztunnel-egress Gateway.

    Allow all workloads in a namespace on any port: The following policy allows all workloads in the bookinfo namespace to transit the ztunnel-egress gateway on any destination port. Any service account in bookinfo is 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"]
    EOF

    Allow a namespace on a specific destination port only: The following policy allows workloads in the bookinfo namespace to transit the gateway, but only to destination port 443. Attempts from bookinfo to 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"]
    EOF

    Explicitly deny a namespace: The following policy explicitly blocks all workloads in the default namespace from transiting the gateway. A DENY policy takes precedence over any matching ALLOW policy, so this blocks default even if a broader ALLOW policy 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"]
    EOF
  4. Apply 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 bookinfo namespace to reach httpbin.org via the httpbin-org ServiceEntry. The namespaces field matches the source namespace of the calling workload, so any service account within bookinfo is 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"]
    EOF

    Allow a specific service account only: The following policy allows only the bookinfo-productpage service account in the bookinfo namespace to reach httpbin.org. All other identities, including other service accounts in bookinfo, are denied. The principals field 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.

  1. Send a request from the productpage-v1 workload in bookinfo to httpbin.org. The request succeeds with a 200 response.

    kubectl -n bookinfo exec deploy/productpage-v1 -- curl -sk "https://httpbin.org/get" -o /dev/null -w "%{http_code}"
  2. Confirm that traffic from an unauthorized workload is denied. Run a temporary pod in the default namespace, 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}"
  3. Review the ztunnel logs to confirm that ztunnel handled the connection.

    1. Get the ztunnel instance on the same node as your workload.

      kubectl get pods -n istio-system -o wide | grep ztunnel
    2. Check 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:

ModeBehavior
ForwardTraffic is forwarded as-is without modification.
DenyTraffic is blocked.
GatewayTraffic 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: Deny

Example: 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
  - ::/0

Each rule supports the following fields:

FieldRequiredDescription
policyYesOne of Forward, Deny, or Gateway.
namespacesNoMatches the source namespace of the client workload.
matchCidrsNoMatches the destination address. Specify both IPv4 and IPv6 ranges if relevant.
gatewayRequired when policy: GatewayHostname 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: {}
EOF

This 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