SAN batching & wildcards
Batch domains onto shared SAN certificates, issue wildcards via DNS-01, and learn hostnames from your app at runtime.
Beyond one host, one cert#
Upstream kamal's automatic TLS is one Let's Encrypt certificate per configured host, HTTP-01 only, single-server. dash-proxy extends the whole surface:
- SAN batching — many domains share one certificate, so a multi-tenant app with hundreds of custom domains doesn't run into Let's Encrypt rate limits one cert at a time.
- Wildcard certificates — issued via DNS-01 challenges against your DNS provider's API.
- Dynamic TLS domains — the proxy polls your application for the hostname list instead of fixing it at deploy time.
- On-demand TLS — the proxy asks your app per-handshake whether to issue for an unknown hostname.
- mTLS —
ssl: client_ca_pem:requires client certificates signed by your CA bundle.
All of it terminates at the load balancer when one fronts the fleet — certificates are edge concerns.
Dynamic TLS domains & SAN batching#
ssl_domains tells the proxy to learn hostnames from your application at runtime: it polls source — a path resolved against a healthy app target, or an absolute URL — for the domain list and manages Let's Encrypt certificates for it automatically. With source set, ssl: true is allowed without host/hosts.
proxy:
ssl: true
ssl_domains:
source: /api/v1/kamal/domains
interval: 300 # poll interval in seconds (proxy default: 300)
batch_size: 25 # 1 = per-domain certs; 2-25 = stable SAN batchingbatch_size is the SAN-batching knob: how many domains share one certificate. The batching is stable — a domain joining or leaving doesn't reshuffle every other domain's certificate.
Authentication tokens for the poll endpoint and the refresh nudge are read from DASH_PROXY_DOMAINS_TOKEN and DASH_PROXY_REFRESH_TOKEN on the proxy container (the KAMAL_PROXY_ names still work as a fallback) — set them via proxy.run.options.env, never as deploy flags. Operate the domain list with:
dash proxy domains list # what the proxy currently serves
dash proxy domains stats # certificate issuance state
dash proxy domains refresh # nudge an immediate re-pollWildcard certificates (DNS-01)#
DNS-01 issuance is configured proxy-wide under proxy: run: acme: — every service on the proxy shares the ACME account and DNS provider. DNS-01 activates only when dns_provider is set; unset means HTTP-01 only, so credentials visible in the proxy's environment can never arm DNS-01 on their own.
proxy:
run:
acme:
email: [email protected]
# One provider for every zone, or a hash pinning zones to the DNS
# host that serves them, with `default` for the rest.
dns_provider:
platform.example: cloudflare
legacy.example: hetzner
default: route53
prefer_wildcard: true # ask for a wildcard when the provider supports it
http_fallback: true # fall back to HTTP-01 when DNS-01 fails
credentials:
- CF_DNS_API_TOKEN # secret names, passed to the proxy as env varsProviders: cloudflare, digitalocean, gcloud, godaddy, hetzner, namecheap, route53, vultr (plus auto, which picks from the credentials it can see and logs which one it armed). credentials names entries in .dash/secrets; they are written to a 0600 env file on the proxy host and passed with --env-file, never on the command line.
While working out a DNS setup, point directory at Let's Encrypt's staging environment so failed attempts don't burn production rate limits.
A host/hosts name is grouped into a wildcard from the name itself. A name learned from ssl_domains is tenant-owned and may sit in a zone you don't run DNS for, so the proxy never guesses a wildcard for it: from proxy v1.1.0.2, it collapses only under a zone the hash form of dns_provider maps explicitly — mapping a zone is you asserting DNS control over it. Under a mapped zone, every single-label name is ordered as that zone's wildcard; the apex and deeper names ride the same order as concrete identifiers. default and auto never collapse a dynamic name. If the wildcard order fails, its names retry once as concrete identifiers, where http_fallback can still answer — a failing DNS-01 degrades the zone to per-name orders instead of taking it off the air.
dash proxy reboot yourself after a rotation.On-demand TLS & custom certificates#
The ssl: hash is home to the rest of the TLS surface:
certificate_pem/private_key_pem— bring your own certificate, loaded from.dash/secrets.on_demand_url— instead of a fixed host list, the proxy asks this endpoint whether it may issue for the hostname in an incoming handshake (2xx approves). Mutually exclusive withhost/hosts,certificate_pem, andssl_domains— dash rejects the combination at config time.client_ca_pem— mutual TLS: clients must present a certificate signed by this CA bundle.
proxy:
ssl:
certificate_pem: CERTIFICATE_PEM
private_key_pem: PRIVATE_KEY_PEMMoving certificates between hosts#
Issued certificates are state worth keeping — reissuing hundreds of domains on a host move would take hours and eat rate limits. dash can export and import the proxy's certificate store:
dash proxy export_certs ./certs.tar.gz # contains private keys — handle accordingly
dash proxy import_certs # from an exported archive, or a Traefik acme.jsonimport_certs also understands Traefik's acme.json, which is the escape hatch for migrating a Traefik-fronted fleet onto dash-proxy without reissuing.