@marcel91200 / caddy Mirror · read-only / commits / 1bfa1115
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 00:45 UTC

caddytls: Prefer managed wildcard certs over individual subdomain certs (#6959)

* caddytls: Prefer managed wildcard certs over individual subdomain certs * Repurpose force_automate as no_wildcard * Fix a couple bugs * Restore force_automate and use automate loader as wildcard override

Matt Holt committed Apr 18, 2025 at 11:44 UTC 1bfa111552eff8b30bc1a5f76516426f29c66a88
13 files changed +172 -491
caddyconfig/httpcaddyfile/httptype.go
+39 -31
@@ -633,12 +633,6 @@ func (st *ServerType) serversFromPairings(
633 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
634 }
635 srv.AutoHTTPS.IgnoreLoadedCerts = true
636 -
637 - case "prefer_wildcard":
638 - if srv.AutoHTTPS == nil {
639 - srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
640 - }
641 - srv.AutoHTTPS.PreferWildcard = true
636 }
637 }
638
@@ -706,16 +700,6 @@ func (st *ServerType) serversFromPairings(
700 return specificity(iLongestHost) > specificity(jLongestHost)
701 })
702
709 - // collect all hosts that have a wildcard in them
710 - wildcardHosts := []string{}
711 - for _, sblock := range p.serverBlocks {
712 - for _, addr := range sblock.parsedKeys {
713 - if strings.HasPrefix(addr.Host, "*.") {
714 - wildcardHosts = append(wildcardHosts, addr.Host[2:])
715 - }
716 - }
717 - }
718 -
703 var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
704 autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
705
@@ -801,7 +785,13 @@ func (st *ServerType) serversFromPairings(
785 cp.FallbackSNI = fallbackSNI
786 }
787
804 - // only append this policy if it actually changes something
788 + // only append this policy if it actually changes something,
789 + // or if the configuration explicitly automates certs for
790 + // these names (this is necessary to hoist a connection policy
791 + // above one that may manually load a wildcard cert that would
792 + // otherwise clobber the automated one; the code that appends
793 + // policies that manually load certs comes later, so they're
794 + // lower in the list)
795 if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) {
796 srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
797 hasCatchAllTLSConnPolicy = len(hosts) == 0
@@ -841,18 +831,6 @@ func (st *ServerType) serversFromPairings(
831 addressQualifiesForTLS = true
832 }
833
844 - // If prefer wildcard is enabled, then we add hosts that are
845 - // already covered by the wildcard to the skip list
846 - if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard {
847 - baseDomain := addr.Host
848 - if idx := strings.Index(baseDomain, "."); idx != -1 {
849 - baseDomain = baseDomain[idx+1:]
850 - }
851 - if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
852 - srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host)
853 - }
854 - }
855 -
834 // predict whether auto-HTTPS will add the conn policy for us; if so, we
835 // may not need to add one for this server
836 autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
@@ -1083,11 +1061,40 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
1061
1062 // if they're exactly equal in every way, just keep one of them
1063 if reflect.DeepEqual(cps[i], cps[j]) {
1086 - cps = append(cps[:j], cps[j+1:]...)
1064 + cps = slices.Delete(cps, j, j+1)
1065 i--
1066 break
1067 }
1068
1069 + // as a special case, if there are adjacent TLS conn policies that are identical except
1070 + // by their matchers, and the matchers are specifically just ServerName ("sni") matchers
1071 + // (by far the most common), we can combine them into a single policy
1072 + if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 {
1073 + if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok {
1074 + if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok {
1075 + // position of policies and the matcher criteria check out; if settings are
1076 + // the same, then we can combine the policies; we have to unmarshal and
1077 + // remarshal the matchers though
1078 + if cps[i].SettingsEqual(*cps[j]) {
1079 + var iSNIMatcher caddytls.MatchServerName
1080 + if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil {
1081 + var jSNIMatcher caddytls.MatchServerName
1082 + if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil {
1083 + iSNIMatcher = append(iSNIMatcher, jSNIMatcher...)
1084 + cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher)
1085 + if err != nil {
1086 + return nil, fmt.Errorf("recombining SNI matchers: %v", err)
1087 + }
1088 + cps = slices.Delete(cps, j, j+1)
1089 + i--
1090 + break
1091 + }
1092 + }
1093 + }
1094 + }
1095 + }
1096 + }
1097 +
1098 // if they have the same matcher, try to reconcile each field: either they must
1099 // be identical, or we have to be able to combine them safely
1100 if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
@@ -1189,12 +1196,13 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
1196 }
1197 }
1198
1192 - cps = append(cps[:j], cps[j+1:]...)
1199 + cps = slices.Delete(cps, j, j+1)
1200 i--
1201 break
1202 }
1203 }
1204 }
1205 +
1206 return cps, nil
1207 }
1208
caddyconfig/httpcaddyfile/tlsapp.go
+7 -7
@@ -92,11 +92,9 @@ func (st ServerType) buildTLSApp(
92 tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
93 }
94
95 - // collect all hosts that have a wildcard in them, and arent HTTP
96 - wildcardHosts := []string{}
97 - // hosts that have been explicitly marked to be automated,
98 - // even if covered by another wildcard
99 - forcedAutomatedNames := make(map[string]struct{})
95 + var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP
96 + forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
97 +
98 for _, p := range pairings {
99 var addresses []string
100 for _, addressWithProtocols := range p.addressesWithProtocols {
@@ -153,7 +151,7 @@ func (st ServerType) buildTLSApp(
151 ap.OnDemand = true
152 }
153
156 - // collect hosts that are forced to be automated
154 + // collect hosts that are forced to have certs automated for their specific name
155 if _, ok := sblock.pile["tls.force_automate"]; ok {
156 for _, host := range sblockHosts {
157 forcedAutomatedNames[host] = struct{}{}
@@ -375,7 +373,9 @@ func (st ServerType) buildTLSApp(
373 return nil, warnings, err
374 }
375 for _, cfg := range ech.Configs {
378 - ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
376 + if cfg.PublicName != "" {
377 + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
378 + }
379 }
380 if tlsApp.Automation == nil {
381 tlsApp.Automation = new(caddytls.AutomationConfig)
caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest deleted
-109
@@ -1,109 +0,0 @@
1 -{
2 - auto_https prefer_wildcard
3 -}
4 -
5 -*.example.com {
6 - tls {
7 - dns mock
8 - }
9 - respond "fallback"
10 -}
11 -
12 -foo.example.com {
13 - respond "foo"
14 -}
15 -----------
16 -{
17 - "apps": {
18 - "http": {
19 - "servers": {
20 - "srv0": {
21 - "listen": [
22 - ":443"
23 - ],
24 - "routes": [
25 - {
26 - "match": [
27 - {
28 - "host": [
29 - "foo.example.com"
30 - ]
31 - }
32 - ],
33 - "handle": [
34 - {
35 - "handler": "subroute",
36 - "routes": [
37 - {
38 - "handle": [
39 - {
40 - "body": "foo",
41 - "handler": "static_response"
42 - }
43 - ]
44 - }
45 - ]
46 - }
47 - ],
48 - "terminal": true
49 - },
50 - {
51 - "match": [
52 - {
53 - "host": [
54 - "*.example.com"
55 - ]
56 - }
57 - ],
58 - "handle": [
59 - {
60 - "handler": "subroute",
61 - "routes": [
62 - {
63 - "handle": [
64 - {
65 - "body": "fallback",
66 - "handler": "static_response"
67 - }
68 - ]
69 - }
70 - ]
71 - }
72 - ],
73 - "terminal": true
74 - }
75 - ],
76 - "automatic_https": {
77 - "skip_certificates": [
78 - "foo.example.com"
79 - ],
80 - "prefer_wildcard": true
81 - }
82 - }
83 - }
84 - },
85 - "tls": {
86 - "automation": {
87 - "policies": [
88 - {
89 - "subjects": [
90 - "*.example.com"
91 - ],
92 - "issuers": [
93 - {
94 - "challenges": {
95 - "dns": {
96 - "provider": {
97 - "name": "mock"
98 - }
99 - }
100 - },
101 - "module": "acme"
102 - }
103 - ]
104 - }
105 - ]
106 - }
107 - }
108 - }
109 -}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest deleted
-268
@@ -1,268 +0,0 @@
1 -{
2 - auto_https prefer_wildcard
3 -}
4 -
5 -# Covers two domains
6 -*.one.example.com {
7 - tls {
8 - dns mock
9 - }
10 - respond "one fallback"
11 -}
12 -
13 -# Is covered, should not get its own AP
14 -foo.one.example.com {
15 - respond "foo one"
16 -}
17 -
18 -# This one has its own tls config so it doesn't get covered (escape hatch)
19 -bar.one.example.com {
20 - respond "bar one"
21 - tls bar@bar.com
22 -}
23 -
24 -# Covers nothing but AP gets consolidated with the first
25 -*.two.example.com {
26 - tls {
27 - dns mock
28 - }
29 - respond "two fallback"
30 -}
31 -
32 -# Is HTTP so it should not cover
33 -http://*.three.example.com {
34 - respond "three fallback"
35 -}
36 -
37 -# Has no wildcard coverage so it gets an AP
38 -foo.three.example.com {
39 - respond "foo three"
40 -}
41 -----------
42 -{
43 - "apps": {
44 - "http": {
45 - "servers": {
46 - "srv0": {
47 - "listen": [
48 - ":443"
49 - ],
50 - "routes": [
51 - {
52 - "match": [
53 - {
54 - "host": [
55 - "foo.three.example.com"
56 - ]
57 - }
58 - ],
59 - "handle": [
60 - {
61 - "handler": "subroute",
62 - "routes": [
63 - {
64 - "handle": [
65 - {
66 - "body": "foo three",
67 - "handler": "static_response"
68 - }
69 - ]
70 - }
71 - ]
72 - }
73 - ],
74 - "terminal": true
75 - },
76 - {
77 - "match": [
78 - {
79 - "host": [
80 - "foo.one.example.com"
81 - ]
82 - }
83 - ],
84 - "handle": [
85 - {
86 - "handler": "subroute",
87 - "routes": [
88 - {
89 - "handle": [
90 - {
91 - "body": "foo one",
92 - "handler": "static_response"
93 - }
94 - ]
95 - }
96 - ]
97 - }
98 - ],
99 - "terminal": true
100 - },
101 - {
102 - "match": [
103 - {
104 - "host": [
105 - "bar.one.example.com"
106 - ]
107 - }
108 - ],
109 - "handle": [
110 - {
111 - "handler": "subroute",
112 - "routes": [
113 - {
114 - "handle": [
115 - {
116 - "body": "bar one",
117 - "handler": "static_response"
118 - }
119 - ]
120 - }
121 - ]
122 - }
123 - ],
124 - "terminal": true
125 - },
126 - {
127 - "match": [
128 - {
129 - "host": [
130 - "*.one.example.com"
131 - ]
132 - }
133 - ],
134 - "handle": [
135 - {
136 - "handler": "subroute",
137 - "routes": [
138 - {
139 - "handle": [
140 - {
141 - "body": "one fallback",
142 - "handler": "static_response"
143 - }
144 - ]
145 - }
146 - ]
147 - }
148 - ],
149 - "terminal": true
150 - },
151 - {
152 - "match": [
153 - {
154 - "host": [
155 - "*.two.example.com"
156 - ]
157 - }
158 - ],
159 - "handle": [
160 - {
161 - "handler": "subroute",
162 - "routes": [
163 - {
164 - "handle": [
165 - {
166 - "body": "two fallback",
167 - "handler": "static_response"
168 - }
169 - ]
170 - }
171 - ]
172 - }
173 - ],
174 - "terminal": true
175 - }
176 - ],
177 - "automatic_https": {
178 - "skip_certificates": [
179 - "foo.one.example.com",
180 - "bar.one.example.com"
181 - ],
182 - "prefer_wildcard": true
183 - }
184 - },
185 - "srv1": {
186 - "listen": [
187 - ":80"
188 - ],
189 - "routes": [
190 - {
191 - "match": [
192 - {
193 - "host": [
194 - "*.three.example.com"
195 - ]
196 - }
197 - ],
198 - "handle": [
199 - {
200 - "handler": "subroute",
201 - "routes": [
202 - {
203 - "handle": [
204 - {
205 - "body": "three fallback",
206 - "handler": "static_response"
207 - }
208 - ]
209 - }
210 - ]
211 - }
212 - ],
213 - "terminal": true
214 - }
215 - ],
216 - "automatic_https": {
217 - "prefer_wildcard": true
218 - }
219 - }
220 - }
221 - },
222 - "tls": {
223 - "automation": {
224 - "policies": [
225 - {
226 - "subjects": [
227 - "foo.three.example.com"
228 - ]
229 - },
230 - {
231 - "subjects": [
232 - "bar.one.example.com"
233 - ],
234 - "issuers": [
235 - {
236 - "email": "bar@bar.com",
237 - "module": "acme"
238 - },
239 - {
240 - "ca": "https://acme.zerossl.com/v2/DV90",
241 - "email": "bar@bar.com",
242 - "module": "acme"
243 - }
244 - ]
245 - },
246 - {
247 - "subjects": [
248 - "*.one.example.com",
249 - "*.two.example.com"
250 - ],
251 - "issuers": [
252 - {
253 - "challenges": {
254 - "dns": {
255 - "provider": {
256 - "name": "mock"
257 - }
258 - }
259 - },
260 - "module": "acme"
261 - }
262 - ]
263 - }
264 - ]
265 - }
266 - }
267 - }
268 -}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest
+1 -7
@@ -131,13 +131,7 @@ shadowed.example.com {
131 {
132 "match": {
133 "sni": [
134 - "automated1.example.com"
135 - ]
136 - }
137 - },
138 - {
139 - "match": {
140 - "sni": [
134 + "automated1.example.com",
135 "automated2.example.com"
136 ]
137 }
internal/logs.go new
+22
@@ -0,0 +1,22 @@
1 +package internal
2 +
3 +import "fmt"
4 +
5 +// MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length
6 +// maxToDisplay. It is useful for logging domains being managed, for example, since a
7 +// map is typically needed for quick lookup, but a slice is needed for logging, and this
8 +// can be quite a doozy since there may be a huge amount (hundreds of thousands).
9 +func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string {
10 + numberOfNamesToDisplay := min(len(subjects), maxToDisplay)
11 + domainsToDisplay := make([]string, 0, numberOfNamesToDisplay)
12 + for domain := range subjects {
13 + domainsToDisplay = append(domainsToDisplay, domain)
14 + if len(domainsToDisplay) >= numberOfNamesToDisplay {
15 + break
16 + }
17 + }
18 + if len(subjects) > maxToDisplay {
19 + domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay))
20 + }
21 + return domainsToDisplay
22 +}
logging.go
+3 -4
@@ -20,6 +20,7 @@ import (
20 "io"
21 "log"
22 "os"
23 + "slices"
24 "strings"
25 "sync"
26 "time"
@@ -490,10 +491,8 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
491 if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
492 // prevent intersections
493 for _, allow := range cl.Include {
493 - for _, deny := range cl.Exclude {
494 - if allow == deny {
495 - return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
496 - }
494 + if slices.Contains(cl.Exclude, allow) {
495 + return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
496 }
497 }
498
modules/caddyhttp/app.go
+1 -1
@@ -152,7 +152,7 @@ type App struct {
152 tlsApp *caddytls.TLS
153
154 // used temporarily between phases 1 and 2 of auto HTTPS
155 - allCertDomains []string
155 + allCertDomains map[string]struct{}
156 }
157
158 // CaddyModule returns the Caddy module information.
modules/caddyhttp/autohttps.go
+8 -43
@@ -25,6 +25,7 @@ import (
25 "go.uber.org/zap"
26
27 "github.com/caddyserver/caddy/v2"
28 + "github.com/caddyserver/caddy/v2/internal"
29 "github.com/caddyserver/caddy/v2/modules/caddytls"
30 )
31
@@ -65,12 +66,6 @@ type AutoHTTPSConfig struct {
66 // enabled. To force automated certificate management
67 // regardless of loaded certificates, set this to true.
68 IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
68 -
69 - // If true, automatic HTTPS will prefer wildcard names
70 - // and ignore non-wildcard names if both are available.
71 - // This allows for writing a config with top-level host
72 - // matchers without having those names produce certificates.
73 - PreferWildcard bool `json:"prefer_wildcard,omitempty"`
69 }
70
71 // automaticHTTPSPhase1 provisions all route matchers, determines
@@ -163,33 +158,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
158 }
159 }
160
166 - // trim the list of domains covered by wildcards, if configured
167 - if srv.AutoHTTPS.PreferWildcard {
168 - wildcards := make(map[string]struct{})
169 - for d := range serverDomainSet {
170 - if strings.HasPrefix(d, "*.") {
171 - wildcards[d[2:]] = struct{}{}
172 - }
173 - }
174 - for d := range serverDomainSet {
175 - if strings.HasPrefix(d, "*.") {
176 - continue
177 - }
178 - base := d
179 - if idx := strings.Index(d, "."); idx != -1 {
180 - base = d[idx+1:]
181 - }
182 - if _, ok := wildcards[base]; ok {
183 - delete(serverDomainSet, d)
184 - }
185 - }
186 - }
187 -
161 // build the list of domains that could be used with ECH (if enabled)
189 - // so the TLS app can know to publish ECH configs for them; we do this
190 - // after trimming domains covered by wildcards because, presumably,
191 - // if the user wants to use wildcard certs, they also want to use the
192 - // wildcard for ECH, rather than individual subdomains
162 + // so the TLS app can know to publish ECH configs for them
163 echDomains := make([]string, 0, len(serverDomainSet))
164 for d := range serverDomainSet {
165 echDomains = append(echDomains, d)
@@ -295,19 +265,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
265 }
266 }
267
298 - // we now have a list of all the unique names for which we need certs;
299 - // turn the set into a slice so that phase 2 can use it
300 - app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
268 + // we now have a list of all the unique names for which we need certs
269 var internal, tailscale []string
270 uniqueDomainsLoop:
271 for d := range uniqueDomainsForCerts {
304 - if !isTailscaleDomain(d) {
305 - // whether or not there is already an automation policy for this
306 - // name, we should add it to the list to manage a cert for it,
307 - // unless it's a Tailscale domain, because we don't manage those
308 - app.allCertDomains = append(app.allCertDomains, d)
309 - }
310 -
272 // some names we've found might already have automation policies
273 // explicitly specified for them; we should exclude those from
274 // our hidden/implicit policy, since applying a name to more than
@@ -346,6 +307,7 @@ uniqueDomainsLoop:
307 }
308 if isTailscaleDomain(d) {
309 tailscale = append(tailscale, d)
310 + delete(uniqueDomainsForCerts, d) // not managed by us; handled separately
311 } else if shouldUseInternal(d) {
312 internal = append(internal, d)
313 }
@@ -475,6 +437,9 @@ redirServersLoop:
437 }
438 }
439
440 + // persist the domains/IPs we're managing certs for through provisioning/startup
441 + app.allCertDomains = uniqueDomainsForCerts
442 +
443 logger.Debug("adjusted config",
444 zap.Reflect("tls", app.tlsApp),
445 zap.Reflect("http", app))
@@ -777,7 +742,7 @@ func (app *App) automaticHTTPSPhase2() error {
742 return nil
743 }
744 app.logger.Info("enabling automatic TLS certificate management",
780 - zap.Strings("domains", app.allCertDomains),
745 + zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)),
746 )
747 err := app.tlsApp.Manage(app.allCertDomains)
748 if err != nil {
modules/caddyhttp/reverseproxy/httptransport.go
+1 -1
@@ -652,7 +652,7 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error)
652 return nil, fmt.Errorf("getting tls app: %v", err)
653 }
654 tlsApp := tlsAppIface.(*caddytls.TLS)
655 - err = tlsApp.Manage([]string{t.ClientCertificateAutomate})
655 + err = tlsApp.Manage(map[string]struct{}{t.ClientCertificateAutomate: {}})
656 if err != nil {
657 return nil, fmt.Errorf("managing client certificate: %v", err)
658 }
modules/caddytls/connpolicy.go
+9
@@ -24,6 +24,7 @@ import (
24 "fmt"
25 "io"
26 "os"
27 + "reflect"
28 "strings"
29
30 "github.com/mholt/acmez/v3"
@@ -461,6 +462,14 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
462 p.InsecureSecretsLog == ""
463 }
464
465 +// SettingsEmpty returns true if p's settings (fields
466 +// except the matchers) are the same as q.
467 +func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool {
468 + p.MatchersRaw = nil
469 + q.MatchersRaw = nil
470 + return reflect.DeepEqual(p, q)
471 +}
472 +
473 // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
474 //
475 // connection_policy {
modules/caddytls/ech.go
-1
@@ -138,7 +138,6 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
138 // all existing configs are now loaded; see if we need to make any new ones
139 // based on the input configuration, and also mark the most recent one(s) as
140 // current/active, so they can be used for ECH retries
141 -
141 for _, cfg := range ech.Configs {
142 publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName))
143
modules/caddytls/tls.go
+81 -19
@@ -33,6 +33,7 @@ import (
33 "go.uber.org/zap/zapcore"
34
35 "github.com/caddyserver/caddy/v2"
36 + "github.com/caddyserver/caddy/v2/internal"
37 "github.com/caddyserver/caddy/v2/modules/caddyevents"
38 )
39
@@ -55,8 +56,10 @@ type TLS struct {
56 //
57 // The "automate" certificate loader module can be used to
58 // specify a list of subjects that need certificates to be
58 - // managed automatically. The first matching automation
59 - // policy will be applied to manage the certificate(s).
59 + // managed automatically, including subdomains that may
60 + // already be covered by a managed wildcard certificate.
61 + // The first matching automation policy will be used
62 + // to manage automated certificate(s).
63 //
64 // All loaded certificates get pooled
65 // into the same cache and may be used to complete TLS
@@ -123,7 +126,7 @@ type TLS struct {
126 dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.)
127
128 certificateLoaders []CertificateLoader
126 - automateNames []string
129 + automateNames map[string]struct{}
130 ctx caddy.Context
131 storageCleanTicker *time.Ticker
132 storageCleanStop chan struct{}
@@ -218,12 +221,13 @@ func (t *TLS) Provision(ctx caddy.Context) error {
221 // special case; these will be loaded in later using our automation facilities,
222 // which we want to avoid doing during provisioning
223 if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil {
224 + if t.automateNames == nil {
225 + t.automateNames = make(map[string]struct{})
226 + }
227 repl := caddy.NewReplacer()
222 - subjects := make([]string, len(*automateNames))
223 - for i, sub := range *automateNames {
224 - subjects[i] = repl.ReplaceAll(sub, "")
228 + for _, sub := range *automateNames {
229 + t.automateNames[repl.ReplaceAll(sub, "")] = struct{}{}
230 }
226 - t.automateNames = append(t.automateNames, subjects...)
231 } else {
232 return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface)
233 }
@@ -283,7 +287,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
287 if err != nil {
288 return fmt.Errorf("provisioning default public automation policy: %v", err)
289 }
286 - for _, n := range t.automateNames {
290 + for n := range t.automateNames {
291 // if any names specified by the "automate" loader do not qualify for a public
292 // certificate, we should initialize a default internal automation policy
293 // (but we don't want to do this unnecessarily, since it may prompt for password!)
@@ -339,8 +343,14 @@ func (t *TLS) Provision(ctx caddy.Context) error {
343
344 // outer names should have certificates to reduce client brittleness
345 for _, outerName := range outerNames {
346 + if outerName == "" {
347 + continue
348 + }
349 if !t.HasCertificateForSubject(outerName) {
343 - t.automateNames = append(t.automateNames, outerNames...)
350 + if t.automateNames == nil {
351 + t.automateNames = make(map[string]struct{})
352 + }
353 + t.automateNames[outerName] = struct{}{}
354 }
355 }
356 }
@@ -449,7 +459,8 @@ func (t *TLS) Cleanup() error {
459 // app instance (which is being stopped) that are not managed or loaded by the
460 // new app instance (which just started), and remove them from the cache
461 var noLongerManaged []certmagic.SubjectIssuer
452 - var reManage, noLongerLoaded []string
462 + var noLongerLoaded []string
463 + reManage := make(map[string]struct{})
464 for subj, currentIssuerKey := range t.managing {
465 // It's a bit nuanced: managed certs can sometimes be different enough that we have to
466 // swap them out for a different one, even if they are for the same subject/domain.
@@ -467,7 +478,7 @@ func (t *TLS) Cleanup() error {
478
479 // then, if the next app is managing a cert for this name, but with a different issuer, re-manage it
480 if ok && nextIssuerKey != currentIssuerKey {
470 - reManage = append(reManage, subj)
481 + reManage[subj] = struct{}{}
482 }
483 }
484 }
@@ -488,7 +499,7 @@ func (t *TLS) Cleanup() error {
499 if err := nextTLSApp.Manage(reManage); err != nil {
500 if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil {
501 c.Write(
491 - zap.Strings("subjects", reManage),
502 + zap.Strings("subjects", internal.MaxSizeSubjectsListForLog(reManage, 1000)),
503 zap.Error(err),
504 )
505 }
@@ -509,17 +520,31 @@ func (t *TLS) Cleanup() error {
520 return nil
521 }
522
512 -// Manage immediately begins managing names according to the
513 -// matching automation policy.
514 -func (t *TLS) Manage(names []string) error {
523 +// Manage immediately begins managing subjects according to the
524 +// matching automation policy. The subjects are given in a map
525 +// to prevent duplication and also because quick lookups are
526 +// needed to assess wildcard coverage, if any, depending on
527 +// certain config parameters (with lots of subjects, computing
528 +// wildcard coverage over a slice can be highly inefficient).
529 +func (t *TLS) Manage(subjects map[string]struct{}) error {
530 // for a large number of names, we can be more memory-efficient
531 // by making only one certmagic.Config for all the names that
532 // use that config, rather than calling ManageAsync once for
533 // every name; so first, bin names by AutomationPolicy
534 policyToNames := make(map[*AutomationPolicy][]string)
520 - for _, name := range names {
521 - ap := t.getAutomationPolicyForName(name)
522 - policyToNames[ap] = append(policyToNames[ap], name)
535 + for subj := range subjects {
536 + ap := t.getAutomationPolicyForName(subj)
537 + // by default, if a wildcard that covers the subj is also being
538 + // managed, either by a previous call to Manage or by this one,
539 + // prefer using that over individual certs for its subdomains;
540 + // but users can disable this and force getting a certificate for
541 + // subdomains by adding the name to the 'automate' cert loader
542 + if t.managingWildcardFor(subj, subjects) {
543 + if _, ok := t.automateNames[subj]; !ok {
544 + continue
545 + }
546 + }
547 + policyToNames[ap] = append(policyToNames[ap], subj)
548 }
549
550 // now that names are grouped by policy, we can simply make one
@@ -530,7 +555,7 @@ func (t *TLS) Manage(names []string) error {
555 if err != nil {
556 const maxNamesToDisplay = 100
557 if len(names) > maxNamesToDisplay {
533 - names = append(names[:maxNamesToDisplay], fmt.Sprintf("(%d more...)", len(names)-maxNamesToDisplay))
558 + names = append(names[:maxNamesToDisplay], fmt.Sprintf("(and %d more...)", len(names)-maxNamesToDisplay))
559 }
560 return fmt.Errorf("automate: manage %v: %v", names, err)
561 }
@@ -555,6 +580,43 @@ func (t *TLS) Manage(names []string) error {
580 return nil
581 }
582
583 +// managingWildcardFor returns true if the app is managing a certificate that covers that
584 +// subject name (including consideration of wildcards), either from its internal list of
585 +// names that it IS managing certs for, or from the otherSubjsToManage which includes names
586 +// that WILL be managed.
587 +func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool {
588 + // TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(),
589 + // but that does not account for how manually-loaded certs may be restricted as to which
590 + // hostnames or ClientHellos they can be used with by tags, etc; I don't *think* anyone
591 + // necessarily wants this anyway, but I thought I'd note this here for now (if we did
592 + // consider manually-loaded certs, we'd probably want to rename the method since it
593 + // wouldn't be just about managed certs anymore)
594 +
595 + // IP addresses must match exactly
596 + if ip := net.ParseIP(subj); ip != nil {
597 + _, managing := t.managing[subj]
598 + return managing
599 + }
600 +
601 + // replace labels of the domain with wildcards until we get a match
602 + labels := strings.Split(subj, ".")
603 + for i := range labels {
604 + if labels[i] == "*" {
605 + continue
606 + }
607 + labels[i] = "*"
608 + candidate := strings.Join(labels, ".")
609 + if _, ok := t.managing[candidate]; ok {
610 + return true
611 + }
612 + if _, ok := otherSubjsToManage[candidate]; ok {
613 + return true
614 + }
615 + }
616 +
617 + return false
618 +}
619 +
620 // RegisterServerNames registers the provided DNS names with the TLS app.
621 // This is currently used to auto-publish Encrypted ClientHello (ECH)
622 // configurations, if enabled. Use of this function by apps using the TLS