In Part 1, we discussed the AppNet architecture, setup process, and waypoint model. This second part shifts focus from mechanics to platform patterns: it covers enforcing service-to-service authorization for a payments platform, and then using AppNet across AKS clusters for active-active regional architectures.

Series navigation

Example one: Zero-trust authorization for a payments platform

Zero-Trust Authorization\u200a—\u200aPayments Namespace Zero-Trust Authorization — Payments Namespace

Imagine a team managing a payments platform on AKS. Their main services include checkout, ledger, fraud detection, notifications, and reports. They follow a straightforward rule that their compliance auditor emphasizes: only checkout and fraud services are allowed to call the ledger, and only via POST requests to /v1/transactions. All other calls must be blocked at the network level, not within the application code, and this restriction should be verifiable through tests without running the application.

Before AppNet, this team faced limited options: running a full Istio with sidecars, which they found too resource-intensive for about a hundred pods, or using Kubernetes NetworkPolicy, which allows restrictions based on pod labels and ports but cannot handle HTTP methods, paths, or workload identities from service accounts. Neither approach was ideal.

With AppNet enrolled in the payments namespace and a waypoint deployed, the authoring process uses only the Istio API. First, they set mTLS to STRICT mode to block any plaintext traffic to the ledger.

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: payments-mtls-strict
  namespace: payments
spec:
  mtls:
    mode: STRICT

They then set a default‑deny baseline. Istio 1.25 added support for binding an AuthorizationPolicy to the istio-waypoint GatewayClass from the root namespace (such as istio-system), allowing you to establish a default authorization stance for all waypoints in the cluster. It’s crucial that this policy is an empty ALLOW rather than a blanket DENY: since Istio evaluates DENY policies before ALLOW policies, a DENY rule that covers everything cannot be overridden by a subsequent ALLOW. An empty ALLOW policy maintains the default-deny stance while permitting more specific ALLOW policies to grant access.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: deny-all-waypoints
  namespace: istio-system
spec:
  targetRefs:
  - kind: GatewayClass
    group: gateway.networking.k8s.io
    name: istio-waypoint
  action: ALLOW

Then, they specify the allow rule. The policy targets the ledger service via targetRefs, the ambient-mode method for attaching a policy to a waypoint. It permits traffic only from the two specified service accounts on the designated method and path.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: ledger-allow-checkout-and-fraud
  namespace: payments
spec:
  targetRefs:
  - kind: Service
    group: ""
    name: ledger
  action: ALLOW
  rules:
  - from:
    - source:
        principals:
        - cluster.local/ns/payments/sa/checkout
        - cluster.local/ns/payments/sa/fraud
    to:
    - operation:
        methods: ["POST"]
        paths: ["/v1/transactions"]

Since the principal is an SPIFFE identity derived from the Kubernetes service account and enforced by ztunnel and the waypoint, there is no bearer token to steal, no header to forge, and no way for another pod, such as a debugging sidecar or a misbehaving cron job, to communicate with Ledger. If the auditor requests proof, the team can run a pod with a different service account and verify that the call is rejected at the waypoint. Note that the kubectl run –serviceaccount shortcut was removed in Kubernetes 1.24, so the current way to specify a service account for a one-time test pod is to use --overrides.

# Pre-create the test service account (it lives only for the test)
kubectl -n payments create serviceaccount probe

kubectl -n payments run curl-test \
  --image=curlimages/curl:8.7.1 \
  --rm -it --restart=Never \
  --overrides='{"spec":{"serviceAccountName":"probe"}}' \
  -- curl -sS -o /dev/null -w "%{http_code}\n" \
     http://ledger.payments.svc.cluster.local/v1/transactions \
     -X POST -d '{}'

The expected response is 403, originating from the waypoint rather than the ledger. The use of the plain http:// URL is intentional: the local ztunnel transparently upgrades outbound connections to HBONE-over-mTLS before they leave the node. As a result, the application code never handles TLS, and the data in transit remains unencrypted in the ambient data plane. This encapsulates the zero-trust approach in a single line: workload identity, transport encryption, and policy enforcement are all managed outside the application.

A practical point often overlooked is that without the sidecar, on-call engineers don’t need to match application logs with sidecar logs within the same pod. Since all Layer 7 policy enforcement occurs on the waypoint, a normal Deployment with consistent logs and metrics, identifying issues at 3 am becomes easier. The scope of potential problematic pods is reduced and more straightforward to observe.

Example two: multi‑cluster active‑active for a retailer

Multi-Cluster Active-Active Topology Multi-Cluster Active-Active Topology

