AI agents need outbound access to models, tools, registries, and APIs. An unrestricted sandbox will use every channel it can find. The durable pattern is a default-deny egress proxy the workload cannot route around, authorizing destinations at CONNECT time rather than trusting a field the agent controls.
This page is the site’s graded agentgateway proof. Open-source agentgateway v1.6.0 can act as an HTTP CONNECT egress proxy in standalone mode: hostname allowlist, default deny, config, curls, and logs. Kubernetes ambient egress is a different documented recipe, not the same YAML. This is not the four-bypass attack lab, not the DNS covert-channel write-up, and not a four-control control-plane overview.
Disclosure: I work at Solo.io, which created agentgateway and sells Solo Enterprise for agentgateway. Everything below is open-source agentgateway unless it is labelled Solo Enterprise, and every claim links to the project’s own docs or release notes as of 2026-10-05.
What an AI agent egress proxy is
An AI agent egress proxy is a forward proxy on the only outbound path an agent can use. It defaults to deny. It allowlists destinations the agent may reach, usually by hostname at CONNECT or TLS SNI time. It logs the decision. Environment-variable proxy settings alone are not enough, because the agent can unset them. The network must leave no other route.
That is a destination control, not a request-body control. The proxy authorizes where the agent may go, not what it sends once the tunnel is open.
The shape is one hop:
agent
| no other outbound route
v
agentgateway (HTTP CONNECT)
| hostname allowlist, default deny
+--> allowed hosts only
x everything else
Public write-ups already name the problem. PipeLab defines an AI egress proxy as a control on what agents send outbound. SimEdw treats an environment-variable proxy as bypassable. guptadeepak states the rule as default-deny at a proxy the agent cannot route around. Those are public definitions of the problem. This page is the agentgateway recipe.
Why env-var proxies fail (and what this page proves instead)
HTTPS_PROXY is configuration the workload can change. Unset it, rewrite /etc/hosts, put a write in a GET, or point an exempt name at a blocked address, and an in-band proxy never sees the request.
I already walked four of those techniques in AI Agent Egress Control: 4 Bypasses, 1 Fix
. That lab’s gateway is a Python CONNECT stand-in (themsquared/agent-egress-control), not agentgateway. It shows why the decision has to happen at CONNECT time, on a path the agent cannot leave. It is not agentgateway evidence. This page is the other half: shipped standalone v1.6.0, allowlisted hosts connect, unmatched hosts do not, and the log records both.
HTTP egress is also not DNS egress. A CONNECT proxy never sees a query that never becomes HTTP. That channel is Your Agent Egress Proxy Never Saw the DNS Query .
agentgateway standalone CONNECT egress (v1.6.0)
Standalone docs say agentgateway can act as an HTTP CONNECT proxy
for outbound traffic (S, Documented). Clients set HTTPS_PROXY and send CONNECT :443. What happens next depends on the CONNECT handling mode.
Tunnel mode
Set tunnelProtocol: connect on a bind. Destinations match TLS SNI through tcpRoutes hostnames. Traffic can stay encrypted end to end. HTTP policies do not apply to the CONNECT request, and client authentication is not available on that path. A blocked destination fails with a TLS error after the tunnel is established. Tunnel mode can also terminate TLS for selected hostnames when you add HTTPS listeners.
The official IMPORTANT note is the failure mode you must expect:
The CONNECT tunnel is established before the destination is checked, so the client sees a TLS error rather than a refusal from the proxy. To refuse a blocked destination at the proxy instead, use route mode, described in Require client authentication.
The docs-literal allowlist uses tcpRoutes[].hostnames (for example pypi.org and files.pythonhosted.org) with backends: [{dynamic: {}}]. An unmatched hostname has no route. The example deny to example.com is a TLS error, not an HTTP 404.
Route mode
Set frontendPolicies.connect.mode: route. Destinations match the CONNECT authority through route hostnames. HTTP policies apply. basicAuth is available. A blocked destination returns 404 to CONNECT. Route mode does not terminate TLS in this pattern. The docs show 407 Proxy Authentication Required without credentials, and 404 Not Found on CONNECT for a non-allowlisted host even with valid proxy credentials. curl hides those proxy statuses unless you pass --verbose.
Mode comparison
| Area | Tunnel mode (S) | Route mode (S) |
|---|---|---|
| Configuration | tunnelProtocol: connect on a bind | frontendPolicies.connect.mode: route |
| Match field | TLS SNI via tcpRoutes hostnames | CONNECT authority via route hostnames |
| Client auth | Not available | basicAuth available |
| Block signal | TLS error after the tunnel opens | 404 on CONNECT |
| TLS terminate option | Yes, for selected hostnames | No, in this pattern |
Neither mode replaces the other. Tunnel mode gives SNI-based control and optional TLS terminate. Route mode gives HTTP policies, including client authentication, and a clean 404 at the proxy. Marketing’s proof below is tunnel mode. Route-mode basicAuth (407 then 404) was not run.
What shipped when (v1.5.0 to v1.6.0)
Grade the timeline, not a slogan.
In released v1.5.0, substrateEgress only adds actor metadata and enforces nothing. The v1.5.0 source at egress.rs still carried // TODO: implement egress policy in Substrate (Substrate, source on tag v1.5.0
).
Actor-identity authorization at CONNECT (PR #3237 , merged 2026-09-01) and hostname or CIDR destination rules with deny-when-nothing-matches (PR #3318 , merged 2026-09-09) first appeared in the v1.6.0 prereleases. They shipped in stable v1.6.0 on 2026-10-02. The v1.6.0 release notes list #3237, #3318, and #3677 (“Fix Substrate egress after the ateom/actor identity split”). #3237 authorizes actor identities before accepting HTTP, TLS, or opaque TCP CONNECT, rejects unknown, stale, or non-running actors, and fails closed when the control plane is unavailable.
That is Substrate code in the same release as the standalone binary Marketing ran. It is not a claim that this page tested Actor egress on Substrate or kagent.
The version table
dates agentgateway 1.6.x as 02 Oct 2026, with Kubernetes 1.32 to 1.37, Gateway API 1.4 to 1.6, and MCP 2026-07-28 (S+K, Documented). GitHub published v1.6.0
on 2026-10-02 (prerelease: false), with linux-amd64 and darwin-arm64 binaries and cr.agentgateway.dev/agentgateway:v1.6.0 (S+K, release notes).
Standalone CONNECT hostname allowlisting is the S recipe this proof runs. It is not the Kubernetes ambient waypoint, and it is not the Substrate Actor policy.
Proof: allowlist and default deny
Marketing ran the open-source standalone binary on 2026-10-05. I am publishing their artifacts under /lab/agentgateway-egress-proxy/
. Scope: OSS standalone v1.6.0 only. Not claimed: Substrate or kagent runtime. No Docker.
Mode: tunnel CONNECT (binds[].tunnelProtocol: connect). Allowlist: example.com and httpbin.org. Unmatched host: no tcpRoute, so after CONNECT 200 the client sees a TLS error (unexpected eof while reading), not HTTP 404. 404 is route mode. Quote the IMPORTANT note above before you treat that TLS error as a surprise.
Version (P1, Tested)
The binary reported version: 1.6.0 and git_revision: ea5608642b9d5c94c5baf5fd9f2f9849b808e963 (version.json
). Asset: agentgateway-linux-amd64 from the v1.6.0 release. Checksum verified: 496ed27e6a96b55a9ad7cabe5c2be7172511f44a3cb113e533d22bf93a7f079d. Test date: 2026-10-05 09:11:12 PDT.
Bind-443 caveat (document this honestly)
The docs-literal pattern gateways.secure.port: 443 opens an OS listener. On an unprivileged box it failed:
Error: failed to start bind listener(s): bind bind/443: bind listener for [::]:443: Permission denied (os error 13)
That is evidence/bind443-error.txt
.
The working pattern is on the same docs page, under “Inspect all HTTPS traffic”: a binds entry for port 443 with mode: internal. That is routing-only. It does not open an OS socket on privileged 443. CONNECT *:443 re-enters the SNI / tcpRoutes allowlist. Semantics stay tunnel allowlist plus default deny.
Working config (P2, Tested)
Committed at egress.yaml
:
binds:
- port: 3000
tunnelProtocol: connect
listeners: []
- port: 443
mode: internal
listeners:
- name: public-egress
hostname: "*"
protocol: TLS
tcpRoutes:
- name: public-egress-allowlist
hostnames:
- example.com
- httpbin.org
backends:
- dynamic: {}
Proxy: http://127.0.0.1:3000.
Allow (P3, Tested)
T1 https://example.com/ via CONNECT: destination HTTP/2 200, curl exit 0 (t1-allow-example.com.txt
). The first hop in that capture is the proxy CONNECT HTTP/1.1 200 OK; the second is the origin HTTP/2 200.
T1c https://httpbin.org/: the gateway log records route=default/public-egress-allowlist endpoint=httpbin.org:443 tls.sni=httpbin.org. The harness notes record HTTP/2 200 and curl exit 0. The T1c curl file was not in the artifact pack; the log line and harness notes are the evidence.
Deny (P4, Tested)
T2 https://never-allowlisted-egress-test.example/: CONNECT HTTP/1.1 200 OK, then curl: (35) TLS connect error: error:0A000126:SSL routines::unexpected eof while reading (t2-deny-never-allowlisted.txt
).
T3 https://pypi.org/ (not on this harness allowlist): the same CONNECT 200, then curl exit 35 unexpected EOF (t3-deny-pypi.org.txt
). The official tunnel-mode example allowlists pypi.org. This harness does not, so a docs-example host is denied when it is absent from tcpRoutes.
Gateway log, same run (agentgateway.log
):
info request ... listener=public-egress route=default/public-egress-allowlist endpoint=example.com:443 tls.sni=example.com
error request ... listener=public-egress tls.sni=never-allowlisted-egress-test.example error="route not found"
error request ... listener=public-egress tls.sni=pypi.org error="route not found"
info request ... listener=public-egress route=default/public-egress-allowlist endpoint=httpbin.org:443 tls.sni=httpbin.org
Allow is a route hit. Deny is route not found after the CONNECT tunnel is already established, matching the official IMPORTANT note. A proxy refusal instead of a TLS error is route mode. Marketing did not run that optional case (P7).
Grade table
| ID | Case | Mode | Grade | Evidence |
|---|---|---|---|---|
| P1 | Binary 1.6.0, git ea560864..., checksum verified | S | Tested | version.json |
| P2 | Tunnel allowlist + mode: internal on 443 | S | Tested | egress.yaml |
| T1 / T1c | ALLOW example.com, httpbin.org → HTTP/2 200 | S | Tested | curl T1; log + harness notes T1c |
| T2 / T3 | DENY unmatched host and pypi.org → CONNECT 200, then TLS EOF | S | Tested | curl + route not found |
| Bind 443 | Docs-literal gateways.secure.port: 443 | S | Tested (fails unprivileged) | Permission denied (os error 13) |
| P7 | Route-mode basicAuth, 407 then 404 | S | Not run | Optional |
| P8 | Kubernetes ambient egress | K | Documented only | Official ambient-egress guide |
| Actor egress | #3237 / #3318 / kagent policy | Substrate | Shipped in v1.6.0; not Tested here | Release notes + kagent docs |
Standalone CONNECT allowlist plus default deny is Tested. That cell is not Kubernetes ambient egress, and it is not a Substrate Actor.
Kubernetes: what the docs cover
Do not paste the standalone YAML into a cluster and call it the same control. Mark the recipe.
Ambient egress waypoint (K, Documented)
The ambient egress guide
is a different path. An Istio ambient client sends to an external host. ztunnel forwards over HBONE to an agentgateway-managed waypoint (:15008 plus an internal HTTP listener). You model the destination with a MESH_EXTERNAL ServiceEntry, an AgentgatewayBackend, and an HTTPRoute. The demo destination is httpbin.org, and the route adds x-agw-waypoint: true. An optional NetworkPolicy limits client egress to DNS, istio-system, and the waypoint on :15008.
The client does not set HTTPS_PROXY and does not send CONNECT to agentgateway. Istio steers matching egress through the waypoint. That is not the standalone tcpRoutes hostname allowlist. I am not claiming the same match field, the same block signal, or a one-click twin of egress.yaml. Marketing did not run this path (P8). Documented is the grade.
Two nearby Kubernetes pages are easy to conflate and are not this control. dynamicForwardProxy
can send to arbitrary destinations from the request host or TLS SNI unless you add access controls (K, Documented). The backend tunnel
policy is the gateway CONNECTing through an upstream HTTP proxy for JWKS or provider access: gateway-as-client, not agent to agentgateway as a CONNECT allowlist.
Substrate / kagent Actor egress (Substrate / K, Documented)
kagent’s networking and egress docs describe a third recipe. An Actor reaches the network only through the egress gateway. Policy is default-deny. Rules are hostname, CIDR, or all. The gateway terminates TLS so hostname rules apply to encrypted traffic; opaque TCP matches by address. kagent builds the allowlist from the AgentTemplate. Related to #3237 and #3318, which shipped in stable v1.6.0. Code shipped. Runtime behaviour on this page is not a Tested cell.
kagent v1.0.0-alpha15
is an alpha release. It adds a reusable EgressPolicy resource that an Agent or SandboxTemplate selects through spec.egressPolicyRefs (PR 3177
). The gateway injects the outbound auth header from a Kubernetes Secret selected by secretKeyRef, so the agent never holds the token. The same release also adds token passthrough for remote MCP servers.
What this does not cover
HTTP CONNECT allowlisting is one channel. DNS as a covert channel is another; that post’s NetworkPolicy pins the resolver.
Body inspection and “AI firewall” claims are how some peer pages define a broader product. They are not what this CONNECT allowlist proves.
Per-key dollar and token spend caps are a different control; see the open-source standalone per-key budgets and the Solo Enterprise cost-controls page .
Outbound identity is the other half of CONNECT-time authorization: SPIFFE for who is asking, secretless agents so the provider key never sits in the process. Once egress is default-deny, shadow MCP servers that are not on the list are unreachable.
CONNECT egress as a scorecard row is already on how to choose an AI agent gateway and, as a factual compare cell, on agentgateway vs LiteLLM vs Portkey . I am not re-scoring seven controls here.
Methodology and disclosure
Last checked 2026-10-05, agentgateway docs labelled 1.6 (latest), releases through v1.6.0.
I fetched the standalone egress-proxy
page (tunnel vs route, IMPORTANT note, allowlist and route-mode examples), the version table
, the v1.6.0 release
and PRs #3237
, #3318
, and #3677
, the Kubernetes ambient-egress
guide, the Kubernetes API
dynamicForwardProxy warning, the backend tunnel
page, and the kagent networking
docs. v1.5.0 substrateEgress is cited from source on that tag.
Proof artifacts are Marketing’s 2026-10-05 standalone run (P1 to P6 PASS). Copies live at /lab/agentgateway-egress-proxy/
. Tunnel CONNECT; bind-443 required mode: internal. I do not cite that harness as Substrate or kagent. P7 (route-mode basicAuth) and P8 (Kubernetes ambient) were not run.
The bypasses post used a Python CONNECT stand-in, not agentgateway. I do not treat it as agentgateway evidence. Nothing on this page is labelled Solo Enterprise. Every agentgateway claim above is marked S, K, Substrate, or S+K. Standalone CONNECT is not the ambient waypoint, and neither is the Substrate Actor policy.
Disclosure: I work at Solo.io, which created agentgateway and sells Solo Enterprise for agentgateway. Everything below is open-source agentgateway unless it is labelled Solo Enterprise, and every claim links to the project’s own docs or release notes as of 2026-10-05.