Skip to content

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 a 401.
  • 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 sets Host correctly yields a WSDL a client can use unchanged.
  • X-Correlation-ID and X-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.