Skip to content

Instantly share code, notes, and snippets.

@rvennam
Created May 12, 2026 18:57
Show Gist options
  • Select an option

  • Save rvennam/50f30d51372b08a4471e656fd14422ef to your computer and use it in GitHub Desktop.

Select an option

Save rvennam/50f30d51372b08a4471e656fd14422ef to your computer and use it in GitHub Desktop.
AgentGateway as a direct egress forward proxy (Istio ambient): SPIFFE RBAC + path allowlist + header injection. No upstream CONNECT proxy.
apiVersion: v1
kind: Namespace
metadata:
name: egress-direct
labels:
istio.io/dataplane-mode: ambient
---
apiVersion: v1
kind: Namespace
metadata:
name: team-a
labels:
istio.io/dataplane-mode: ambient
---
apiVersion: v1
kind: Namespace
metadata:
name: team-b
labels:
istio.io/dataplane-mode: ambient
---
# Egress waypoint: HBONE listener so ztunnel can tunnel mTLS in and carry
# SPIFFE identity through to AGW.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: egress
namespace: egress-direct
spec:
gatewayClassName: enterprise-agentgateway-waypoint
listeners:
- name: mesh
port: 15008
protocol: HBONE
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: team-a
namespace: team-a
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: team-b
namespace: team-b
---
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: team-a
labels:
app: client
spec:
serviceAccountName: team-a
containers:
- name: curl
image: curlimages/curl:8.10.1
command: ["sleep", "infinity"]
---
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: team-b
labels:
app: client
spec:
serviceAccountName: team-b
containers:
- name: curl
image: curlimages/curl:8.10.1
command: ["sleep", "infinity"]
# Mesh-external destination. The label routes traffic to the egress waypoint.
# Workloads call http://api.github.com (port 80) -> waypoint receives it on
# HBONE -> AGW HTTPRoute matches Host -> AgentgatewayBackend originates TLS.
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: github-api
namespace: egress-direct
labels:
istio.io/use-waypoint: egress
istio.io/use-waypoint-namespace: egress-direct
spec:
hosts:
- api.github.com
location: MESH_EXTERNAL
resolution: DNS
ports:
- number: 80
name: http
protocol: HTTP
---
# AGW-native backend with TLS origination baked in (system CAs, auto SNI).
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
name: github-api
namespace: egress-direct
spec:
static:
host: api.github.com
port: 443
policies:
tls: {}
---
# HTTPRoute parented to the ServiceEntry: anything the waypoint receives for
# api.github.com is matched here and forwarded to the TLS-originating backend.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: github-api
namespace: egress-direct
spec:
parentRefs:
- kind: ServiceEntry
group: networking.istio.io
name: github-api
rules:
- backendRefs:
- name: github-api
kind: AgentgatewayBackend
group: agentgateway.dev
# Authorize based on the SPIFFE identity from the ztunnel HBONE peer cert.
# source.identity exposes namespace, serviceAccount, trustDomain — parsed from
# spiffe://<trust>/ns/<ns>/sa/<sa>.
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: github-api-rbac
namespace: egress-direct
spec:
targetRefs:
- kind: Gateway
group: gateway.networking.k8s.io
name: egress
traffic:
authorization:
action: Allow
policy:
matchExpressions:
- 'source.identity.namespace == "team-a" && source.identity.serviceAccount == "team-a"'
# Path allowlist via Deny (Deny beats Allow). Targets the Gateway, but the
# CEL expression scopes to api.github.com so httpbingo and friends aren't hit.
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: github-api-paths
namespace: egress-direct
spec:
targetRefs:
- kind: Gateway
group: gateway.networking.k8s.io
name: egress
traffic:
authorization:
action: Deny
policy:
matchExpressions:
- |
request.host == "api.github.com" &&
!(request.path == "/zen" || request.path.startsWith("/repos/"))
# Header injection on the upstream request. value is a pure CEL expression
# (not a template) — use string concatenation to build dynamic values.
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: github-api-headers
namespace: egress-direct
spec:
targetRefs:
- kind: Gateway
group: gateway.networking.k8s.io
name: egress
traffic:
transformation:
request:
set:
- name: X-Caller-Identity
value: source.identity.namespace + "/" + source.identity.serviceAccount
add:
- name: X-Forwarded-By
value: '"agentgateway-egress"'
remove:
- X-Internal-Debug
# Optional verification target: httpbingo.org echoes the request headers back,
# so you can see exactly what AGW injected before it left the cluster.
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: httpbingo
namespace: egress-direct
labels:
istio.io/use-waypoint: egress
istio.io/use-waypoint-namespace: egress-direct
spec:
hosts:
- httpbingo.org
location: MESH_EXTERNAL
resolution: DNS
ports:
- number: 80
name: http
protocol: HTTP
---
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
name: httpbingo
namespace: egress-direct
spec:
static:
host: httpbingo.org
port: 443
policies:
tls: {}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: httpbingo
namespace: egress-direct
spec:
parentRefs:
- kind: ServiceEntry
group: networking.istio.io
name: httpbingo
rules:
- backendRefs:
- name: httpbingo
kind: AgentgatewayBackend
group: agentgateway.dev

AgentGateway as a Direct Egress Forward Proxy (HTTP → HTTPS)

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
Loading

Tested on Solo Enterprise Istio (ambient) + AgentGateway 2.3.3.

Prereqs

  • 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.
  • kubectl pointed at the cluster.

1. Lay down the egress waypoint and two team workloads

kubectl apply -f 01-setup.yaml

This creates:

  • egress-direct namespace + an AGW waypoint Gateway listening on HBONE
  • team-a and team-b namespaces, each with a client pod under its own SA

All three namespaces are labeled istio.io/dataplane-mode: ambient, so ztunnel carries SPIFFE identity through to the waypoint.

2. Wire the direct egress to api.github.com

kubectl apply -f 02-egress-direct.yaml

Three resources do the work:

  • ServiceEntry — declares api.github.com as mesh-external and labels it with istio.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:443 with policies.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.

3. Identity-based RBAC (SPIFFE)

kubectl apply -f 03-policy-rbac.yaml

This 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]

4. Path allowlist

kubectl apply -f 04-policy-path.yaml

A 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              # 403

5. Header injection / stripping (with SPIFFE-derived values)

kubectl apply -f 05-policy-headers.yaml

Sets 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.

What else can AGW inspect or enforce here?

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 real Authorization header (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 when EnterpriseAgentgatewayParameters.rawConfig.config.logging.fields.add.caller: source is enabled.

Why this isn't AGW's sweet spot

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.

Files

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment