AI app builders are everywhere now. You enter a prompt, get a deployed product onDocumentation Index
Fetch the complete documentation index at: https://mint.skeptrune.com/llms.txt
Use this file to discover all available pages before exploring further.
your-app.builder.com, and ship. Replit, Bolt, Lovable, v0, and dozens of other similar platforms launched in the past few months, and they all need instant subdomain provisioning with HTTPS for every user. This pattern isn’t new—multi-tenant SaaS has used tenant-id.foo.com subdomains forever—but the explosion of AI builders that spin up hundreds of new subdomains daily makes the certificate management problem more visible. You can’t provision individual certificates for every generated app; you need wildcard certificates.
The problem: per-tenant certificates don’t scale
If you provision individual certificates for each tenant, you’re running ACME challenges for every new tenant signup, managing certificate renewals for potentially tens of thousands of certificates, and hitting rate limits from Let’s Encrypt. The full set of Let’s Encrypt rate limits relevant here:- 50 certificates per registered domain per week
- 5 failed validation attempts per account per hostname per hour
- 300 new orders per account per 3 hours
Wildcard certificates: one cert, infinite tenants
A wildcard certificate for*.foo.com covers all first-level subdomains. Any subdomain directly under your base domain gets automatic TLS coverage from a single certificate.
Why you must use DNS-01 challenges
To get a wildcard certificate from Let’s Encrypt (or any ACME-compliant CA), you must use the DNS-01 challenge type. The more common HTTP-01 challenge does not work for wildcards. With HTTP-01, the CA verifies domain ownership by requesting a specific file athttp://your-domain/.well-known/acme-challenge/token. For *.foo.com there’s no single HTTP endpoint to verify—the wildcard represents infinite possible subdomains.
DNS-01 solves this by verifying ownership at the DNS level:
- Your ACME client requests a wildcard certificate for
*.foo.com. - Let’s Encrypt generates a challenge token and instructs you to create a TXT record at
_acme-challenge.foo.comwith that token as its value. - Let’s Encrypt queries public DNS for that TXT record.
- If the record exists with the correct value, Let’s Encrypt knows you control the domain and issues the certificate.
How DNS-01 automation works
The key to wildcard certificates is automating the DNS-01 challenge. This requires your web server or load balancer to have API access to your DNS provider. When Let’s Encrypt needs to verify domain ownership, your system creates a temporary TXT record, waits for DNS propagation, completes the challenge, and cleans up the record. The examples below use Caddy as the reverse proxy and Cloudflare as the DNS provider, but the architecture is the same regardless of your stack. Nginx with cert-manager on Kubernetes works the same way. HAProxy with acme.sh works the same way. The pattern is universally:The architecture (Cloudflare example)
The system has three layers:- Caddy — the web server that needs TLS certificates
caddy-dns/cloudflare— a thin adapter (~120 lines of Go) that sits between Caddy and the actual DNS API clientlibdns/cloudflare— handles the real work of talking to Cloudflare’s API
certmagic handles certificate management and renewal, libdns/cloudflare handles DNS API calls, and the plugin just connects them together.
This same pattern exists for every major DNS provider:
- Cloudflare
- AWS Route53
- GCP Cloud DNS
- Azure DNS
Building Caddy with DNS provider support
Standard Caddy doesn’t include DNS provider modules. You need to build a custom binary with the plugin compiled in.caddy executable that includes the DNS provider integration. You can include multiple providers if you manage domains across different DNS platforms.
Configuring your Caddyfile
Once you’ve built Caddy with the DNS provider plugin, the configuration is minimal:Getting DNS provider credentials
Your web server needs API credentials to manage DNS records. The required permissions are consistent across providers: read access to list zones/domains, and write access to create and delete TXT records.- Cloudflare
- AWS Route53
- GCP Cloud DNS
Create an API token at
https://dash.cloudflare.com/profile/api-tokens with these permissions:Zone.Zone:ReadZone.DNS:Edit
{env.CF_API_TOKEN} placeholder in the Caddyfile is replaced with the environment variable’s value when Caddy starts.
What happens under the hood
When you start Caddy with the configuration above, the complete certificate provisioning flow runs automatically.Configuration parsing
Caddy reads your Caddyfile and encounters the
dns cloudflare directive. The plugin’s UnmarshalCaddyfile() function extracts the token from {env.CF_API_TOKEN}.Token validation
The plugin validates the token format using the regex
^[A-Za-z0-9_-]{35,50}$. This catches common mistakes—such as wrapping the token in quotes or leaving the environment variable unset—before they produce cryptic API errors.Module provisioning
Caddy calls the plugin’s
Provision() function, which replaces environment variable placeholders with actual values and performs final validation.Certificate check
Caddy checks its certificate cache (default
~/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/) to see if a valid certificate for *.foo.com already exists. If so, it loads it and the flow ends here.ACME challenge request
If no valid certificate exists, Caddy’s ACME client requests a certificate from Let’s Encrypt. Let’s Encrypt responds with a DNS-01 challenge: “Prove you control
foo.com by creating a TXT record at _acme-challenge.foo.com with value xyz123_random_token.”DNS record creation
The plugin calls the Cloudflare API to create the challenge record. First, it queries for the zone ID:Then it creates the TXT record with the challenge token:The short TTL (2 minutes) is intentional—these records are temporary. AWS Route53 uses
ChangeResourceRecordSets, GCP uses managedZones.changes.create, Azure uses their DNS REST API. Different endpoints, same result.DNS propagation wait
Caddy polls public DNS servers to verify the TXT record has propagated. By default, it uses your system’s DNS resolver. You can configure a custom resolver for faster propagation checks:Using your DNS provider’s public resolver (1.1.1.1 for Cloudflare, 8.8.8.8 for Google) is often faster because records propagate to the provider’s own resolvers first. This step is critical—if propagation is incomplete when Let’s Encrypt checks, the challenge fails.
Challenge completion
Caddy tells Let’s Encrypt “The TXT record is ready, check it.” Let’s Encrypt queries multiple DNS servers worldwide to verify the record exists. Once verified, it issues the wildcard certificate.
The code: how the plugin works
The entirecaddy-dns/cloudflare plugin is ~120 lines of Go. Here are the key parts.
Module registration
github.com/libdns/cloudflare and registers itself as a Caddy module with the ID dns.providers.cloudflare. When you write dns cloudflare in your Caddyfile, Caddy loads this module.
Caddyfile parsing
The parsing logic handles both inline and block configuration syntaxes:Token validation
The actual DNS operations
The plugin doesn’t implement DNS operations directly. It delegates tolibdns/cloudflare, which implements the libdns interface:
Debugging and common issues
"Invalid request headers"
"Invalid request headers"
Your API token is malformed or the environment variable isn’t set. Verify it:If the output is empty, that’s the problem. When the environment variable isn’t set, Caddy tries to use
{env.CF_API_TOKEN} literally as the token value, which causes authentication failures."timed out waiting for record to fully propagate"
"timed out waiting for record to fully propagate"
The DNS propagation check is timing out. There are three common causes:
- DNS caching — your local resolver is caching the old “record doesn’t exist” response. Add
resolvers 1.1.1.1to your TLS block. - Private DNS —
foo.comis defined in/etc/hostsor resolved by a private DNS server, causing public verification to fail. Use a public resolver or temporarily remove the private DNS entry. - Zone access — the token doesn’t have
Zone:Readpermission. Verify permissions in your DNS provider dashboard.
"expected 1 zone, got 0"
"expected 1 zone, got 0"
The plugin can’t find the zone for your domain. This happens if:
- The domain isn’t in Cloudflare DNS
- The API token doesn’t have
Zone:Readpermission - The zone name doesn’t match (e.g., you’re requesting
*.sub.foo.combut onlyfoo.comis registered in Cloudflare)
Certificate Transparency logs
Certificate Transparency logs
All certificates issued by public CAs are logged to Certificate Transparency logs. You can inspect your wildcard cert at https://crt.sh—search for
%.foo.com to find wildcard certificates.This is a feature, not a bug. It proves certificates were issued legitimately and helps detect mis-issuance. It does mean anyone can see that foo.com has a wildcard certificate, but they cannot enumerate individual tenant subdomains from that.Production deployment patterns
Docker Compose
caddy_data volume persists certificates across container restarts. The caddy_config volume persists Caddy’s runtime configuration.
Dockerfile with Cloudflare plugin
Kubernetes with cert-manager
If you’re running Kubernetes, consider using cert-manager instead of running ACME clients on your web servers. Cert-manager is purpose-built for Kubernetes certificate lifecycle management and supports DNS-01 challenges with all major cloud providers.cloudflare for route53, clouddns, or azuredns with the appropriate credential references.
Multi-region deployments
File-based certificate storage works for single-server deployments, but multi-region requires shared storage. You have three options:- Mount the certificate directory from a network filesystem (NFS, EFS, or cloud-provider equivalents)
- Use Caddy storage plugins for S3, Consul, Redis, or other distributed stores
- Run certificate provisioning centrally and distribute via your secrets management system
Security considerations
The wildcard certificate’s private key protects all your tenant subdomains. If it leaks, an attacker can impersonate any tenant. Protect it like you’d protect your database credentials.Public Suffix List registration
If you’re running a multi-tenant platform where each tenant gets a subdomain, you should submit your domain to the Public Suffix List. The PSL is a registry that browsers use to determine security boundaries between sites. Without PSL registration, browsers treattenant-a.foo.com and tenant-b.foo.com as the same site. This means one tenant could potentially set cookies readable by another tenant—a serious security and privacy issue.
When you add foo.com to the PSL, browsers treat each tenant subdomain as an independent site. Cookies set by tenant-a.foo.com cannot be read by tenant-b.foo.com. Major platforms including GitHub (github.io), Vercel (vercel.app), and Netlify (netlify.app) are all registered on the PSL. If you’re building tenant infrastructure, you should be too.
Submit via the PSL GitHub repository with documentation proving you control the domain and explaining your multi-tenant use case.
Token scope limiting
- Cloudflare — scope tokens to specific zones with only
Zone.Zone:ReadandZone.DNS:Edit - AWS Route53 — use IAM policies that grant access only to specific hosted zones, not all DNS resources in your account
- GCP Cloud DNS — create service accounts with the
dns.adminrole scoped to individual zones, not project-wide access
Certificate revocation tradeoffs
If you need to revoke a wildcard certificate, you can’t selectively revoke it for one tenant—revocation affects all tenants. This is a fundamental tradeoff of wildcard certificates. If you need per-tenant revocation capability, you need per-tenant certificates. For most systems, the operational simplicity of wildcards outweighs this limitation.When not to use wildcard certificates
Wildcards are the wrong choice in these situations:- Tenants bring their own domains — if tenants use
tenant-a.cominstead oftenant-a.foo.com, you need per-tenant certificates with ACME HTTP-01 challenges - Deep subdomain nesting —
*.foo.comdoesn’t coverapi.tenant-a.foo.com; if your architecture requires nested subdomains, you need multiple wildcard certificates or per-tenant certificates - Regulatory compliance requiring certificate isolation — some compliance frameworks require cryptographic isolation between tenants; if your wildcard private key is compromised, all tenants are affected
- Per-tenant certificate revocation — if you need to revoke access for individual tenants by revoking their certificate, wildcards won’t work
Summary
For multi-tenant systems withtenant-id.foo.com subdomains, wildcard certificates are the right choice. The implementation pattern is the same regardless of your infrastructure: pick a web server (Caddy, Nginx, HAProxy), integrate with your DNS provider’s API (Cloudflare, Route53, Cloud DNS, Azure DNS), and let ACME automation handle the rest.
The alternative—per-tenant certificates—is operationally complex, technically fragile, and doesn’t scale past a few hundred tenants. Wildcard certificates are the pragmatic choice, and modern tooling makes them trivial to implement across any cloud platform.