Skip to content

Egress proxy

The egress proxy's configuration document, credential placements, audit record, and the reason code for every decision.

The egress proxy is a forward proxy that runs as a sidecar beside an untrusted workload. It terminates the workload’s TLS, allows or denies each connection, request, and DNS lookup against an endpoint list, and writes a credential from the Zone into each allowed request that needs one. Run agents in a sandbox shows a deployment.

This page covers the settings that decide what the proxy allows and what it records, while the repository’s configuration reference lists every key, including listeners, the CA, limits, and the admin socket.

The proxy reads one YAML or JSON document at start-up, from --config or EGRESS_PROXY_CONFIG, so restart the proxy after a change. The parser refuses unknown keys, repeated keys, and keys that differ only in case.

version: 1
sandbox:
id: <sandbox-name>
identity:
zone_url: <keycard-issuer-url>
client_id: <workload-identity-credential-id>
workload_token_file: /var/run/secrets/keycard/token
impersonate:
user: <user-id>
policy:
cluster_cidrs: ["<pod-cidr>", "<service-cidr>"]
endpoints:
- url: https://api.github.com
credential:
placement: {kind: header, name: Authorization, template: "Bearer {credential}"}
- url: https://pypi.org

endpoints and policy come from the document only, but every other section can also come from an EGRESS_PROXY_* environment variable or a flag, which override the document.

When identity is present, the proxy runs an impersonation token exchange against the Zone for each allowed request to an endpoint with a credential. It does not run an exchange for a request that the endpoint list denies.

Key Description
zone_url The Zone’s issuer URL. The Zone’s metadata must name this URL as its issuer.
client_id The ID of the Application credential the proxy authenticates as.
workload_token_file The path of the projected ServiceAccount token. The proxy reads it again for every exchange, so a rotated token does not need a restart.
client_secret_file The path of a file that holds the Application credential’s secret. Use it only where no OIDC issuer signs a workload token.
impersonate.user The Keycard user ID the proxy acts for.

Set exactly one of workload_token_file and client_secret_file.

Key Description
cluster_cidrs The cluster’s pod and service ranges. The proxy adds them to its built-in deny list.
deny_list_exceptions Address ranges to remove from the deny list. The proxy refuses an exception that covers 169.254.169.254, and logs every exception at start-up. Review this list as you review the endpoints.

The built-in deny list covers loopback, RFC 1918 ranges, link-local ranges, shared address space, their IPv6 equivalents, cloud metadata host names, and the large public DNS-over-HTTPS resolvers. The proxy refuses a document that names a covered host, and when it dials, it refuses an allowed name that resolves into a covered range.

Each endpoint is one URL: https://host[:port][/path]. The proxy refuses a destination that no endpoint covers, so an empty list refuses everything.

Part Rule
Host A DNS name, matched exactly and case-insensitively against the TLS server name. The host cannot be a wildcard or an IP address. A subdomain is a different host.
Port 443 for https unless the URL names another. An endpoint covers one port.
Path / when the URL has none, which covers every path. /api covers /api and /api/v1, not /apis. The longest match wins.
Resource The canonical URL: lowercase scheme and host, no default port, the path as written. The proxy sends it as the exchange’s resource. It must equal the identifier of a Resource in the Zone.

The proxy does not forward cleartext HTTP, even to a listed host.

The placement says where the proxy writes the credential; it replaces any value the workload sent there, and it writes the credential when the workload did not send a value.

kind Fields Result
header name, template Sets the header to the template with {credential} replaced. Authorization is allowed. Hop-by-hop headers, Host, Content-Length, Forwarded, X-Forwarded-*, and Sec-* are refused.
query name Sets the query parameter. The query is re-encoded with its parameters sorted by name.
basic user Sets Authorization to HTTP basic auth with user and the credential as the password. Use it for git over HTTPS.

The credential does not appear in a log line or an audit record.

The proxy writes one JSON line to stdout for each decision, and you cannot turn off this audit stream. Logs go to stderr and do not include decisions.

