For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Egress proxy
Run agentgateway as an HTTP CONNECT proxy to control which destinations your clients can reach.
Agentgateway can act as an HTTP CONNECT proxy for outbound traffic. Clients point their proxy settings at agentgateway, and agentgateway decides which destinations they are allowed to reach.
About the egress proxy
A client selects the proxy through the standard environment variable that most tools read.
export HTTPS_PROXY=http://127.0.0.1:3000The client then sends CONNECT <host>:443 to agentgateway instead of connecting to the destination directly. What agentgateway does with that request depends on which of the two CONNECT handling modes you configure.
| Area | Tunnel mode | Route mode |
|---|---|---|
| Configuration | tunnelProtocol: connect on a bind | frontendPolicies.connect.mode: route |
| How a destination is matched | TLS SNI, through tcpRoutes hostnames | The CONNECT authority, through route hostnames |
| HTTP policies apply to the CONNECT request | No | Yes |
| Client authentication | Not available | Available, through basicAuth |
| A blocked destination fails with | A TLS error, after the tunnel is established | A 404 response to the CONNECT request |
| Can terminate TLS for selected hostnames | Yes | No |
Neither mode is a replacement for the other. Tunnel mode gives you SNI-based control and the option to terminate TLS, and route mode gives you the HTTP policy engine, including client authentication.
To send the requests that agentgateway makes on its own behalf, such as JWKS fetches, through an upstream proxy instead of proxying traffic from your clients, see Callout proxy.
Note
Policies that you attach to a listener on a tunnelProtocol: connect bind are not applied to the CONNECT request, because tunnel mode handles the request before route matching. To authenticate clients, use route mode.
By default, dynamic backends refuse to connect to loopback and link-local addresses. For more information, see Loopback and link-local destinations.
Before you begin
Install theagentgateway binary.Allowlist destinations without decrypting traffic
Use this pattern when you need to restrict outbound HTTPS by hostname and the traffic must stay encrypted end to end. Agentgateway reads the TLS SNI hostname for routing and forwards the encrypted connection without decrypting it.
Create a configuration file that accepts CONNECT requests on port 3000 and allowlists destinations by hostname.
cat <<EOF > egress.yaml # yaml-language-server: \$schema=https://agentgateway.dev/schema/config binds: - port: 3000 tunnelProtocol: connect listeners: [] gateways: secure: port: 443 listeners: - name: public-egress hostname: "*" protocol: TLS tcpRoutes: - name: public-egress-allowlist gateways: secure/public-egress hostnames: - pypi.org - files.pythonhosted.org backends: - dynamic: {} EOFReview the following table to understand this configuration.
Setting Description binds[].tunnelProtocolSet to connectso that the bind accepts HTTP CONNECT requests. The other values aredirect, which is the default,proxy,hboneWaypoint, andhboneGateway.binds[].listenersEmpty, because the tunneled traffic is handled by the gatewaysentry rather than by a listener on this bind.gateways.secure.listeners[].protocolSet to TLSso that agentgateway inspects the SNI hostname without terminating the connection.tcpRoutes[].hostnamesThe destinations that clients are allowed to reach. A destination that matches no entry has no route. backends[].dynamicSends the connection to the hostname from SNI, on the original destination port. Start agentgateway.
agentgateway -f egress.yamlIn another terminal, verify that an allowed destination works.
curl --proxy http://127.0.0.1:3000 --head https://pypi.org/Example output:
HTTP/1.1 200 OKVerify that a destination that is not in the allowlist is refused.
curl --proxy http://127.0.0.1:3000 --head https://example.com/Example output:
curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to example.com:443Important
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.
Package managers often download from several hostnames. Include artifact and redirect destinations, such as files.pythonhosted.org, not only the primary site.
Send a permitted hostname to a different destination
To redirect an allowed hostname, set a TCP dynamic target expression. The expression must return a host:port string.
tcpRoutes:
- hostnames:
- pypi.org
backends:
- dynamic:
target: 'destination.hostname == "pypi.org" ? "mirror.example.com:8443" : destination.hostname + ":" + string(destination.port)'TCP target expressions can use destination.hostname, destination.address, destination.port, and source.*. This form applies to tcpRoutes only. A dynamic backend is not accepted as the destination for a policy call.
Require client authentication
Use this pattern when only known clients may use the proxy. Route mode sends the CONNECT request through normal route matching, so HTTP policies such as basicAuth apply to it.
Create a user database in htpasswd format.
printf 'egressuser:%s\n' "$(openssl passwd -apr1 'secretpw')" > htpasswd.txtCreate a configuration file that enables route mode and requires credentials.
cat <<EOF > egress-auth.yaml # yaml-language-server: \$schema=https://agentgateway.dev/schema/config frontendPolicies: connect: mode: route binds: - port: 3000 listeners: - name: connect protocol: HTTP policies: basicAuth: htpasswd: file: htpasswd.txt mode: strict realm: agentgateway-egress authorizationLocation: header: name: proxy-authorization prefix: "Basic " routes: - name: egress-allowlist hostnames: - pypi.org - files.pythonhosted.org backends: - dynamic: {} EOFReview the following table to understand this configuration.
Setting Description frontendPolicies.connect.modeSet to routeso that CONNECT requests go through route matching. The other values aretunnel, which behaves like tunnel mode, anddeny, which refuses CONNECT requests.basicAuth.htpasswdThe user database, either inline or loaded from a file. basicAuth.modeSet to strictto require valid credentials. The default isoptional, which validates credentials when a client sends them but allows requests that omit them.basicAuth.realmThe realm that agentgateway returns in the challenge. basicAuth.authorizationLocationSet to the proxy-authorizationheader. Reading the credential from this header is what makes agentgateway answer with407andProxy-Authenticateinstead of401andWWW-Authenticate.routes[].hostnamesThe destinations that clients are allowed to reach, matched against the CONNECT authority. Start agentgateway.
agentgateway -f egress-auth.yamlVerify that a request without credentials is challenged. The
--verboseflag is required, because the proxy response is not the response thatcurlreports.curl --verbose --proxy http://127.0.0.1:3000 --head https://pypi.org/Example output:
* CONNECT tunnel: HTTP/1.1 negotiated < HTTP/1.1 407 Proxy Authentication Required < proxy-authenticate: Basic realm="agentgateway-egress" * CONNECT tunnel failed, response 407Verify that a request with credentials reaches an allowed destination.
curl --proxy http://127.0.0.1:3000 --proxy-user egressuser:secretpw --head https://pypi.org/Example output:
HTTP/1.1 200 OKVerify that a destination that is not in the allowlist is refused, even for an authenticated client. Agentgateway returns
404to the CONNECT request, because no route matches the destination.curl --verbose --proxy http://127.0.0.1:3000 --proxy-user egressuser:secretpw --head https://example.com/Example output:
* CONNECT tunnel: HTTP/1.1 negotiated < HTTP/1.1 404 Not Found * CONNECT tunnel failed, response 404
Tip
A proxy response is not the response that curl reports. When the proxy refuses a request, curl writes 000 for the HTTP status and exits with code 56, so the 407 and the 404 are visible only with --verbose.
Terminate TLS for selected hostnames
Use this pattern when most destinations must stay encrypted end to end, but agentgateway must serve selected hostnames itself. This pattern uses tunnel mode, and adds HTTPS listeners for the hostnames that agentgateway terminates.
Generate a certificate authority and a certificate for the hostnames that agentgateway serves.
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca-cert.pem -days 30 \ -subj "/CN=agentgateway egress CA" openssl req -newkey rsa:2048 -nodes -keyout key.pem -out csr.pem -subj "/CN=egress" printf "subjectAltName=DNS:llm.example.com,DNS:static.example.com\n" > san.ext openssl x509 -req -in csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial \ -out cert.pem -days 30 -extfile san.extWarning
This certificate authority is for local testing. Do not distribute it outside your test environment. Generate and protect a certificate authority that is appropriate for your environment.
Create a configuration file that terminates TLS for two hostnames and passes every other allowed hostname through.
cat <<EOF > egress-conditional.yaml # yaml-language-server: \$schema=https://agentgateway.dev/schema/config binds: - port: 3000 tunnelProtocol: connect listeners: [] gateways: secure: port: 443 listeners: - name: llm hostname: llm.example.com protocol: HTTPS tls: cert: cert.pem key: key.pem - name: direct-response hostname: static.example.com protocol: HTTPS tls: cert: cert.pem key: key.pem - name: public-egress hostname: "*" protocol: TLS tcpRoutes: - name: public-egress-allowlist gateways: secure/public-egress hostnames: - pypi.org - files.pythonhosted.org backends: - dynamic: {} routes: - name: static-response gateways: secure/direct-response policies: directResponse: status: 200 body: hello from agentgateway llm: gateways: secure/llm models: - name: smart provider: openAI params: model: gpt-5.5 EOFThe listener with the
*hostname matches last, so the two named hostnames are terminated and every other allowed hostname passes through encrypted.Start agentgateway. The LLM routes need a provider key to be present, but listing models is handled locally and does not call the provider.
OPENAI_API_KEY=dummy agentgateway -f egress-conditional.yamlVerify that an allowlisted public destination still passes through unchanged.
curl --proxy http://127.0.0.1:3000 --head https://pypi.org/Example output:
HTTP/1.1 200 OKVerify the hostname that returns a configured response.
curl --proxy http://127.0.0.1:3000 --cacert ca-cert.pem https://static.example.com/Example output:
hello from agentgatewayVerify that the LLM hostname serves the configured model catalog.
curl --proxy http://127.0.0.1:3000 --cacert ca-cert.pem https://llm.example.com/v1/modelsExample output:
{"data":[{"id":"smart","object":"model","created":1787773708,"owned_by":"openai"}],"object":"list"}
Inspect all HTTPS traffic
Use this pattern when agentgateway must apply HTTP policies to every outbound request. Agentgateway issues a certificate for each requested hostname from a certificate authority that you supply, then opens a separate TLS connection to the destination.
Create a configuration file that terminates every HTTPS stream with a dynamic certificate.
cat <<EOF > egress-inspect.yaml # yaml-language-server: \$schema=https://agentgateway.dev/schema/config binds: - port: 3000 tunnelProtocol: connect listeners: [] - port: 443 mode: internal listeners: - protocol: HTTPS tls: mode: dynamicCa cert: ca-cert.pem key: ca-key.pem routes: - backends: - dynamic: {} policies: backendTLS: {} policies: transformations: request: set: x-agentgateway-req-message: "'Hello from agentgateway!'" response: set: x-agentgateway-resp-message: "'Hello from agentgateway!'" EOFReview the following table to understand this configuration.
Setting Description binds[].modeSet to internalso that the bind routes traffic without opening a listener socket on port 443. The default isstandard, which binds the port.tls.modeSet to dynamicCaso thatcertandkeyare treated as a certificate authority that issues a leaf certificate for each requested hostname. The default isstatic, which treats them as the leaf certificate.backendTLSAdds TLS back to the outgoing request, because agentgateway terminated the client connection. transformationsIncluded to show that HTTP policies now apply. Replace this policy with the policies that you need. Start agentgateway.
agentgateway -f egress-inspect.yamlSend a request to an endpoint that echoes request headers, and trust the certificate authority.
curl --proxy http://127.0.0.1:3000 --cacert ca-cert.pem --include https://httpbingo.org/headersThe response headers include the header that the response policy adds:
x-agentgateway-resp-message: Hello from agentgateway!The response body shows the header that the request policy added to the upstream request:
"X-Agentgateway-Req-Message": [ "Hello from agentgateway!" ]
Warning
This configuration has no hostname allowlist, so it permits every HTTPS destination. To restrict destinations as well, add route hostnames entries, or combine this pattern with one of the allowlist patterns on this page.
Choose a pattern
- To restrict outbound traffic by hostname and keep it encrypted, use Allowlist destinations without decrypting traffic.
- To restrict who may use the proxy, use Require client authentication. This pattern also refuses a blocked destination at the proxy rather than through a TLS error.
- To serve selected hostnames from agentgateway while everything else passes through, use Terminate TLS for selected hostnames.
- To apply HTTP policies to every outbound request, use Inspect all HTTPS traffic.
Run one configuration at a time. Each configuration on this page listens for proxy requests on port 3000.
Loopback and link-local destinations
A dynamic backend uses the destination named in each request. Without restrictions, clients could reach services on the gateway host.
Examples include the admin interface on localhost:15000 and the cloud metadata server at 169.254.169.254, which can issue cloud credentials. This is called server-side request forgery (SSRF).
To prevent this attack, a dynamic backend refuses to connect to the following addresses.
| Address type | IPv4 | IPv6 |
|---|---|---|
| Loopback | 127.0.0.0/8 | ::1 |
| Link-local, including the cloud metadata server | 169.254.0.0/16 | fe80::/10 |
| Unspecified, which connects to the local host | 0.0.0.0 | :: |
The check has the following scope:
- Checks run after DNS resolution. Hostnames resolving to blocked addresses, such as
localhost, are also blocked. The check covers IPv4-mapped forms of blocked addresses, such as::ffff:127.0.0.1. - Private networks remain reachable. To restrict destinations in ranges such as
10.0.0.0/8and192.168.0.0/16, use routehostnamesallowlists. - Only dynamic backends are checked. A backend with a fixed address, such as
host: localhost:8080, still connects to loopback.
A refused request fails with a 503 response and the body backend not allowed. The request log records the reason as dynamic backend resolved to a loopback or link-local address.
Allow loopback destinations
Set config.backend.allowDynamicLoopback to true to allow local destinations.
Warning
This setting affects every dynamic backend in the configuration. Clients using these routes can reach the cloud metadata server at 169.254.169.254 and every service on the gateway host, including the admin interface. Keep a hostnames allowlist on every route with a dynamic backend.
These steps first verify the default behavior using agentgateway’s readiness endpoint at localhost:15021.
Create a configuration file that forwards proxy requests for
localhostto a dynamic backend.cat <<EOF > egress-loopback.yaml # yaml-language-server: \$schema=https://agentgateway.dev/schema/config binds: - port: 3000 listeners: - name: forward protocol: HTTP routes: - name: local-services hostnames: - localhost backends: - dynamic: {} EOFStart agentgateway.
agentgateway -f egress-loopback.yamlIn another terminal, send a request through the proxy to the readiness endpoint. The request is refused.
curl -i --proxy http://127.0.0.1:3000 http://localhost:15021/healthz/readyExample output:
HTTP/1.1 503 Service Unavailable content-type: text/plain content-length: 19 backend not allowedThe agentgateway log records the reason.
error request gateway=default/default listener=forward route=default/local-services endpoint=localhost:15021 ... http.status=503 protocol=http error="dynamic backend resolved to a loopback or link-local address" reason=NoHealthyBackendStop agentgateway. Then add the
config.backend.allowDynamicLoopbacksetting to the top of the configuration file.config: backend: allowDynamicLoopback: trueSetting Description config.backend.allowDynamicLoopbackAllows loopback, link-local, and unspecified destinations when true. Defaults tofalse. Read at startup; restart agentgateway after changing it.Start agentgateway again.
agentgateway -f egress-loopback.yamlSend the request again. The proxy now forwards it to the readiness endpoint.
curl -i --proxy http://127.0.0.1:3000 http://localhost:15021/healthz/readyExample output:
HTTP/1.1 200 OK content-type: text/plain content-length: 6 ready