Repository navigation
feat(proxy): add wildcard domains with on-demand TLS - #27
Conversation
- Route hosts one label below a wildcard domain (*.example.com) to the app; exact domains win - Issue a Let's Encrypt certificate per host on its first TLS handshake (HTTP-01) and renew it with the others - Guard on-demand issuance: optional ON_DEMAND_TLS_ASK approval endpoint, ON_DEMAND_TLS_PER_HOUR limit, one-hour backoff after a failure - Reject malformed wildcards and wildcards as an app's primary domain
Preview domains (preview-<label>.<primary>) fall under a wildcard on the primary domain. Their certificates stay on disk after the preview is removed and were picked up as on-demand hosts, so they would have been renewed indefinitely.
Review: wildcard domains with on-demand TLSOverall the approach is sound: no wildcard certificates (which would need DNS-01), instead one certificate per host issued during the first TLS handshake over HTTP-01, the same way exact domains and previews already get theirs in the SNI callback. Routing, Three things must be fixed before merging, the rest is optional. Must fix1. Renewal gates exact domains of other apps through
This hits the default setup directly: apps get writeCert('api.example.com')
const ask = vi.fn(async () => false)
await certs.renewExpiringCerts(['api.example.com', '*.example.com'], renew, now, ask)
// returns [] instead of ['api.example.com']Fix: remove every exact (non-wildcard) entry of 2. Removing the primary domain can promote a wildcard to primary ( The guard "a wildcard cannot be the primary domain" only exists in Fix: in the DELETE route, reject removing 3. A failed ask call is treated as a denial and locks the host out for an hour (
Fix: distinguish "denied" from "ask failed". On a failed call, throw without recording the host in Should fix
Docs and CLIREADME and CHANGELOG are updated, good. The CLI is untouched:
|
- Renew exact domains (of any app, or DOMAIN) below a wildcard as regular domains, never through ON_DEMAND_TLS_ASK - Distinguish a denial (4xx, one-hour backoff) from an ask failure (timeout, network error, 5xx): retry the host after a minute and a due renewal on the next run - Check the hourly cap before calling the ask endpoint and prune expired backoff entries - Release the ask response body for keep-alive
- Refuse removing the primary domain when a wildcard would take its place - Validate the domain passed to zero deploy like zero domain add does - Never match preview-* hosts with a wildcard, so a destroyed preview is not routed to the production app and its leftover certificate is not renewed
|
Thanks for the thorough review! All points are addressed in e8efbed..3d66ac3: Must fix
Should fix
Docs and CLI
I only touched the two strings in Tests: 302 server (12 new), 37 CLI, both |
rbluethl
left a comment
There was a problem hiding this comment.
Verified e8efbed..3d66ac3: every point from the review is addressed, the exact-domain renewal case is covered by a test, and tests (302), both tsc runs and biome check src pass on the branch. Reserving the preview- label space is the cleaner answer to the preview question, fine with the trade-off.
One minor thing, not blocking: the comment "a flood of random names costs neither ask calls nor certificates" only holds once 20 approved certificates were issued in the hour. Denied names don't count against ON_DEMAND_TLS_PER_HOUR, so until then every distinct random name still costs one ask call. Fine for us since the ask endpoint is cheap, but if that ever matters, ask calls would need their own hourly budget.
Summary
Adds wildcard domains:
zero domain add myapp '*.myapp.com'routes every host one label below the wildcard to the app. Useful for multi-tenant apps that tell customers apart by theHostheader (in our case: one subdomain per device, e.g.club1.example.com, served by one app).Today this needs one
zero domain addper host, because the proxy looks hosts up exactly, the TLS server rejects unknown SNI names, and certificates are only issued over HTTP-01 (no wildcard certificates).How it works
a.myapp.comfalls back to*.myapp.com. Exactly one label deep, like DNS and certificate wildcards (myapp.comanda.b.myapp.comdon't match). HTTP and WebSocket upgrades both use the same lookup.ON_DEMAND_TLS_ASK(optional, recommended): zero callsGET <url>?domain=<host>and only issues or renews on2xxON_DEMAND_TLS_PER_HOUR(default20): cap on new on-demand certificates*.com,a.*.myapp.com) are rejected, and a wildcard can't be an app's primary domain, since that one is used for the app URL and preview subdomains.No behaviour changes for existing apps without a wildcard domain.
Testing
npm test: 290 passednpm run buildworks andbiome check srcis clean.npm run lintstill reports the formatting ofcli/src/commands/mcp.ts, which already fails onmainand isn't touched here.README (Domains, server configuration, TLS) and CHANGELOG (
[Unreleased]) are updated.