Skip to content

Add wildcard route support (MIR-781)#659

Merged
evanphx merged 10 commits intomainfrom
mir-781-add-ability-to-route-wildcard-routes-to-an-app
Mar 11, 2026
Merged

Add wildcard route support (MIR-781)#659
evanphx merged 10 commits intomainfrom
mir-781-add-ability-to-route-wildcard-routes-to-an-app

Conversation

@evanphx
Copy link
Copy Markdown
Contributor

@evanphx evanphx commented Mar 11, 2026

Summary

  • Add LookupWithWildcard to ingress client: chains exact match → wildcard subdomain (*.example.com) → wildcard bare domain → nil, all using existing entity index for O(1) lookups
  • Add ValidateWildcardHost to reject invalid patterns like *.com or foo.*.com
  • Update HTTP ingress to use wildcard-aware lookup for incoming requests
  • Update RouteSet.HasRoute in autotls to provision TLS certs for wildcard subdomains
  • Add CLI validation when setting wildcard routes (miren route set *.example.com app)

Test plan

  • hack/it ./api/ingress/... — 27 tests pass (wildcard subdomain, bare domain, exact priority, case insensitivity, validation)
  • hack/it ./components/autotls/... — 8 tests pass
  • make test — 3582 tests pass, 20 skipped
  • make lint — 0 issues

Allow routing wildcard hostnames (e.g., *.pds.miren.garden) to apps.
Lookup chains exact match → wildcard subdomain → wildcard bare domain,
all using existing entity index for O(1) lookups. Includes CLI
validation, TLS cert provisioning for wildcard subdomains, and tests.
@evanphx evanphx requested a review from a team as a code owner March 11, 2026 00:18
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c58e01c-8d6b-4dca-a84e-93dc82dbbc65

📥 Commits

Reviewing files that changed from the base of the PR and between 45aa3ce and e29d7b9.

📒 Files selected for processing (1)
  • cli/commands/server.go

📝 Walkthrough

Walkthrough

Adds wildcard routing: Client.LookupWithWildcard and ValidateWildcardHost, CLI host validation, tests, and docs. Replaces the route-watcher/autocert integration with a controller-based TLS model: removes RouteWatcher/RouteSet, introduces CertificateProvider and HTTPChallengeProvider interfaces, adds ServeTLSWithController, and wires an AutocertController plus coordinator support for ACME HTTP-01 (including readiness signaling). Exposes LoadOrGenerateFallbackCert as LoadOrGenerateFallbackCert. HTTP ingress lookup now uses LookupWithWildcard. Tests and documentation updated to cover wildcard behavior and the new certificate controller.


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

Update traffic-routing guide with wildcard route section, add TLS note
for wildcard subdomain cert provisioning, add changelog entry, and
update route terminology to mention wildcard patterns.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
components/autotls/autotls.go (1)

39-62: Consider sharing the wildcard-candidate generation with ingress lookup.

HasRoute now reimplements the same exact → first-label wildcard → bare-domain wildcard order as api/ingress/client.go. Pulling that host-to-candidate logic into one helper would reduce drift between TLS certificate decisions and request routing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/autotls/autotls.go` around lines 39 - 62, HasRoute duplicates the
host→candidate generation logic used by the ingress lookup; extract that logic
into a shared helper (e.g., HostCandidates(host string) []string) that returns
candidates in the exact → first-label-wildcard → bare-domain-wildcard order,
then change RouteSet.HasRoute to iterate those candidates against rs.hosts (and
update api/ingress/client.go to call the same helper) so both TLS decisioning
and routing use the identical candidate generation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/ingress/client.go`:
- Around line 97-111: Update ValidateWildcardHost to reject any '*' characters
that are not part of a single leading '*.' pattern: if host doesn't start with
"*." and contains any '*' return an error; if it starts with "*." continue
existing checks (remainder non-empty, no additional '*', and remainder contains
a dot). Also ensure callers like SetRoute call ValidateWildcardHost(host) and
propagate the error, and update TestValidateWildcardHost to mark cases like
"foo.*.com", "*" and "**" as expecting an error.

---

Nitpick comments:
In `@components/autotls/autotls.go`:
- Around line 39-62: HasRoute duplicates the host→candidate generation logic
used by the ingress lookup; extract that logic into a shared helper (e.g.,
HostCandidates(host string) []string) that returns candidates in the exact →
first-label-wildcard → bare-domain-wildcard order, then change RouteSet.HasRoute
to iterate those candidates against rs.hosts (and update api/ingress/client.go
to call the same helper) so both TLS decisioning and routing use the identical
candidate generation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8beabeae-7b26-4bef-a631-34d5c7c9e945

📥 Commits

Reviewing files that changed from the base of the PR and between 2266660 and dbc0853.

📒 Files selected for processing (5)
  • api/ingress/client.go
  • api/ingress/client_test.go
  • cli/commands/route_set.go
  • components/autotls/autotls.go
  • servers/httpingress/httpingress.go

Comment on lines +97 to +111
// ValidateWildcardHost validates a wildcard host pattern.
// Valid patterns: *.example.com, *.sub.example.com
// Invalid: *.com, foo.*.com, **, *
func ValidateWildcardHost(host string) error {
if !strings.HasPrefix(host, "*.") {
return nil
}
remainder := host[2:]
if remainder == "" || strings.Contains(remainder, "*") {
return fmt.Errorf("invalid wildcard pattern: %s (must be *.domain.tld)", host)
}
if !strings.Contains(remainder, ".") {
return fmt.Errorf("invalid wildcard pattern: %s (must have at least two domain labels after *)", host)
}
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject any * outside a single leading *. pattern.

ValidateWildcardHost currently returns nil for foo.*.com, *, and ** because it exits early unless the host starts with *.. That contradicts the helper’s own docstring and the PR contract, and right now the CLI path is the only place this validation runs. Please fail on any other * placement and enforce the same check from SetRoute so non-CLI callers can’t persist bad hosts.

🔧 Possible fix
 func ValidateWildcardHost(host string) error {
-	if !strings.HasPrefix(host, "*.") {
+	if !strings.Contains(host, "*") {
 		return nil
 	}
+	if !strings.HasPrefix(host, "*.") || strings.Count(host, "*") != 1 {
+		return fmt.Errorf("invalid wildcard pattern: %s (must be *.domain.tld)", host)
+	}
 	remainder := host[2:]
-	if remainder == "" || strings.Contains(remainder, "*") {
+	if remainder == "" {
 		return fmt.Errorf("invalid wildcard pattern: %s (must be *.domain.tld)", host)
 	}
 	if !strings.Contains(remainder, ".") {
 		return fmt.Errorf("invalid wildcard pattern: %s (must have at least two domain labels after *)", host)
 	}
 	return nil
 }
// Also enforce this in SetRoute:
if err := ValidateWildcardHost(host); err != nil {
	return nil, err
}

TestValidateWildcardHost should then flip foo.*.com to wantErr: true and add * / ** cases.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ValidateWildcardHost validates a wildcard host pattern.
// Valid patterns: *.example.com, *.sub.example.com
// Invalid: *.com, foo.*.com, **, *
func ValidateWildcardHost(host string) error {
if !strings.HasPrefix(host, "*.") {
return nil
}
remainder := host[2:]
if remainder == "" || strings.Contains(remainder, "*") {
return fmt.Errorf("invalid wildcard pattern: %s (must be *.domain.tld)", host)
}
if !strings.Contains(remainder, ".") {
return fmt.Errorf("invalid wildcard pattern: %s (must have at least two domain labels after *)", host)
}
return nil
// ValidateWildcardHost validates a wildcard host pattern.
// Valid patterns: *.example.com, *.sub.example.com
// Invalid: *.com, foo.*.com, **, *
func ValidateWildcardHost(host string) error {
if !strings.Contains(host, "*") {
return nil
}
if !strings.HasPrefix(host, "*.") || strings.Count(host, "*") != 1 {
return fmt.Errorf("invalid wildcard pattern: %s (must be *.domain.tld)", host)
}
remainder := host[2:]
if remainder == "" {
return fmt.Errorf("invalid wildcard pattern: %s (must be *.domain.tld)", host)
}
if !strings.Contains(remainder, ".") {
return fmt.Errorf("invalid wildcard pattern: %s (must have at least two domain labels after *)", host)
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/ingress/client.go` around lines 97 - 111, Update ValidateWildcardHost to
reject any '*' characters that are not part of a single leading '*.' pattern: if
host doesn't start with "*." and contains any '*' return an error; if it starts
with "*." continue existing checks (remainder non-empty, no additional '*', and
remainder contains a dot). Also ensure callers like SetRoute call
ValidateWildcardHost(host) and propagate the error, and update
TestValidateWildcardHost to mark cases like "foo.*.com", "*" and "**" as
expecting an error.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
docs/docs/tls.md (1)

15-15: Consider clarifying that wildcard routes also match the bare domain.

The phrase "each matching subdomain" may not fully convey that wildcard routes also match the bare domain itself. Based on the detailed explanation in traffic-routing.md (lines 31-33), *.myapp.example.com matches both subdomains and myapp.example.com (the bare domain).

📝 Optional refinement for clarity
-For wildcard routes (e.g., `*.myapp.example.com`), TLS certificates are provisioned for each matching subdomain as requests arrive. See [Wildcard Routes](/traffic-routing#wildcard-routes) for details.
+For wildcard routes (e.g., `*.myapp.example.com`), TLS certificates are provisioned for each matching hostname (including subdomains and the bare domain) as requests arrive. See [Wildcard Routes](/traffic-routing#wildcard-routes) for details.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/docs/tls.md` at line 15, Update the sentence about wildcard routes so it
explicitly states that a pattern like '*.myapp.example.com' matches both all
subdomains and the bare domain ('myapp.example.com'); edit the line containing
the example '*.myapp.example.com' (and the phrase "each matching subdomain") to
say "each matching subdomain and the bare domain (e.g., 'myapp.example.com')"
and ensure the existing "Wildcard Routes" link remains for details.
docs/docs/terminology.md (1)

43-43: Clarify that wildcard routes match both subdomains and the bare domain.

The phrase "match all subdomains" is slightly imprecise. According to the detailed documentation in traffic-routing.md (lines 31-33), wildcard routes like *.myapp.example.com match both subdomains (foo.myapp.example.com) and the bare domain itself (myapp.example.com).

📝 Suggested refinement for precision
-Maps a hostname to an application. Routes determine how HTTP traffic reaches your apps. Your first app gets a default route automatically. Routes can use wildcard patterns (e.g., `*.example.com`) to match all subdomains of a domain.
+Maps a hostname to an application. Routes determine how HTTP traffic reaches your apps. Your first app gets a default route automatically. Routes can use wildcard patterns (e.g., `*.example.com`) to match subdomains and the bare domain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/docs/terminology.md` at line 43, Update the sentence describing wildcard
routes so it explicitly states that patterns like `*.example.com` match both
subdomains and the bare (root) domain; e.g., change the line containing "Routes
can use wildcard patterns (e.g., `*.example.com`) to match all subdomains of a
domain." to say they "match both subdomains and the bare domain (for example,
`*.myapp.example.com` matches `foo.myapp.example.com` and `myapp.example.com`)"
and ensure consistency with the explanation in traffic-routing.md.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/docs/terminology.md`:
- Line 43: Update the sentence describing wildcard routes so it explicitly
states that patterns like `*.example.com` match both subdomains and the bare
(root) domain; e.g., change the line containing "Routes can use wildcard
patterns (e.g., `*.example.com`) to match all subdomains of a domain." to say
they "match both subdomains and the bare domain (for example,
`*.myapp.example.com` matches `foo.myapp.example.com` and `myapp.example.com`)"
and ensure consistency with the explanation in traffic-routing.md.

In `@docs/docs/tls.md`:
- Line 15: Update the sentence about wildcard routes so it explicitly states
that a pattern like '*.myapp.example.com' matches both all subdomains and the
bare domain ('myapp.example.com'); edit the line containing the example
'*.myapp.example.com' (and the phrase "each matching subdomain") to say "each
matching subdomain and the bare domain (e.g., 'myapp.example.com')" and ensure
the existing "Wildcard Routes" link remains for details.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb1fc769-9dca-4060-bad9-f719c4ec706f

📥 Commits

Reviewing files that changed from the base of the PR and between dbc0853 and 21feb98.

📒 Files selected for processing (4)
  • docs/docs/changelog.md
  • docs/docs/terminology.md
  • docs/docs/tls.md
  • docs/docs/traffic-routing.md
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/changelog.md

Copy link
Copy Markdown
Contributor

@phinze phinze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude and I sat down with this one and traced through the lookup chains, the autotls cert provisioning path, and the validation logic end-to-end. Clean feature, well-structured.

The three-step fallback in LookupWithWildcard (exact → wildcard subdomain → wildcard bare domain) is a nice design — deterministic priority with no iteration or regex, just index lookups. And mirroring the same logic in RouteSet.HasRoute for cert provisioning means wildcards get per-subdomain certs on demand rather than a blanket wildcard cert, which is the right call.

One thing we noted: on the HTTP ingress hot path, a wildcard-routed subdomain now does 2 serial entity lookups (exact miss → wildcard hit), and a complete miss does 3 before falling through to the default route check. These are local RPC calls so latency is low, but if we ever want to tighten this up, a batched LookupAny(ctx, ...keys) that resolves multiple index keys in one RPC round-trip would collapse the chain. Not urgent — exact matches (the common case) are still 1 call — just flagging it as a future optimization opportunity.

When `miren route set` links a subdomain to an app, the first HTTPS
request would time out because autocert only provisions certs lazily
on the first TLS handshake — blocking the request for 30-90s while
ACME completes.

Introduces AutocertController, which plugs into the existing
controller framework to watch http_route entities and eagerly call
autocert.Manager.GetCertificate() when routes appear. This replaces
the bespoke RouteWatcher + ServeTLS machinery with the same
reconcile pattern the DNS-01 certificate controller already uses.

The coordinator now always creates a certificate controller: DNS-01
via lego when a DNS provider is configured, HTTP-01 via autocert
otherwise. A HostPolicy on the autocert manager restricts cert
provisioning to domains with active routes, and a ready channel
handles the startup ordering between the controller manager and the
port-80 ACME challenge server.

MIR-771
phinze added 5 commits March 11, 2026 14:23
Matches the port-80 HTTP server's timeout for consistency and
slowloris protection.
HTTP-01 challenges can't provision wildcard certs (that requires
DNS-01), so wildcard routes like *.example.com need special handling.

When Reconcile sees a wildcard route, it stores the wildcard pattern
in allowedHosts for HostPolicy matching, but eagerly provisions only
the base domain cert. Individual subdomains provision inline when
they first arrive via GetCertificate.

isAllowedHost checks both exact matches and wildcard coverage, so
foo.example.com is allowed when *.example.com is in the set.
Wildcard routes (*.example.com) can't be eagerly provisioned — HTTP-01
can't issue wildcard certs and we don't know which subdomains will be
requested. Just store the pattern in allowedHosts so HostPolicy accepts
subdomains, and let them provision inline on first TLS handshake.

The bare domain (example.com) is not covered by its wildcard, matching
DNS semantics — it needs its own explicit route to get a cert.
…t-when-miren-route-set-is-run

Prefetch TLS certs eagerly when routes are configured
@evanphx
Copy link
Copy Markdown
Contributor Author

evanphx commented Mar 11, 2026

@phinze agree on the round trips, we can do that as a follow up. I think we might want to also add a cache on the check side since most of the pain will come from something doing requests over and over, so even a 5 min TTL cache would eliminate most of the pain.

evanphx added 2 commits March 11, 2026 14:31
…le.com

Wildcard routes match exactly one DNS label, so *.foo.example.com
matches blah.foo.example.com but not foo.example.com. Removes the
bare-domain fallback from LookupWithWildcard and isAllowedHost to
align routing and TLS cert provisioning on the same semantics.
@evanphx evanphx merged commit 0e7a5e3 into main Mar 11, 2026
13 checks passed
@evanphx evanphx deleted the mir-781-add-ability-to-route-wildcard-routes-to-an-app branch March 11, 2026 21:44
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.

2 participants