diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 76988767080..0ac137305ae 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -573,6 +573,12 @@ func (st *ServerType) serversFromPairings( srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) } srv.AutoHTTPS.IgnoreLoadedCerts = true + + case "prefer_wildcard": + if srv.AutoHTTPS == nil { + srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) + } + srv.AutoHTTPS.PreferWildcard = true } } @@ -725,6 +731,13 @@ func (st *ServerType) serversFromPairings( } } + wildcardHosts := []string{} + for _, addr := range sblock.keys { + if strings.HasPrefix(addr.Host, "*.") { + wildcardHosts = append(wildcardHosts, addr.Host[2:]) + } + } + for _, addr := range sblock.keys { // if server only uses HTTP port, auto-HTTPS will not apply if listenersUseAnyPortOtherThan(srv.Listen, httpPort) { @@ -740,6 +753,18 @@ func (st *ServerType) serversFromPairings( } } + // If prefer wildcard is enabled, then we add hosts that are + // already covered by the wildcard to the skip list + if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" { + baseDomain := addr.Host + if idx := strings.Index(baseDomain, "."); idx != -1 { + baseDomain = baseDomain[idx+1:] + } + if !strings.HasPrefix(addr.Host, "*.") && sliceContains(wildcardHosts, baseDomain) { + srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host) + } + } + // If TLS is specified as directive, it will also result in 1 or more connection policy being created // Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without // specifying prefix "https://" @@ -887,7 +912,10 @@ func (st *ServerType) serversFromPairings( if addressQualifiesForTLS && !hasCatchAllTLSConnPolicy && (len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") { - srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI}) + srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{ + DefaultSNI: defaultSNI, + FallbackSNI: fallbackSNI, + }) } // tidy things up a bit diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 73f57665f3c..7b18d356b17 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -443,10 +443,11 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { case "disable_redirects": case "disable_certs": case "ignore_loaded_certs": + case "prefer_wildcard": break default: - return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'") + return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'") } } return val, nil diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 269056fb2f4..8e0e720b115 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -413,7 +413,10 @@ func (st ServerType) buildTLSApp( } // consolidate automation policies that are the exact same - tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) + tlsApp.Automation.Policies = consolidateAutomationPolicies( + tlsApp.Automation.Policies, + sliceContains(autoHTTPS, "prefer_wildcard"), + ) // ensure automation policies don't overlap subjects (this should be // an error at provision-time as well, but catch it in the adapt phase @@ -541,7 +544,7 @@ func newBaseAutomationPolicy( // consolidateAutomationPolicies combines automation policies that are the same, // for a cleaner overall output. -func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy { +func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy { // sort from most specific to least specific; we depend on this ordering sort.SliceStable(aps, func(i, j int) bool { if automationPolicyIsSubset(aps[i], aps[j]) { @@ -626,6 +629,31 @@ outer: j-- } } + + if preferWildcard { + // remove subjects from i if they're covered by a wildcard in j + iSubjs := aps[i].SubjectsRaw + for iSubj := 0; iSubj < len(iSubjs); iSubj++ { + for jSubj := range aps[j].SubjectsRaw { + if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") { + continue + } + if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) { + iSubjs = append(iSubjs[:iSubj], iSubjs[iSubj+1:]...) + iSubj-- + break + } + } + } + aps[i].SubjectsRaw = iSubjs + + // remove i if it has no subjects left + if len(aps[i].SubjectsRaw) == 0 { + aps = append(aps[:i], aps[i+1:]...) + i-- + continue outer + } + } } } diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 54a2d9ccd22..b3906918050 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -64,6 +64,12 @@ type AutoHTTPSConfig struct { // enabled. To force automated certificate management // regardless of loaded certificates, set this to true. IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` + + // If true, automatic HTTPS will prefer wildcard names + // and ignore non-wildcard names if both are available. + // This allows for writing a config with top-level host + // matchers without having those names produce certificates. + PreferWildcard bool `json:"prefer_wildcard,omitempty"` } // Skipped returns true if name is in skipSlice, which @@ -167,6 +173,27 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } + if srv.AutoHTTPS.PreferWildcard { + wildcards := make(map[string]struct{}) + for d := range serverDomainSet { + if strings.HasPrefix(d, "*.") { + wildcards[d[2:]] = struct{}{} + } + } + for d := range serverDomainSet { + if strings.HasPrefix(d, "*.") { + continue + } + base := d + if idx := strings.Index(d, "."); idx != -1 { + base = d[idx+1:] + } + if _, ok := wildcards[base]; ok { + delete(serverDomainSet, d) + } + } + } + // nothing more to do here if there are no domains that qualify for // automatic HTTPS and there are no explicit TLS connection policies: // if there is at least one domain but no TLS conn policy (F&&T), we'll