Skip to content

gateway: a wildcard hostname claim builds a listener name the CRD rejects, failing the whole reconcile #3787

Description

A tenant route may claim a wildcard hostname, and the listener name the controller builds from it is rejected by the Gateway CRD schema. The reconcile then fails for the whole TenantGateway, not just for that route.

The chain, each link checked against main:

The route policy admits a wildcard from a tenant namespace. "*.foo.example.com".endsWith("." + "foo.example.com") is true, so a route declaring *.<apex> passes admission.

Nothing filters wildcards out of the claim path. Grepping internal/controller/tenantgateway/reconciler.go for wildcard handling returns only the lines that build one for the certificate; none that drop one coming in.

hostnameFirstLabel takes everything before the first dot:

func hostnameFirstLabel(hostname string) string {
	hostname = strings.ToLower(hostname)
	if i := strings.Index(hostname, "."); i >= 0 {
		return hostname[:i]
	}
	return hostname
}

For *.apex that returns *, and perListenerName composes https- + * + - + hex, giving https-*-<hex>.

The listener name pattern in the CRD that actually ships (packages/system/gateway-api-crds/templates/crds-experimental.yaml, same for Gateway v1 and v1beta1) is:

^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$

(taken from spec.listeners.name by walking the schema path, not by grepping the file. A grep lands on .group first, and that one carries an extra ^$| alternative because an empty group is legal while an empty listener name is not.)

Checked against it directly:

https-*-06a3e25d        REJECT
https-harbor-06a3e25d   ACCEPT

So the Gateway write is refused, and because the controller renders all listeners in one object, the failure is not scoped to the offending route: certificate issuance for every other hostname on that gateway stops with it.

Two things are inferred rather than observed, and worth stating as such. The apiserver's refusal follows from the pattern rather than from a live cluster run. And unit tests will not catch it: the fake client used there does not validate SectionName against the schema.

This is not introduced by any open PR. Neither the missing wildcard filter nor hostnameFirstLabel differs between main and the branches currently in flight.

The fix is a filter on the claim path. Whether a wildcard claim should be dropped silently, rejected at admission with a message, or mapped to a stable label is a design call, not an obvious one.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)
    on Aug 12, 2026
  2. dosubot commented on Aug 12, 2026

    @dosubot

    Good write-up. I can confirm the analysis against the code and fill in one detail you flagged as inferred.

    The VAP does admit the wildcard

    The cozystack-route-hostname-policy CEL expression only checks suffix containment: for *.foo.example.com with an apex of foo.example.com, "*.foo.example.com".endsWith(".foo.example.com") → true. It never inspects whether the first label is *.

    collectHostnameClaims passes wildcards straight through

    Every hostname from route.Spec.Hostnames is added to the output map with no filter on the leading label [1]. That map feeds dynHostnames, which is passed to both reconcileGateway and reconcilePerListenerCertificates [2]. There is no guard between admission and listener construction.

    perListenerName produces the invalid name

    hostnameFirstLabel("*.foo.example.com") returns "*" [3], so perListenerName composes "https-*-<hex>" [4]. That string fails the Gateway CRD's listener name regex, and because all listeners are written in a single Gateway object, the apiserver rejects the entire write — not just the offending entry.

    The bug surface is HTTP-01 only

    Both DNS-01 and existingSecret modes return nil from collectHostnameClaims immediately [5], so wildcards from tenant routes never reach perListenerName.

    On the design question

    Your three options map to concrete insertion points:

    1. Drop silently at collection time — skip any h where strings.HasPrefix(string(h), "*.") in the collectHostnameClaims loop. Simplest patch; the route stays admitted but gets no listener or cert, so it sits Accepted=False with no message explaining why.
    2. Reject at admission — add a CEL clause to the VAP rejecting h.startsWith("*.") in tenant namespaces. Makes the failure visible to the route author at creation time and preserves the invariant that anything the VAP admits is safe to reconcile.
    3. Map to a stable label — replace * with a fixed string (e.g. "wildcard") inside hostnameFirstLabel. Lowest disruption, but silently swallows the wildcard semantics and could issue an incorrect per-listener certificate.

    Option 2 looks cleanest: the reconciler's contract becomes "anything in dynHostnames has a safe first label", and the test gap you identified (fake client not validating SectionName) can be closed with a unit test asserting no * appears in any listener name produced by perListenerName.

    To reply, just mention @dosu.


    Share context across your team and agents. Try Dosu.

    Leave Feedback Learn about cozystack with Dosu Add Dosu to your team

  3. lexfrei commented on Sep 17, 2026

    @lexfrei
    ContributorAuthor

    Fixed on main by #3342 (2bf9ae4), in both places.

    judgeHTTPRouteClaim now refuses an HTTPRoute wildcard that wants the terminate listener under HTTP-01, and the route gets a status message. hostnameFirstLabel rewrites a leading * to wildcard, so the listener name is https-wildcard-06a3e25d and the CRD pattern accepts it.

    The test gap is closed as well. TestRenderGateway_ListenerNamesAreAdmissibleForEveryRouteHostname checks rendered names against the pattern of the shipped Gateway CRD, so the fake client is out of the picture. It passes on main at 8a6fe02 together with TestReconcile_WildcardHTTPRouteHostnameIsRefusedUnderHTTP01.

    Same caveat as in the report: the apiserver refusing https-*-<hex> was inferred from the pattern, I never saw it on a live cluster.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)kind/bugCategorizes issue or PR as related to a bugtriage/needs-triageIndicates an issue needs triage by a maintainer

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions