gateway
gateway is the only door into a cafaye deployment. It authenticates callers,
limits what they may ask for, and forwards each request to the service that
actually owns the work. It is written in Go — net/http plus chi, with the
server generated from openapi/v1.yaml by oapi-codegen, so the contract is the
framework.
Two behaviours that are load-bearing and will not be relaxed
Section titled “Two behaviours that are load-bearing and will not be relaxed”The token never chooses how it is checked. The algorithm is pinned server-side
and the protected header is read before any key is fetched, so an alg: none
or HS256 token costs no network call and never reaches a verifier.
Liveness never touches a dependency. /healthz is unconditional while only
/readyz may fail because something else is down, so a database or identity
outage cannot get the process restarted out from under in-flight requests. Both
probes are also exempt from rate limiting, because a throttled probe is an
orchestrator that cannot see a healthy process.
Rate limiting
Section titled “Rate limiting”The limiter is a sliding window (GCRA) counting in one atomic step, rather than a fixed window that gives every client its full allowance at the boundary and therefore permits a burst of twice the limit across it. GCRA spends the allowance continuously, so two requests two milliseconds apart cannot both be admitted at the end of an exhausted bucket.
There is a per-route limit table: gateway-api, gateway-auth,
gateway-auth-register, gateway-auth-login, gateway-auth-logout and
gateway-api-keys — six policies, read out of the shipped table rather than
guessed from the routes. The probes are not in it, deliberately. The sixth is a
leftover: /v1/api-keys is still in the table at 60/min, and no such route
exists in either the router or the OpenAPI document — so it prices a surface
that is not served. It is recorded here rather than quietly dropped, because the
entry is the seam the packet that brings the document up will mount against.
What is shared and what is not, exactly:
Default (unset REDIS_URL) |
With REDIS_URL |
|
|---|---|---|
| Rate-limit counters | one process’s memory | Redis, one GCRA Lua script — shared by every replica, survives restart |
| Browser sessions | no browser surface at all | Redis — shared by every replica, survives restart |
The session store rows are the cutover’s biggest operator-visible change, and
the direction is not the obvious one. A Go gateway without REDIS_URL serves no
/auth/* routes at all rather than a degraded ones: browserSurfaceFromEnv
builds the surface only when IDENTITY_URL and REDIS_URL are both set,
because a session store has to be shared to be worth having. So REDIS_URL is
now a hard requirement for the browser surface, and the old advice to run one
replica because sessions were a per-process Map no longer applies — a session
survives a restart and is shared by every replica. The TypeScript reference that
kept the Map was deleted at cutover, so the two implementations no longer
differ here. What is still absent is a CSRF token: the origin gate is the
defence, and a double-submit token would be a second thing to get right for the
same protection.
Set REDIS_URL and the counters are shared; without it, every replica grants its
own allowance and every restart resets them. REDIS_URL=redis//redis is refused
at startup, and a malformed value is an error rather than a silent default. The
connection is opened on first use, and Redis is a registered readiness
dependency, so /readyz reports {"deps":{"identity":"ok","redis":"unavailable"}}
rather than failing every request.
Two numbers to plan against
Section titled “Two numbers to plan against”The fetched key set is cached for IDENTITY_JWKS_TTL_MS (default 300000 — five
minutes), and that TTL is also the revocation window: a key identity
withdraws keeps verifying until the cache expires.
A failed key-set fetch is a 503, not a 401, so a platform outage is never
mistaken for a caller’s expired token and sends them to re-authenticate against a
healthy service.
API keys — gone with the cutover, not shipped
Section titled “API keys — gone with the cutover, not shipped”A script cannot hold a scoped API key today. This section said it could, and
that was true of the TypeScript implementation: src/middleware/apiKey.ts
issued, hashed, scoped and revoked keys, and a key authenticated on any route a
token did. It had no Go counterpart at any depth — no ApiKeyStore, no
hashing, no prefix check, no gate — and src/ was deleted at cutover, so the
capability is not in this repository in either language. The only Go trace is
the seam the limiter reads (auth.WithAPIKeyID / APIKeyIDOf), and nothing
sets it, so a key source is always empty.
This is a document gap before a code one: openapi/v1.yaml declares seven
paths and zero api-keys operations (grep -c api-keys openapi/v1.yaml → 0),
so the generated router has nowhere to mount a store. It also makes a Go gateway
less strict than the TypeScript one was for a caller holding a key — that class
of caller is now un-refused rather than refused without a scope. Until the
document changes, a bearer JWT is the only script credential, and revoking
one means waiting out IDENTITY_JWKS_TTL_MS.
- Repository: github.com/cafaye/gateway
- Language: Go (
net/http+chi, server generated fromopenapi/v1.yaml) - Namespace / routing prefix:
gateway - Container port: 8080
- Environment:
PORT,IDENTITY_ISSUER,IDENTITY_JWKS_URL,IDENTITY_JWKS_TTL_MS,GATEWAY_CLIENT_ID,IDENTITY_URL,REDIS_URL,RATE_LIMIT_REQUESTS,RATE_LIMIT_WINDOW_MS,TRUSTED_PROXIES, and the two groups that switch routing on:ROUTE_TABLE, plusASSERTION_SIGNING_KEY_FILE,ASSERTION_SIGNING_KEY_ID,ASSERTION_ISSUERandASSERTION_TTL_MS— see Topology