@marcel91200 / caddy Mirror · read-only / commits / 5ee9a2d4
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, 04:45 UTC

httpcaddyfile: new `tls_automate_names` global option (#8015)

* httpcaddyfile: new tls_automate_names global option Provisioning a certificate for a name that is not served requires giving it a site block of its own: *.example.com { } foo.example.com { respond "Real site" } That asks the tls app for the wildcard, but it also adds a route to the http app, so every name pointed at the server that has no site block of its own -- bar.example.com here -- gets an empty but valid response rather than no match at all. Wanting a certificate and wanting to serve a name are separate things, and the Caddyfile had no way to say only the first. Name them in the new global option instead: { tls_automate_names *.example.com } foo.example.com { respond "Real site" } The names are added to the automate certificate loader and to an automation policy built from the global options, so a name listed here is managed exactly as it would be from a site block, with the same issuers; the only difference in the adapted config is that no route is added for it. Repeating the option appends rather than replaces, so a long list can be split over several lines. Names that cannot get a public certificate are given the internal issuer, the same treatment a site block gives them. Names that already appear in the automate list are skipped, but a name that also has a site block is left listed here as well: a site block for http:// only is not managed by auto-HTTPS, so dropping the name because a block exists could silently leave it without a certificate. The option needs no http app at all, so a config consisting only of global options now adapts to a tls app on its own -- enough to keep certificates renewed for a mail or XMPP server, or for names served by a layer 4 app. Closes #7122 * httpcaddyfile: reuse an existing policy for an automated name A name given to tls_automate_names may already have an automation policy from its own site block. Adding a second policy for the same subject is not just redundant: the adapter rejects overlapping subjects, so { email nobody@example.com tls_automate_names foo.example.com } foo.example.com { tls { ca https://acme.example.test/directory } } failed to adapt at all, with "hostname appears in more than one automation policy, making certificate management ambiguous". Keep such a name in the automate loader but leave its policy alone. The site block's policy is the more specific of the two, and the loader entry is still wanted, since a site block served only over HTTP is not managed by auto-HTTPS. * Pin that tls_automate_names overrides auto_https off The names given to the option are managed whether or not auto-HTTPS is disabled: unlike the hostless-key block above it, that code is not gated on auto_https, because naming a subject explicitly is a stronger signal than the general switch. Nothing enforced it, so add the adaptation fixture @steadytao asked for.

Islam Elsayed committed Sep 30, 2026 at 13:49 UTC 5ee9a2d424dc37ea20911026b7bf876de8f122a8
7 files changed +418 -6
caddyconfig/httpcaddyfile/options.go
+17
@@ -65,6 +65,7 @@ func init() {
65 RegisterGlobalOption("persist_config", parseOptPersistConfig)
66 RegisterGlobalOption("dns", parseOptDNS)
67 RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers)
68 + RegisterGlobalOption("tls_automate_names", parseOptAutomateNames)
69 RegisterGlobalOption("ech", parseOptECH)
70 RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
71 }
@@ -320,6 +321,22 @@ func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) {
321 return resolvers, nil
322 }
323
324 +// parseOptAutomateNames parses the tls_automate_names global option, which
325 +// names subjects to manage certificates for without serving them. Repeating
326 +// the option appends to the list rather than replacing it, so a long list can
327 +// be split over several lines.
328 +func parseOptAutomateNames(d *caddyfile.Dispenser, existing any) (any, error) {
329 + d.Next() // consume option name
330 + names := d.RemainingArgs()
331 + if len(names) == 0 {
332 + return nil, d.ArgErr()
333 + }
334 + if previous, ok := existing.([]string); ok {
335 + names = append(previous, names...)
336 + }
337 + return names, nil
338 +}
339 +
340 func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
341 d.Next() // consume option name
342
caddyconfig/httpcaddyfile/tlsapp.go
+80 -6
@@ -424,6 +424,58 @@ func (st ServerType) buildTLSApp(
424 }
425 al = append(al, name)
426 }
427 + // names from the tls_automate_names global option are managed without a
428 + // site block of their own, so that asking for a certificate does not also
429 + // mean serving the name; like force_automate, an explicitly listed name is
430 + // managed even where auto-HTTPS would not have chosen it. Names that cannot
431 + // get a public certificate are given the internal issuer, the same
432 + // treatment they would get from a site block.
433 + if automateNames, ok := options["tls_automate_names"].([]string); ok {
434 + var publicNames []string
435 + for _, name := range automateNames {
436 + if slices.Contains(al, name) {
437 + continue
438 + }
439 + al = append(al, name)
440 + // a name that a site block already wrote a policy for keeps that
441 + // policy: it is more specific than anything the global options can
442 + // say, and a second policy naming the same subject is ambiguous --
443 + // adapting would fail outright. The name still belongs in the
444 + // automate loader, since a site block served only over HTTP does
445 + // not get its certificate managed by auto-HTTPS.
446 + if automationPolicyExistsForSubject(tlsApp.Automation, name) {
447 + continue
448 + }
449 + if certmagic.SubjectQualifiesForPublicCert(name) {
450 + publicNames = append(publicNames, name)
451 + } else {
452 + internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, name)
453 + }
454 + }
455 + // the names still need an automation policy of their own, or they would
456 + // miss the issuer configured by global options -- the catch-all policy
457 + // that would otherwise carry it is dropped once every other policy
458 + // names its subjects. Consolidation folds this back into an identical
459 + // policy, so a name listed here ends up in the same place it would have
460 + // had it been given a site block.
461 + if len(publicNames) > 0 {
462 + // only worth a policy if it would carry something: either global
463 + // automation options, or ACME defaults filled in further below.
464 + // Without either, the names are managed with the defaults anyway,
465 + // and an empty policy would just be noise in the output.
466 + ap, err := newBaseAutomationPolicy(options, warnings, hasGlobalACMEDefaults(options))
467 + if err != nil {
468 + return nil, warnings, err
469 + }
470 + if ap != nil {
471 + ap.SubjectsRaw = publicNames
472 + if tlsApp.Automation == nil {
473 + tlsApp.Automation = new(caddytls.AutomationConfig)
474 + }
475 + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
476 + }
477 + }
478 + }
479 slices.Sort(al) // to stabilize the adapt output
480 if len(al) > 0 {
481 tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
@@ -440,12 +492,7 @@ func (st ServerType) buildTLSApp(
492 if tlsApp.Automation != nil {
493 globalEmail := options["email"]
494 globalACMECA := options["acme_ca"]
443 - globalACMECARoot := options["acme_ca_root"]
444 - _, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
445 - globalACMEEAB := options["acme_eab"]
446 - globalPreferredChains := options["preferred_chains"]
447 - hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
448 - if hasGlobalACMEDefaults {
495 + if hasGlobalACMEDefaults(options) {
496 for i := range tlsApp.Automation.Policies {
497 ap := tlsApp.Automation.Policies[i]
498 if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
@@ -895,6 +942,33 @@ func appendUniqueStrings(existing []string, additions ...string) []string {
942 return existing
943 }
944
945 +// automationPolicyExistsForSubject reports whether some automation policy
946 +// already names subject. Subjects are compared exactly, which is the same
947 +// comparison the adapter uses to reject overlapping policies; a catch-all
948 +// policy names no subjects and so never matches.
949 +func automationPolicyExistsForSubject(automation *caddytls.AutomationConfig, subject string) bool {
950 + if automation == nil {
951 + return false
952 + }
953 + return slices.ContainsFunc(automation.Policies, func(ap *caddytls.AutomationPolicy) bool {
954 + return slices.Contains(ap.SubjectsRaw, subject)
955 + })
956 +}
957 +
958 +// hasGlobalACMEDefaults reports whether any global option is set that an
959 +// automation policy without issuers of its own would later be filled in with.
960 +// A policy is worth creating for a subject when this is true, even if the
961 +// policy is otherwise empty at the time it is made.
962 +func hasGlobalACMEDefaults(options map[string]any) bool {
963 + _, hasACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
964 + return options["email"] != nil ||
965 + options["acme_ca"] != nil ||
966 + options["acme_ca_root"] != nil ||
967 + hasACMEDNS ||
968 + options["acme_eab"] != nil ||
969 + options["preferred_chains"] != nil
970 +}
971 +
972 // newBaseAutomationPolicy returns a new TLS automation policy that gets
973 // its values from the global options map. It should be used as the base
974 // for any other automation policies. A nil policy (and no error) will be
caddytest/integration/caddyfile_adapt/tls_automate_names.caddyfiletest new
+77
@@ -0,0 +1,77 @@
1 +{
2 + email nobody@example.com
3 + tls_automate_names *.example.com
4 +}
5 +
6 +foo.example.com {
7 + respond "Hello world"
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 + "foo.example.com"
24 + ]
25 + }
26 + ],
27 + "handle": [
28 + {
29 + "handler": "subroute",
30 + "routes": [
31 + {
32 + "handle": [
33 + {
34 + "body": "Hello world",
35 + "handler": "static_response"
36 + }
37 + ]
38 + }
39 + ]
40 + }
41 + ],
42 + "terminal": true
43 + }
44 + ]
45 + }
46 + }
47 + },
48 + "tls": {
49 + "certificates": {
50 + "automate": [
51 + "*.example.com"
52 + ]
53 + },
54 + "automation": {
55 + "policies": [
56 + {
57 + "subjects": [
58 + "foo.example.com",
59 + "*.example.com"
60 + ],
61 + "issuers": [
62 + {
63 + "email": "nobody@example.com",
64 + "module": "acme"
65 + },
66 + {
67 + "ca": "https://acme.zerossl.com/v2/DV90",
68 + "email": "nobody@example.com",
69 + "module": "acme"
70 + }
71 + ]
72 + }
73 + ]
74 + }
75 + }
76 + }
77 +}
caddytest/integration/caddyfile_adapt/tls_automate_names_auto_https_off.caddyfiletest new
+62
@@ -0,0 +1,62 @@
1 +{
2 + auto_https off
3 + tls_automate_names mail.example.com
4 +}
5 +
6 +foo.example.com {
7 + respond "hi"
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 + "foo.example.com"
24 + ]
25 + }
26 + ],
27 + "handle": [
28 + {
29 + "handler": "subroute",
30 + "routes": [
31 + {
32 + "handle": [
33 + {
34 + "body": "hi",
35 + "handler": "static_response"
36 + }
37 + ]
38 + }
39 + ]
40 + }
41 + ],
42 + "terminal": true
43 + }
44 + ],
45 + "tls_connection_policies": [
46 + {}
47 + ],
48 + "automatic_https": {
49 + "disable": true
50 + }
51 + }
52 + }
53 + },
54 + "tls": {
55 + "certificates": {
56 + "automate": [
57 + "mail.example.com"
58 + ]
59 + }
60 + }
61 + }
62 +}
caddytest/integration/caddyfile_adapt/tls_automate_names_existing_policy.caddyfiletest new
+92
@@ -0,0 +1,92 @@
1 +{
2 + email nobody@example.com
3 + tls_automate_names foo.example.com bar.example.com
4 +}
5 +
6 +foo.example.com {
7 + tls {
8 + ca https://acme.example.test/directory
9 + }
10 + respond "served with its own issuer"
11 +}
12 +----------
13 +{
14 + "apps": {
15 + "http": {
16 + "servers": {
17 + "srv0": {
18 + "listen": [
19 + ":443"
20 + ],
21 + "routes": [
22 + {
23 + "match": [
24 + {
25 + "host": [
26 + "foo.example.com"
27 + ]
28 + }
29 + ],
30 + "handle": [
31 + {
32 + "handler": "subroute",
33 + "routes": [
34 + {
35 + "handle": [
36 + {
37 + "body": "served with its own issuer",
38 + "handler": "static_response"
39 + }
40 + ]
41 + }
42 + ]
43 + }
44 + ],
45 + "terminal": true
46 + }
47 + ]
48 + }
49 + }
50 + },
51 + "tls": {
52 + "certificates": {
53 + "automate": [
54 + "bar.example.com",
55 + "foo.example.com"
56 + ]
57 + },
58 + "automation": {
59 + "policies": [
60 + {
61 + "subjects": [
62 + "foo.example.com"
63 + ],
64 + "issuers": [
65 + {
66 + "ca": "https://acme.example.test/directory",
67 + "email": "nobody@example.com",
68 + "module": "acme"
69 + }
70 + ]
71 + },
72 + {
73 + "subjects": [
74 + "bar.example.com"
75 + ],
76 + "issuers": [
77 + {
78 + "email": "nobody@example.com",
79 + "module": "acme"
80 + },
81 + {
82 + "ca": "https://acme.zerossl.com/v2/DV90",
83 + "email": "nobody@example.com",
84 + "module": "acme"
85 + }
86 + ]
87 + }
88 + ]
89 + }
90 + }
91 + }
92 +}
caddytest/integration/caddyfile_adapt/tls_automate_names_internal.caddyfiletest new
+74
@@ -0,0 +1,74 @@
1 +{
2 + tls_automate_names localhost 192.168.1.5 real.example.com
3 + tls_automate_names second.example.com
4 +}
5 +
6 +example.org {
7 + respond "hi"
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.org"
24 + ]
25 + }
26 + ],
27 + "handle": [
28 + {
29 + "handler": "subroute",
30 + "routes": [
31 + {
32 + "handle": [
33 + {
34 + "body": "hi",
35 + "handler": "static_response"
36 + }
37 + ]
38 + }
39 + ]
40 + }
41 + ],
42 + "terminal": true
43 + }
44 + ]
45 + }
46 + }
47 + },
48 + "tls": {
49 + "certificates": {
50 + "automate": [
51 + "192.168.1.5",
52 + "localhost",
53 + "real.example.com",
54 + "second.example.com"
55 + ]
56 + },
57 + "automation": {
58 + "policies": [
59 + {
60 + "subjects": [
61 + "localhost",
62 + "192.168.1.5"
63 + ],
64 + "issuers": [
65 + {
66 + "module": "internal"
67 + }
68 + ]
69 + }
70 + ]
71 + }
72 + }
73 + }
74 +}
caddytest/integration/caddyfile_adapt/tls_automate_names_without_site_block.caddyfiletest new
+16
@@ -0,0 +1,16 @@
1 +{
2 + tls_automate_names mail.example.com xmpp.example.com
3 +}
4 +----------
5 +{
6 + "apps": {
7 + "tls": {
8 + "certificates": {
9 + "automate": [
10 + "mail.example.com",
11 + "xmpp.example.com"
12 + ]
13 + }
14 + }
15 + }
16 +}