Skip to content
agentgateway has joined the Agentic AI Foundation — Learn more

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

Page as Markdown

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:3000

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

AreaTunnel modeRoute mode
ConfigurationtunnelProtocol: connect on a bindfrontendPolicies.connect.mode: route
How a destination is matchedTLS SNI, through tcpRoutes hostnamesThe CONNECT authority, through route hostnames
HTTP policies apply to the CONNECT requestNoYes
Client authenticationNot availableAvailable, through basicAuth
A blocked destination fails withA TLS error, after the tunnel is establishedA 404 response to the CONNECT request
Can terminate TLS for selected hostnamesYesNo

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 the agentgateway 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.

  1. 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: {}
    EOF

    Review the following table to understand this configuration.

    SettingDescription
    binds[].tunnelProtocolSet to connect so that the bind accepts HTTP CONNECT requests. The other values are direct, which is the default, proxy, hboneWaypoint, and hboneGateway.
    binds[].listenersEmpty, because the tunneled traffic is handled by the gateways entry rather than by a listener on this bind.
    gateways.secure.listeners[].protocolSet to TLS so 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.
  2. Start agentgateway.

    agentgateway -f egress.yaml
  3. In 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 OK
    
  4. Verify 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:443
    

    Important

    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.

  1. Create a user database in htpasswd format.

    printf 'egressuser:%s\n' "$(openssl passwd -apr1 'secretpw')" > htpasswd.txt
  2. Create 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: {}
    EOF

    Review the following table to understand this configuration.

    SettingDescription
    frontendPolicies.connect.modeSet to route so that CONNECT requests go through route matching. The other values are tunnel, which behaves like tunnel mode, and deny, which refuses CONNECT requests.
    basicAuth.htpasswdThe user database, either inline or loaded from a file.
    basicAuth.modeSet to strict to require valid credentials. The default is optional, 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-authorization header. Reading the credential from this header is what makes agentgateway answer with 407 and Proxy-Authenticate instead of 401 and WWW-Authenticate.
    routes[].hostnamesThe destinations that clients are allowed to reach, matched against the CONNECT authority.
  3. Start agentgateway.

    agentgateway -f egress-auth.yaml
  4. Verify that a request without credentials is challenged. The --verbose flag is required, because the proxy response is not the response that curl reports.

    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 407
    
  5. Verify 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 OK
    
  6. Verify that a destination that is not in the allowlist is refused, even for an authenticated client. Agentgateway returns 404 to 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.

  1. 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.ext

    Warning

    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.

  2. 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
    EOF

    The listener with the * hostname matches last, so the two named hostnames are terminated and every other allowed hostname passes through encrypted.

  3. 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.yaml
  4. Verify 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 OK
    
  5. Verify 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 agentgateway
    
  6. Verify 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/models

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

  1. 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!'"
    EOF

    Review the following table to understand this configuration.

    SettingDescription
    binds[].modeSet to internal so that the bind routes traffic without opening a listener socket on port 443. The default is standard, which binds the port.
    tls.modeSet to dynamicCa so that cert and key are treated as a certificate authority that issues a leaf certificate for each requested hostname. The default is static, 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.
  2. Start agentgateway.

    agentgateway -f egress-inspect.yaml
  3. Send 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/headers

    The 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

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 typeIPv4IPv6
Loopback127.0.0.0/8::1
Link-local, including the cloud metadata server169.254.0.0/16fe80::/10
Unspecified, which connects to the local host0.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/8 and 192.168.0.0/16, use route hostnames allowlists.
  • 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.

  1. Create a configuration file that forwards proxy requests for localhost to 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: {}
    EOF
  2. Start agentgateway.

    agentgateway -f egress-loopback.yaml
  3. In 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/ready

    Example output:

    HTTP/1.1 503 Service Unavailable
    content-type: text/plain
    content-length: 19
    
    backend not allowed
    

    The 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=NoHealthyBackend
    
  4. Stop agentgateway. Then add the config.backend.allowDynamicLoopback setting to the top of the configuration file.

    config:
      backend:
        allowDynamicLoopback: true
    SettingDescription
    config.backend.allowDynamicLoopbackAllows loopback, link-local, and unspecified destinations when true. Defaults to false. Read at startup; restart agentgateway after changing it.
  5. Start agentgateway again.

    agentgateway -f egress-loopback.yaml
  6. Send 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/ready

    Example output:

    HTTP/1.1 200 OK
    content-type: text/plain
    content-length: 6
    
    ready
    
Was this page helpful?
Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

Tip: one topic per conversation gives the best results. Use the + button in the chat header to start a new conversation.

Switching topics? Starting a new conversation improves accuracy.
↑↓ navigate ↵ select esc dismiss

What could be improved?

Your feedback helps us improve assistant answers and identify docs gaps we should fix.

Need more help? Join us on Discord: https://discord.gg/y9efgEmppm

Want to use your own agent? Add the Solo MCP server to query our docs directly. Get started here: https://search.solo.io/.