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, 07:00 UTC
httpcaddyfile: Fixes for `prefer_wildcard` mode (#6636)
* httpcaddyfile: Fixes for prefer_wildcard mode The wildcard hosts need to be collected first, then considered after, because there's no guarantee that all non-wildcards will appear after all wildcards when looping. Also we should not add a domain to Skip if it doesn't qualify for TLS anyway. * Alternate solution by avoiding adding APs altogether if covered by wildcard
Francis Lavoie committed
Oct 30, 2024 at 13:09 UTC
b129ed6be88e40667a843bfab74abb3e5239bc8f
4 files changed
+353
-51
caddyconfig/httpcaddyfile/httptype.go
+23
-19
@@ -706,6 +706,16 @@ func (st *ServerType) serversFromPairings(
706
return specificity(iLongestHost) > specificity(jLongestHost)
707
})
708
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
+
719
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
720
autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
721
@@ -791,13 +801,6 @@ func (st *ServerType) serversFromPairings(
801
}
802
}
803
794
- wildcardHosts := []string{}
795
- for _, addr := range sblock.parsedKeys {
796
- if strings.HasPrefix(addr.Host, "*.") {
797
- wildcardHosts = append(wildcardHosts, addr.Host[2:])
798
- }
799
- }
800
-
804
for _, addr := range sblock.parsedKeys {
805
// if server only uses HTTP port, auto-HTTPS will not apply
806
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
@@ -813,18 +816,6 @@ func (st *ServerType) serversFromPairings(
816
}
817
}
818
816
- // If prefer wildcard is enabled, then we add hosts that are
817
- // already covered by the wildcard to the skip list
818
- if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" {
819
- baseDomain := addr.Host
820
- if idx := strings.Index(baseDomain, "."); idx != -1 {
821
- baseDomain = baseDomain[idx+1:]
822
- }
823
- if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
824
- srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
825
- }
826
- }
827
-
819
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
820
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
821
// specifying prefix "https://"
@@ -841,6 +832,19 @@ func (st *ServerType) serversFromPairings(
832
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
833
addressQualifiesForTLS = true
834
}
835
+
836
+ // If prefer wildcard is enabled, then we add hosts that are
837
+ // already covered by the wildcard to the skip list
838
+ if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard {
839
+ baseDomain := addr.Host
840
+ if idx := strings.Index(baseDomain, "."); idx != -1 {
841
+ baseDomain = baseDomain[idx+1:]
842
+ }
843
+ if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
844
+ srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host)
845
+ }
846
+ }
847
+
848
// predict whether auto-HTTPS will add the conn policy for us; if so, we
849
// may not need to add one for this server
850
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
caddyconfig/httpcaddyfile/tlsapp.go
+59
-32
@@ -92,6 +92,25 @@ 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
+ for _, p := range pairings {
98
+ var addresses []string
99
+ for _, addressWithProtocols := range p.addressesWithProtocols {
100
+ addresses = append(addresses, addressWithProtocols.address)
101
+ }
102
+ if !listenersUseAnyPortOtherThan(addresses, httpPort) {
103
+ continue
104
+ }
105
+ for _, sblock := range p.serverBlocks {
106
+ for _, addr := range sblock.parsedKeys {
107
+ if strings.HasPrefix(addr.Host, "*.") {
108
+ wildcardHosts = append(wildcardHosts, addr.Host[2:])
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
for _, p := range pairings {
115
// avoid setting up TLS automation policies for a server that is HTTP-only
116
var addresses []string
@@ -115,6 +134,12 @@ func (st ServerType) buildTLSApp(
134
return nil, warnings, err
135
}
136
137
+ // make a plain copy so we can compare whether we made any changes
138
+ apCopy, err := newBaseAutomationPolicy(options, warnings, true)
139
+ if err != nil {
140
+ return nil, warnings, err
141
+ }
142
+
143
sblockHosts := sblock.hostsFromKeys(false)
144
if len(sblockHosts) == 0 && catchAllAP != nil {
145
ap = catchAllAP
@@ -217,9 +242,21 @@ func (st ServerType) buildTLSApp(
242
catchAllAP = ap
243
}
244
245
+ hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
246
+ sort.Strings(hostsNotHTTP) // solely for deterministic test results
247
+
248
+ // if the we prefer wildcards and the AP is unchanged,
249
+ // then we can skip this AP because it should be covered
250
+ // by an AP with a wildcard
251
+ if slices.Contains(autoHTTPS, "prefer_wildcard") {
252
+ if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) &&
253
+ reflect.DeepEqual(ap, apCopy) {
254
+ continue
255
+ }
256
+ }
257
+
258
// associate our new automation policy with this server block's hosts
221
- ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort)
222
- sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
259
+ ap.SubjectsRaw = hostsNotHTTP
260
261
// if a combination of public and internal names were given
262
// for this same server block and no issuer was specified, we
@@ -258,6 +295,7 @@ func (st ServerType) buildTLSApp(
295
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
296
}
297
}
298
+
299
if tlsApp.Automation == nil {
300
tlsApp.Automation = new(caddytls.AutomationConfig)
301
}
@@ -418,10 +456,7 @@ func (st ServerType) buildTLSApp(
456
}
457
458
// consolidate automation policies that are the exact same
421
- tlsApp.Automation.Policies = consolidateAutomationPolicies(
422
- tlsApp.Automation.Policies,
423
- slices.Contains(autoHTTPS, "prefer_wildcard"),
424
- )
459
+ tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
460
461
// ensure automation policies don't overlap subjects (this should be
462
// an error at provision-time as well, but catch it in the adapt phase
@@ -567,7 +602,7 @@ func newBaseAutomationPolicy(
602
603
// consolidateAutomationPolicies combines automation policies that are the same,
604
// for a cleaner overall output.
570
-func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy {
605
+func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
606
// sort from most specific to least specific; we depend on this ordering
607
sort.SliceStable(aps, func(i, j int) bool {
608
if automationPolicyIsSubset(aps[i], aps[j]) {
@@ -652,31 +687,6 @@ outer:
687
j--
688
}
689
}
655
-
656
- if preferWildcard {
657
- // remove subjects from i if they're covered by a wildcard in j
658
- iSubjs := aps[i].SubjectsRaw
659
- for iSubj := 0; iSubj < len(iSubjs); iSubj++ {
660
- for jSubj := range aps[j].SubjectsRaw {
661
- if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") {
662
- continue
663
- }
664
- if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) {
665
- iSubjs = slices.Delete(iSubjs, iSubj, iSubj+1)
666
- iSubj--
667
- break
668
- }
669
- }
670
- }
671
- aps[i].SubjectsRaw = iSubjs
672
-
673
- // remove i if it has no subjects left
674
- if len(aps[i].SubjectsRaw) == 0 {
675
- aps = slices.Delete(aps, i, i+1)
676
- i--
677
- continue outer
678
- }
679
- }
690
}
691
}
692
@@ -748,3 +758,20 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
758
func isTailscaleDomain(name string) bool {
759
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
760
}
761
+
762
+func hostsCoveredByWildcard(hosts []string, wildcards []string) bool {
763
+ if len(hosts) == 0 || len(wildcards) == 0 {
764
+ return false
765
+ }
766
+ for _, host := range hosts {
767
+ for _, wildcard := range wildcards {
768
+ if strings.HasPrefix(host, "*.") {
769
+ continue
770
+ }
771
+ if certmagic.MatchWildcard(host, "*."+wildcard) {
772
+ return true
773
+ }
774
+ }
775
+ }
776
+ return false
777
+}
caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
+3
@@ -74,6 +74,9 @@ foo.example.com {
74
}
75
],
76
"automatic_https": {
77
+ "skip_certificates": [
78
+ "foo.example.com"
79
+ ],
80
"prefer_wildcard": true
81
}
82
}
caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest
new
+268
@@ -0,0 +1,268 @@
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