Gateway API v1.6.0 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.0 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.0 refresh" below). 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 — 376 rows in
01-clause-inventory.mdat v1.5.1, by type (378 after the v1.6.0 refresh added GW-106 and RG-06). - 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 (v1.5.1 first-pass classification, 376 clauses)¶
| Status | Count |
|---|---|
| MET | 221 |
| PARTIAL | 34 |
| GAP (first pass) | 25 |
| N/A (tunnel architecture / exempt) | 96 |
Conformance ground truth: 76 top-level subtests PASS, 54 SKIP (documented TLS/TCP/UDP/Mesh/WebSocket/GRPCRouteWeight/HTTPS-listener), 0 FAIL (go test ... ok 293s). The suite is green; the audit's value is the normative surface the suite does not exercise.
v1.5.1 → v1.6.0 refresh¶
The v1.6.0 baseline bump was audited against the verified upstream tag diff. 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).internal/controller/route_status.go:112resets the wholeParentsslice and writes via fullStatus().Update, so a Route co-managed by another controller loses that controller's parent-status entry every reconcile. Fix: preserve entries whoseControllerNamediffers, mirroringbackendtlspolicy_controller.go:717-762. Highest severity (multi-controller correctness). Related: listener-status rebuild has the same shape (GW-96/GW-98 PARTIAL). - Status writers lack an observedGeneration regression guard (SH-51, GC-21, GW-81, GW-100, POL-11; MUST NOT). Status conditions are stamped with the current generation unconditionally; the spec forbids updating a condition whose stored observedGeneration is greater than the writer's known generation. Mitigated by fresh-Get + RetryOnConflict (narrow race), so low severity. Fix: a shared guard, or an accepted-risk note.
- 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.
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.