A short, working workshop showing AgentGateway acting as an inspecting forward
egress proxy for an Istio ambient mesh. No upstream corporate proxy / Squid /
CONNECT hop — AGW terminates the workload's plain HTTP, applies policies on
the decrypted request, and originates a fresh TLS connection to the real origin.
sequenceDiagram
participant Pod as testpod<br/>(team-a)
participant ZT as ztunnel<br/>(ambient)
participant AGW as AgentGateway<br/>(egress waypoint)
participant GH as api.github.com
Pod->>ZT: curl http://api.github.com/zen
Note over ZT: Wraps in HBONE mTLS
ZT->>AGW: HBONE (carries SPIFFE identity)
Note over AGW: 1. RBAC: SPIFFE namespace + SA check<br/>2. Path allowlist<br/>3. Header inject/strip<br/>4. Audit log w/ caller identity
AGW->>GH: GET /zen HTTP/1.1 (over fresh TLS)
GH-->>Pod: 200 + zen quote
Tested on Solo Enterprise Istio (ambient) + AgentGateway 2.3.3.
- A Kubernetes cluster with Solo Enterprise Istio (ambient) and AgentGateway installed. See the upstream egress workshop for install steps — we reuse the exact same install.
kubectlpointed at the cluster.
kubectl apply -f 01-setup.yamlThis creates:
egress-directnamespace + an AGW waypoint Gateway listening on HBONEteam-aandteam-bnamespaces, each with aclientpod under its own SA
All three namespaces are labeled istio.io/dataplane-mode: ambient, so ztunnel
carries SPIFFE identity through to the waypoint.
kubectl apply -f 02-egress-direct.yamlThree resources do the work:
- ServiceEntry — declares
api.github.comas mesh-external and labels it withistio.io/use-waypoint: egress, which tells ztunnel to route the traffic through our waypoint instead of straight to the internet. - AgentgatewayBackend — points at
api.github.com:443withpolicies.tls: {}— that single empty block enables TLS origination with system trust roots and automatic SNI from the host header. - HTTPRoute — glues the ServiceEntry (the source of traffic) to the AgentgatewayBackend (the destination).
Verify:
kubectl -n team-a exec client -- curl -sS http://api.github.com/zen
# Half measures are as bad as nothing at all.The workload spoke plain HTTP. AGW terminated it, opened a fresh TLS
connection to api.github.com, and returned the body. No certs in the pod, no
HTTP_PROXY env var, no CONNECT tunnel.
kubectl apply -f 03-policy-rbac.yamlThis EnterpriseAgentgatewayPolicy is an Allow rule that matches
source.identity.namespace == "team-a" && source.identity.serviceAccount == "team-a".
source.identity is populated from the ztunnel HBONE peer cert — the
authoritative SPIFFE SVID for the calling pod.
# team-a is allowed
kubectl -n team-a exec client -- curl -sS -w "[%{http_code}]\n" http://api.github.com/zen
# Anything added dilutes everything else. [200]
# team-b is denied
kubectl -n team-b exec client -- curl -sS -w "[%{http_code}]\n" http://api.github.com/zen
# authorization failed [403]kubectl apply -f 04-policy-path.yamlA Deny policy with a hostname guard restricts api.github.com calls to /zen
and /repos/*. Deny beats Allow, so it layers cleanly on the RBAC policy.
kubectl -n team-a exec client -- curl -sS -o /dev/null -w "%{http_code}\n" http://api.github.com/zen # 200
kubectl -n team-a exec client -- curl -sS -o /dev/null -w "%{http_code}\n" http://api.github.com/repos/anthropics/claude-code # 200
kubectl -n team-a exec client -- curl -sS -o /dev/null -w "%{http_code}\n" http://api.github.com/users/torvalds # 403kubectl apply -f 05-policy-headers.yamlSets X-Caller-Identity from the SPIFFE namespace + SA, adds an
X-Forwarded-By audit tag, and strips any inbound X-Internal-Debug header
before the request leaves the cluster. The value: field takes a CEL
expression (not a Mustache template) — use + for string concatenation.
To actually see the injected headers on the wire, point at httpbingo (which echoes them back):
kubectl apply -f 06-verify-headers.yaml
kubectl -n team-a exec client -- \
curl -sS -H "X-Internal-Debug: secret" http://httpbingo.org/headers | jq '.headers'You'll see:
{
"X-Caller-Identity": ["team-a/team-a"],
"X-Forwarded-By": ["agentgateway-egress"]
}…and X-Internal-Debug is gone — stripped before the upstream call.
Everything in traffic: and backend: on the EnterpriseAgentgatewayPolicy
applies the moment the workload sends plain HTTP. The big ones beyond what's
shown above:
traffic.entRateLimit— per-identity rate quotas (uses the built-in rate-limiter sidecar).traffic.extAuth/entExtAuth— punt the request to an external authz server (OPA, custom service) before forwarding.traffic.extProc— Envoy-style external processing for arbitrary request/response mutation, body scanning, DLP, etc.traffic.directResponse— short-circuit (mock, maintenance page).traffic.urlRewrite/requestRedirect— rewrite the upstream path so workloads call a stable internal URL even if the external API moves.backend.auth— inject a realAuthorizationheader (OAuth token, Basic, API key from a Secret) without the workload ever seeing the token.backend.tls.mtlsCertificateRef— present a client cert to the upstream for partner APIs that require mTLS.- Access logs — every request line above (
http.method,http.path,src.addr, etc.) is structured and includes the caller's SPIFFE identity whenEnterpriseAgentgatewayParameters.rawConfig.config.logging.fields.add.caller: sourceis enabled.
The workload has to call plain HTTP. That's fine when you control it
(in-cluster services, your own SDKs), but if you want to inspect outbound
https://... from arbitrary uncooperative workloads, you need a transparent
intercepting forward proxy with a CA distributed to every workload's trust
store — that's Squid/Zscaler/Netskope territory. AGW does the cooperative
case extremely well.
| File | What it does |
|---|---|
01-setup.yaml |
Namespaces, egress waypoint Gateway, team-a/team-b SAs + client pods |
02-egress-direct.yaml |
ServiceEntry + AgentgatewayBackend (TLS origination) + HTTPRoute |
03-policy-rbac.yaml |
SPIFFE identity-based allow rule |
04-policy-path.yaml |
Path allowlist deny rule |
05-policy-headers.yaml |
Header inject/strip with SPIFFE-derived values |
06-verify-headers.yaml |
httpbingo egress route, for echoing back injected headers |