Several places describe how the Gateway security model behaves for a tenant whose apex is not derived from its parent, and for what the port-80 listener filter protects. The descriptions do not agree with the code, and they do not agree with each other. This is not a documentation nit: the descriptions corroborate one another, so a reader who checks two of them gets what looks like confirmation, and the tree answers consistently and wrongly.
I hit this while working on an unrelated branch and reproduced the wrong model three times in text I was writing, each time after checking the tree. A code reviewer then reached the same wrong conclusion from the same line, independently. That is the reason to fix it rather than leave it: it is currently generating fresh errors, not just recording an old one.
The deciding sites. tenant.gatewayEffective in packages/apps/tenant/templates/_helpers.tpl returns false whenever .Values.gateway is unset, via kindIs "invalid", with no reference to the apex at all. cozystack-gateway-hostname-policy matches on object.metadata.namespace.startsWith("tenant-") and compares each listener hostname against the host label of the namespace holding the Gateway, which is the parent's when a child inherits. Those two decide the behaviour; everything below only describes it.
What actually happens for a child on an independent apex. The child namespace is stamped with the parent's gateway, collectInheritingChildApexes applies no apex-containment filter, and the parent's Gateway gains a listener for the child apex. The Gateway policy then compares that listener against the parent's own host label and denies the write: *.customer1.io neither equals example.org nor ends with .example.org. reconcileGateway runs before the redirect and the status step, so the parent TenantGateway stops converging. The blast radius is the parent, and nothing is served for the child on any port.
Site 1, packages/extra/gateway/README.md:96. "A child tenant with an independent apex (customer1.io instead of a subdomain) is handled correctly because the VAP reads the per-namespace label rather than assuming a subdomain hierarchy." True of the route policy, which reads the route's own namespace. The sentence says "the VAP" in the singular, and the Gateway policy reads a different namespace and reaches the opposite conclusion. This is the line that misled the reviewer.
Site 2, packages/apps/tenant/values.yaml:17, mirrored into values.schema.json:26. The gateway parameter is documented as auto-enabling the Gateway for tenants whose apex is derived from the parent. The helper does nothing of the kind: unset means false regardless of the apex.
Site 3, packages/apps/tenant/templates/namespace.yaml:46-49. States that gatewayEffective returns "true" for tenants with a custom non-derived host. It returns "false". This one matters most in practice, because it reads as though the wedge above cannot occur, when the helper is exactly what makes it reachable.
Site 4, packages/apps/tenant/tests/gateway_default_test.yaml. The custom-apex case carries the comment "The HTTPRoutes for this tenant's apps will attach to the parent Gateway but fail TLS". They never attach; the Gateway write is denied at admission first. The test itself is sound and asserts only that no own HelmRelease is rendered, but it is green, and a green test lends its comment an authority the assertion does not support.
Site 5, internal/controller/tenantgateway/reconciler_test.go:209-216. The doc comment says app HTTPRoutes attaching by hostname "cannot bind to port 80 and silently serve plaintext". Scoped to the cozy-* namespaces named beside it this holds, so it is imprecise rather than false; stated generally it is not, because a tenant that owns its Gateway is its own gateway namespace and its app routes are admitted by that filter. The test body asserts only the cozy-* exclusion and is correct.
Site 6, the redirect route has no direct e2e coverage. The Gateway chainsaw suite never names it. Coverage is indirect, through a Ready=True assertion that would fail if the route were rejected, which is real but does not pin what the route contains.
Suggested shape of a fix. Pick one canonical description, the chart README being the natural home, state the behaviour there once, and reduce the others to a pointer. The specific corrections needed are that an unset gateway never auto-enables anything, that an independent-apex child under an inherited Gateway wedges the parent rather than degrading gracefully, and that the port-80 filter excludes cozy-* and inheriting children but not the publishing tenant's own apps.
Several places describe how the Gateway security model behaves for a tenant whose apex is not derived from its parent, and for what the port-80 listener filter protects. The descriptions do not agree with the code, and they do not agree with each other. This is not a documentation nit: the descriptions corroborate one another, so a reader who checks two of them gets what looks like confirmation, and the tree answers consistently and wrongly.
I hit this while working on an unrelated branch and reproduced the wrong model three times in text I was writing, each time after checking the tree. A code reviewer then reached the same wrong conclusion from the same line, independently. That is the reason to fix it rather than leave it: it is currently generating fresh errors, not just recording an old one.
The deciding sites.
tenant.gatewayEffectiveinpackages/apps/tenant/templates/_helpers.tplreturnsfalsewhenever.Values.gatewayis unset, viakindIs "invalid", with no reference to the apex at all.cozystack-gateway-hostname-policymatches onobject.metadata.namespace.startsWith("tenant-")and compares each listener hostname against the host label of the namespace holding the Gateway, which is the parent's when a child inherits. Those two decide the behaviour; everything below only describes it.What actually happens for a child on an independent apex. The child namespace is stamped with the parent's gateway,
collectInheritingChildApexesapplies no apex-containment filter, and the parent's Gateway gains a listener for the child apex. The Gateway policy then compares that listener against the parent's own host label and denies the write:*.customer1.ioneither equalsexample.orgnor ends with.example.org.reconcileGatewayruns before the redirect and the status step, so the parent TenantGateway stops converging. The blast radius is the parent, and nothing is served for the child on any port.Site 1,
packages/extra/gateway/README.md:96. "A child tenant with an independent apex (customer1.ioinstead of a subdomain) is handled correctly because the VAP reads the per-namespace label rather than assuming a subdomain hierarchy." True of the route policy, which reads the route's own namespace. The sentence says "the VAP" in the singular, and the Gateway policy reads a different namespace and reaches the opposite conclusion. This is the line that misled the reviewer.Site 2,
packages/apps/tenant/values.yaml:17, mirrored intovalues.schema.json:26. Thegatewayparameter is documented as auto-enabling the Gateway for tenants whose apex is derived from the parent. The helper does nothing of the kind: unset meansfalseregardless of the apex.Site 3,
packages/apps/tenant/templates/namespace.yaml:46-49. States thatgatewayEffectivereturns"true"for tenants with a custom non-derived host. It returns"false". This one matters most in practice, because it reads as though the wedge above cannot occur, when the helper is exactly what makes it reachable.Site 4,
packages/apps/tenant/tests/gateway_default_test.yaml. The custom-apex case carries the comment "The HTTPRoutes for this tenant's apps will attach to the parent Gateway but fail TLS". They never attach; the Gateway write is denied at admission first. The test itself is sound and asserts only that no own HelmRelease is rendered, but it is green, and a green test lends its comment an authority the assertion does not support.Site 5,
internal/controller/tenantgateway/reconciler_test.go:209-216. The doc comment says app HTTPRoutes attaching by hostname "cannot bind to port 80 and silently serve plaintext". Scoped to thecozy-*namespaces named beside it this holds, so it is imprecise rather than false; stated generally it is not, because a tenant that owns its Gateway is its own gateway namespace and its app routes are admitted by that filter. The test body asserts only thecozy-*exclusion and is correct.Site 6, the redirect route has no direct e2e coverage. The Gateway chainsaw suite never names it. Coverage is indirect, through a
Ready=Trueassertion that would fail if the route were rejected, which is real but does not pin what the route contains.Suggested shape of a fix. Pick one canonical description, the chart README being the natural home, state the behaviour there once, and reduce the others to a pointer. The specific corrections needed are that an unset
gatewaynever auto-enables anything, that an independent-apex child under an inherited Gateway wedges the parent rather than degrading gracefully, and that the port-80 filter excludescozy-*and inheriting children but not the publishing tenant's own apps.