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.
Configuration document
Section titled “Configuration document”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: 1sandbox: 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.orgendpoints 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.
identity
Section titled “identity”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.
policy
Section titled “policy”| 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.
endpoints
Section titled “endpoints”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.
endpoints[].credential.placement
Section titled “endpoints[].credential.placement”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.
Audit record
Section titled “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. |
Reason codes
Section titled “Reason codes”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. |
How traffic reaches the proxy
Section titled “How traffic reaches the proxy”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.
Containment
Section titled “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 withallowPrivilegeEscalation: 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.