@marcel91200 / caddy Mirror · read-only / commits / eb04ed52
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 11, 03:30 UTC

httpcaddyfile: preserve automation policy precedence when merging (#8287)

* httpcaddyfile: Don't merge automation policies if it would shadow another Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com> * httpcaddyfile: add reverse case for shadowing merge, use force_automate to match the real life usage and explain why we're not checking the inverse Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com> * httpcaddyfile: Check overlap in both directions when merging policies Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com> --------- Signed-off-by: Bartlomiej Wisniowski <56162793+bwisn@users.noreply.github.com>

bwisn committed Oct 30, 2026 at 07:23 UTC eb04ed52be5e66c467e9e614bd917b8bb4150d1b
3 files changed +281 -1
caddyconfig/httpcaddyfile/tlsapp.go
+32 -1
@@ -1155,7 +1155,8 @@ outer:
1155 i--
1156 continue outer
1157 }
1158 - } else {
1158 + } else if !automationPolicyMergeShadows(i, j, aps) {
1159 + // merge only if it won't change which policy handles a name
1160 // avoid repeated subjects
1161 for _, subj := range aps[j].SubjectsRaw {
1162 if !slices.Contains(aps[i].SubjectsRaw, subj) {
@@ -1207,6 +1208,36 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
1208 return -1
1209 }
1210
1211 +// automationPolicyMergeShadows returns true if merging aps[j] into aps[i]
1212 +// (where i < j) would change which policy handles a name. This happens
1213 +// when a subject of a policy between i and j overlaps with a subject of
1214 +// aps[j], in either direction.
1215 +//
1216 +// For example, say the policies are for "example.com" (i),
1217 +// "foo.example.com" (i+1), and "*.example.com" (j). Merging j into i moves
1218 +// "*.example.com" before the i+1 policy. Then the wildcard matches
1219 +// "foo.example.com" first, and the i+1 policy is never used.
1220 +//
1221 +// The reverse case matters too. If the wildcard is in i+1 and the name is
1222 +// in j, merging moves the name before the wildcard, and its policy changes.
1223 +// A merge should not change the result, so it is skipped here as well.
1224 +//
1225 +// This uses certmagic.MatchWildcard, which the TLS app also uses to pick
1226 +// a policy for a name.
1227 +func automationPolicyMergeShadows(i, j int, aps []*caddytls.AutomationPolicy) bool {
1228 + for k := i + 1; k < j; k++ {
1229 + for _, kSubj := range aps[k].SubjectsRaw {
1230 + for _, jSubj := range aps[j].SubjectsRaw {
1231 + if certmagic.MatchWildcard(kSubj, jSubj) ||
1232 + certmagic.MatchWildcard(jSubj, kSubj) {
1233 + return true
1234 + }
1235 + }
1236 + }
1237 + }
1238 + return false
1239 +}
1240 +
1241 // subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
1242 // that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
1243 // if the automation policy has OnDemand enabled (i.e. this function is more lenient).
caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing_merge.caddyfiletest new
+120
@@ -0,0 +1,120 @@
1 +# Identical policies for "a.example.com, b.example.com" and "*.api.example.com"
2 +# must not be merged: the wildcard would move before the policy for
3 +# "foo.api.example.com" and match that name first. force_automate makes
4 +# foo.api.example.com get its own certificate instead of using the wildcard's.
5 +a.example.com, b.example.com {
6 + tls {
7 + issuer acme
8 + }
9 +}
10 +
11 +foo.api.example.com {
12 + tls force_automate {
13 + issuer internal
14 + }
15 +}
16 +
17 +*.api.example.com {
18 + tls {
19 + issuer acme
20 + }
21 +}
22 +----------
23 +{
24 + "apps": {
25 + "http": {
26 + "servers": {
27 + "srv0": {
28 + "listen": [
29 + ":443"
30 + ],
31 + "routes": [
32 + {
33 + "match": [
34 + {
35 + "host": [
36 + "foo.api.example.com"
37 + ]
38 + }
39 + ],
40 + "terminal": true
41 + },
42 + {
43 + "match": [
44 + {
45 + "host": [
46 + "a.example.com",
47 + "b.example.com"
48 + ]
49 + }
50 + ],
51 + "terminal": true
52 + },
53 + {
54 + "match": [
55 + {
56 + "host": [
57 + "*.api.example.com"
58 + ]
59 + }
60 + ],
61 + "terminal": true
62 + }
63 + ],
64 + "tls_connection_policies": [
65 + {
66 + "match": {
67 + "sni": [
68 + "foo.api.example.com"
69 + ]
70 + }
71 + },
72 + {}
73 + ]
74 + }
75 + }
76 + },
77 + "tls": {
78 + "certificates": {
79 + "automate": [
80 + "foo.api.example.com"
81 + ]
82 + },
83 + "automation": {
84 + "policies": [
85 + {
86 + "subjects": [
87 + "a.example.com",
88 + "b.example.com"
89 + ],
90 + "issuers": [
91 + {
92 + "module": "acme"
93 + }
94 + ]
95 + },
96 + {
97 + "subjects": [
98 + "foo.api.example.com"
99 + ],
100 + "issuers": [
101 + {
102 + "module": "internal"
103 + }
104 + ]
105 + },
106 + {
107 + "subjects": [
108 + "*.api.example.com"
109 + ],
110 + "issuers": [
111 + {
112 + "module": "acme"
113 + }
114 + ]
115 + }
116 + ]
117 + }
118 + }
119 + }
120 +}
caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing_merge_reverse.caddyfiletest new
+129
@@ -0,0 +1,129 @@
1 +# The *.example.com policy has more subjects, so it is sorted before the
2 +# foo.example.com policy and covers foo.example.com. The first and last
3 +# policies have the same settings, but they must not be merged: that would
4 +# move foo.example.com before the wildcard and change its issuer.
5 +# force_automate makes foo.example.com get its own certificate.
6 +a.example.com, b.example.com, c.example.com {
7 + tls {
8 + issuer acme
9 + }
10 +}
11 +
12 +*.example.com, other.com, third.com {
13 + tls internal
14 +}
15 +
16 +foo.example.com, e.com {
17 + tls force_automate {
18 + issuer acme
19 + }
20 +}
21 +----------
22 +{
23 + "apps": {
24 + "http": {
25 + "servers": {
26 + "srv0": {
27 + "listen": [
28 + ":443"
29 + ],
30 + "routes": [
31 + {
32 + "match": [
33 + {
34 + "host": [
35 + "foo.example.com",
36 + "e.com"
37 + ]
38 + }
39 + ],
40 + "terminal": true
41 + },
42 + {
43 + "match": [
44 + {
45 + "host": [
46 + "a.example.com",
47 + "b.example.com",
48 + "c.example.com"
49 + ]
50 + }
51 + ],
52 + "terminal": true
53 + },
54 + {
55 + "match": [
56 + {
57 + "host": [
58 + "*.example.com",
59 + "other.com",
60 + "third.com"
61 + ]
62 + }
63 + ],
64 + "terminal": true
65 + }
66 + ],
67 + "tls_connection_policies": [
68 + {
69 + "match": {
70 + "sni": [
71 + "e.com",
72 + "foo.example.com"
73 + ]
74 + }
75 + },
76 + {}
77 + ]
78 + }
79 + }
80 + },
81 + "tls": {
82 + "certificates": {
83 + "automate": [
84 + "e.com",
85 + "foo.example.com"
86 + ]
87 + },
88 + "automation": {
89 + "policies": [
90 + {
91 + "subjects": [
92 + "a.example.com",
93 + "b.example.com",
94 + "c.example.com"
95 + ],
96 + "issuers": [
97 + {
98 + "module": "acme"
99 + }
100 + ]
101 + },
102 + {
103 + "subjects": [
104 + "*.example.com",
105 + "other.com",
106 + "third.com"
107 + ],
108 + "issuers": [
109 + {
110 + "module": "internal"
111 + }
112 + ]
113 + },
114 + {
115 + "subjects": [
116 + "e.com",
117 + "foo.example.com"
118 + ],
119 + "issuers": [
120 + {
121 + "module": "acme"
122 + }
123 + ]
124 + }
125 + ]
126 + }
127 + }
128 + }
129 +}