Revision 0.7
Release 0.7.1 · tested, not yet run live
| Item | State | What that means |
| Mutual TLS, inbound |
△ TESTED |
Client certificates on the TLS listener against your CA, with CRLs that reload on change. auth.mode = "mtls" makes the certificate the credential: its SPIFFE ID, DNS name, email or CN is the principal that limits and budgets key on. TLS session resumption is off while it is on, because a resumed session is not verified again; the tests revoke a certificate and fail with resumption back on. Behind a balancer that does the mTLS, its forwarded certificate header can be trusted by the balancer's own client certificate (trusted_proxy_names), not only by its address. |
| TLS and mTLS to upstreams |
△ TESTED |
An https:// upstream used to load and then answer every request 502. It is now verified against the public roots, or only your CA with [upstream_tls] ca_path, and can present a client certificate that reloads when it changes on disk. There is no option to skip verification. |
| Private PKI to the control plane |
△ TESTED |
The control plane, log shipper, tracing and OTLP exporters, the JWKS fetch and alerts each take ca_path, a client certificate and an explicit proxy, so a corporate CA and an egress proxy no longer stop them connecting. [outbound] sets the proxy, the client certificate and extra_ca_path once for all of them; a CA there is added to the public roots, never a replacement. ACME issuance and renewal go through the same egress proxy. A last-known-good policy is kept on disk across restarts (control_plane.cache_path). |
| Upstream pools |
△ TESTED |
Priority, weighted round robin, least connections or rendezvous hashing (by client IP, path, header, cookie or query:<name>), with sticky cookies (a Max-Age, stripped before the app if you like), a backup tier, active HTTP or TCP health checks (expect_status, expect_body or expect_body_regex, a method and headers, a separate management port), ejection on connect errors or repeated 5xx, and failover for any request that reached nothing. GET /__edgeguard/upstreams says why a target is out. Checks can also switch scheme (health_check.scheme) and dial a per-target check_host. |
| PROXY protocol |
△ TESTED |
v1 and v2 from an L4 balancer, believed only from proxy_protocol_trusted; from anyone else a connection that starts with one is closed, because it would be a client choosing its own address. The admin port reads it too, as optional, so an NLB's HTTP health check of /__edgeguard/ready works there, and the PROXY header and the request head share one header_read_timeout rather than getting one each. |
| API-usage monitoring |
△ TESTED |
Per-route and per-consumer series, per-upstream results and latency, access records with schema: 2, and monitoring bundle 1.6.2: an API-usage dashboard and alerts with promtool tests. Also edgeguard_requests_in_flight and edgeguard_tls_handshakes_total per listener, and edgeguard_tls_no_client_hello_total for the health checks that used to count as failed handshakes. Route labels come from the config, never the request path, so the series count is bounded. |
| Repeated credential header |
△ TESTED |
A security fix. The auth gate read the first Authorization or API-key header, but the request went upstream with the last, so a valid key followed by a forged token passed the edge and reached the app as the forged one. A credential header sent twice is now 401 and never forwarded. Every other repeated header now reaches the app with all its values in order, and HTTP/2 Cookie headers are joined into one. |
| Client address behind an L7 balancer |
△ TESTED |
A security fix. Up to 0.5.0, with trust_forwarded_for, the client was the first X-Forwarded-For entry, which the client writes itself, so any client could pick its address through the balancer: a fresh rate-limit bucket per request, a way past [access] rules, a forged address in the logs. The client is now the entry server.forwarded_hops from the right (default 1; 2 for a GCP external Application Load Balancer or CloudFront in front of an ALB; per balancer), the one the trusted proxy appended, counted in edgeguard_forwarded_for_total{result}. X-Forwarded-Proto is read from its last value. The app receives eggrd's own X-Forwarded-For, X-Real-IP, Forwarded and X-Forwarded-Host, never the client's, and no True-Client-IP. |
| Connection gate |
△ TESTED |
One accept loop for the public, redirect and admin listeners, with header_read_timeout, max_connections and a graceful drain: before 0.7 a connection sending a byte at a time was held open indefinitely, and HTTPS requests were cut on every restart. |
These rows are red because each is proven by tests that fail without it,
against rustls endpoints, stub control planes and OpenSSL-made certificates, rather than by a
run against live infrastructure. A row turns green once it has been run the way the rows in the
revision block below were.
# mutual TLS: the certificate is the credential
[tls]
enabled = true
cert_path = "/etc/eggrd/server.pem"
key_path = "/etc/eggrd/server.key"
[tls.client_auth]
mode = "required"
ca_path = "/etc/eggrd/client-ca.pem"
crl_paths = ["/etc/eggrd/client-ca.crl"]
forward_client_cert = "xfcc"
[auth]
mode = "mtls"
[auth.mtls]
allowed = ["spiffe://prod.example/ns/web/*"]
# a pool: two servers, a standby behind them
[[upstreams]]
path = "/"
name = "app"
targets = [{ url = "http://10.0.1.5:8080", weight = 2 },
"http://10.0.1.6:8080"]
balance = "least_conn"
backup = ["https://standby.example.com"]
[upstreams.health_check]
path = "/healthz"
expect_status = "2xx"
[server]
header_read_timeout = "30s"
max_connections = 10000
# every client the edge opens itself, set once
[outbound]
proxy = "http://egress.internal:3128"
extra_ca_path = "/etc/eggrd/corp-ca.pem"
△ REVISION 0.7 behaviour that changes on
upgrade. A request that sends its credential header (Authorization, or the API-key
header) more than once is now 401; under mode = "none" or
"mtls", which read no header, duplicates are forwarded for the app to judge, and
every repeated header now arrives with all its values rather than only the last. Connecting to an upstream now gives up after 5 s with a
502 rather than waiting out upstream_timeout, which is what lets a pool
move on in time; validation.upstream_connect_timeout = "0" restores the old wait.
Keep-alive connections idle for 30 s are closed, by the same
header_read_timeout. A streamed response's access line is written when its last
byte is sent, so its size and duration are what was actually sent. Every key is in the
configuration reference.
△ NEW IN 0.7.0 a pool's health check
can use another scheme (health_check.scheme), for a TLS app whose management port
speaks plain HTTP or the reverse, and each target can be checked at another address
(check_host), such as its management network, while requests still go to its
url. The check still sends the target's own name as Host, and an
https:// check of an https:// target verifies the target's own name.
# a TLS app, checked on a plain-HTTP management port on another network
[[upstreams]]
path = "/"
targets = [{ url = "https://app-1.internal:8443", check_host = "10.0.9.5" },
{ url = "https://app-2.internal:8443", check_host = "10.0.9.6" }]
[upstreams.health_check]
path = "/healthz"
port = 9443
scheme = "http" # "" (default) keeps the target's own scheme
△ UPGRADING FROM 0.5
With trust_forwarded_for, the client is now the
X-Forwarded-For entry forwarded_hops from the right, not the
first. Set it from what your balancer appends: 1 for an AWS ALB (default
append mode), Cloudflare, Azure Application Gateway, nginx or Traefik;
2 for a GCP external Application Load Balancer, which appends two entries, or
CloudFront in front of an ALB. Too low makes every client the balancer; anything else, check
before use (how). The header is still believed from
every peer, so let only the balancer reach eggrd's traffic port, and in a chain every proxy but
the outermost only from the one in front. eggrd also now sets X-Real-IP and
Forwarded to the resolved client and X-Forwarded-Host to the
received Host, and removes True-Client-IP. NLB health checks,
rolling restarts and client IP preservation are in the
operations notes.