Edge gateway ingress
Preserve layer-4 identity
Every service address must resolve to one exact local listener. A public address assigned directly to the host binds as-is and needs no map entry. Behind NAT, map it to one distinct private listener address. A firewall or load balancer must pass TCP and TLS through without terminating TLS or collapsing several advertised addresses onto one local IP.
The edge gateway is the only process that binds customer HTTP and HTTPS service addresses on a gateway-enabled edge. It performs a bounded lookup on destination address plus HTTP Host or TLS SNI, then proxies the untouched connection to an assigned OpenResty cell. It does not terminate customer TLS, load certificates, cache content, run WAF rules, contact origins, or call the control plane.
Data and activation flow
PostgreSQL remains the desired-state source. Signed edge artifacts carry hostname and pool assignment. The edge agent validates and atomically activates those artifacts, combines them with operator-owned service bindings, and writes gateway.json. The gateway validates the complete candidate before swapping one immutable routing table. It writes last-valid.json before activation and uses it when a candidate is absent or invalid at restart.
On the first start of a new edge, neither file exists yet. The gateway remains running with no listeners and reports not-ready while polling for the agent's first atomic candidate. This fail-closed bootstrap state is normal and does not create a synthetic last-valid map. Other read or validation failures remain fatal when no valid fallback exists.
An explicit candidate with zero listeners and zero routes is valid desired state for an edge with no eligible endpoints, or after every endpoint is withdrawn. Activating it closes any previous listeners and reports the empty revision as ready. A listener-only candidate is also valid when assigned cells back the first endpoint before any hostname is proxied; unmatched HTTP and TLS connections are rejected without dialing a cell. A route without its matching address/protocol listener remains invalid.
The agent fetches the bounded, revisioned bindings generated from ready pool endpoints and participating cells over its mTLS control connection. Production cells use their loopback ports. Containerized development may set EDGE_CELL_TARGETS, a bounded cell-name-to-HTTP/HTTPS-endpoint map, to replace targets with private container DNS names and ports.
Production keeps endpoint identity separate from translated host listeners. A public address assigned directly to the host binds as-is with the default empty map. Behind one-to-one NAT, set EDGE_GATEWAY_ADDRESS_MAP to an exact mapping such as {"198.51.100.40":"10.20.0.40"}. The firewall or layer-4 load balancer forwards the advertised address without terminating TLS. Cross-family pairs, wildcards, public-to-different-public mappings, and reused local addresses reject the candidate and preserve the previous valid map. Set EDGE_GATEWAY_REQUIRE_ADDRESS_MAP=true only when policy requires complete explicit coverage.
EDGE_GATEWAY_BINDINGS remains an emergency rollout override. When set, its static JSON is authoritative and dynamic endpoint changes are intentionally ignored. Its shape is a JSON array with at most 32 entries:
[
{
"address": "192.0.2.10",
"pool": "shared-default",
"http": "127.0.0.1:18081",
"https": "127.0.0.1:18444"
},
{
"address": "2001:db8::10",
"pool": "shared-default",
"http": "127.0.0.1:18081",
"https": "127.0.0.1:18444"
}
]The address fields remain advertised identities. The production agent applies the optional NAT translations in EDGE_GATEWAY_ADDRESS_MAP before compiling this emergency override; direct local addresses remain unchanged.
For a multi-cell pool, replace the top-level http and https target with a bounded cells array. Each item has the stable name, http, and https target for that cell. The compiler uses each cell's assigned hostname set, so unrelated cells receive neither routes nor domain artifacts:
[{"address":"192.0.2.10","pool":"shared-default","cells":[
{"name":"cell-01","http":"127.0.0.1:18081","https":"127.0.0.1:18444"},
{"name":"cell-03","http":"127.0.0.1:18083","https":"127.0.0.1:18446"},
{"name":"cell-04","http":"127.0.0.1:18084","https":"127.0.0.1:18447"}
]}]Every mapped local address must be assigned to the host. Each address/pool pair expands only to hostnames assigned by signed artifacts. Candidates are rejected for unknown pools, invalid addresses or targets, incomplete local mapping, duplicate address/hostname pairs, more than 64 listeners, more than 100,000 protocol routes, or a size over 32 MiB.
The gateway sends PROXY protocol version 2 to private cell ports 8081 and 8444. This is the trusted client-identity contract for HTTP and encrypted HTTPS. Cell ports 8080 and 8443 are not published. All customer traffic is forced through the gateway and reaches cells only on the private contract ports.
Deployment and migration
Production uses host networking so sockets see the destination address. Grant only NET_BIND_SERVICE; drop all other capabilities. Restrict port 9105 to the edge agent and monitoring source. Set EDGE_GATEWAY_MAX_CONNECTIONS from the qualified host ceiling; invalid or out-of-range values use 8,192. The immutable image remains non-root and applies cap_net_bind_service only to the gateway executable so the Compose capability is effective after exec. Never replace this boundary with a privileged or root container.
- For a directly addressed host, confirm each advertised service address is assigned to a local interface; no map entry is needed. Otherwise allocate one distinct private/local address for every advertised service address.
- For the private-address case, configure one-to-one DNAT or layer-4 forwarding for TCP
80and443, preserving the connection and TLS stream. Put every translated pair inEDGE_GATEWAY_ADDRESS_MAP. - Confirm every ready endpoint and participating cell appears in the agent-fetched gateway candidate. Do not set the static override during normal operation.
- Start the agent, cells, state initializer, and gateway. Require gateway readiness and a map revision equal to the latest desired gateway revision.
- Probe every configured address/Host and address/SNI combination. Run IPv6 probes only when an IPv6 service address is configured.
- Confirm cell HTTP/HTTPS ports are not publicly published before enabling customer DNS.
Never edit generated gateway, cell, or last-valid files. Change desired state or operator bindings and let reconciliation render a candidate.
Monitoring and failures
/metrics exposes readiness, active revision, listener and route counts, connections, bounded errors, activations, and candidate rejections. The agent reports this bounded snapshot in its heartbeat and the administrator edge page displays it and refreshes live runtime state every five seconds. Readiness is false when the gateway is unavailable or its active map revision differs from the latest desired gateway revision. A revision-only change is activated even when the rendered routes are otherwise identical.
The active configuration sequence and gateway map revision are monotonic identities, not retained configurations or allocated resources. They can safely grow to millions; do not reset them daily or weekly. Resetting would remove the ordering used to reject stale candidates and acknowledgements. PostgreSQL artifact retention may remove old payload rows without reusing or resetting their sequence values.
Gateway candidates retain every assigned, non-drained fixed cell even when its latest health report is degraded. Health affects traffic readiness and placement decisions, but it never turns desired state into an empty invalid binding or prevents the agent from reporting recovery.
Gateway routes is the size of the current in-memory routing map. One hostname may contribute an HTTP and an HTTPS route for each service address on which it is available. It is not a lifetime counter. Inspect desired hostname placement in the domain and cell views; inspect the generated map only through bounded gateway metrics and qualification tooling, never by editing the derived file.
- Unknown or malformed Host/SNI: close before dialing a cell.
- Unknown destination/name pair: close before dialing a cell.
- Invalid candidate: retain the active table and increment rejection metrics.
- Cell outage: only routes targeting that cell fail.
- Control-plane outage: continue from local active state.
- Gateway restart: load a valid candidate or
last-valid.json; on a clean installation with neither file, remain not-ready and wait for the agent. - Agent restart: rebuild derived files from durable signed local state.
The first distinct candidate error is logged immediately. An unchanged error continues to be retried and counted in metrics but is summarized at most once per five minutes; successful activation emits one recovery event. Runtime-reader generation mismatches receive a short convergence grace and are logged as one bounded aggregate, also at most once per five minutes while unchanged.
If the gateway process is ready but an endpoint says gateway_not_ready, first confirm the edge heartbeat is fresh. A stale panel snapshot can be distinguished from a real outage because the live status section refreshes every five seconds. For a real failure, compare gateway readiness and revision with the agent's active sequence, then inspect the agent log for a rejected heartbeat or candidate.
Logs contain the listener and a bounded reason, never bodies, maps, certificates, customer content, or secrets.
Qualification evidence
On 2026-07-27 python3 tests/e2e/gateway_ingress.py passed real HTTP Host and HTTPS SNI routing through OpenResty on dual-stack and IPv4-only gateways, unknown-route rejection, restart, invalid-candidate rejection, and last-valid recovery. HTTPS used the active Pebble ACME root with strict hostname and chain verification; no insecure client override was used.
The reproducible 50,000-mapping test ran on a VMware x86_64 host with 32 Intel Xeon E5-2697 v4 vCPUs, 15 GiB RAM, Docker 29.1.3, and Go 1.24.6. It used two dual-stack pairs, eight listeners, 50,000 host mappings, 100,000 protocol routes, and 64 concurrent workers.
| Result | Value |
|---|---|
| Candidate validation | 242.192 ms |
| Additional Go heap | 12.59 MiB |
| Concurrent lookups | 1,280,000 |
| Lookup wall / CPU | 0.209 s / 3.439 CPU-s (16.45 average cores) |
| Lookup throughput | 6,120,620/s |
| Average lookup latency | 0.163 µs |
| Saturation | Not observed at 64 workers |
| Accepted qualification ceiling | 64 concurrent lookup workers |
The live-socket load used 50,000 mappings and a bounded local TCP upstream. At 16, 64, and 128 concurrent connections it completed 3,200, 12,800, and 25,600 requests with zero errors. The 128-concurrency tier sustained 8,683 requests/s with p50/p95/p99 latency of 13.480/22.356/26.960 ms; saturation was not observed, so 128 is the accepted connection-concurrency ceiling from this run.
The runtime suite separately qualifies socket parsing, PROXY protocol identity, OpenResty handoff, and TLS pass-through. Operators must load-test their NIC, kernel, and uplink before setting production limits.