Restricted-egress reference
Detail behind Behind a forward proxy.
How the proxy variables are read
| Rule | Detail |
|---|---|
| Spelling | Lowercase (https_proxy) wins over uppercase. An empty value counts as unset |
| Which proxy | https:// calls use HTTPS_PROXY, or HTTP_PROXY when only that one is set. http:// calls use HTTP_PROXY only |
| Value | http://host:port or https://host:port. A value with no scheme is read as http://. Basic credentials go in the URL (http://user:password@proxy.example.com:3128) and never reach a log line or /admin/health |
| Bad value | The gateway stops at boot with a message that names the variable, never its value |
NO_PROXY | Comma or space separated, case-insensitive. example.com, .example.com, and *.example.com each match the host and every subdomain. host:port matches that port only. * sends nothing through the proxy. CIDR ranges are not matched; private addresses are direct anyway |
| Always direct | Loopback, private, link-local, and CGNAT addresses; cloud metadata hosts; the suffixes .local, .localdomain, .internal, .intranet, .lan, .home, .corp, .test, .invalid, .onion, .localhost; any name with no dot. The optimizer is always direct, whatever its address. The database never uses the proxy |
SSRF checks behind a proxy
The SSRF policy still runs before
a destination someone names goes to the proxy. The gateway resolves the name and refuses a private,
loopback, or metadata answer, with the same exemptions as a direct connection. It cannot pin that
answer, because the proxy resolves the name again. That is why the proxy must deny internal
destinations itself, and why a configured upstream on a private address belongs in NO_PROXY: the
proxy would refuse it.
When the server's own DNS has no answer for the name:
| Destination | Result |
|---|---|
Provider APIs, the Billing App, github.com | Sent to the proxy |
| A provider Base URL or routing upstream you configured | Sent to the proxy |
| An MCP server configured in the console, or a URL an admin tests from it | Sent to the proxy |
| An alert webhook | Sent to the proxy |
| Organization MCP sign-in through WorkOS AuthKit (the issuer the Billing App names, and only that host) | Sent to the proxy |
| An MCP sign-in endpoint discovered from a server's metadata | Refused |
A caller-supplied custom host (x-anyray-custom-host, x-anyray-config) | Refused, unless it is in TRUSTED_CUSTOM_HOSTS or is the built-in ChatGPT seat host (chatgpt.com) |
Cloud metadata is refused on every path.
TLS-inspecting proxies
Mount the proxy's root CA and point NODE_EXTRA_CA_CERTS at it.
- docker-compose.override.yml (Docker Compose)
- my-values.yaml (Helm)
services:
gateway:
environment:
NODE_EXTRA_CA_CERTS: /etc/anyray/proxy-ca.pem
volumes:
- ./proxy-ca.pem:/etc/anyray/proxy-ca.pem:ro
gateway:
extraEnv: # the same list as HTTPS_PROXY
- name: NODE_EXTRA_CA_CERTS
value: /etc/anyray/proxy-ca/ca.pem
extraVolumes:
- name: proxy-ca
secret:
secretName: proxy-ca
extraVolumeMounts:
- name: proxy-ca
mountPath: /etc/anyray/proxy-ca
readOnly: true
On Helm, create the secret first:
kubectl create secret generic proxy-ca -n anyray --from-file=ca.pem=proxy-ca.pem
NODE_TLS_REJECT_UNAUTHORIZED=0 turns off verification for every connection the gateway makes.