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