Gateway API v1.6.1 spec compliance matrix¶
Clause-by-clause audit of the implementation against the normative (RFC-2119) surface of the vendored sigs.k8s.io/gateway-api v1.6.1 Standard channel. The full clause extraction and adversarial verification were performed at v1.5.1; the audit was then refreshed against the verified v1.5.1 → v1.6.0 tag diff (see "v1.5.1 → v1.6.1 refresh" below) — v1.6.1 followed as a conformance/test-infrastructure-only patch release with no API or CRD changes (upstream v1.6.1 release notes), so the v1.6.0 clause diff still covers the currently-vendored baseline. This is the deliverable the closed audit issue asked for: every implemented resource's normative clauses classified honoured / justified-deviation / violated, with code evidence.
Method¶
- Extracted every MUST / MUST NOT / SHOULD / SHOULD NOT / MAY clause from the vendored godoc into
01-clause-inventory.md, by type. The counts here and in the dashboard are of the classified clause set inrows-*.md: 376 at v1.5.1, 378 after the v1.6.0 refresh added GW-106 and RG-06, 380 after GW-107 and SH-78 were added for the tunnel-ownership refusal. That set runs eight rows ahead of the inventory itself (372 today), which holds the field-godoc extraction alone — the cross-cutting GEP-01..GEP-08 rows come from step 2's02-gep-notes.md. - Added cross-cutting GEP/concept requirements not in field godoc (policy attachment GEP-713, route-attachment semantics) —
02-gep-notes.md. - Classified each clause CRD-enforced / controller-actionable / N/A-tunnel and assessed status MET / PARTIAL / GAP / NA against the real code — per-type detail in
rows-<TYPE>.md. - Ran the official conformance suite (Gateway HTTP + gRPC profiles) against a fresh kind cluster + real Cloudflare test tunnel as pass/fail ground truth.
- Adversarially re-verified every GAP — a skeptic tried to refute each (CRD enforcement, N/A, conditional-satisfied, documented-deviation) before it was allowed to stand. 22 of 25 first-pass GAPs did not survive.
Dashboard (380 clauses: 376 from the v1.5.1 first-pass classification, 2 added by the v1.6.0 refresh — GW-106 MET, RG-06 NA — and 2 covering the tunnel-ownership refusal — GW-107 MET, SH-78 GAP)¶
Counts are the current rows-*.md verdicts (cat rows-*.md | grep -E '^\| [A-Z]+-[0-9]+ \|' | awk -F'|' '{print $5}' | sort | uniq -c); rows move as fixes land, so the table drifts from the first-pass split of 222 MET / 34 PARTIAL / 25 GAP / 97 N/A described under "Adversarial verification".
| Status | Count |
|---|---|
| MET | 239 |
| PARTIAL | 31 |
| GAP | 13 |
| N/A (tunnel architecture / exempt) | 97 |
Conformance ground truth (v1.5.1 run): 76 top-level subtests PASS, 54 SKIP (documented TLS/TCP/UDP/Mesh/WebSocket/GRPCRouteWeight/HTTPS-listener), 0 FAIL (go test ... ok 293s). GRPCRouteWeight and HTTPRouteBackendProtocolWebSocket were among the SKIPs at that run; both are de-skipped in the current suite configuration (test/conformance/conformance_test.go, pinned by TestStaleSkipsStayLifted) now that gateway-api v1.6.0 added the injectable suite.GRPCClient / suite.WebSocketDialer hooks those tests needed, so the current skip categories are TLS/TCP/UDP/Mesh/HTTPS-listener plus the BackendTLSPolicy-gated tests. Conformance ground truth (v1.6.1 run): 77 top-level subtests PASS, 76 SKIP, 0 FAIL (go test ... ok 487s, kind + real Cloudflare Tunnel). Both runs were green; the audit's value is the normative surface the suite does not exercise.
v1.5.1 → v1.6.1 refresh¶
The v1.6.0 baseline bump was audited against the verified upstream tag diff; v1.6.1 followed as a conformance/test-infrastructure-only patch (upstream v1.6.1 release notes: TCPRoute/UDPRoute conformance timeout and flake fixes, no API or CRD changes), so the vendored baseline is now v1.6.1 but every verdict below still applies unchanged. Every delta below cites the upstream PR; pre-existing verdicts stand unless a row carries an explicit v1.6.0 note.
| Delta | Upstream PR | Classification | Where it landed |
|---|---|---|---|
ReferenceGrant.spec is REQUIRED in both served versions (breaking at admission) | kubernetes-sigs/gateway-api#4845 | CRD-enforced; no controller obligation. The validator (internal/referencegrant/validator.go) is fail-closed on an empty/missing spec anyway (nil From/To → no match → deny), so a legacy spec-less object persisted from before the upgrade cannot grant access. | Inventory RG-06; rows-RG.md RG-06. |
| Gateway listeners description: traffic matching no listener hostname MUST be rejected — HTTP 404, gRPC Unimplemented | kubernetes-sigs/gateway-api#4408 | New controller-actionable clause, MET on both halves: HTTP no-match returns 404, and an unmatched gRPC request gets a trailers-only response (HTTP 200 + grpc-status: 12) so the client observes Unimplemented. Verified on both response writers — standalone Go HTTP/2 (real grpc-go client) and the production cloudflared writer via the trailer bridge; a bare 404 is seen as Internal ("stream closed without trailers") over the tunnel, so the trailer is load-bearing. | Inventory GW-106; rows-GW.md GW-106. |
infrastructure.annotations maxProperties 8 → 16 | kubernetes-sigs/gateway-api#4707 | CRD limit relaxation, informational. GW-84 (annotation propagation, per-Gateway plane) is a size-independent map copy — unaffected. | No row change. |
frontendValidation.caCertificateRefs maxItems 8 → 16 | kubernetes-sigs/gateway-api#4088 | N/A — frontendValidation is not implemented (GW-63..GW-71 exempt, edge terminates TLS). | No row change. |
| Shared hostnames between HTTPRoute and GRPCRoute: site docs relaxed MUST-reject to MAY-reject | kubernetes-sigs/gateway-api#4598 | The controller's cross-type rejection (internal/controller/route_crosstype.go) was MET under the v1.5.1 MUST and remains compliant under the v1.6.0 MAY (enforcing is one of the permitted options); serving both route types on an intersecting hostname without rejection is now also spec-permitted, so the enforcement is a product choice, not an obligation. Upstream inconsistency: the v1.6.0 API godoc (grpcroute_types.go Hostnames) still carries the old MUST wording — candidate upstream docs issue. | Inventory GR-14/GR-15 notes; rows-GR.md GR-14/GR-15 notes; shouldmay-GRSH.md MAY row. |
| TCPRoute/UDPRoute went GA into the Standard channel | kubernetes-sigs/gateway-api#4920, #4923 | Channel inventory only — the tunnel data plane is HTTP(S)-only, so both remain unsupported/exempt; they now ship in the standard CRD bundle rather than experimental-only. | Inventory OTHER channel note; rows-OTHER.md header note. |
SessionPersistence.IdleTimeout removed from the Go API | kubernetes-sigs/gateway-api#4771 | Experimental feature; SessionPersistence is unimplemented here and no audit row referenced IdleTimeout (SH-77 covers SessionName only). | No row change. |
| HTTPRoute Standard schema: NO changes | kubernetes-sigs/gateway-api#4639 (CORS repeated-filter CEL was already in v1.5.1), #4907 (retry validation is experimental-only; the Standard HTTPRoute CRD has no retry field in v1.6.0) | HR verdicts stand, including the HR-26..HR-39 retry N/A block. | rows-HR.md header note. |
| GRPCRoute / GatewayClass: doc-only changes | (tag diff) | No normative delta; verdicts stand. | No row change. |
| Well-known labels for generated resources (GEP-1762) | kubernetes-sigs/gateway-api#4705 | Informational — apis/v1/well_known_labels.go adds gateway.networking.k8s.io/gateway-name / gateway-class-name constants with lowercase must/should godoc (non-normative per the RFC-8174 caveat). Implemented: the per-Gateway rendered plane stamps both well-known keys on every rendered resource's metadata (internal/render/render.go resourceLabels) in addition to its own selector label (cf.k8s.lex.la/gateway); the Deployment selector itself stays controller-specific. | 02-gep-notes.md GEP-16. |
Adversarial verification: 25 first-pass GAPs → final verdicts¶
| Clauses | First pass | Final verdict | Basis |
|---|---|---|---|
| HR-41, HR-42, HR-43, HR-44, GR-34, GR-35 | GAP (duplicate match-name first-wins not honoured) | DOWNGRADE-CRD | Headers/QueryParams are +listType=map++listMapKey=name (vendor/sigs.k8s.io/gateway-api/apis/v1/httproute_types.go:767-783, grpcroute_types.go:325); the API server rejects duplicate names at admission. Header names match case-insensitively, so case-variant names (Foo/foo) bypass the case-sensitive listMapKey and are ANDed rather than first-wins — a negligible header-only edge (doc note). Query-param names are exact-match per spec, so case-variants are legitimately distinct and there is no residual. |
| SH-47, SH-57 | GAP | CONFIRMED (MUST NOT) | route_status.go:112 Parents = nil + full Status().Update (not SSA) wipes other controllers' RouteParentStatus every reconcile; backendtlspolicy_controller.go:717 preserves foreign entries — route status does not. |
| SH-51, GC-21 | GAP (also GW-81, GW-100, POL-11 same class) | CONFIRMED (MUST NOT), low | No observedGeneration regression guard; status writers stamp ObservedGeneration: generation unconditionally. Get+RetryOnConflict guards resourceVersion only, not a stale-generation overwrite. Narrow race. |
| HR-04 (and GR-16) | GAP | CONFIRMED (MUST), minor | Rule-name uniqueness CEL is experimental-channel only (httproute_types.go:125); shipped Standard CRD strips it; no controller-side uniqueness check. |
| GC-05 | GAP | CONFIRMED but SHOULD | Bad parametersRef is surfaced on Gateway status, not as GatewayClass Accepted=False/InvalidParameters. Defensible design deviation; feeds the SHOULD audit. |
| GC-02 | GAP | RESOLVED (was: CONFIRMED but SHOULD) | gateway-exists-finalizer is now managed by the GatewayClass reconciler (added while any Gateway uses the class, removed when none do). |
| GW-31, GW-87 | GAP | DOWNGRADE-NA | spec.addresses is never user-selectable for a tunnel; same territory as the exempt SupportGatewayStaticAddresses. Doc note only. |
| GW-75, GW-86 | GAP | DOWNGRADE-MET | Precondition is "if empty value NOT supported"; the controller supports empty (claims SupportGatewayAddressEmpty, always auto-assigns the tunnel CNAME), so the obligation is vacuously satisfied. |
| GC-09 | GAP | DOWNGRADE-DEFENSIBLE | Controller reconciles only classes naming its controllerName and supports all of them, so Accepted=True is correct; no "will not support" scenario arises. |
| GC-22 | GAP | DOWNGRADE-CONDITIONAL | Publishing status.supportedFeatures is optional; the "MUST be sorted" clause governs order only if published. Not published → vacuously satisfied. |
| SH-36 | GAP | REFUTED | The "Dropped Rule" PartiallyInvalid approach is implemented and tested (route_status.go:334, route_status_diagnostics_test.go:76); the spec requires only one of two approaches. |
| SH-43 | GAP | DOWNGRADE-CONDITIONAL | The per-reconcile full rebuild re-adds only currently-valid own parentRefs, so stale own-entries are dropped naturally; the SHOULD is satisfied for the realistic case. |
| HR-61 | GAP | DOWNGRADE-NA | Redirect Scheme enum is http;https; both have well-known ports, so the "scheme without well-known port" precondition is unreachable. |
| GR-44, GR-45 | GAP | DOWNGRADE-NA | A GRPCRoute backend is gRPC-over-HTTP/2 by definition; forcing h2c is correct, and the one protocol-relevant signal (TLS via BackendTLSPolicy) is honoured. |
| OR-03 | GAP | DOWNGRADE-DOCUMENTED | ExternalName Service support is a deliberate, documented deviation (limitations.md:10/32/66) with a stated trust-boundary rationale. Recommend adding an explicit CVE-2021-25740 citation. |
Confirmed findings (post-verification)¶
Code bugs (file as kind/bug)¶
- Route status reconcile clobbers other controllers'
RouteParentStatus(SH-47, SH-57; MUST NOT). Fixed:route_status.gopartitions the stored parents by controllerName, carries foreign entries over verbatim, and reserves their slots before truncating its own. The listener-status rebuild had the same shape and is fixed inpreserveConditionTransitionson both the reconcile and config-error paths (GW-96/GW-98 MET). - Status writers lack an observedGeneration regression guard (SH-51, GC-21, GW-81, GW-100, POL-11; MUST NOT). Fixed: every status writer now runs
statusGenerationStale/ownedConditionsStale(internal/controller/status_generation.go) or, for per-listener conditions,ownedListenerConditionsStale, and skips the write when a stored own condition already carries a newer observedGeneration. - HTTPRoute/GRPCRoute rule-name uniqueness not enforced (HR-04, GR-16; MUST). The uniqueness CEL is experimental-channel; the shipped Standard CRD omits it and the controller does not validate. Minor. Fix: controller-side validation or a documented limitation.
Documentation additions (justified deviations, recorded in limitations.md by this change)¶
- spec.addresses is not honoured/validated (tunnel address is not user-selectable; same basis as the exempt static-addresses feature) — recorded under Gateway Listener Configuration.
- ExternalName Service support now cites CVE-2021-25740 in its existing trust-boundary rationale.
- Case-variant duplicate header match names (
Foovsfoo) bypass the case-sensitive CRD listMapKey and are ANDed rather than first-wins — negligible header-only edge (query-param names are exact-match, so unaffected); recorded under Route Conflict Resolution. - A route bound only to a Gateway that cannot serve it reports
Accepted=FalsewithReason=Pending, where the spec listsPendingunderAccepted=Unknownfor a route not yet reconciled (SH-78). The polarity is deliberate for the permanent causes — a refused tunnel claim, or a dedicated data plane whose resolve failed deterministically — since the condition stands until the Gateway is fixed andUnknownwould read as "not looked at yet". The retryable causes (a transient resolve failure, a failed tunnel sync) carry the same reason without that justification, which is what keeps the row a GAP.
SHOULD / MAY tiers (verified)¶
The SHOULD and MAY tiers were re-verified in a second pass after the MUST audit — per-type adversarial review for SHOULD, catalogue for MAY. Per-clause detail in shouldmay-<TYPE>.md.
SHOULD / SHOULD NOT (51 clauses)¶
- HONOURED-TESTED (~22) and N/A for the tunnel architecture (~23) account for the bulk.
- HONOURED-TESTED since the audit (was HONOURED-UNTESTED, 7): HR-21, HR-24, HR-63, BTLS-06, SH-31, SH-32, LS-05 — each now pinned by a regression test (explicit-zero timeouts, redirect Location port, BackendTLS HTTP/gRPC equivalence, reason-vocabulary AST guard, ListenerSet status leak guard).
- DEVIATED-DOCUMENTED (5): GW-74, BTLS-04, GC-05, SH-43, OR-03 — permitted deviations with a written rationale in limitations.md.
- DEVIATED-SILENT (originally 3 distinct gaps across 4 clause IDs) — all resolved since the audit: GC-02 and its v1beta1 alias OTHER-45 are HONOURED (the reconciler now manages the gateway-exists-finalizer); GEP-08 (discoverability condition on the policy ancestor status, not the affected Gateway/Service) and HR-61 (no redirect-port fallback to the listener port — unreachable through the Standard CRD scheme enum http/https) are DEVIATED-DOCUMENTED with rationales in limitations.md. Also resolved earlier: GR-44 / GR-45 (gRPC silently dialing cleartext when a Service declared a TLS appProtocol without a BackendTLSPolicy) now fails the backend closed, matching the HTTP path — #438.
MAY (34 clauses)¶
Catalogued implemented / intentionally-omitted; zero worthwhile candidates surfaced. Every MAY is either IMPLEMENTED or OMITTED-INTENTIONAL (edge-terminated TLS, status-only reconciler, single flattened ingress). The optional surface is a deliberate product choice.
Provenance¶
01-clause-inventory.md— verbatim clause extraction (376 rows at v1.5.1, 378 after the v1.6.0 refresh).02-gep-notes.md— GEP/concept cross-cutting requirements.rows-<TYPE>.md— first-pass per-clause classification + evidence (GW, HR, GR, SH, GC, RG, BTLS, LS, OTHER). For the 25 first-pass GAPs, the verdicts in the verification table above supersede the per-row status.- shouldmay-
.md — verified SHOULD-tier verdicts and MAY catalogue, per type.