Skip to content

docs(networking): GatewayClass selection, edge TLS mode, and the Cloudflare Tunnel package - #655

Open
Aleksei Sviridkin (lexfrei) wants to merge 9 commits into
mainfrom
docs/gateway-classes-and-tunnel
Open

docs(networking): GatewayClass selection, edge TLS mode, and the Cloudflare Tunnel package#655
Aleksei Sviridkin (lexfrei) wants to merge 9 commits into
mainfrom
docs/gateway-classes-and-tunnel

Conversation

@lexfrei

Copy link
Copy Markdown
Contributor

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 edge cert mode

A tenant Gateway's class is a bounded per-tenant choice rather than a fixed platform setting. Three platform values bound it — gateway.className, gateway.tenantSelectableClasses and gateway.edgeTerminatedClasses — and a tenant picks with tenant.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 no Issuer and no Certificate.

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 edge alongside HTTP-01, DNS-01 and existingSecret, 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-controller system 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.edgeTerminatedClasses and gateway.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.md in 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 v1alpha2 and that graduation to v1 is pending upstream. The experimental bundle Cozystack vendors serves tlsroutes at v1 as the storage version, plus v1alpha2 and v1alpha3 both 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 address v1alpha2. Every v1.5.* and v1.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.matchPolicy is unset on both policies and so defaults to Equivalent, 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 a TLSRoute at v1 skips the policy.

How HTTP-01 picks per-listener hostnames. The description said one HTTPS listener per attached HTTPRoute hostname; the controller collects hostnames from HTTPRoute and TLSRoute alike with no kind filter. 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. 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 restrict allowedRoutes.kinds to HTTPRoute, so a hostname won by a TLSRoute gets a listener and a certificate that TLSRoute cannot 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 reads 1 + N + 3 and is 1 + N + 6 with 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.md and 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. collectHostnameClaims returns 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 a HostnameConflict condition that is never written outside HTTP-01, where its absence reads as "no collision". next names three such modes, the released pages two, because edge exists only on next.

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 the edge, class-selection or tunnel material is backported, because those features are not in those releases.

Verification

hugo --gc --minify completes with no errors, and so does hugo --environment development --gc --minify. Both were run, because config/production/hugo.yaml excludes docs/next/** from every language mount and hugo defaults 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 under next/. 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.md gains the Cloudflare Tunnel Gateway Controller card; guides/platform-stack/_index.md carries 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-logo form the first uses.

next does not say that a stale HostnameConflict condition 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. next has 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, existingSecret or edge cluster 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 v1beta1 coverage by the versions its rule names; coverage actually comes from matchConstraints.matchPolicy defaulting to Equivalent, 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 or existingSecret.

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 lint job is red on main for 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 running update-digests alone turns the check green while recording the stale translations as approved, so it is a deliberate choice rather than the quick option.

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>
@netlify

netlify Bot commented Aug 17, 2026

Copy link
Copy Markdown

Deploy Preview for cozystack ready!

Name Link
🔨 Latest commit dc2fca0
🔍 Latest deploy log https://app.netlify.com/projects/cozystack/deploys/6a833cbb32725200087adf8d
😎 Deploy Preview https://deploy-preview-655--cozystack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 34fd084e-7e13-46d0-89fe-a83174e01324


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant