@marcel91200 / caddy Mirror · read-only / commits / c8591a1e
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: keep a site block's TLS policy for ECH public names (#8156)

* httpcaddyfile: keep a site block's TLS policy for ECH public names The global ech option added its own automation policy for every public name, even when a site block already had a policy for that name. If the site block set its own TLS options, adapting failed with "hostname appears in more than one automation policy". If a hostless catch-all with the same settings was also present, the site's policy was merged away and the name got only the global settings. Skip public names that already have a policy, as tls_automate_names does, and only add the ECH policy when it still has subjects, since a policy without subjects acts as a catch-all. * httpcaddyfile: check ECH public names the way the TLS app matches them The check for an existing site policy only matched a subject spelled exactly like the ECH public name. A site policy for *.example.com, or for example.com when the public name is written Example.COM, already covers that name at runtime, but the global ECH policy was still added and sorted ahead of it, so the site's issuer settings were overridden. Lowercase and trim the public name as ECH provisioning does and compare it with certmagic.MatchWildcard, which is how the TLS app picks a policy for a name.

awss committed Oct 9, 2026 at 07:48 UTC c8591a1e46e4d0e39b6ca20633c44cd42f874f71
6 files changed +399 -6
caddyconfig/httpcaddyfile/tlsapp.go
+30 -6
@@ -344,20 +344,27 @@ func (st ServerType) buildTLSApp(
344 tlsApp.EncryptedClientHello = ech
345
346 // outer server names will need certificates, so make sure they're included
347 - // in an automation policy for them that applies any global options
347 + // in an automation policy for them that applies any global options. A name
348 + // that a site block's policy already covers keeps that policy, because a
349 + // second policy for it would be rejected as ambiguous or picked ahead of
350 + // the site's.
351 ap, err := newBaseAutomationPolicy(options, warnings, true)
352 if err != nil {
353 return nil, warnings, err
354 }
355 for _, cfg := range ech.Configs {
353 - if cfg.PublicName != "" {
354 - ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
356 + if cfg.PublicName == "" || automationPolicyCoversName(tlsApp.Automation, cfg.PublicName) {
357 + continue
358 }
359 + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
360 }
357 - if tlsApp.Automation == nil {
358 - tlsApp.Automation = new(caddytls.AutomationConfig)
361 + // without subjects, the policy would apply to every name as a catch-all
362 + if len(ap.SubjectsRaw) > 0 {
363 + if tlsApp.Automation == nil {
364 + tlsApp.Automation = new(caddytls.AutomationConfig)
365 + }
366 + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
367 }
360 - tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
368 }
369
370 // if the storage clean interval is a boolean, then it's "off" to disable cleaning
@@ -955,6 +962,23 @@ func automationPolicyExistsForSubject(automation *caddytls.AutomationConfig, sub
962 })
963 }
964
965 +// automationPolicyCoversName reports whether some automation policy with
966 +// subjects would be selected for the ECH public name at runtime. The TLS app
967 +// trims and lowercases public names, then picks a policy for each one with
968 +// certmagic.MatchWildcard, so a wildcard or differently cased subject covers
969 +// it too. A catch-all policy names no subjects and so never matches.
970 +func automationPolicyCoversName(automation *caddytls.AutomationConfig, publicName string) bool {
971 + if automation == nil {
972 + return false
973 + }
974 + name := strings.ToLower(strings.TrimSpace(publicName))
975 + return slices.ContainsFunc(automation.Policies, func(ap *caddytls.AutomationPolicy) bool {
976 + return slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
977 + return certmagic.MatchWildcard(name, subj)
978 + })
979 + })
980 +}
981 +
982 // hasGlobalACMEDefaults reports whether any global option is set that an
983 // automation policy without issuers of its own would later be filled in with.
984 // A policy is worth creating for a subject when this is true, even if the
caddytest/integration/caddyfile_adapt/ech_site_block_mixed_case.caddyfiletest new
+56
@@ -0,0 +1,56 @@
1 +{
2 + ech Example.COM
3 +}
4 +
5 +example.com {
6 + tls internal
7 +}
8 +----------
9 +{
10 + "apps": {
11 + "http": {
12 + "servers": {
13 + "srv0": {
14 + "listen": [
15 + ":443"
16 + ],
17 + "routes": [
18 + {
19 + "match": [
20 + {
21 + "host": [
22 + "example.com"
23 + ]
24 + }
25 + ],
26 + "terminal": true
27 + }
28 + ]
29 + }
30 + }
31 + },
32 + "tls": {
33 + "automation": {
34 + "policies": [
35 + {
36 + "subjects": [
37 + "example.com"
38 + ],
39 + "issuers": [
40 + {
41 + "module": "internal"
42 + }
43 + ]
44 + }
45 + ]
46 + },
47 + "encrypted_client_hello": {
48 + "configs": [
49 + {
50 + "public_name": "Example.COM"
51 + }
52 + ]
53 + }
54 + }
55 + }
56 +}
caddytest/integration/caddyfile_adapt/ech_site_block_tls_internal.caddyfiletest new
+76
@@ -0,0 +1,76 @@
1 +{
2 + email nobody@example.com
3 + ech example.com ech.example.net
4 +}
5 +
6 +example.com {
7 + tls internal
8 +}
9 +----------
10 +{
11 + "apps": {
12 + "http": {
13 + "servers": {
14 + "srv0": {
15 + "listen": [
16 + ":443"
17 + ],
18 + "routes": [
19 + {
20 + "match": [
21 + {
22 + "host": [
23 + "example.com"
24 + ]
25 + }
26 + ],
27 + "terminal": true
28 + }
29 + ]
30 + }
31 + }
32 + },
33 + "tls": {
34 + "automation": {
35 + "policies": [
36 + {
37 + "subjects": [
38 + "example.com"
39 + ],
40 + "issuers": [
41 + {
42 + "module": "internal"
43 + }
44 + ]
45 + },
46 + {
47 + "subjects": [
48 + "ech.example.net"
49 + ],
50 + "issuers": [
51 + {
52 + "email": "nobody@example.com",
53 + "module": "acme"
54 + },
55 + {
56 + "ca": "https://acme.zerossl.com/v2/DV90",
57 + "email": "nobody@example.com",
58 + "module": "acme"
59 + }
60 + ]
61 + }
62 + ]
63 + },
64 + "encrypted_client_hello": {
65 + "configs": [
66 + {
67 + "public_name": "example.com"
68 + },
69 + {
70 + "public_name": "ech.example.net"
71 + }
72 + ]
73 + }
74 + }
75 + }
76 +}
caddytest/integration/caddyfile_adapt/ech_site_block_tls_resolvers.caddyfiletest new
+85
@@ -0,0 +1,85 @@
1 +{
2 + acme_dns mock foo
3 + ech example.com
4 +}
5 +
6 +example.com {
7 + tls {
8 + resolvers 1.1.1.1
9 + }
10 +}
11 +----------
12 +{
13 + "apps": {
14 + "http": {
15 + "servers": {
16 + "srv0": {
17 + "listen": [
18 + ":443"
19 + ],
20 + "routes": [
21 + {
22 + "match": [
23 + {
24 + "host": [
25 + "example.com"
26 + ]
27 + }
28 + ],
29 + "terminal": true
30 + }
31 + ]
32 + }
33 + }
34 + },
35 + "tls": {
36 + "automation": {
37 + "policies": [
38 + {
39 + "subjects": [
40 + "example.com"
41 + ],
42 + "issuers": [
43 + {
44 + "challenges": {
45 + "dns": {
46 + "provider": {
47 + "argument": "foo",
48 + "name": "mock"
49 + },
50 + "resolvers": [
51 + "1.1.1.1"
52 + ]
53 + }
54 + },
55 + "module": "acme"
56 + }
57 + ]
58 + },
59 + {
60 + "issuers": [
61 + {
62 + "challenges": {
63 + "dns": {
64 + "provider": {
65 + "argument": "foo",
66 + "name": "mock"
67 + }
68 + }
69 + },
70 + "module": "acme"
71 + }
72 + ]
73 + }
74 + ]
75 + },
76 + "encrypted_client_hello": {
77 + "configs": [
78 + {
79 + "public_name": "example.com"
80 + }
81 + ]
82 + }
83 + }
84 + }
85 +}
caddytest/integration/caddyfile_adapt/ech_site_block_wildcard.caddyfiletest new
+76
@@ -0,0 +1,76 @@
1 +{
2 + email nobody@example.com
3 + ech ech.example.com ech.example.net
4 +}
5 +
6 +*.example.com {
7 + tls internal
8 +}
9 +----------
10 +{
11 + "apps": {
12 + "http": {
13 + "servers": {
14 + "srv0": {
15 + "listen": [
16 + ":443"
17 + ],
18 + "routes": [
19 + {
20 + "match": [
21 + {
22 + "host": [
23 + "*.example.com"
24 + ]
25 + }
26 + ],
27 + "terminal": true
28 + }
29 + ]
30 + }
31 + }
32 + },
33 + "tls": {
34 + "automation": {
35 + "policies": [
36 + {
37 + "subjects": [
38 + "*.example.com"
39 + ],
40 + "issuers": [
41 + {
42 + "module": "internal"
43 + }
44 + ]
45 + },
46 + {
47 + "subjects": [
48 + "ech.example.net"
49 + ],
50 + "issuers": [
51 + {
52 + "email": "nobody@example.com",
53 + "module": "acme"
54 + },
55 + {
56 + "ca": "https://acme.zerossl.com/v2/DV90",
57 + "email": "nobody@example.com",
58 + "module": "acme"
59 + }
60 + ]
61 + }
62 + ]
63 + },
64 + "encrypted_client_hello": {
65 + "configs": [
66 + {
67 + "public_name": "ech.example.com"
68 + },
69 + {
70 + "public_name": "ech.example.net"
71 + }
72 + ]
73 + }
74 + }
75 + }
76 +}
caddytest/integration/caddyfile_adapt/ech_site_block_with_catch_all.caddyfiletest new
+76
@@ -0,0 +1,76 @@
1 +{
2 + ech example.com
3 +}
4 +
5 +example.com {
6 + tls internal
7 +}
8 +
9 +:8443 {
10 + tls internal
11 + respond "catch-all"
12 +}
13 +----------
14 +{
15 + "apps": {
16 + "http": {
17 + "servers": {
18 + "srv0": {
19 + "listen": [
20 + ":443"
21 + ],
22 + "routes": [
23 + {
24 + "match": [
25 + {
26 + "host": [
27 + "example.com"
28 + ]
29 + }
30 + ],
31 + "terminal": true
32 + }
33 + ]
34 + },
35 + "srv1": {
36 + "listen": [
37 + ":8443"
38 + ],
39 + "routes": [
40 + {
41 + "handle": [
42 + {
43 + "body": "catch-all",
44 + "handler": "static_response"
45 + }
46 + ]
47 + }
48 + ],
49 + "tls_connection_policies": [
50 + {}
51 + ]
52 + }
53 + }
54 + },
55 + "tls": {
56 + "automation": {
57 + "policies": [
58 + {
59 + "issuers": [
60 + {
61 + "module": "internal"
62 + }
63 + ]
64 + }
65 + ]
66 + },
67 + "encrypted_client_hello": {
68 + "configs": [
69 + {
70 + "public_name": "example.com"
71 + }
72 + ]
73 + }
74 + }
75 + }
76 +}