Custom-domain HTTPS
Two paths, depending on whether you control DNS for the tenant.
Path 1; Cloudflare for SaaS
The least-friction option. Tenants point a CNAME from their root domain to acme.app.example.com; Cloudflare issues and rotates the cert on their edge. Your origin only ever sees the apex hostname.
Steps
Enable Cloudflare for SaaS on your zone.
Set the fallback origin to
app.example.com.POST to Cloudflare's API when a tenant adds a custom domain:
tsawait fetch('https://api.cloudflare.com/client/v4/zones/<zone>/custom_hostnames', { method: 'POST', headers: { authorization: `Bearer ${env.get('CF_API_TOKEN')}`, 'content-type': 'application/json', }, body: JSON.stringify({ hostname: tenant.customDomain, ssl: { method: 'http', type: 'dv' }, }), })The tenant adds the
CNAMEand Cloudflare issues the cert. Validate via the Cloudflare webhook or by polling the API.Your
CustomDomainMiddlewarealready maps the hostname to the tenant viabranding.custom_domain. Done.
When to use it
- You want zero touch on the origin (no cert renewal, no Ingress changes).
- Tenants are end customers without DevOps teams.
- The 4 ¢ / month per hostname is acceptable.
Path 2; cert-manager + DNS-01
You manage everything in-house. Wildcard cert for the apex (*.app.example.com), per-tenant certs for custom domains.
Wildcard apex (DNS-01)
# Issuer (cert-manager) — one-time setup
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-dns01
solvers:
- dns01:
route53:
region: us-east-1
# IRSA / IAM role for the cert-manager pod# Wildcard cert
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-example-com-tls
namespace: lasagna
spec:
secretName: app-example-com-tls
issuerRef:
name: letsencrypt-dns01
kind: ClusterIssuer
dnsNames:
- app.example.com
- "*.app.example.com"Per-tenant custom domain
When a tenant adds acme.com:
- Validate they own it (e.g.,
_lasagna-verifyTXT record). - Create a
Certificateresource per tenant; single-name, HTTP-01 solver because Let's Encrypt allows HTTP-01 for non-wildcard issuance. - Reference the resulting
Secretin the Ingresstlsentries.
A small operator (or a controller in your app) is the cleanest way to manage the lifecycle. The package does not ship one; this recipe gives you the moving parts.
Common pitfalls
- Wildcard cert with HTTP-01: doesn't work. Wildcards require DNS-01.
Hostheader mismatch: cert isacme.com, but the request arrives aswww.acme.com. Add both names to theCertificatespec, or strip thewww.inCustomDomainMiddleware.- HSTS preload: tenants who enable HSTS preload through your apex domain commit to TLS for the apex and every subdomain for ~2 years. Confirm with each tenant before adding.
- Header-domain disagreement: by default, an explicit
x-tenant-idheader wins over theHost-resolved tenant. That preserves backwards compatibility but lets a caller who knows your custom domain hop tenants. Opt intomiddleware.customDomain({ strict: true })to reject conflicts withE_TENANT_HEADER_DOMAIN_MISMATCH(400). See Routing — strict mode.
Read next
- Branding satellite; where
custom_domainis stored. - Routing, custom domain mapping