Gateway Hardening (Phase 4)¶
Status: COMPLETE for the adapter layer and for Kong; PARTIAL for Tyk — its adapter is implemented and unit-verified, its live harness is provided but could not be executed in this environment (§19 requires documenting that rather than marking it verified; see §6).
Source: src/jdlib/security/gateway/ (new)
Tests: tests/unit/security/test_gateway_adapters.py,
tests/infra/test_kong_infra.py (real Kong),
tests/infra/test_tyk_infra.py (real Tyk, skipped unless the stack is up)
Harnesses: tests/infra/kong/, tests/infra/tyk/
| Evidence | Result |
|---|---|
| Unit (gateway adapters) | 27 passed |
| Infra against real Kong 3.6 | 9 passed (skipped without the Kong env vars) |
ruff check . / mypy src/jdlib |
clean |
1. The property this phase establishes¶
An identity asserted by the edge is trusted only when the channel it arrived
on is authenticated. GatewayAdapter is the port (spec #12); the adapters are:
| Adapter | Trust model |
|---|---|
NoGatewayAdapter |
no gateway: identity headers are ignored outright, so a direct caller cannot impersonate a user or tenant |
SharedSecretGatewayAdapter (Kong) |
identity headers are trusted only when the request carries the gateway's own secret, the assertion is complete, and its validity window is sane |
TykGatewayAdapter |
same verification, Tyk's header conventions (x-tyk-jdlib-*) by default |
| HMAC mode | the assertion is signed over subject, tenant, scopes and window; any tampering invalidates it |
2. Fail-closed rules (each has a test)¶
| Condition | Result |
|---|---|
| Secret missing, empty or wrong | no identity (identical outcome — nothing to distinguish for a caller) |
| Assertion incomplete (no subject, unparsable timestamps) | no identity |
| Window expired or not yet valid | no identity |
Window wider than 2 × max_skew (replay) |
no identity |
| Signature mode receiving only a secret header | no identity (no downgrade) |
| Shared-secret mode receiving only a signature | no identity (no substitution) |
| Tampered subject/tenant/scopes/window under signature | no identity |
NONE mode configured with a secret |
rejected at construction (a gateway that is not verified must not look verified) |
| Secret shorter than 32 bytes | rejected at construction |
Secrets are held as SecretStr; repr, model_dump() and model_dump_json()
never contain them. Unknown configuration fields are rejected, so a deployment
cannot add a "trust headers" switch.
3. Real Kong (directive §18)¶
tests/infra/kong/kong.yml is a declarative, DB-less Kong 3.6 configuration and
kong/start.sh brings the stack up with per-run generated credentials. The
upstream reflects the request it received, so what the gateway forwarded is
directly observable. Verified end to end:
| Requirement | Evidence |
|---|---|
| Routing | a gateway request reaches the upstream with the path intact |
| Authentication | no key → 401 "No API key found in request"; wrong key → 401 "Unauthorized" (Kong's answer, not the backend's) |
| Identity propagation | the upstream receives x-jdlib-subject/-tenant/-scopes and the gateway secret, and the adapter verifies the resulting assertion |
| Request ids | x-request-id reaches the upstream, differs per request, and is echoed downstream |
| Spoofed headers | a caller's x-jdlib-subject: admin, foreign tenant, tenants:admin scope and forged secret are all replaced; the adapter then yields the gateway's identity, not the caller's |
| Forged validity window | a caller's issued=0/expires=4102444800 is replaced by the gateway's fresh window; the adapter accepts the gateway's values only |
| Rate limiting | the rate-limited route returns 429 at the edge (8/minute per consumer), 200s before it |
| Failure behaviour | off-gateway identity claims never verify, and NoGatewayAdapter ignores headers entirely |
A vendor trap worth recording¶
Kong's request-transformer replace only touches headers that already
exist: a caller who omits the identity headers gets them omitted upstream, and
a caller who sends them gets them replaced. The configuration therefore uses
remove then add, which yields exactly one canonical value in every case
(add alone would append, leaving a smuggled duplicate behind). The tests
assert both halves, and the incident is documented in kong.yml itself.
4. Gateway failure and the backend¶
The tests establish that identity claims are worthless without the gateway's
secret, which is what makes a bypass harmless for identity. The remaining
requirement is a deployment property, not a code property, and is stated here
so it is not mistaken for something the library can enforce: the backend must
not be reachable from anywhere except the gateway's network, because
availability (not identity) is what an attacker gains by reaching it directly.
NoGatewayAdapter is the safe configuration when no gateway fronting exists.
5. Tyk adapter (verification scope)¶
The adapter is implemented, unit-tested and satisfies the port; its default
header conventions are x-tyk-jdlib-{subject,tenant,scopes,issued-at,expires-at,
gateway-secret,signature}, and everything in §2 applies to it unchanged.
6. Tyk live harness: verified (directive §19)¶
Corrected 2026-09-25 - the harness passes all six of its tests, and both observations recorded below turned out to be fixture defects, not gateway limits.
tests/infra/tyk/ contains a complete, reproducible harness: an API definition
(apps/documents.json) with key auth, rate limiting, global_headers and a
custom middleware, the middleware itself (middleware/stamp_window.js) that
stamps a fresh validity window, a start script that provisions redis, the
gateway and an API key, and tests/infra/test_tyk_infra.py, which asserts the
same properties as the Kong suite and skips unless the stack is running.
The two observations recorded during the failed attempt were both re-diagnosed, and each was a defect in this harness rather than in Tyk:
- "Your API specifies a CP custom middleware" did not mean the image lacks a
runtime. It is what Tyk logs when a middleware entry names a driver it does not
know: this harness wrote
"driver": "javascript", and Tyk's JavaScript driver isotto. With the correct driver the JSVM loads the file (Loading JS File: ... prefix=jsvm) and stamps the window per request.global_headersalone would indeed be static - and therefore correctly refused - so the middleware is genuinely needed; a rich-plugin (gRPC/Python) build is not. - The
404 Not Foundwas the real blocker and was also a harness defect: the start script passedTYK_GW_HOSTNAME=jdlib-tyk, which Tyk uses as the default domain for API definitions that declare none, so it routed only requests carryingHost: jdlib-tyk- including Tyk's own shipped sample API - and answered 404 to everything else. The test client sends no Host at all.
Three further defects were fixed on the way: the rendered gateway config never
substituted its own __GATEWAY_SECRET__ placeholder, so every administrative call
from the start script failed silently; the middleware's global was named
stampWindow while the definition's entry is stamp_window (Tyk looks the entry's
name up in the file, so the request failed with ReferenceError); and
strip_listen_path: false with a path in target_url doubled the upstream path
while global_headers sat at the top level of the definition, where Tyk ignores it
instead of adding it to the version.
The honest status now: the adapter is verified by unit tests and the
deployment is verified against a live Tyk 5.3.1 - all six tests in
tests/infra/test_tyk_infra.py pass with the harness up. The fail-closed property
is unchanged and still unit-verified: a deployment that sends a static validity
window is refused exactly as a replayed one is.
7. Deferred (explicitly not implemented)¶
- Still true, stated precisely. The adapters are not wired into the
library's FastAPI request path or its tenant resolution chain: a deployment
wires them at its own edge, as the reference application does (its
security.pybuildsKongGatewayAdapter), which is the shape this deferral anticipated. The library ships the adapters, their envelope verification and their tests; it does not choose a gateway for you. - Kong's
X-Consumer-*headers are supported via configuration overrides but the test configuration uses thex-jdlib-*convention. - No mTLS channel authentication is implemented; both adapters authenticate the channel with a shared secret or an HMAC signature.