SAN Certificate Batching
Relevant source files
The following files were used as context for generating this wiki page:
SAN (Subject Alternative Name) Certificate Batching is a core resilience and optimization feature in kamal-proxy. It aggregates multiple domains—up to 100 per certificate—into a single ACME order. This mechanism significantly reduces the total number of certificates managed by the proxy and ensures the system remains well within the rate limits imposed by Certificate Authorities like Let's Encrypt.
The system handles two primary types of domains: deploy-registered hosts, which trigger synchronous provisioning during a TLS handshake, and dynamic domains (learned via tls-domains-source), which are queued for asynchronous issuance. By batching these domains and optionally collapsing sibling subdomains into wildcards when DNS-01 providers are available, the proxy provides a scalable solution for multi-tenant environments.
Sources: internal/server/san_cert_manager.go:48-52, internal/server/san_cert_issuance.go:81-87, CLAUDE.md:120-125
Architecture and Components
The SAN Certificate Batching system is centered around the SANCertManager, which coordinates ACME clients, state persistence, and domain mapping.
Core Components
- SANCertManager: The central orchestrator that manages the lifecycle of SAN certificates, including registration, provisioning, and retrieval.
- DomainGrouper: Analyzes domain sets to determine if they can be optimized into wildcards.
- Token Bucket: Implements global rate limiting for ACME orders across all issuance paths (handshake, dynamic, and renewal).
- ACME User: A shared identity (
acme_user.json) used for all ACME interactions, regardless of the challenge type (HTTP-01 or DNS-01).
Sources: internal/server/san_cert_manager.go:68-115, internal/server/san_cert_issuance.go:13-22
System Data Flow
The following diagram illustrates how a TLS handshake triggers the certificate retrieval and provisioning logic.
The proxy uses a hard allowlist to prevent unauthorized domains from consuming ACME rate limits.
Sources: internal/server/san_cert_manager.go:303-345, internal/server/san_cert_dynamic.go:65-75
Domain Provisioning Logic
The provisioning process differs based on how the domain was discovered and the available ACME challenge providers.
Synchronous Provisioning (Handshake-driven)
When a deploy-registered domain is accessed via TLS and no valid certificate exists, the proxy initiates synchronous provisioning. It doesn't just order a certificate for the requested domain; it sweeps all current pendingDomains (up to a limit of 100) into the same order to maximize batching efficiency.
Key functions:
provisionCertificate(ctx, domain): Manages the batch collection and ACME resource acquisition.planIssuanceDomains(domains): Rewrites the batch to include wildcards ifPreferWildcardis enabled and a DNS provider is available.
Sources: internal/server/san_cert_manager.go:362-390, internal/server/san_cert_issuance.go:78-90
Asynchronous Issuance (Dynamic Domains)
Dynamic domains discovered at runtime are handled by the DynamicDomainManager. These are never issued on the handshake path to avoid blocking clients. Instead, they are queued for the domainIssuer to process in the background.
Sources: internal/server/san_cert_dynamic.go:10-14, internal/server/dynamic_domains.go:88-95
Challenge Strategy
The proxy supports both HTTP-01 and DNS-01 challenges. Wildcard certificates require DNS-01, while standard SAN batches can fallback to HTTP-01 if DNS validation fails and --acme-http-fallback is enabled.
| Challenge Type | Requirement | Usage |
|---|---|---|
| DNS-01 | DNS Provider (e.g., Cloudflare, Route53) | Wildcards and mapped zones |
| HTTP-01 | Port 80 access | Default fallback for concrete domains |
Sources: internal/server/san_cert_issuance.go:24-50, internal/server/san_cert_manager.go:56-65
Batching and Wildcard Optimization
The planIssuanceDomains function optimizes the identifier set before placing an ACME order.
Wildcard Collapsing
If sibling subdomains (e.g., a.example.com and b.example.com) are present in a batch and a DNS provider covers that zone, the manager collapses them into *.example.com. The apex domain (example.com) is kept as a separate entry because wildcards do not cover the apex.
Optimization logic ensures that a wildcard is only ordered if a provider exists to validate it; otherwise, concrete names are used to allow HTTP-01 validation.
Sources: internal/server/san_cert_issuance.go:94-110, internal/server/acme_gate_test.go:214-250
Configuration and Limits
The behavior of the SAN Certificate Manager is controlled via SANCertManagerConfig.
| Parameter | Description | Default |
|---|---|---|
MaxSANsPerCertificate | Maximum domains allowed in one SAN cert. | 100 |
Directory | The ACME directory URL. | Let's Encrypt Production |
CachePath | Directory for storing cert.pem, key.pem, and acme_user.json. | Derived from data dir |
PreferWildcard | Whether to collapse siblings into wildcards. | false |
HTTPFallback | Whether to retry failed DNS-01 orders via HTTP-01. | false |
Sources: internal/server/san_cert_manager.go:34-45, internal/server/san_cert_manager.go:54-66, internal/server/config_test.go:42-55
Persistence and State Management
The manager persists both the certificates themselves and a mapping state file (acme.state).
State Structure
The managerState struct tracks which domains are covered by which certificate IDs.
type managerState struct {
Certificates map[string]*ManagedCert `json:"certificates"`
DomainMap map[string]string `json:"domain_map"`
SavedAt time.Time `json:"saved_at"`
}
Citations: internal/server/san_cert_manager.go:629-633
Atomic Writes
State persistence uses a "temporary file and rename" strategy to ensure that concurrent writes from handshake-driven provisioning, dynamic issuance, and the renewer do not corrupt the state file.
Sources: internal/server/san_cert_manager.go:668-682
Summary
SAN Certificate Batching provides kamal-proxy with a robust mechanism for handling large-scale TLS requirements. By batching up to 100 domains per certificate and intelligently applying wildcard optimization, the system minimizes ACME overhead and rate-limit consumption. The combination of a strict allowlist and a unified rate-limiting token bucket ensures security and operational stability across both static and dynamic domain sets.
Sources: internal/server/san_cert_manager.go:48-52, internal/server/acme_gate_test.go:132-140