How Caddy automatically obtains and renews TLS certificates for your sites
One of Caddy’s most powerful features is Automatic HTTPS: the ability to automatically obtain, renew, and manage TLS certificates without any manual intervention. This page explains how the system works under the hood.
Automatic HTTPS is enabled by default when Caddy detects qualifying domain names in your configuration. No additional configuration is required for basic use.
Caddy uses CertMagic under the hood—a powerful library for automated certificate management that Caddy’s creator also maintains.
From autohttps.go:71-518, Caddy analyzes the configuration:
1
Extract Qualifying Domains
Caddy scans all server blocks for domain names that qualify for certificates:
autohttps.go:148-168
// Find all qualifying domain names in this serverserverDomainSet := make(map[string]struct{})for routeIdx, route := range srv.Routes { for matcherSetIdx, matcherSet := range route.MatcherSets { for matcherIdx, m := range matcherSet { if hm, ok := m.(*MatchHost); ok { for hostMatcherIdx, d := range *hm { var err error d, err = repl.ReplaceOrErr(d, true, false) if err != nil { return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v", srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err) } if !slices.Contains(srv.AutoHTTPS.Skip, d) { serverDomainSet[d] = struct{}{} } } } } }}
2
Filter Qualifying Names
Not all names qualify for public certificates:
autohttps.go:202-227
for d := range serverDomainSet { if certmagic.SubjectQualifiesForCert(d) && !slices.Contains(srv.AutoHTTPS.SkipCerts, d) { // If a certificate for this name is already loaded, // don't obtain another one for it, unless we are // supposed to ignore loaded certificates if !srv.AutoHTTPS.IgnoreLoadedCerts && app.tlsApp.HasCertificateForSubject(d) { logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", zap.String("domain", d), zap.String("server_name", srvName), ) continue } // Most clients don't accept wildcards like *.tld if strings.Contains(d, "*") && strings.Count(strings.Trim(d, "."), ".") == 1 { logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", zap.String("domain", d)) } uniqueDomainsForCerts[d] = struct{}{} }}
3
Create Automation Policies
Domains are assigned to automation policies based on their characteristics:
autohttps.go:293-339
var internal, tailscale []stringfor d := range uniqueDomainsForCerts { // Check if domain already has an explicit automation policy if app.tlsApp.Automation != nil { for _, ap := range app.tlsApp.Automation.Policies { for _, apHost := range ap.Subjects() { if apHost == d { // If the automation policy has all internal subjects but no issuers, // use our internal issuer instead if len(ap.Issuers) == 0 && ap.AllInternalSubjects() { iss := new(caddytls.InternalIssuer) if err := iss.Provision(ctx); err != nil { return err } ap.Issuers = append(ap.Issuers, iss) } continue uniqueDomainsLoop } } } } // Separate internal, tailscale, and public domains shouldUseInternal := func(ident string) bool { usingDefaultIssuersAndIsIP := certmagic.SubjectIsIP(ident) && (app.tlsApp == nil || app.tlsApp.Automation == nil || len(app.tlsApp.Automation.Policies) == 0) return !certmagic.SubjectQualifiesForPublicCert(d) || usingDefaultIssuersAndIsIP } if isTailscaleDomain(d) { tailscale = append(tailscale, d) delete(uniqueDomainsForCerts, d) } else if shouldUseInternal(d) { internal = append(internal, d) }}
4
Set Up HTTP→HTTPS Redirects
Automatic redirects are configured for each qualifying domain:
You can control automatic HTTPS behavior per-server:
autohttps.go:32-69
type AutoHTTPSConfig struct { // If true, automatic HTTPS will be entirely disabled, // including certificate management and redirects. Disabled bool `json:"disable,omitempty"` // If true, only automatic HTTP->HTTPS redirects will // be disabled, but other auto-HTTPS features will // remain enabled. DisableRedir bool `json:"disable_redirects,omitempty"` // If true, automatic certificate management will be // disabled, but other auto-HTTPS features will // remain enabled. DisableCerts bool `json:"disable_certificates,omitempty"` // Hosts/domain names listed here will not be included // in automatic HTTPS (they will not have certificates // loaded nor redirects applied). Skip []string `json:"skip,omitempty"` // Hosts/domain names listed here will still be enabled // for automatic HTTPS (unless in the Skip list), except // that certificates will not be provisioned and managed // for these names. SkipCerts []string `json:"skip_certificates,omitempty"` // By default, automatic HTTPS will obtain and renew // certificates for qualifying hostnames. However, if // a certificate with a matching SAN is already loaded // into the cache, certificate management will not be // enabled. To force automated certificate management // regardless of loaded certificates, set this to true. IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`}
For local/internal domains, Caddy uses its internal CA:
autohttps.go:690-724
if len(internalNames) > 0 { internalIssuer := new(caddytls.InternalIssuer) // Shallow-copy the base policy policyCopy := *basePolicy newPolicy := &policyCopy // Provision the issuer if err := internalIssuer.Provision(ctx); err != nil { return err } // This policy should apply only to the given names // and should use our issuer newPolicy.SubjectsRaw = internalNames newPolicy.Issuers = []certmagic.Issuer{internalIssuer} err := app.tlsApp.AddAutomationPolicy(newPolicy) if err != nil { return err }}
The TLS app manages certificates through automation policies:
tls.go:547-605
func (t *TLS) Manage(subjects map[string]struct{}) error { // Bin names by AutomationPolicy for efficiency policyToNames := make(map[*AutomationPolicy][]string) for subj := range subjects { ap := t.getAutomationPolicyForName(subj) // By default, if a wildcard that covers the subj is also being // managed, prefer using that over individual certs if t.managingWildcardFor(subj, subjects) { if _, ok := t.automateNames[subj]; !ok { continue } } policyToNames[ap] = append(policyToNames[ap], subj) } // Make one certmagic.Config for each group and call ManageAsync for ap, names := range policyToNames { err := ap.magic.ManageAsync(t.ctx.Context, names) if err != nil { return fmt.Errorf("automate: manage %v: %v", names, err) } for _, name := range names { // Associate subject with issuer key var issuerKey string if len(ap.Issuers) == 1 { if intIss, ok := ap.Issuers[0].(*InternalIssuer); ok && intIss != nil { issuerKey = intIss.IssuerKey() } } t.managing[name] = issuerKey } } return nil}
Certificates are obtained asynchronously after servers start. Your sites will be available immediately, with certificates obtained in the background.
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { acmeChallenge := certmagic.LooksLikeHTTPChallenge(r) zerosslValidation := certmagic.LooksLikeZeroSSLHTTPValidation(r) if !acmeChallenge && !zerosslValidation { return false } // Try all the issuers until we find the one that initiated the challenge ap := t.getAutomationPolicyForName(r.Host) if acmeChallenge { type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer } for _, iss := range ap.magic.Issuers { if acmeIssuer, ok := iss.(acmeCapable); ok { if acmeIssuer.GetACMEIssuer().issuer.HandleHTTPChallenge(w, r) { return true } } } // It's possible another server in this process initiated the challenge if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok { return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge) } } return false}