A retailer manages two AKS clusters, one in East US 2 and another in Central US, with an Azure Front Door that routes traffic based on latency. They aim for an active‑active setup where the catalog service in either region can communicate with the inventory service in either region, ensuring all traffic is end‑to‑end mTLS‑encrypted regardless of the cluster involved. Additionally, they want the system to handle regional failures transparently. They prefer not to operate their own federated Istio, as their previous attempt two years ago led to a quarter of troubleshooting certificate rotation issues across clusters.

In an Azure regional-pair setup, East US 2 and Central US make a clearer example pair than combining a US primary region with a European secondary region. However, you can select a different region if your needs for latency, compliance, or user location suggest otherwise.

AppNet natively supports multiple clusters, as detailed here. Both clusters are part of a single AppNet resource, and the managed control plane considers them a unified service network within a shared trust domain, with mTLS ensuring end-to-end security across cluster boundaries.

# Attach the East US 2 cluster
az appnet member join \
  --resource-group "$APPNET_RG" \
  --appnet-name "$APPNET_NAME" \
  --member-name "aks-prod-eus2-01" \
  --member-resource-id "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-aks-prod-eus2/providers/Microsoft.ContainerService/managedClusters/aks-prod-eus2-01" \
  --upgrade-mode FullyManaged

# Attach the Central US cluster
az appnet member join \
  --resource-group "$APPNET_RG" \
  --appnet-name "$APPNET_NAME" \
  --member-name "aks-prod-cus-01" \
  --member-resource-id "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-aks-prod-cus/providers/Microsoft.ContainerService/managedClusters/aks-prod-cus-01" \
  --upgrade-mode FullyManaged

Both clusters require network reachability to AppNet’s east–west gateways, involving VNet peering, Azure Virtual WAN, route tables, and NSGs; these are concerns beyond what the service mesh can handle. AppNet does not eliminate the need for proper underlying networking; instead, it adds features like transparent mTLS, identity propagation, and service routing across cluster boundaries.

Setting up multi-cluster calls requires minimal effort from the application team. The key step is making the participating services global. They deploy inventory in both clusters with identical Service names in the same namespace, label the application Service and waypoint Service with istio.io/global=”true”, and the AppNet control plane connects them. This allows a catalog pod request in East US 2 to inventory.catalog.svc.cluster.local to access endpoints across all AppNet clusters, while still using the same in-cluster DNS name.

The team designates the service and waypoint as global by applying the AppNet labels specified in the traffic management guide.

kubectl -n catalog label service inventory \
  istio.io/global="true" --overwrite

kubectl -n catalog label service catalog-waypoint \
  istio.io/global="true" --overwrite

For explicit traffic shifting, the documented AppNet pattern involves creating waypoint-enrolled global Services that represent local and remote backend pools. An Istio VirtualService is then used to allocate traffic between them through weighting. For an active-active production setup, I recommend beginning with local-first routing at the application or service-discovery layer, followed by weighted shifting for controlled failovers, evacuation tests, and regional maintenance windows.

apiVersion: v1
kind: Service
metadata:
  name: inventory-local
  namespace: catalog
  labels:
    app: inventory
    service: inventory-local
    istio.io/global: "true"
    istio.io/use-waypoint: catalog-waypoint
spec:
  ports:
  - name: http
    port: 8080
    targetPort: 8080
  selector:
    app: inventory
    region: local
---
apiVersion: v1
kind: Service
metadata:
  name: inventory-remote
  namespace: catalog
  labels:
    app: inventory
    service: inventory-remote
    istio.io/global: "true"
    istio.io/use-waypoint: catalog-waypoint
spec:
  ports:
  - name: http
    port: 8080
    targetPort: 8080
  selector:
    app: inventory
    region: remote
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: inventory
  namespace: catalog
spec:
  hosts:
  - inventory
  http:
  - route:
    - destination:
        host: inventory-local.catalog.svc.cluster.local
        port:
          number: 8080
      weight: 100
    - destination:
        host: inventory-remote.catalog.svc.cluster.local
        port:
          number: 8080
      weight: 0

Under normal conditions, weights keep traffic localized. In a regional drain or incident, the platform team can redirect some or all traffic to inventory-remote without modifying the calling code. With PeerAuthentication set to STRICT at the namespace level and an AuthorizationPolicy that permits only the catalog service account to access inventory, the topology can shift between regions without requiring certificate rotation. Because the mTLS handshake relies on the same trust domain across both clusters, a catalog call from East US 2 to Central US’s inventory appears, in identity verification, identical to a local call.

The cost discipline enabled by this is significant. Previously, the team operated a dedicated ingress for each cluster to handle inter-cluster calls with mTLS. Now, with AppNet, these ingress points are eliminated, reducing the public surface area. Additionally, east-west traffic costs are now contained within VNet peering, rather than routing through a load balancer and back.

Part 3 concludes the series by discussing the shared AI gateway pattern, observability, security considerations, and identifying when AppNet is suitable or not.

Sources

Validated against the following sources in May 2026.