The Gateway chart's security model says every listener is gated by the built-in kubernetes.io/metadata.name label, which the apiserver writes and no one can spoof. Only the port-80 listener uses that label. Every HTTPS and TLS-passthrough listener selects on namespace.cozystack.io/gateway, an ordinary label written by cozystack's own controller. The property the text claims does hold today, but it is enforced by RBAC rather than by the apiserver, and the text does not say so.
The claim
Two places assert the same property. Stated as a claim rather than quoted, because the wording is likely to change before this is picked up:
Every listener on a tenant Gateway restricts allowedRoutes.namespaces by a selector on kubernetes.io/metadata.name, and that label is written by the apiserver, so the restriction cannot be spoofed.
It appears as the first item of the security model in packages/extra/gateway/README.md (the "Namespace whitelist on listeners" bullet, at :87 on main) and again in the godoc on renderGateway in internal/controller/tenantgateway/reconciler.go (at :737-742 on main). Locate them by the assertion, not by those line numbers: grep -rn "metadata.name" packages/extra/gateway/README.md internal/controller/tenantgateway/reconciler.go finds both, and any surviving sentence that names kubernetes.io/metadata.name as the gate for every listener, or calls the selector unspoofable without qualification, is the sentence this issue is about.
Mechanism
What the renderer builds:
- The port-80 listener uses
buildHTTPListenerAllowedRoutes (internal/controller/tenantgateway/renderers.go:135-141), which calls allowedRoutesFromValues (renderers.go:143-159) and does produce kubernetes.io/metadata.name In [tenant namespace, cert-manager challenge namespace].
- Every port-443 listener uses
buildAllowedRoutes (renderers.go:106-118), which produces MatchLabels{namespace.cozystack.io/gateway: <gateway namespace>}. That covers the HTTPS wildcard and apex listeners (reconciler.go:792, :805), the per-child-apex listeners (:833), the per-hostname HTTP-01 listeners (:855), and the TLS-passthrough listeners (:877-887).
The label-based selector is deliberate and correct — buildAllowedRoutes's own doc comment at renderers.go:100-105 explains that the previous static kubernetes.io/metadata.name In [list] shape foreclosed inheritance, because a child tenant's namespace is not literally on the list. The defect is that the two prose descriptions were not updated when the selector changed.
Why the guarantee still holds: namespace.cozystack.io/gateway is written by the TenantGateway controller (reconciler.go:978-1036) and by the tenant chart, and no tenant identity can write it, because no cozy:tenant:* ClusterRole in packages/system/cozystack-basics/templates/clusterroles.yaml grants any verb on namespaces. The wildcard rule in that file is scoped (resources: ["*"] under apiGroups: [apps.cozystack.io]), and the group gateway.networking.k8s.io does not appear at all. So the property is real; its source is the RBAC grant list, not a label the apiserver owns.
That distinction matters because the two have different failure modes. An apiserver-written label cannot be forged by anyone. An RBAC-protected label survives exactly as long as nobody grants patch on namespaces to a tenant subject — through a future role, a dashboard feature, an operator's convenience binding, or an operator running an addon with a broad ClusterRole. Someone reading the current text has no reason to treat a namespace-patch grant as a security decision.
Observing it
Render the chart and read the Gateway:
kubectl get gateway cozystack --namespace tenant-root --output jsonpath='{range .spec.listeners[*]}{.name}{"\t"}{.allowedRoutes.namespaces.selector}{"\n"}{end}'
The http listener prints a matchExpressions entry on kubernetes.io/metadata.name; every other listener prints matchLabels on namespace.cozystack.io/gateway.
Impact
No exploitable hole today, for the RBAC reason above. This is a correctness problem in the security documentation, and the register is the reason it matters: a reader auditing the model is told the guarantee comes from a source that cannot be weakened by a configuration change, when in fact it can. #3792 records the same class of problem in the neighbouring text and the reason it deserves fixing rather than tolerating — the descriptions corroborate one another, so checking two of them looks like confirmation.
Fix shape
Whatever the surrounding text says by then, the corrected description has to assert three things and no more: the port-80 listener selects on kubernetes.io/metadata.name; every other listener selects on namespace.cozystack.io/gateway; and that second label is protected by the absence of any namespaces verb in tenant RBAC, not by the apiserver. Stating the third explicitly — "granting a tenant subject write access to namespace labels breaks this layer" — is the part that makes the constraint checkable by whoever proposes such a grant later, and it is the part the current text omits entirely.
The RBAC half deserves a test rather than a sentence, since prose about another file is exactly what rots: assert against the rendered clusterroles.yaml that no cozy:tenant:* rule carries namespaces. Without it the corrected description is one merge away from being wrong again in the same way.
Related
Three smaller defects in the same two files, each verifiable independently of the above.
The same two sites carry a second stale claim: that HTTPS listeners restrict allowedRoutes.kinds to HTTPRoute while TLS-passthrough listeners restrict it to TLSRoute, so GRPCRoute, TCPRoute and UDPRoute cannot attach outside the route-hostname policy's coverage. Both listener families now carry the same {HTTPRoute, TLSRoute} set (built at reconciler.go:763-766 on main, applied to the HTTPS listeners at :768 and to the passthrough listeners at :878), deliberately, so that Cilium does not collapse the port-443 listeners into one (cilium#45559). The conclusion still holds — those three kinds remain excluded — but the mechanism given for it does not exist. Check it against the rendered Gateway rather than against the paragraph: every port-443 listener should report the same two kinds.
renderGateway builds the passthrough allowedRoutes with a shallow struct copy, passthroughAllowed := *allowedRoutes at reconciler.go:877. It is the only place in that function that does not DeepCopy — compare :767, :833, :855. The Namespaces field is a pointer, so every passthrough listener and the base allowedRoutes share one RouteNamespaces value. Nothing mutates it after construction today, so there is no live bug; it is a trap set for the next edit that does, and it is inconsistent with the three neighbouring lines.
The comment above the child-apex listener loop states the number of child listeners available as 64, describing it as the Gateway API cap on spec.listeners minus the fixed http / https / https-apex slots. 64 is the cap itself; after subtracting three the remainder is 61. The comment gives the number the subtraction starts from as the number it ends at. On main this is reconciler.go:815-819; it is the only place in that file mentioning 64, so grep -n 64 internal/controller/tenantgateway/reconciler.go locates it regardless of movement.
The "Known limitations" list in packages/extra/gateway/README.md contains two bullets twice each, verbatim: the one about upstream application gaps and the one about supported ACME issuers. On main the pairs are at :128/:132 and :129/:133. If the branch rewrites the section, check whether the duplication survived rather than checking those lines.
The Gateway chart's security model says every listener is gated by the built-in
kubernetes.io/metadata.namelabel, which the apiserver writes and no one can spoof. Only the port-80 listener uses that label. Every HTTPS and TLS-passthrough listener selects onnamespace.cozystack.io/gateway, an ordinary label written by cozystack's own controller. The property the text claims does hold today, but it is enforced by RBAC rather than by the apiserver, and the text does not say so.The claim
Two places assert the same property. Stated as a claim rather than quoted, because the wording is likely to change before this is picked up:
It appears as the first item of the security model in
packages/extra/gateway/README.md(the "Namespace whitelist on listeners" bullet, at:87onmain) and again in the godoc onrenderGatewayininternal/controller/tenantgateway/reconciler.go(at:737-742onmain). Locate them by the assertion, not by those line numbers:grep -rn "metadata.name" packages/extra/gateway/README.md internal/controller/tenantgateway/reconciler.gofinds both, and any surviving sentence that nameskubernetes.io/metadata.nameas the gate for every listener, or calls the selector unspoofable without qualification, is the sentence this issue is about.Mechanism
What the renderer builds:
buildHTTPListenerAllowedRoutes(internal/controller/tenantgateway/renderers.go:135-141), which callsallowedRoutesFromValues(renderers.go:143-159) and does producekubernetes.io/metadata.name In [tenant namespace, cert-manager challenge namespace].buildAllowedRoutes(renderers.go:106-118), which producesMatchLabels{namespace.cozystack.io/gateway: <gateway namespace>}. That covers the HTTPS wildcard and apex listeners (reconciler.go:792,:805), the per-child-apex listeners (:833), the per-hostname HTTP-01 listeners (:855), and the TLS-passthrough listeners (:877-887).The label-based selector is deliberate and correct —
buildAllowedRoutes's own doc comment atrenderers.go:100-105explains that the previous statickubernetes.io/metadata.name In [list]shape foreclosed inheritance, because a child tenant's namespace is not literally on the list. The defect is that the two prose descriptions were not updated when the selector changed.Why the guarantee still holds:
namespace.cozystack.io/gatewayis written by the TenantGateway controller (reconciler.go:978-1036) and by the tenant chart, and no tenant identity can write it, because nocozy:tenant:*ClusterRole inpackages/system/cozystack-basics/templates/clusterroles.yamlgrants any verb onnamespaces. The wildcard rule in that file is scoped (resources: ["*"]underapiGroups: [apps.cozystack.io]), and the groupgateway.networking.k8s.iodoes not appear at all. So the property is real; its source is the RBAC grant list, not a label the apiserver owns.That distinction matters because the two have different failure modes. An apiserver-written label cannot be forged by anyone. An RBAC-protected label survives exactly as long as nobody grants
patchonnamespacesto a tenant subject — through a future role, a dashboard feature, an operator's convenience binding, or an operator running an addon with a broad ClusterRole. Someone reading the current text has no reason to treat a namespace-patch grant as a security decision.Observing it
Render the chart and read the Gateway:
kubectl get gateway cozystack --namespace tenant-root --output jsonpath='{range .spec.listeners[*]}{.name}{"\t"}{.allowedRoutes.namespaces.selector}{"\n"}{end}'The
httplistener prints amatchExpressionsentry onkubernetes.io/metadata.name; every other listener printsmatchLabelsonnamespace.cozystack.io/gateway.Impact
No exploitable hole today, for the RBAC reason above. This is a correctness problem in the security documentation, and the register is the reason it matters: a reader auditing the model is told the guarantee comes from a source that cannot be weakened by a configuration change, when in fact it can. #3792 records the same class of problem in the neighbouring text and the reason it deserves fixing rather than tolerating — the descriptions corroborate one another, so checking two of them looks like confirmation.
Fix shape
Whatever the surrounding text says by then, the corrected description has to assert three things and no more: the port-80 listener selects on
kubernetes.io/metadata.name; every other listener selects onnamespace.cozystack.io/gateway; and that second label is protected by the absence of anynamespacesverb in tenant RBAC, not by the apiserver. Stating the third explicitly — "granting a tenant subject write access to namespace labels breaks this layer" — is the part that makes the constraint checkable by whoever proposes such a grant later, and it is the part the current text omits entirely.The RBAC half deserves a test rather than a sentence, since prose about another file is exactly what rots: assert against the rendered
clusterroles.yamlthat nocozy:tenant:*rule carriesnamespaces. Without it the corrected description is one merge away from being wrong again in the same way.Related
Three smaller defects in the same two files, each verifiable independently of the above.
The same two sites carry a second stale claim: that HTTPS listeners restrict
allowedRoutes.kindstoHTTPRoutewhile TLS-passthrough listeners restrict it toTLSRoute, so GRPCRoute, TCPRoute and UDPRoute cannot attach outside the route-hostname policy's coverage. Both listener families now carry the same{HTTPRoute, TLSRoute}set (built atreconciler.go:763-766onmain, applied to the HTTPS listeners at:768and to the passthrough listeners at:878), deliberately, so that Cilium does not collapse the port-443 listeners into one (cilium#45559). The conclusion still holds — those three kinds remain excluded — but the mechanism given for it does not exist. Check it against the rendered Gateway rather than against the paragraph: every port-443 listener should report the same two kinds.renderGatewaybuilds the passthroughallowedRouteswith a shallow struct copy,passthroughAllowed := *allowedRoutesatreconciler.go:877. It is the only place in that function that does notDeepCopy— compare:767,:833,:855. TheNamespacesfield is a pointer, so every passthrough listener and the baseallowedRoutesshare oneRouteNamespacesvalue. Nothing mutates it after construction today, so there is no live bug; it is a trap set for the next edit that does, and it is inconsistent with the three neighbouring lines.The comment above the child-apex listener loop states the number of child listeners available as 64, describing it as the Gateway API cap on
spec.listenersminus the fixedhttp/https/https-apexslots. 64 is the cap itself; after subtracting three the remainder is 61. The comment gives the number the subtraction starts from as the number it ends at. Onmainthis isreconciler.go:815-819; it is the only place in that file mentioning 64, sogrep -n 64 internal/controller/tenantgateway/reconciler.golocates it regardless of movement.The "Known limitations" list in
packages/extra/gateway/README.mdcontains two bullets twice each, verbatim: the one about upstream application gaps and the one about supported ACME issuers. Onmainthe pairs are at:128/:132and:129/:133. If the branch rewrites the section, check whether the duplication survived rather than checking those lines.