{
"ts": "<timestamp>",
"sandbox_id": "<sandbox-name>",
"document": {"digest": "sha256:<hex>"},
"config": {"digest": "sha256:<hex>"},
"connection_id": "<connection-id>",
"request_id": "<request-id>",
"scope": "request",
"destination": {"host": "api.github.com", "port": 443, "sni": "api.github.com"},
"request": {"method": "GET", "path": "/user"},
"decision": {"verdict": "allow", "reason": "endpoint_allowed", "endpoint": "api.github.com:443/"},
"outcome": {"status": 200}
}
Field Description
sandbox_id The sandbox this proxy serves, from sandbox.id.
document.digest A SHA-256 digest of the mounted document.
config.digest A SHA-256 digest of the settings in force after overrides and defaults. Two proxies with the same settings have the same value.
scope connection for a new connection, request for a request inside it, dns for a lookup. connection_id joins a connection to its requests.
destination The host the proxy decided on. original_address is present when the connection was captured by the firewall rules.
request The method and the escaped path, without the query.
query For dns records: the query type, class, protocol, and response code.
decision.verdict allow or deny.
decision.reason One of the reason codes below.
decision.endpoint The endpoint that matched, as host:port and path.
decision.error Why a check failed, when a failure caused the denial. For an exchange, the Zone’s error description.
outcome The upstream’s status for an allowed request.

A refused request gets the reason in the X-Keycard-Deny-Reason response header, and the ID in X-Keycard-Request-Id. A refused TLS handshake does not get an HTTP response, so the client sees a TLS error and only the record has the reason. A blank status below means the client does not get an HTTP response.

Reason Status Meaning
endpoint_allowed An endpoint matched. The verdict is allow.
no_matching_endpoint 403 No endpoint covers the host, port, or path. For a DNS lookup, the name is not an endpoint host or the Zone host, and the answer is NXDOMAIN.
deny_always 403 The host is on the deny list, or resolved into a denied range.
cleartext_not_forwarded 403 The request uses cleartext HTTP. Use https://.
host_mismatch 403 The TLS server name, the CONNECT target, and the Host header disagree.
no_sni The TLS handshake did not name a server, as when a client connects to an IP address.
not_redirected A connection was opened to the proxy’s own listener rather than redirected to it.
credential_unavailable 403 The proxy could not write a credential into the request. decision.error says why.
exchange_denied 403 The Zone refused the exchange on policy with access_denied, unauthorized_client, or invalid_grant. A retry gets the same answer.
exchange_failed 502 The exchange did not complete: the Zone was unreachable, refused for a reason other than policy, or took longer than the exchange timeout. A retry can succeed.
upstream_failed 502 The upstream did not answer an allowed request.
certificate_unavailable The proxy could not mint a certificate for an allowed host.
malformed_request 400 The request target, path, or query does not parse, or the path has dot segments.
not_a_proxy_request 400 A plain HTTP request reached the CONNECT listener.

The proxy has two frontends: the CONNECT frontend is always on, and the transparent frontend is on when listen.transparent_port is set.

Frontend Key Default How the workload reaches it
CONNECT listen.connect 127.0.0.1:3128 The workload sets HTTPS_PROXY=http://127.0.0.1:3128.
Transparent listen.transparent_port unset Firewall rules redirect the workload’s outbound TCP to this port. The workload does not need proxy settings. Linux only.

The CONNECT frontend does not need firewall rules in the sandbox, but it works only for clients that honor HTTPS_PROXY. A client that ignores the variable connects directly unless the sandbox blocks every other route out. The reference deployment uses the transparent frontend, with the rules under Containment.

The proxy is not a sandbox by itself, so the pod around it must send all of the workload’s traffic to it. The egress proxy repository ships an init container, egress-netsetup, that installs iptables rules in the pod’s network namespace:

  • Outbound TCP goes to the proxy’s transparent listener, and DNS on UDP and TCP port 53 goes to the proxy’s resolver.
  • The rules drop every other packet the workload sends, including UDP and QUIC.
  • The proxy’s own user ID is exempt, so the workload must run as a different user, without CAP_SETUID, and with allowPrivilegeEscalation: false.

The init container drops NET_ADMIN and writes a receipt of the rules it installed, and the proxy reads the receipt and refuses to start if the rules or capabilities differ from its own settings. The repository’s containers document sets out the pod requirements, and its security model lists each threat, its mitigation, the test that covers it, and the known limits.