From afa778ae05503f563af0d1015cdf7e5e78b1eeec Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 24 Dec 2024 10:58:40 -0500 Subject: [PATCH] httpcaddyfile: Implement experimental `force_automate` option (#6712) --- caddyconfig/httpcaddyfile/builtins.go | 17 +- caddyconfig/httpcaddyfile/httptype.go | 22 ++- caddyconfig/httpcaddyfile/tlsapp.go | 16 ++ ...tion_wildcard_force_automate.caddyfiletest | 180 ++++++++++++++++++ ...utomation_wildcard_shadowing.caddyfiletest | 102 ++++++++++ 5 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 99a4d916..45570d01 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -84,7 +84,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // parseTLS parses the tls directive. Syntax: // -// tls [|internal]|[ ] { +// tls [|internal|force_automate]|[ ] { // protocols [] // ciphers // curves @@ -107,6 +107,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // dns_challenge_override_domain // on_demand // reuse_private_keys +// force_automate // eab // issuer [...] // get_certificate [...] @@ -126,6 +127,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var certManagers []certmagic.Manager var onDemand bool var reusePrivateKeys bool + var forceAutomate bool firstLine := h.RemainingArgs() switch len(firstLine) { @@ -133,8 +135,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) { case 1: if firstLine[0] == "internal" { internalIssuer = new(caddytls.InternalIssuer) + } else if firstLine[0] == "force_automate" { + forceAutomate = true } else if !strings.Contains(firstLine[0], "@") { - return nil, h.Err("single argument must either be 'internal' or an email address") + return nil, h.Err("single argument must either be 'internal', 'force_automate', or an email address") } else { acmeIssuer = &caddytls.ACMEIssuer{ Email: firstLine[0], @@ -569,6 +573,15 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } + // if enabled, the names in the site addresses will be + // added to the automation policies + if forceAutomate { + configVals = append(configVals, ConfigValue{ + Class: "tls.force_automate", + Value: true, + }) + } + // custom certificate selection if len(certSelector.AnyTag) > 0 { cp.CertSelection = &certSelector diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index c169b92a..37a6f6b2 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -763,6 +763,14 @@ func (st *ServerType) serversFromPairings( } } + // collect hosts that are forced to be automated + forceAutomatedNames := make(map[string]struct{}) + if _, ok := sblock.pile["tls.force_automate"]; ok { + for _, host := range hosts { + forceAutomatedNames[host] = struct{}{} + } + } + // tls: connection policies if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { // tls connection policies @@ -794,7 +802,7 @@ func (st *ServerType) serversFromPairings( } // only append this policy if it actually changes something - if !cp.SettingsEmpty() { + if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) { srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) hasCatchAllTLSConnPolicy = len(hosts) == 0 } @@ -1661,6 +1669,18 @@ func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool { return false } +func mapContains[K comparable, V any](m map[K]V, keys []K) bool { + if len(m) == 0 || len(keys) == 0 { + return false + } + for _, key := range keys { + if _, ok := m[key]; ok { + return true + } + } + return false +} + // specificity returns len(s) minus any wildcards (*) and // placeholders ({...}). Basically, it's a length count // that penalizes the use of wildcards and placeholders. diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 397323f7..09a862e7 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -94,6 +94,9 @@ func (st ServerType) buildTLSApp( // collect all hosts that have a wildcard in them, and arent HTTP wildcardHosts := []string{} + // hosts that have been explicitly marked to be automated, + // even if covered by another wildcard + forcedAutomatedNames := make(map[string]struct{}) for _, p := range pairings { var addresses []string for _, addressWithProtocols := range p.addressesWithProtocols { @@ -150,6 +153,13 @@ func (st ServerType) buildTLSApp( ap.OnDemand = true } + // collect hosts that are forced to be automated + if _, ok := sblock.pile["tls.force_automate"]; ok { + for _, host := range sblockHosts { + forcedAutomatedNames[host] = struct{}{} + } + } + // reuse private keys tls if _, ok := sblock.pile["tls.reuse_private_keys"]; ok { ap.ReusePrivateKeys = true @@ -407,6 +417,12 @@ func (st ServerType) buildTLSApp( } } } + for name := range forcedAutomatedNames { + if slices.Contains(al, name) { + continue + } + al = append(al, name) + } if len(al) > 0 { tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings) } diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest new file mode 100644 index 00000000..4eb6c4f1 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest @@ -0,0 +1,180 @@ +automated1.example.com { + tls force_automate + respond "Automated!" +} + +automated2.example.com { + tls force_automate + respond "Automated!" +} + +shadowed.example.com { + respond "Shadowed!" +} + +*.example.com { + tls cert.pem key.pem + respond "Wildcard!" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "automated1.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Automated!", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "automated2.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Automated!", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "shadowed.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Shadowed!", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "*.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Wildcard!", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "automated1.example.com" + ] + } + }, + { + "match": { + "sni": [ + "automated2.example.com" + ] + } + }, + { + "match": { + "sni": [ + "*.example.com" + ] + }, + "certificate_selection": { + "any_tag": [ + "cert0" + ] + } + }, + {} + ] + } + } + }, + "tls": { + "certificates": { + "automate": [ + "automated1.example.com", + "automated2.example.com" + ], + "load_files": [ + { + "certificate": "cert.pem", + "key": "key.pem", + "tags": [ + "cert0" + ] + } + ] + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest new file mode 100644 index 00000000..2be54377 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest @@ -0,0 +1,102 @@ +subdomain.example.com { + respond "Subdomain!" +} + +*.example.com { + tls cert.pem key.pem + respond "Wildcard!" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "subdomain.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Subdomain!", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "*.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Wildcard!", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "*.example.com" + ] + }, + "certificate_selection": { + "any_tag": [ + "cert0" + ] + } + }, + {} + ] + } + } + }, + "tls": { + "certificates": { + "load_files": [ + { + "certificate": "cert.pem", + "key": "key.pem", + "tags": [ + "cert0" + ] + } + ] + } + } + } +} \ No newline at end of file