docs(networking): GatewayClass selection, edge TLS mode, and the Cloudflare Tunnel package - #655
Open
Aleksei Sviridkin (lexfrei) wants to merge 9 commits into
Open
docs(networking): GatewayClass selection, edge TLS mode, and the Cloudflare Tunnel package#655Aleksei Sviridkin (lexfrei) wants to merge 9 commits into
Aleksei Sviridkin (lexfrei) wants to merge 9 commits into
Conversation
The page stated in three places that Gateway API ships TLSRoute at v1alpha2 and that graduation to v1 is still pending upstream. The experimental CRD bundle Cozystack vendors serves tlsroutes at v1 as the storage version, alongside v1alpha2 and v1alpha3, both served and both marked deprecated. What is actually outstanding is on this side: the platform's own TLSRoute templates address v1alpha2. State that instead. The Layer 5 description leaned on the policy's API-version list as though that were what gates coverage. It is not: matchConstraints .matchPolicy is unset on both policies and so defaults to Equivalent, which converts a request submitted under another served version of the same resource into one the rule does name. Conversion crosses versions of one resource and not resource kinds, which is why GRPCRoute is outside both policies rather than converted into them. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
A tenant Gateway's class is a bounded per-tenant choice rather than a fixed platform setting, and a class whose provider terminates TLS upstream puts the tenant into a fourth cert mode that issues no certificates at all. Document the three platform values that bound the choice (gateway.className, gateway.tenantSelectableClasses, gateway.edgeTerminatedClasses) next to the platform flag, and tenant.spec.gatewayClass next to the per-tenant opt-in, including the whitespace refusal, the trimming, and the absence of case folding. Document edge alongside the three existing cert modes: what it drops from the Gateway, what a move between classes collects, and what keeps the wildcard replica in place. In edge mode the controller writes no route status conditions and prunes none, so a route orphaned by the switch keeps displaying the Accepted=True it was given under the previous mode. Two claims elsewhere on the page did not survive the change: that a tenant cannot pick a class at all, and that gatewayClassName must be exactly cilium. An unmatched class is also not a cause of Programmed=False — with no controller to write conditions the Gateway keeps the CRD defaults of Unknown/Pending, and the symptom surfaces on the TenantGateway as Ready=False/GatewayNotAccepted. The page title and description named Cilium alone, which no longer describes a page covering class selection across providers. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
gateway.className, gateway.tenantSelectableClasses and gateway.edgeTerminatedClasses had no rows in the reference table. Document each with the failure it produces when set wrong, since none of the three is validated against the classes the cluster actually has and two of them fail a tenant release rather than the platform one. Putting the default on an edge-terminated class also unpublishes whichever TLS-passthrough endpoints are published at all, and nothing on the Gateway or the TenantGateway reports it. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
Add a page for the optional cloudflare-tunnel-gateway-controller system package: what it is, when a tunnel is the right way to publish, the Cloudflare-side prerequisites, the credentials Secret, the two-step enable, how a Gateway joins the class, and the removal order. The page leads with the inputs to the decision rather than the full operator reference. Two are easy to miss: the controller holds a cluster-wide grant that reads and creates Secrets in every namespace and can create a Deployment in any of them, and a per-tenant Gateway needs its class listed in gateway.edgeTerminatedClasses before this controller can serve it — the standalone Gateway is simply the path that needs no platform configuration. The Gateway example carries an HTTPS listener with no tls block, which is deliberate and is what the chart's own installation notes prescribe: tls is not a required listener field and the CRD's only rule tying the two together constrains tls.mode when a tls block is present, so a listener without one is admitted. Also record the controller in the licenses reference, which is hand-curated. Its license is BSD-3-Clause. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
Both released snapshots state that Gateway API ships TLSRoute at v1alpha2 and that graduation to v1 is still pending upstream. Every v1.5.x and v1.6.x release vendors CRD bundle v1.5.1, which serves tlsroutes at v1 as the storage version plus v1alpha2 and v1alpha3 marked deprecated, so the claim was wrong for those releases as shipped. This is a factual error about upstream rather than a record of what the release did, so correcting it does not rewrite history, and there is no automatic backport between version directories. The edit is deliberately minimal: it fixes the served-version statement and nothing else. The Layer 5 description on these pages names v1alpha2 for the TLSRoute policy, which is what both releases actually shipped, so it stays as it is. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
The description said one HTTPS listener per attached HTTPRoute hostname. The controller collects hostnames from HTTPRoute and TLSRoute alike with no kind filter, so a passthrough route's hostname also produces a terminating listener and its own Certificate. The hash behind the suffix takes the lower-cased hostname, not the hostname as written, which matters to anyone reproducing a listener name by hand. State what a hostname race does and does not cost. resolveHostnameOwners keys winners by hostname and losers by route, so every claimed hostname gets exactly one listener and one Certificate; a losing route is marked Accepted=False rather than rejected, and the hostname stays served. That matches the HostnameConflict section further down the same page, which this sentence links to. Checked per release rather than assumed: v1.5.0, v1.5.3, v1.6.0 and v1.6.1 all lower-case the hash input and all walk both route lists, so the correction is right for both released lines as shipped. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
The v1.6 page tells an operator that a gateway-owning child needs the wildcard Secret replicated into its namespace by hand. v1.6 ships the wildcard-secret controller, which mirrors it into every tenant namespace that owns a TLS termination point with no operator input, and treats a Secret of that name it did not write as a foreign collision — non-retryable for that namespace, surfaced only as a Warning event. Following the instruction therefore disables the replication it was meant to substitute for, silently. Scoped to v1.6 deliberately. v1.5 ships no such controller, so the same sentence is correct on the v1.5 page and is left alone. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
Both released pages state that every admission-time check is fail-closed. That is true of policy-error posture and not of what each CEL does when the namespace host label is missing, where the layers differ: the route policy denies, the Gateway-listener policy short-circuits to allow. A reader auditing security posture takes the blanket at face value, and it is the sentence most likely to be quoted rather than verified. Verified at both tags rather than inferred: gateway-hostname-policy carries the same tenantHost-empty short-circuit at v1.5.3 and v1.6.1, so the correction is true verbatim on both pages. Split by release for the citation. v1.6 ships the threat model under docs/security/ and points at it as the document that owns this posture; v1.5 does not ship it, so that page states the behaviour without a pointer to a file its release does not contain. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
Both released pages state the winner/loser mechanic unconditionally, and the Security chapter points at it as what handles the within-apex cross-namespace case. collectHostnameClaims returns nothing when the mode serves every hostname off a single wildcard listener, which at these releases means DNS-01 and existingSecret — so in those two no claims are gathered, no winner is picked and no route status is written. An operator reading the Security chapter is told a collision class is resolved in modes where nothing resolves it. Verified at both tags rather than inferred: the short-circuit names DNS-01 and existingSecret at v1.5.3 and v1.6.1 alike. The next page names a third mode because edge exists only there. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
Aleksei Sviridkin (lexfrei)
requested review from
Andrei Kvapil (kvaps),
Timofei Larkin (lllamnyp),
myasnikovdaniil and
Timur Tukaev (tym83)
as code owners
August 17, 2026 16:54
✅ Deploy Preview for cozystack ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
Important Review available on request
Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Documents two changes landing for the next release, and corrects five factual errors the pages were already carrying independently of both.
GatewayClass selection and the
edgecert modeA tenant Gateway's class is a bounded per-tenant choice rather than a fixed platform setting. Three platform values bound it —
gateway.className,gateway.tenantSelectableClassesandgateway.edgeTerminatedClasses— and a tenant picks withtenant.spec.gatewayClass. A class whose provider terminates TLS upstream puts the tenant into a fourth certificate mode,edge, which renders port-80 listeners only and issues noIssuerand noCertificate.This is documented inside the page's existing structure: the platform values next to the platform flag, the tenant field next to the per-tenant opt-in, and
edgealongside HTTP-01, DNS-01 andexistingSecret, with the class-move table and the conditions that keep a wildcard replica in place.Adding a fourth mode falsified a number of statements that were true when the page had three, and those are corrected in the same commit rather than left for a reader to trip over. The ones worth naming: Layer 1's security guarantee — "app HTTPRoutes cannot bind to port 80 and silently serve plaintext" — does not hold in edge mode, where the port-80 listeners carry the wide selector by design, so the residual now sits under "What this does NOT defend"; the Security preamble's blanket "all admission-time checks are fail-closed" now names its axis, since it is about policy error while Layer 2 is fail-open on a missing host label; three statements promising every opted-in tenant a LoadBalancer Service and an LB IP were scoped by the page's old "(Cilium)" title, which this PR removes; and the passthrough listeners are no longer "always rendered".
The three new values also get rows in the Platform Package reference, each with the failure it produces when set wrong, since none is validated against the classes the cluster actually has and two fail a tenant release rather than the platform one.
The Cloudflare Tunnel package
A new page for the optional
cloudflare-tunnel-gateway-controllersystem package, following the shape the other optional-package pages in this section use. It leads with the inputs to the decision rather than reproducing the full operator reference: the Cloudflare-side prerequisites, the credentials Secret, the two-step enable and the not-ready window it opens, how a Gateway joins the class, and the removal order.Two things an operator needs before enabling are called out rather than buried. The controller holds a cluster-wide grant — it reads and creates Secrets in every namespace and can create a Deployment in any of them. And a per-tenant Gateway can use this class, but only once its class is in
gateway.edgeTerminatedClassesandgateway.tenantSelectableClasses; the standalone Gateway shown on the page is simply the path that needs no platform configuration.The controller is also recorded in the licenses reference, which is hand-curated. Its license is BSD-3-Clause, confirmed against the upstream repository.
This page and
docs/cloudflare-tunnel-gateway.mdin the main repository describe the same mechanism, so to say which to edit: this page is the operator-facing canonical text and is versioned per release, the in-repo guide serves someone reading the tree, and where the two disagree the in-repo copy on its own branch is the fresher of the two.Corrections that stand on their own
Five, each in its own commit so any can be dropped without the rest.
The TLSRoute version claim. The pages said in three places that Gateway API ships TLSRoute at
v1alpha2and that graduation tov1is pending upstream. The experimental bundle Cozystack vendors servestlsroutesatv1as the storage version, plusv1alpha2andv1alpha3both deprecated — and has since Gateway API v1.5.0, so the claim was wrong when it was written. What is actually outstanding is on the Cozystack side: the platform's own TLSRoute templates addressv1alpha2. Everyv1.5.*andv1.6.*tag vendors the same bundle, so the correction applies to both released lines as shipped.Relatedly, the Layer 5 description leaned on the policy's API-version list as though that were what gates coverage. It is not:
matchConstraints.matchPolicyis unset on both policies and so defaults toEquivalent, which has the apiserver convert a request submitted under another served version of the same resource into one the rule does name. That clause is on all three pages, since the policies ship identically at both released tags — without it, the new served-version fact reads as though aTLSRouteatv1skips the policy.How HTTP-01 picks per-listener hostnames. The description said one HTTPS listener per attached
HTTPRoutehostname; the controller collects hostnames fromHTTPRouteandTLSRoutealike with no kind filter.resolveHostnameOwnerskeys winners by hostname and losers by route, so every claimed hostname gets exactly one listener and oneCertificate— a losing route is markedAccepted=Falserather than rejected, and the hostname stays served. And the hash behind the-<8-hex>suffix takes the lower-cased hostname, which matters to anyone reproducing a listener name by hand. On v1.5 there is one extra consequence, noted only there: HTTPS listeners at that release restrictallowedRoutes.kindstoHTTPRoute, so a hostname won by aTLSRoutegets a listener and a certificate thatTLSRoutecannot attach to.That rule has an instance in the platform's own three TLS-passthrough routes, which declare hostnames and attach to the tenant Gateway from namespaces in the default
attachedNamespaces. Each therefore claims an HTTPS listener and an ACME certificate on top of its passthrough listener, so the same commit corrects two statements that assumed otherwise: the 64-listener budget, which reads1 + N + 3and is1 + N + 6with the default passthrough services, and the rate-limit note that counted one certificate per published app. The budget one matters because the page tells operators to plan against that cap and says exceeding it fails admission.Wildcard-Secret replication on v1.6. The v1.6 page told operators a gateway-owning child needs the wildcard Secret replicated into its namespace by hand. v1.6 ships the wildcard-secret controller, which mirrors it automatically, and treats a Secret of that name it did not write as a foreign collision — non-retryable for that namespace, surfaced only as a Warning event. Following the instruction silently disables the replication it was meant to substitute for. Scoped to v1.6 deliberately: v1.5 ships no such controller, so the same sentence is correct there and is left alone.
Fail-closed on the released pages. Both v1.5 and v1.6 state that every admission-time check is fail-closed. That is true of policy-error posture and not of what each CEL does when the namespace host label is missing, where the layers differ: the route policy denies, the Gateway-listener policy short-circuits to allow. It is the sentence an auditor is most likely to quote rather than verify. Split by release for the citation — v1.6 ships
docs/security/threat-model.mdand points at it as the document that owns this posture; v1.5 does not ship that file, so the correction states the behaviour without a pointer to something its release does not contain.Hostname-conflict resolution runs in HTTP-01 only.
collectHostnameClaimsreturns nothing for the modes that serve every hostname off apex-wide listeners, so no claims are gathered, no winner is picked and no route condition is written or removed. All three pages stated the winner/loser mechanic unconditionally and the Security chapter pointed at it as what handles within-apex cross-namespace collisions — so in the other modes an operator was told a collision class is resolved when nothing resolves it. The troubleshooting recipe had the same problem in the other direction: it sent an operator to look for aHostnameConflictcondition that is never written outside HTTP-01, where its absence reads as "no collision".nextnames three such modes, the released pages two, becauseedgeexists only onnext.On the released pages
Each claim was verified at that version's tag rather than inferred from
next, and the answer differed every time: the TLSRoute version claim is wrong on both lines identically, the replication advice is wrong on v1.6 only, and the HTTPS-listener-kinds caveat belongs on v1.5 only. None of theedge, class-selection or tunnel material is backported, because those features are not in those releases.Verification
hugo --gc --minifycompletes with no errors, and so doeshugo --environment development --gc --minify. Both were run, becauseconfig/production/hugo.yamlexcludesdocs/next/**from every language mount andhugodefaults to production — so the production build never reads the new page. Injecting a deliberately broken{{% ref %}}confirms it: production exits 0 with zero errors, development exits 1 naming the file and column. The development build is the real check for anything undernext/. Every internal anchor and cross-page reference on all six touched pages resolves against the generated heading IDs.Every behavioural statement was checked against the code that implements it rather than against the prose it was ported from — the vendored CRD bundle for served versions, the admission policy templates for the match rules and their CEL, the chart templates for certificate-mode resolution and the render-time guards, the controller for the edge listener set and the hostname-conflict semantics, and the released tags for anything landing on a versioned page.
Known residuals
Three things this PR leaves imprecise, named rather than silently carried. All are prose, none changes what an operator does, and each is a sentence or a line to close.
The two OSS-component card lists have diverged by one entry.
licenses.mdgains the Cloudflare Tunnel Gateway Controller card;guides/platform-stack/_index.mdcarries the same Networking block and did not. At the merge base the two card sets were exactly identical, so this diff broke an invariant that held before it. Closing it is one{{< oss-card >}}line in the second file, in the no-logoform the first uses.nextdoes not say that a staleHostnameConflictcondition can outlive the mode that wrote it. Both released pages state it — a cluster moved off HTTP-01 keeps whatever conditions the controller wrote under the old mode, because the other modes write and remove nothing.nexthas the "or removed" half but not the consequence, so on the page for the release that actually ships this, nothing warns that a present condition on a DNS-01,existingSecretoredgecluster may be a leftover. The troubleshooting recipe on all three pages warns about the opposite direction only, that an absent condition means the mechanism did not run.Layer 2 keeps a parenthetical whose reason the Layer 5 rewrite supersedes. It explains
v1beta1coverage by the versions its rule names; coverage actually comes frommatchConstraints.matchPolicydefaulting toEquivalent, which applies to that policy too. The conclusion is right and the reason is wrong.Three smaller ones, for completeness:
gateway-api.md's "See also" does not name the new tunnel page although the tunnel page names it; two of the four Traffic-path bullets kept an unqualified Cilium framing while the other two were scoped; and the tunnel page describes the Cilium path as needing "an ACME challenge that completes against the cluster", which is the HTTP-01 path specifically rather than DNS-01 orexistingSecret.Notes for review
Both upstream changes this documents are open, not merged. The pages are written for
next, the trunk for the upcoming release, and state nothing about availability in a released version. The two versioned pages are corrections to already-shipped documentation and are independent of both.Two links point at paths that exist once the tunnel package merges: the upstream package directory in the monorepo, and the package's own guide. They resolve on merge and are dead until then.
The
i18n lintjob is red onmainfor four stale translations of an unrelated blog post, so it will be red here for a reason this branch did not cause. That is cozystack/website#652, which carries the reproduction and the two fix shapes — note that runningupdate-digestsalone turns the check green while recording the stale translations as approved, so it is a deliberate choice rather than the quick option.