The gateway boundary
NexusFabric listens for plain HTTP and plain h2c. It is meant to run behind a reverse proxy, and the two halves divide the work of guarding a request between them. This page draws the line: what the platform decides, what the proxy decides, and what each one needs the other to leave alone.
What the platform decides
Every one of these is configured in the platform and travels with the tenant, the key or the flow —
so it holds identically on all four doors (/run, /enqueue, the gRPC door, and a cron tick, where
it applies).
| Check | Where it is configured | Refusal |
|---|---|---|
| The caller holds a valid key | nexus keys create, per tenant |
401 |
| The key still exists and has not expired | nexus keys revoke, --expires-at |
401 |
| The tenant is registered and enabled | nexus tenant create / disable |
404, identical to an unknown flow |
| The key carries the scope the flow demands | --scopes on the key, required_scope: on the flow |
403 |
| The key is allowed to call this flow | --flows on the key |
403 |
| The caller is inside its request rate | --rate-limit on the key |
429 with Retry-After |
| The body is within the ceiling for this access point | max_message_bytes, three layers |
413 |
| The body carries a valid HMAC signature | nexus serve --secret |
401 |
Two properties of that list matter when you design the front.
The decision happens before the body is read. The gate runs first, in the same step that resolves
the size ceiling, so an unauthenticated caller announcing a large body is refused at 401 and never
causes a byte of it to be read or parsed. A consequence worth knowing when reading logs: with the
gate on you will not see 413 from an anonymous caller — you will see 401.
A refusal is recorded. Every refusal emits an access_denied event into the structured log, and
a refusal that names a subject — a valid key denied a scope, a flow, or a disabled tenant — also
writes a access.denied entry into that tenant’s audit chain. Refusals made by the proxy are in the
proxy’s log and nowhere else, which is the main reason to keep the two sets of checks distinct.
See Authentication for the full behaviour of keys, scopes, allowlists and rate limits.
What the proxy decides
| Concern | Why it sits in front |
|---|---|
| TLS termination | The platform has no TLS option on the inbound side, by design — one certificate store, one renewal job, one place to configure ciphers |
| Client certificates (mTLS) | The platform’s inbound credential is the API key; a certificate is a transport identity and is verified where the transport ends |
| Address allowlists | The platform authorises a key, not an address; allow / deny belongs to the listener |
| Per-route and per-path rate limits | The platform’s limit is per key, across every route that key can call — see below |
| Request filtering by method, path or content | A request the proxy drops never reaches a worker |
| Identity-provider tokens (OIDC / OAuth 2.0) | Validated in front; see below |
| Spreading load across engines | Several engines behind one address — see High availability |
Rate limiting, per key and per route
The platform’s limiter is a token bucket on the key: capacity is twice the configured rate, and it is shared by every route that key is entitled to call. That is the right shape for “this partner may send twenty messages a second”, and it is the shape that bounds the audit chain, because an attributable refusal spends a token too.
It cannot express “this path may be called twenty times a second regardless of who calls it”. Write that in the proxy, on the location, where the path is what you have. The two compose: the proxy’s limit shapes the traffic reaching the port, and the platform’s limit holds per partner once it is through.
Identity-provider tokens
Inbound, the platform reads one credential: Authorization: Bearer nxk_…, its own key. A token
issued by an identity provider is validated in front — the proxy checks the signature, the issuer,
the audience and the expiry, and forwards the request with the API key that names the tenant. That
keeps token validation, key rotation and JWKS fetching where the certificates already live, and
keeps the platform’s decision on one credential it can attribute to a registered tenant.
Outbound is the other direction and the platform does it itself: an oauth2_token step performs a
client_credentials exchange and the next step sends the token on. See
Effects.
A worked front
The pieces that matter are the size ceiling, the address lists, the per-location rate limit, and leaving the request otherwise untouched.
limit_req_zone $binary_remote_addr zone=flows:10m rate=20r/s;
upstream nexusfabric { server 127.0.0.1:9090; }
server { listen 443 ssl http2; server_name integrations.example.org;
ssl_certificate /etc/ssl/certs/integrations.pem; ssl_certificate_key /etc/ssl/private/integrations.key;
# At or below the installation's max_message_bytes, so an oversized body # is refused here rather than read twice. client_max_body_size 1m;
location /flows/ { allow 10.0.0.0/8; deny all;
limit_req zone=flows burst=20 nodelay;
proxy_pass http://nexusfabric; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
# The management UI has its own session login. Restrict it separately. location /ui/ { allow 10.1.0.0/16; deny all; proxy_pass http://nexusfabric; }
# Liveness carries no credentials and no body. location = /health { proxy_pass http://nexusfabric; }}The gRPC door is a separate port and speaks h2c. Terminate TLS the same way and hand the plaintext
stream on with grpc_pass:
server { listen 8443 ssl http2; server_name integrations.example.org;
ssl_certificate /etc/ssl/certs/integrations.pem; ssl_certificate_key /etc/ssl/private/integrations.key;
location / { grpc_pass grpc://127.0.0.1:9091; }}What the proxy must leave alone
Authorization. It is the credential. A proxy that strips or rewrites it turns every request into a401.- The path. The tenant and the flow are path segments, and the platform resolves the artifact
from them. Rewrite the prefix if you must, but keep
/flows/{tenant}/{name}/…intact behind it. Host. The generated WSDL builds its endpoint address from the request’s scheme and host, so a proxy that setsHostcorrectly yields a WSDL a client can use unchanged.X-Correlation-IDandX-Request-ID, when the caller sends one. The platform adopts the first it finds and threads it through every log record and every outbound call; a new one per hop makes a trace unreadable. See Observability.X-Nexus-Signature, when you run with--secret. The signature is over the raw body, so a proxy that re-encodes, decompresses or reformats the body invalidates it.
Two doors that answer without a key
GET /health and GET /flows/{tenant}/{name}/wsdl are public by design: a liveness probe carries no
credentials, and a WSDL is a discovery contract read by tooling that has none. The WSDL route still
checks that the tenant is visible, so a disabled or unregistered tenant gets the same 404 as an
unknown flow.
If a deployment should not publish its service contracts to the network at large, that is a decision
for the front: keep /health open to the load balancer’s subnet and /wsdl open to the partners who
need it.