Edge settings¶
Environment variables only; there are no flags. An empty value means the default. Lists are comma-separated. Durations use Go syntax (500ms, 30s, 168h). If any value is invalid, the edge logs every problem and exits with code 2.
Trust¶
These decide whom the edge believes. They come only from the deployment, never from admin.
| Variable | Default | Meaning |
|---|---|---|
CLIENT_IP_SOURCE |
required | cloudfront, cloudflare, gcp, header:<Name>:ip, header:<Name>:ip_port, xff or remote_addr. See Client IP presets |
TRUSTED_PROXIES |
required unless remote_addr |
IPs or CIDRs of the hop directly in front of the edge, usually your LB. A request from any other peer is rejected; the edge never falls back to the peer address. See which hop to list |
VIEWER_HEADERS |
from the preset | field=Header,... for country, continent, ASN, scheme, HTTP version, client-certificate verification and the JA4 and JA3 TLS fingerprints. A rule can use a viewer field only if a trusted source is configured |
INGRESS_SECRET_HEADER, INGRESS_SECRET_VALUES |
off | A header your CDN adds with a secret value. Requests without a matching value are rejected, and the header is removed before forwarding. Several values allow rotation |
ALLOW_REGEX |
false |
Allows the matches operator in rules |
Client IP presets¶
CLIENT_IP_SOURCE names where the client IP comes from. cloudfront, cloudflare and gcp are presets: they also set VIEWER_HEADERS, which you can extend or override field by field. The other sources read no viewer headers unless you set VIEWER_HEADERS.
| Source | Client IP read from | Viewer headers it sets |
|---|---|---|
cloudfront |
CloudFront-Viewer-Address, as ip:port |
ip.src.country=CloudFront-Viewer-Country, ip.src.asnum=CloudFront-Viewer-ASN, ssl=CloudFront-Forwarded-Proto, http.request.version=CloudFront-Viewer-Http-Version |
cloudflare |
CF-Connecting-IP, as ip |
ip.src.country=CF-IPCountry, ssl=X-Forwarded-Proto |
gcp |
X-Gclb-Client-Ip, as ip |
ip.src.country=X-Gclb-Region, http.request.version=X-Gclb-Protocol, ssl=X-Gclb-Encrypted |
header:<Name>:ip or header:<Name>:ip_port |
The named header, as ip or ip:port |
none |
xff |
X-Forwarded-For, the rightmost entry not in TRUSTED_PROXIES, or the leftmost if every entry is trusted |
none |
remote_addr |
The TCP peer. Use it only when nothing sits in front of the edge | none; VIEWER_HEADERS is refused |
The viewer fields are ip.src.country, ip.src.continent, ip.src.asnum, ssl, http.request.version, client_cert.verified, tls.client.ja4 and tls.client.ja3. ssl is true when its header is https or true; client_cert.verified when its header is true. The HTTP version is read as 1.0, 1.1, 2.0 or 3.0 from either CloudFront's form or the HTTP/2 form, for every source; any other value is absent. A rule comparing it with any other literal gets a validation warning, since it can never match. A fingerprint header that is empty or not a well-formed JA4 or JA3 is absent. With an ssl source, a fingerprint on a request that wasn't HTTPS is absent too: TLS fingerprints exist only for TLS, so any such value came from the client. On those requests no X-Edge-TLS-JA4 is sent and browser_tls_mismatch can't fire. Map ssl whenever you map a fingerprint.
gcp: a Google Cloud external Application Load Balancer has no fixed viewer headers, so add these custom request headers to the backend service: X-Gclb-Client-Ip: {client_ip_address}, X-Gclb-Region: {client_region}, X-Gclb-Protocol: {client_protocol} and X-Gclb-Encrypted: {client_encrypted}. The load balancer overwrites only the headers it adds: any other header reaches the edge exactly as the client sent it. So every header the edge reads must be a custom header on the load balancer, including any you add to VIEWER_HEADERS. Send INGRESS_SECRET_HEADER as a custom header too. It is required when the edge can be reached through an internet NEG, or by any path not limited to your own VPC; see the production checklist.
ASN and the TLS fingerprints are opt-in. Add X-Gclb-Asn: {asn}, X-Gclb-Ja4: {tls_ja4_fingerprint} and X-Gclb-Ja3: {tls_ja3_fingerprint} as custom headers first, and only then set VIEWER_HEADERS=ip.src.asnum=X-Gclb-Asn,tls.client.ja4=X-Gclb-Ja4,tls.client.ja3=X-Gclb-Ja3. Use only the variables Google documents for your load balancer type: the global external and classic Application Load Balancers have all three, and the regional external Application Load Balancer has the fingerprints but no {asn}.
Fingerprints on other CDNs are opt-in through VIEWER_HEADERS, so a preset's deployment doesn't change:
| CDN | Add to VIEWER_HEADERS |
Also needed |
|---|---|---|
| CloudFront | tls.client.ja4=CloudFront-Viewer-JA4-Fingerprint,tls.client.ja3=CloudFront-Viewer-JA3-Fingerprint |
Add both headers to the cache behavior's origin request policy; a cache policy can't carry them. They are present only for HTTPS requests, and the edge ignores them on a request whose ssl is false. The preset's viewer headers need the same policy |
| Cloudflare | tls.client.ja4=<Header> |
JA4 fingerprints are available only with Enterprise Bot Management. Have Cloudflare set a request header from the fingerprint and name that header here |
A preset can be extended this way, such as VIEWER_HEADERS=tls.client.ja4=CloudFront-Viewer-JA4-Fingerprint, which keeps the preset's other headers.
Which hop goes in TRUSTED_PROXIES: the edge matches it against its own TCP peer, the hop directly in front of it. With CDN → load balancer → edge that is the load balancer (its subnets or addresses), not the CDN. A request from any other peer gets 400. A request from a trusted peer whose client-IP header is missing or unparseable, or, for the single-header sources, sent more than once, also gets 400: the edge never falls back to the peer address, which would count every client as the load balancer.
With xff, the same list also tells the edge which X-Forwarded-For entries to skip, so it must include every proxy whose address appears in that header. Behind CDN → load balancer → edge, a load balancer that appends to X-Forwarded-For adds the CDN's address, so list the CDN's ranges as well, or the edge reads the CDN as the client.
Upstream¶
The edge forwards every request it allows to one upstream: the load balancer or gateway that routes your services. It doesn't route itself, and it has no list of hosts or services.
| Variable | Default | Meaning |
|---|---|---|
UPSTREAM_URL |
required | http(s)://host[:port][/path] of your load balancer or gateway, such as an ALB or an Istio gateway Service, with no query or fragment. Host is forwarded unchanged so it can route. Over HTTPS the edge sends the request's host as the TLS server name and checks the certificate against it, so the upstream must present a certificate for each host it serves; the connection still goes to this URL's address. Only the deployment sets it: policy can't, and admin shows it read-only on Fleet |
UPSTREAM_DIAL_TIMEOUT |
5s |
Connect and TLS handshake timeout |
UPSTREAM_MAX_IDLE |
256 |
Idle connections kept to the upstream, in total. Over HTTPS each host has its own pool within this total |
Policy source¶
| Variable | Default | Meaning |
|---|---|---|
ADMIN_URL |
none | Admin base URL. At least one of ADMIN_URL, SNAPSHOT_CACHE_PATH or BOOTSTRAP_PATH is required |
EDGE_SERVICE_TOKEN or EDGE_SERVICE_TOKEN_FILE |
required with ADMIN_URL |
An edge token created in admin, or the deployment's own token when admin reads the same file through its EDGE_SERVICE_TOKEN_FILE. Set one, not both. Prefer the file form, mounted from a secret |
POLL_INTERVAL |
5s |
How often to poll admin. Backs off to about a minute while admin fails |
SNAPSHOT_CACHE_PATH |
none | Where to keep the last good policy, so a restart works without admin |
BOOTSTRAP_PATH |
none | A policy file exported from admin, used when admin and the cache both fail |
BOOTSTRAP_MAX_AGE |
168h |
A bootstrap file older than this is refused |
BOOTSTRAP_ALLOW_STALE |
false |
Serve an old bootstrap anyway. Logged at error level and shown in Fleet |
STATS_INTERVAL |
10s |
How often stats are sent to admin. At most 30s: admin counts an edge toward the fleet size, which divides local fallback limits, for a minute after its last report |
INSTANCE |
hostname | The name shown in Fleet |
JWKS_ALLOWED_HOSTS |
none | JWKS hosts (as in the URL) allowed to resolve to private addresses. JWKS URLs are entered in admin, so every JWKS fetch goes through the SSRF (server-side request forgery) guard: it is HTTPS only, uses no proxy and follows redirects only to the same host. Except for the listed hosts, it is also refused when the address actually dialed is loopback, private, link-local (cloud metadata), shared (100.64.0.0/10), multicast, unspecified, or a NAT64 (64:ff9b::/96) form of one of those |
Valkey¶
| Variable | Default | Meaning |
|---|---|---|
VALKEY_URL |
none | redis://, rediss://, valkey://, valkeys:// or unix://. For a cluster, list nodes with ?addr=host:port&addr=.... Without it every rule uses its fail_mode |
VALKEY_TIMEOUT |
50ms |
Per-request budget for the rate-limit round trip. On timeout the rule's fail_mode applies |
EXPECTED_REPLICAS |
1 |
Fleet size for fail_mode: local when the policy carries no live fleet count, for example when loaded from cache or bootstrap. Admin normally sends the live count |
Capacity and timeouts¶
| Variable | Default | Meaning |
|---|---|---|
LISTEN_ADDR |
:8080 |
Traffic |
INTERNAL_ADDR |
:9090 |
/livez, /readyz, /metrics |
MAX_INFLIGHT |
GOMEMLIMIT ÷ 256 KiB, at most 10000; 10000 without GOMEMLIMIT |
Concurrent ordinary requests per replica; more get 503 overloaded. Each in-flight proxied request holds memory, about 100 KiB in a load test on Kubernetes, kernel socket buffers included, which GOMEMLIMIT doesn't cap. The default keeps that, plus streams and headroom, under GOMEMLIMIT, so set GOMEMLIMIT to about 90% of the container's memory limit. Past what memory holds, a slow upstream gets the edge OOM-killed instead of answering 503. Settings → Requests in progress in admin can lower it for every edge, never raise it |
MAX_STREAMS |
1000 |
Concurrent WebSocket and SSE requests per replica |
TOKEN_CACHE_SIZE |
10000 |
Verified tokens kept per replica so repeat requests skip the signature check. When full, new tokens are verified on every request but not stored. Raise it when edge_auth_total{result="verified"} stays high next to cached |
MAX_HEADER_BYTES |
65536 |
|
READ_HEADER_TIMEOUT |
10s |
|
IDLE_TIMEOUT |
75s |
Keep-alive idle timeout, on the public and internal listeners. Set your LB's idle timeout below it |
DRAIN_DELAY |
10s |
After SIGTERM, how long /readyz fails before the edge stops accepting |
SHUTDOWN_TIMEOUT |
30s |
How long in-flight requests get to finish |
LOG_SAMPLE_RATE |
1 |
Share of allowed, non-5xx requests to log (0 to 1). Rejections and 5xx are always logged |
Per-route body limits and upstream timeouts are policy, not deployment settings; see Policy.
Tracing¶
Off unless an endpoint is set. When on, the edge joins the caller's trace, records one span per request that isn't shed for overload, and passes its own span to the upstream in traceparent. See Monitoring.
| Variable | Default | Meaning |
|---|---|---|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
unset (off) | Full OTLP/HTTP traces URL, such as http://collector:4318/v1/traces |
OTEL_EXPORTER_OTLP_ENDPOINT |
unset | Base OTLP/HTTP URL; /v1/traces is appended. Used when the traces endpoint is unset |
OTEL_SERVICE_NAME |
edge-proxy |
service.name on every span. service.instance.id is INSTANCE |
TRACE_SAMPLE_RATE |
0.01 |
Share of new traces to sample (0 to 1). A caller's sampling decision is followed |
TRACE_MAX_PER_SECOND |
100 |
Most traces one replica starts and samples per second, and separately the most it samples per second from callers' traces. Callers choose their trace IDs and sampled flag, so only this cap bounds the export volume a client can force, and a flood of forged flags can't use up the edge's own budget |
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS |
none | Headers for the collector, such as credentials: key=value pairs separated by commas, values percent-encoded. A malformed value fails startup without being logged, because the exporter would log it |
TRACE_SHUTDOWN_TIMEOUT |
5s |
At shutdown, after SHUTDOWN_TIMEOUT: how long to wait for open streams to record their spans and then flush, together |
The exporter also reads the other standard variables it supports, such as OTEL_EXPORTER_OTLP_CERTIFICATE for a private CA and OTEL_EXPORTER_OTLP_COMPRESSION, and the batch processor reads OTEL_BSP_* (OpenTelemetry environment variables, OTLP exporter settings). The SDK checks those itself: a malformed value is ignored without a log line and the default is used. A plain http endpoint with a certificate set does fail startup.