This document expands the eleven canonical ranking policy profiles
defined in TASK.md §4 — plus the one supplemental
capability scenario p03-jwks-rs256-basic (off-grid; documented in
§ p03-jwks-rs256-basic below) — into concrete,
testable configurations. Every gateway under test must be configured to
produce externally observable behaviour identical to what is
described below, or the corresponding cell is marked as a deviation /
feature-missing.
Numbering note. The repository ships 12 numbered profile directories (
gateways/<gw>/p01-…/through…/p12-…/).p03-jwks-rs256-basicis the supplemental capability scenario; the other 11 (p01, p02, p04..p12) are the canonical ranking matrix. See § Canonical profile list for the full mapping.
- One profile — one aspect. Profiles are orthogonal; they are not
combined except in the explicit
full-pipelineprofile. - Parity before metrics. A gateway's cell is included in the final ranking only if its configuration passes parity attestation.
- Identical values across gateways. Every secret, limit, header
name and JSON field listed here is the ground truth. The same
constants live under
gateways/_reference/. - No third-party plugins unless explicitly called out (e.g. the
Traefik JWT community plugin). Such cases become entries in
docs/GATEWAYS.md.
All profiles run over plaintext HTTP/1.1 unless stated otherwise. HTTP/2 and HTTP/3 must be forcibly disabled on every gateway.
| # | ID | Profile name | Role | Description |
|---|---|---|---|---|
| p01 | vanilla |
Vanilla | ranking | Pure proxy, no policies applied |
| p02 | jwt |
JWT verification (HS256) | ranking | HS256 against a shared secret (BENCH_JWT_SECRET) |
| p03 | jwks-rs256-basic |
JWT verification (RS256 + JWKS) | supplemental | RS256 + static inline JWKS — capability axis, off-grid (see § p03-jwks-rs256-basic) |
| p04 | rl-static |
Static service-wide rate limit | ranking | 1000 req/s per service, rolling 1s window |
| p05 | rl-endpoint |
Per-endpoint static rate limit | ranking | 100 req/s scoped to /anything/limited; /anything/free stays unrestricted |
| p06 | rl-dynamic-low |
Dynamic rate limit, low cardinality | ranking | 10 req/s per client IP, pool of 100 distinct IPs |
| p07 | rl-dynamic-high |
Dynamic rate limit, high cardinality | ranking | 100 req/s per client IP, pool of 50 000 distinct IPs |
| p08 | req-headers |
Request headers rewrite | ranking | Add X-Bench-In: 1, remove X-Forwarded-For |
| p09 | resp-headers |
Response headers rewrite | ranking | Add X-Bench-Out: 1, remove Server |
| p10 | req-body |
Request body rewrite (JSON) | ranking | Add .bench.injected = true, remove .secret |
| p11 | resp-body |
Response body rewrite (JSON) | ranking | Add .bench.injected = true, remove .origin |
| p12 | full-pipeline |
Full pipeline | ranking | JWT ▶ rl-static ▶ req-headers ▶ req-body ▶ upstream ▶ resp-headers ▶ resp-body |
Two of the canonical ranking profiles are additionally exercised over HTTP/1.1 + TLS (per TASK.md §6):
| Scenario ID | Drives policy | Protocol | Notes |
|---|---|---|---|
s13-vanilla-https |
p01-vanilla |
HTTPS | Same as vanilla but with TLS termination |
s14-full-pipeline-https |
p12-full-pipeline |
HTTPS | Same as full-pipeline but with TLS termination |
That is the full 13-scenario matrix referenced in
TASK.md §11: 11 ranking HTTP scenarios (one per ranking
profile) + 2 HTTPS scenarios. The supplemental p03-jwks-rs256-basic
runs through parity attestation only and is not part of the
ranking matrix.
Before any load is generated, the orchestrator runs
scripts/parity-attestation.sh
against the gateway in the current configuration. The script fires a
small, deterministic set of probes (see fixtures/) and
asserts that:
- The response status code matches the expected value.
- Response headers contain / do not contain the expected markers.
- If applicable, the backend observed the expected transformation of
the request (the backend is our own
backend— we read its echo).
The script emits machine-readable JSON per cell:
{
"gateway": "wallarm",
"profile": "p02-jwt",
"status": "PASS",
"checks": 24,
"passed": 24,
"failed": 0,
"deviations": []
}Possible terminal statuses:
PASS— the cell contributes to the ranking.FAIL— the cell is excluded from the ranking; coloured red.FEATURE-MISSING— the gateway cannot implement the policy natively; coloured amber. A distinct status fromFAIL.DEVIATION— the cell runs, but one or more observable aspects differ; counted separately.
All constants live once in
gateways/_reference/values.yaml
and are loaded by the attestation script and by every gateway config
generator.
| Name | Value |
|---|---|
BENCH_SERVICE_NAME |
bench-service |
BENCH_UPSTREAM_HOST |
backend |
BENCH_UPSTREAM_PORT |
8080 |
BENCH_UPSTREAM_PATH |
/anything |
BENCH_CLIENT_HEADER_ADD |
X-Bench-In: 1 |
BENCH_CLIENT_HEADER_DROP |
X-Forwarded-For |
BENCH_SERVER_HEADER_ADD |
X-Bench-Out: 1 |
BENCH_SERVER_HEADER_DROP |
Server |
BENCH_JSON_FIELD_ADD_PATH |
$.bench.injected |
BENCH_JSON_FIELD_ADD_VAL |
true |
BENCH_JSON_FIELD_DROP_IN |
$.secret |
BENCH_JSON_FIELD_DROP_OUT |
$.origin |
- Algorithm: HS256 (HMAC-SHA-256). Core p02 is deliberately a
symmetric-secret probe so every gateway gets asked the exact
same question. The asymmetric RS256 + JWKS axis lives in the
supplemental scenario
p03-jwks-rs256-basicbelow, so this profile never has to flex its binding shape to accommodate gateways whose JWT primitive is asymmetric-only. - Shared secret:
bench-jwt-hs256-secret-2026— public by design; this secret has never been used in any production system. - Canonical payload:
{ "sub": "bench", "role": "tester", "iss": "gateway-benchmarks" }. - Expiry: every probe token is minted with
exp = now + 3600seconds. - Header name carrying the token:
Authorization: Bearer <jwt>. - JWKS fallback: if a gateway only supports JWKS-based validation
(not a shared secret), a static HS256 (
kty: oct) JWKS is served fromgateways/_reference/jwks/jwks.jsonover the benchmark network. That JWKS is intentionally NOT the same asset as the p03 RS256 JWKS — it just wraps the same HS256 secret in JWK form. Gateways that fall back this way are listed in GATEWAYS.md § deviations.
Parity probes (p02):
| # | Probe | Expected status |
|---|---|---|
| 1 | No Authorization |
401 |
| 2 | Garbage bearer | 401 |
| 3 | Valid HS256 token | 200 |
| 4 | Expired HS256 token | 401 |
| 5 | Wrong-secret HS256 token | 401 |
- Limit: 1000 req/s per service, rolling window = 1 second.
- Key: the service itself (all requests share one bucket).
- Above-limit behaviour: HTTP 429 with
Retry-After: 1. - Storage: every gateway's native store (no shared Redis).
Parity probes (p04): fire 1200 requests in 1 second. Expect at least 150 responses with status 429 (tolerance ±50). Latency of the 2XX responses is measured in the load phase, not here.
- Limit: 100 req/s, rolling window = 1 second.
- Scope: a single specific endpoint path (the "limited" endpoint
at
/anything/limited), not the whole service. Every other path on the same gateway must stay unrestricted. - Key: the endpoint itself. All requests to the limited path share one bucket; requests to any other path bypass the bucket.
- Above-limit behaviour: HTTP 429 with
Retry-After: 1. - Storage: every gateway's native store (no shared Redis).
This profile is orthogonal to p04 / p06 / p07:
p04answers "can the gateway rate-limit the whole service?".p06/p07answer "can the gateway rate-limit per client IP?".p05answers "can the gateway scope a rate-limit to one route without leaking into its neighbours?" — a distinct production axis (per-endpoint policy attachment, route selector precision) that's particularly relevant for gateways whose rate-limit primitive is authored as an API-level policy (Tyk, Kong, APISIX) rather than a network-level filter.
Parity probes (p05): four deterministic probes exercise both sides of the scoping invariant.
| # | Probe | Expected |
|---|---|---|
| 1 | GET /anything/free (below any limit) |
200 |
| 2 | GET /anything/limited (below the 100-rps limit) |
200 |
| 3 | 1200-request burst on /anything/limited in 1 s |
>= 150 × 429 (tolerance ±50) |
| 4 | 1200-request burst on /anything/free in 1 s |
0 × 429, >= 1100 × 2xx |
Probe 4 is the scoping check: the same gateway process, the same TCP listener, the same client pool — a burst that would trivially trip the 100-rps limit fires against the unrestricted endpoint instead and must not see a single 429.
Both profiles key the limit by the source IP as the gateway sees
it. The load generator rotates through an IP pool using the X-Real-IP
header (see docs/LOAD-PROFILES.md) because
physical IPs cannot be rotated from a single container.
Parity note: gateways are configured to trust X-Real-IP only
from the loadgen's network; the benchmark pinned-cluster-placement-group
topology makes that trust boundary safe.
| Profile | Limit | IP pool size | Trust source |
|---|---|---|---|
p06-rl-dynamic-low |
10 req/s per IP | 100 | X-Real-IP |
p07-rl-dynamic-high |
100 req/s per IP | 50 000 | X-Real-IP |
Parity probes (p06): with 10 distinct IPs firing 15 req/s each for 3 seconds, each IP must see about 15 × 429 responses (tolerance ±5).
Reshape applied by the gateway:
add: X-Bench-In: 1
remove: X-Forwarded-For
Parity probes (p08): the backend (go-httpbin /headers) must echo
X-Bench-In: 1 and must not echo X-Forwarded-For regardless of
what the client sent.
Reshape applied by the gateway to the upstream's response:
add: X-Bench-Out: 1
remove: Server
Parity probes (p09): the client must receive X-Bench-Out: 1 and
must not see a Server: header.
-
Incoming body (client ⇒ gateway):
{ "msg": "hello", "secret": "please-drop-me", "bench": { "from_client": true } } -
Outgoing body (gateway ⇒ upstream):
{ "msg": "hello", "bench": { "from_client": true, "injected": true } } -
Rule: add
$.bench.injected = true, remove$.secret. -
Content-LengthandTransfer-Encodingmust be recomputed by the gateway; the parity script inspects both.
Parity probes (p10): the backend's /anything echoes the incoming
body. Assert .json.bench.injected == true and .json.secret absent.
The upstream is go-httpbin and every /anything response has the
shape:
{
"method": "GET",
"url": "...",
"origin": "172.19.0.3",
"headers": { ... }
}- Rule: add
$.bench.injected = true, remove$.origin. $.originis chosen because go-httpbin always returns it, so the drop rule is always exercised.Content-Lengthmust be recomputed by the gateway.
Delivered body (gateway ⇒ client):
{
"method": "GET",
"url": "...",
"headers": { ... },
"bench": { "injected": true }
}Parity probes (p11): the load generator asserts $.bench.injected == true
and $.origin is absent from the received body.
Composition of p02 + p04 + p08 + p10 + p09 + p11 in that order:
client
│ Authorization: Bearer <valid-hs256> (p02)
│ body = { "msg": "hello", "secret": "..."}
▼
gateway
│ validate JWT (p02)
│ decrement rate-limit bucket (p04)
│ add X-Bench-In, drop X-Forwarded-For (p08)
│ body: drop .secret, add .bench.injected (p10)
▼
backend
│ echo
▼
gateway
│ body: drop .origin, add .bench.injected (p11)
│ add X-Bench-Out, drop Server (p09)
▼
client
Parity probes (p12): the test script runs every per-profile probe and combined probes exercising the full chain at once. A single missed transformation fails the cell.
The s14-full-pipeline-https scenario is identical in payload and
transformations — the only added layer is TLS termination at the
gateway edge using the shared cert from
gateways/_reference/tls/.
p03-jwks-rs256-basic is a supplemental capability scenario that
sits outside the 11-profile ranking matrix. It rides the same
parity-attestation harness (scripts/parity-attestation.sh, one
fixture, same probe schema) and is driven through the same entry
point:
make parity-gateway \
PARITY_GATEWAY=<gw> \
PARITY_PROFILE=p03-jwks-rs256-basic— but it is never pulled into make parity-gateway-all, never
contributes to the throughput ranking, and never reshapes an existing
ranking profile to accommodate itself. Running it is always a
deliberate opt-in. (The number p03 was chosen so the canonical
p02-jwt + RS256 capability sit next to each other in directory
listings; the canonical p02 stays HS256-only.)
The rationale: some capabilities (RS256+JWKS, mTLS client auth, OPA, gRPC-transcoding, …) are genuine axes worth measuring but are not things every gateway should be graded on. Locking them into the core matrix would force the canonical p01…p12 ranking questions to bend, which breaks "identical values across gateways" (§ Principles). A separate supplemental track keeps the ranking matrix honest while still letting us publish a capability read-out per gateway.
- Algorithm: RS256 (RSA-2048 + PKCS#1 v1.5 over SHA-256).
- Key distribution: a static, inline JWKS passed directly to the
gateway's policy binding. Not
jwks_uri— the first iteration deliberately has zero moving network parts. - JWKS content: one JWK, derived from
gateways/_reference/jwks-rs256/public.pemand checked in atgateways/_reference/jwks-rs256/jwks.json. The JWK carries the canonicalkid: bench-rs256-2026. - Private key:
gateways/_reference/jwks-rs256/private.pem— public by design, like every other key material under_reference/. It is used only byscripts/gen-jwt-rs256.shto mint probe tokens. - Payload: same template as p02 (
sub/role/iss), expirynow + 3600, header envelopeAuthorization: Bearer <jwt>. - Token variants (see
scripts/gen-jwt-rs256.sh):valid— headerkid = bench-rs256-2026, signed with the canonical private key.unknown-kid— headerkid = unknown-kid-2026, signed with the same canonical private key (so the signature itself is valid against the private key, but no JWK with thatkidexists in the inline JWKS — a correct JWKS verifier must reject).
Parity probes (p03-jwks-rs256-basic):
| # | Probe | Expected | Axis |
|---|---|---|---|
| 1 | No Authorization header |
401 |
Missing credential |
| 2 | Valid RS256 token, kid = bench-rs256-2026 |
200 |
JWKS kid→JWK lookup + RS256 signature verify |
| 3 | RS256 token, kid = unknown-kid-2026 (sig valid) |
401 |
JWKS kid-lookup rejects unknown key id |
Probe 3 is the one that makes this scenario meaningful: a verifier
that just tries every JWK against the signature would accept the
token; a correct JWKS verifier keys the lookup by kid and must
reject because no JWK with that id exists in the inline JWKS.
Reference fixture:
fixtures/p03-jwks-rs256-basic.jsonl.
Token generator:
scripts/gen-jwt-rs256.sh. Reference
assets:
gateways/_reference/jwks-rs256/README.md.
jwks-rs256-uri— RS256 + JWKS served overjwks_uri, measuring the JWKS-rotation path (cache TTL, cold-fetch latency, unavailable JWKS server). Built on the same key material asp03-jwks-rs256-basic.mtls-basic— mutual TLS with client certificate validation.
Each new supplemental scenario gets its own directory under
gateways/_reference/<slug>/ and its own fixture under
fixtures/<slug>.jsonl. No supplemental scenario may modify the
canonical ranking assets (p01, p02, p04..p12).
Known or expected limitations per gateway (refined continuously in GATEWAYS.md § deviations):
| Profile | wallarm | nginx | envoy | kong | apisix | traefik | tyk |
|---|---|---|---|---|---|---|---|
| p01 vanilla | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| p02 jwt (HS256) | ✓ | Lua* | Lua* | ✓ | ✓ | plugin* | ✓ |
| p04 rl-static | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| p05 rl-endpoint | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| p06 rl-dyn-low | ✓ | ✓ | ✓ | ✓ | ✓ | ✓‡ | ✓ |
| p07 rl-dyn-high | ✓ | ✓† | ✓ | ✓ | ✓ | ✓‡ | ✓ |
| p08 req-headers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| p09 resp-headers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| p10 req-body | ✓ | Lua* | Lua* | ✓ | ✓ | plugin* | ✓ |
| p11 resp-body | ✓ | Lua* | Lua* | ✓ | ✓ | plugin* | ✓ |
| p12 full-pipeline | ✓ | Lua* | Lua* | ✓ | ✓ | plugin* | ✓ § |
The supplemental
p03-jwks-rs256-basiccapability is reported in its own table below — by design it is not part of the ranking matrix.
Legend: ✓ native · Lua* via lua-nginx-module / Lua filter · plugin*
local Yaegi plugin shipped under gateways/<gw>/_shared/plugins-local/
(not a third-party catalogue dependency) · † requires explicit zone
sizing for the 50 k key pool · ‡ requires
entryPoints.web.forwardedHeaders.insecure: true in the static config
so traefik trusts X-Real-IP on the bench-net (safe because loadgen
runs against localhost; see
GATEWAYS.md § Deviations [gw=traefik, p=p06/p07])
· § Tyk's JWT middleware (mw_jwt.go) hard-codes 400/403 instead
of the canonical 401 on rejection paths — capability works (PASS on
every probe that exercises the underlying primitive) but the
status-code axis is a documented PARTIAL — see
GATEWAYS.md § Deviations [gw=tyk, p=p02-jwt]
· — no known way to implement without pulling in a full programmability
layer the gateway does not ship.
When a cell is —, the corresponding run is marked
FEATURE-MISSING and contributes only to the "features" summary, not
to the throughput ranking.
The supplemental p03-jwks-rs256-basic scenario gets its own
capability read-out. It does NOT contribute to the ranking; it
documents which gateways can natively cover the RS256 + JWKS axis.
| Scenario | wallarm | nginx | envoy | kong | apisix | traefik | tyk |
|---|---|---|---|---|---|---|---|
p03-jwks-rs256-basic |
✓† | ✓◊ | ✓‡ | ✓★ | ✓¶ | ✓♦ | ✓§ |
Legend: ✓ native · † native jwt_validation policy on the
from-source Wallarm API Gateway build (passed via WALLARM_IMAGE) —
PASS 3/3 · ‡ native
envoy.filters.http.jwt_authn with local_jwks.inline_string on
envoyproxy/envoy:distroless-v1.32.6 — PASS 3/3
(asymmetric-only — exactly the primitive this axis measures; notably
this is also why canonical p02-jwt needs a Lua-filter fallback on
envoy) · § native jwt_signing_method: rsa + JWKS-over-HTTP on
tykio/tyk-gateway:v5.11.1 — PASS 1/3: capability (JWKS fetch,
kid lookup, RS256 verification, unknown-kid rejection) works
correctly, but Tyk's rejection status codes are hard-coded in
mw_jwt.go as 400 "Authorization field missing" and 403 "Key not authorized" instead of the canonical 401, so probes 1 and 3 FAIL
on the status-code axis while probe 2 PASSes cleanly. See
gateways/tyk/p03-jwks-rs256-basic/NOTES.md
for the full breakdown · ¶ native openid-connect plugin
(use_jwks: true + OIDC discovery URL) on
apache/apisix:3.15.0-debian — PASS 3/3: capability (JWKS
fetch, kid lookup, RS256 verification, unknown-kid rejection,
canonical 401 status code on every rejection path) all work
natively via lua-resty-openidc's bearer_jwt_verify. APISIX is
deployed in standalone mode (no etcd, no Admin API); the plugin
reads jwks_uri out of an OIDC discovery document served by a
tiny oidc-server sidecar on the private bench-net, alongside
the canonical JWKS. The simpler jwt-auth plugin was NOT used —
it accepts a single inline public_key per Consumer and does NOT
perform kid lookup, which would collapse probe 3 into a spurious
PASS (same trap Tyk's PEM path falls into; see
apisix#12791).
See
gateways/apisix/p03-jwks-rs256-basic/NOTES.md
for the full breakdown · ★ native jwt plugin with
key_claim_name: kid + per-consumer jwt_secret carrying
algorithm: RS256 and rsa_public_key: <PEM> on
kong/kong:3.9.1 — PASS 3/3: Kong's plugin hashes credentials
by key (wired to the JWT's kid claim via key_claim_name), so
the kid→key dispatch and RS256 verify both happen inside the native
plugin with zero custom Lua. Missing auth and unknown-kid both
reject with the canonical 401. See
gateways/kong/p03-jwks-rs256-basic/NOTES.md
for the full breakdown · ◊ LuaJIT-FFI to libcrypto.so's
EVP_DigestVerify* (OpenSSL 3.x) on
openresty/openresty:1.27.1.2-alpine — PASS 3/3: pure LuaJIT
FFI calls against the libcrypto.so.3 that OpenResty itself links
against, no extra image layers and no third-party lua-resty-*
dependency. JWKS + kid dispatch is pure Lua on top of an
{kid → EVP_PKEY*} map initialised in init_by_lua_block from the
canonical gateways/_reference/jwks-rs256/ bind-mount.
nginx:1.27.3-alpine (the mainline image most nginx profiles run on)
lacks LuaJIT FFI, so the p03 profile pins OpenResty
via an in-directory .env. See
gateways/nginx/p03-jwks-rs256-basic/NOTES.md
for the full breakdown · ♦ native forwardAuth middleware on
traefik:v3.3.4 delegating to an OpenResty sidecar that reuses
the nginx-column Lua modules verbatim — PASS 3/3: Yaegi's
stdlib allowlist excludes crypto/rsa and crypto/x509, so an
in-process plugin for asymmetric verify is architecturally off the
table (unlike HS256, which the in-repo jwt_hs256 Yaegi plugin
already closes). The sidecar lives under the Docker Compose
profile p03-jwks-rs256-basic, so none of the 12 traefik profile
runs see it boot — scripts/parity-gateway.sh exports
COMPOSE_PROFILES="${PROFILE}" so the sidecar is opt-in per
profile. See
gateways/traefik/p03-jwks-rs256-basic/NOTES.md
for the full breakdown · ? pending capability pass (none
outstanding).
The k6 load harness ships two scenarios that re-exercise the canonical
p01-vanilla and p12-full-pipeline policies over HTTPS/1.1
instead of plain HTTP:
| Scenario | Drives policy | Protocol | Activation |
|---|---|---|---|
k6/scenarios/s13-vanilla-https.js |
p01-vanilla |
HTTPS | Phase 5 prerequisite (TLS plumbing) |
k6/scenarios/s14-full-pipeline-https.js |
p12-full-pipeline |
HTTPS | Phase 5 prerequisite (TLS plumbing) |
These two scenarios are orthogonal to the 12 HTTP scenarios
(s01..s12): they do not replace s01 or s12, they sit alongside on a
separate protocol axis. The canonical policy → scenario mapping in
scripts/load-orchestrator.sh
stays p01 → s01-vanilla-http / p12 → s12-full-pipeline-http; s13
and s14 are invoked explicitly via --scenarios s13-vanilla-https
(or a --scenarios list pairing p01,p12 with s13-…,s14-…).
The TLS handshake cost is uniform across policy profiles — the
gateway terminates the same ClientHello / Finished bytes regardless
of whether the downstream request then hits a pure proxy (p01), a
JWT validator (p02), a rate-limit bucket (p04..p07), a header / body
rewriter (p08..p11), or the full pipeline (p12). Measuring TLS
overhead on p01 (the simplest downstream path, minimum gateway
work) and on p12 (the most complex downstream path, maximum gateway
work) sandwiches the real TLS impact:
s13 − s01→ isolates the TLS-termination cost with every other downstream stage held at zero. Gives the "pure TLS" number.s14 − s12→ isolates the TLS-termination cost when the gateway is also running every policy axis in parallel. Gives the "TLS under load" number.(s14 − s12) − (s13 − s01)→ any non-zero residual is an interaction between TLS and the policy pipeline (e.g. a gateway doing synchronous TLS record-sealing on the same event-loop turn that runs Lua filters).
Running HTTPS variants for p02..p11 individually would add no information — every one of them would land somewhere inside the bracket defined by s13 and s14 (exactly by construction, since each single-stage policy is a strict subset of p12). The two-scenario bracket is the canonical design; a full per-policy HTTPS sweep is explicitly out of scope.
s13 and s14 are dead code until Phase 5 ships the TLS plumbing:
- Cert chain under
gateways/_reference/tls/— the canonicalbench.localleaf + CA + key, same uniform shape every gateway mounts. listen 443 ssl;(or equivalent) on every gateway's configuration, reading the cert from the shared reference mount.:8443host-port binding in eachgateways/<gw>/docker-compose.yamlso the k6 loadgen onbench-net(and operators on the host) can reach the TLS listener.
Each scenario validates at init that BENCH_TARGET_URL_HTTPS is
both non-empty AND starts with https://, so an operator who tries
to run s13 / s14 before Phase 5 lands — or with a plain-HTTP URL by
mistake — gets a clear, actionable error message pointing at the
missing plumbing. Until Phase 5, the orchestrator leaves
BENCH_TARGET_URL_HTTPS unset and the scenarios are never invoked.
Phase 5's cert and TLS-config work ships in infra/local/ (Docker
Compose stack with PKI) and infra/aws/ (Terraform module for the
3-host cluster); the scenarios themselves are already landed and
dormant until the TLS plumbing lights them up.
- Phase 3 — done: this document,
gateways/_reference/,fixtures/, andscripts/parity-attestation.share in place; the feature availability matrix above reflects the current parity-pass reality across all 7 gateways × 12 numbered profiles. - Phase 8 — done: every per-cell deviation called out above is
reproduced one-to-one in GATEWAYS.md § Deviations summary
table and gated by
bench compare-runs(see REPRODUCIBILITY.md).