A complete configuration for serving multiple tenants on a single HTTPS listener, where each tenant has its own ACME-managed certificate with an independent renewal lifecycle. This example uses per-SNI ACME, available since 26.05_1.
Use case
You operate a SaaS platform that serves customer-owned domains (customer-a.com, customer-b.com, etc.) alongside your own (example.com). You need:
- Each tenant’s certificate issued and renewed automatically.
- A failure renewing one tenant (e.g. DNS-01 stuck waiting for propagation) must not delay any other tenant’s renewal.
- Independent ACME accounts per tenant — different contact emails, different storage paths so cert state stays isolated, optionally different challenge providers.
- The platform’s primary domain on the same listener as a “root” ACME-managed certificate.
Architecture
┌─ Tenant A scheduler (HTTP-01) ─→ tenant-a.com
│
Client ──TLS─▶ Zentinel listener ────┼─ Tenant B scheduler (DNS-01) ─→ *.tenant-b.com
│
├─ Manual cert ─────────────────→ partner-domain.com
│
└─ Root ACME scheduler ─────────→ example.com
Each ACME block instantiates its own RenewalScheduler and AcmeClient (“Option B” architecture). They run as independent background tasks so a stuck issuance is contained to that tenant’s lifecycle.
Configuration
listener "https" {
address "0.0.0.0:443"
protocol "https"
tls {
// Root ACME: covers the platform's primary domain.
acme {
email "[email protected]"
domains "example.com" "www.example.com"
}
additional-certs {
// Tenant A: HTTP-01, default storage layout.
sni-cert {
acme {
email "[email protected]"
domains "customer-a.com" "www.customer-a.com"
}
}
// Tenant B: DNS-01 wildcard via Cloudflare,
// separate storage path, separate ACME account.
sni-cert {
acme {
email "[email protected]"
domains "*.customer-b.com" "customer-b.com"
challenge-type "dns-01"
storage "/var/lib/zentinel/acme/tenant-b"
dns-provider {
cloudflare {
api-token-file "/etc/zentinel/secrets/tenant-b-cf-token"
}
}
}
}
// A partner who supplied their own certificate — manual
// and ACME-managed sni-certs coexist on the same listener.
sni-cert {
hostnames "partner-domain.com"
cert-file "/etc/zentinel/certs/partner.crt"
key-file "/etc/zentinel/certs/partner.key"
}
}
}
}
Notes
Implicit hostnames
The Tenant A and Tenant B blocks omit hostnames and rely on the implicit derivation: routing hostnames come from each block’s acme.domains list. The wildcard pattern is preserved (*.customer-b.com becomes a wildcard SNI route, exact customer-b.com becomes an exact match).
Storage path isolation
Tenant B uses an explicit storage directory. Tenant A and the root ACME share the default storage. Zentinel enforces global domain uniqueness at config-validation time, so even if two blocks accidentally claimed the same domain, the validator rejects the config before any storage collision can occur.
First-start behavior
On the very first run, none of the ACME-managed certificates exist on disk. Zentinel logs a structured warning per missing cert (with listener_id, sni_index, and primary_domain) and increments zentinel_tls_sni_cert_skip_total. Issuance starts in the background; once a challenge completes, that cert is hot-reloaded into the SNI resolver without restart.
If a cert is still missing after issuance should have completed (typical: minutes for HTTP-01, up to an hour for DNS-01 propagation), check the metric and the structured warning — they identify the listener and SNI slot that is stuck.
Operational checklist
-
Each tenant’s
emailis reachable for ACME account recovery and CA notifications. -
Each
storagepath is on persistent disk (not ephemeral container storage), and backed up. - DNS-01 credentials are scoped per tenant — the Cloudflare API token in the example is restricted to Tenant B’s zone only.
- HTTP-01 challenges require the listener to also be reachable on port 80 (Zentinel’s challenge manager binds there). Add a port-80 listener if you have not already.
-
Monitor
zentinel_tls_sni_cert_skip_total— a non-zero value an hour after startup means an issuance is failing and the corresponding tenant is being served the listener default certificate (which will produce a CN/SAN-mismatch warning in clients).