mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	* Implement BindHost fallback in ACME issuer * Fix indentation * Skip creating empty challenges stub in adapted json config * Skip setting BindHost for DNS Challenge * golangci-lint fix --------- Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
		
			
				
	
	
		
			869 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			869 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Matthew Holt and The Caddy Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package httpcaddyfile
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"reflect"
 | |
| 	"slices"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/caddyserver/certmagic"
 | |
| 	"github.com/mholt/acmez/v3/acme"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| 	"github.com/caddyserver/caddy/v2/caddyconfig"
 | |
| 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 | |
| 	"github.com/caddyserver/caddy/v2/modules/caddytls"
 | |
| )
 | |
| 
 | |
| func (st ServerType) buildTLSApp(
 | |
| 	pairings []sbAddrAssociation,
 | |
| 	options map[string]any,
 | |
| 	warnings []caddyconfig.Warning,
 | |
| ) (*caddytls.TLS, []caddyconfig.Warning, error) {
 | |
| 	tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
 | |
| 	var certLoaders []caddytls.CertificateLoader
 | |
| 
 | |
| 	httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
 | |
| 	if hp, ok := options["http_port"].(int); ok {
 | |
| 		httpPort = strconv.Itoa(hp)
 | |
| 	}
 | |
| 	autoHTTPS := []string{}
 | |
| 	if ah, ok := options["auto_https"].([]string); ok {
 | |
| 		autoHTTPS = ah
 | |
| 	}
 | |
| 
 | |
| 	// find all hosts that share a server block with a hostless
 | |
| 	// key, so that they don't get forgotten/omitted by auto-HTTPS
 | |
| 	// (since they won't appear in route matchers)
 | |
| 	httpsHostsSharedWithHostlessKey := make(map[string]struct{})
 | |
| 	if !slices.Contains(autoHTTPS, "off") {
 | |
| 		for _, pair := range pairings {
 | |
| 			for _, sb := range pair.serverBlocks {
 | |
| 				for _, addr := range sb.parsedKeys {
 | |
| 					if addr.Host != "" {
 | |
| 						continue
 | |
| 					}
 | |
| 
 | |
| 					// this server block has a hostless key, now
 | |
| 					// go through and add all the hosts to the set
 | |
| 					for _, otherAddr := range sb.parsedKeys {
 | |
| 						if otherAddr.Original == addr.Original {
 | |
| 							continue
 | |
| 						}
 | |
| 						if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
 | |
| 							httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
 | |
| 						}
 | |
| 					}
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// a catch-all automation policy is used as a "default" for all subjects that
 | |
| 	// don't have custom configuration explicitly associated with them; this
 | |
| 	// is only to add if the global settings or defaults are non-empty
 | |
| 	catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
 | |
| 	if err != nil {
 | |
| 		return nil, warnings, err
 | |
| 	}
 | |
| 	if catchAllAP != nil {
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
 | |
| 	}
 | |
| 
 | |
| 	var wildcardHosts []string                        // collect all hosts that have a wildcard in them, and aren't HTTP
 | |
| 	forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
 | |
| 
 | |
| 	for _, p := range pairings {
 | |
| 		var addresses []string
 | |
| 		for _, addressWithProtocols := range p.addressesWithProtocols {
 | |
| 			addresses = append(addresses, addressWithProtocols.address)
 | |
| 		}
 | |
| 		if !listenersUseAnyPortOtherThan(addresses, httpPort) {
 | |
| 			continue
 | |
| 		}
 | |
| 		for _, sblock := range p.serverBlocks {
 | |
| 			for _, addr := range sblock.parsedKeys {
 | |
| 				if strings.HasPrefix(addr.Host, "*.") {
 | |
| 					wildcardHosts = append(wildcardHosts, addr.Host[2:])
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, p := range pairings {
 | |
| 		// avoid setting up TLS automation policies for a server that is HTTP-only
 | |
| 		var addresses []string
 | |
| 		for _, addressWithProtocols := range p.addressesWithProtocols {
 | |
| 			addresses = append(addresses, addressWithProtocols.address)
 | |
| 		}
 | |
| 		if !listenersUseAnyPortOtherThan(addresses, httpPort) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		for _, sblock := range p.serverBlocks {
 | |
| 			// check the scheme of all the site addresses,
 | |
| 			// skip building AP if they all had http://
 | |
| 			if sblock.isAllHTTP() {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// get values that populate an automation policy for this block
 | |
| 			ap, err := newBaseAutomationPolicy(options, warnings, true)
 | |
| 			if err != nil {
 | |
| 				return nil, warnings, err
 | |
| 			}
 | |
| 
 | |
| 			// make a plain copy so we can compare whether we made any changes
 | |
| 			apCopy, err := newBaseAutomationPolicy(options, warnings, true)
 | |
| 			if err != nil {
 | |
| 				return nil, warnings, err
 | |
| 			}
 | |
| 
 | |
| 			sblockHosts := sblock.hostsFromKeys(false)
 | |
| 			if len(sblockHosts) == 0 && catchAllAP != nil {
 | |
| 				ap = catchAllAP
 | |
| 			}
 | |
| 
 | |
| 			// on-demand tls
 | |
| 			if _, ok := sblock.pile["tls.on_demand"]; ok {
 | |
| 				ap.OnDemand = true
 | |
| 			}
 | |
| 
 | |
| 			// collect hosts that are forced to have certs automated for their specific name
 | |
| 			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
 | |
| 			}
 | |
| 
 | |
| 			if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
 | |
| 				ap.KeyType = keyTypeVals[0].Value.(string)
 | |
| 			}
 | |
| 
 | |
| 			// certificate issuers
 | |
| 			if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
 | |
| 				var issuers []certmagic.Issuer
 | |
| 				for _, issuerVal := range issuerVals {
 | |
| 					issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
 | |
| 				}
 | |
| 				if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
 | |
| 					// this more correctly implements an error check that was removed
 | |
| 					// below; try it with this config:
 | |
| 					//
 | |
| 					// :443 {
 | |
| 					// 	bind 127.0.0.1
 | |
| 					// }
 | |
| 					//
 | |
| 					// :443 {
 | |
| 					// 	bind ::1
 | |
| 					// 	tls {
 | |
| 					// 		issuer acme
 | |
| 					// 	}
 | |
| 					// }
 | |
| 					return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
 | |
| 				}
 | |
| 				ap.Issuers = issuers
 | |
| 			}
 | |
| 
 | |
| 			// certificate managers
 | |
| 			if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
 | |
| 				for _, certManager := range certManagerVals {
 | |
| 					certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
 | |
| 					ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
 | |
| 				}
 | |
| 			}
 | |
| 			// custom bind host
 | |
| 			for _, cfgVal := range sblock.pile["bind"] {
 | |
| 				for _, iss := range ap.Issuers {
 | |
| 					// if an issuer was already configured and it is NOT an ACME issuer,
 | |
| 					// skip, since we intend to adjust only ACME issuers; ensure we
 | |
| 					// include any issuer that embeds/wraps an underlying ACME issuer
 | |
| 					var acmeIssuer *caddytls.ACMEIssuer
 | |
| 					if acmeWrapper, ok := iss.(acmeCapable); ok {
 | |
| 						acmeIssuer = acmeWrapper.GetACMEIssuer()
 | |
| 					}
 | |
| 					if acmeIssuer == nil {
 | |
| 						continue
 | |
| 					}
 | |
| 
 | |
| 					// proceed to configure the ACME issuer's bind host, without
 | |
| 					// overwriting any existing settings
 | |
| 					if acmeIssuer.Challenges == nil {
 | |
| 						acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
 | |
| 					}
 | |
| 					if acmeIssuer.Challenges.BindHost == "" {
 | |
| 						// only binding to one host is supported
 | |
| 						var bindHost string
 | |
| 						if asserted, ok := cfgVal.Value.(addressesWithProtocols); ok && len(asserted.addresses) > 0 {
 | |
| 							bindHost = asserted.addresses[0]
 | |
| 						}
 | |
| 						acmeIssuer.Challenges.BindHost = bindHost
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// we used to ensure this block is allowed to create an automation policy;
 | |
| 			// doing so was forbidden if it has a key with no host (i.e. ":443")
 | |
| 			// and if there is a different server block that also has a key with no
 | |
| 			// host -- since a key with no host matches any host, we need its
 | |
| 			// associated automation policy to have an empty Subjects list, i.e. no
 | |
| 			// host filter, which is indistinguishable between the two server blocks
 | |
| 			// because automation is not done in the context of a particular server...
 | |
| 			// this is an example of a poor mapping from Caddyfile to JSON but that's
 | |
| 			// the least-leaky abstraction I could figure out -- however, this check
 | |
| 			// was preventing certain listeners, like those provided by plugins, from
 | |
| 			// being used as desired (see the Tailscale listener plugin), so I removed
 | |
| 			// the check: and I think since I originally wrote the check I added a new
 | |
| 			// check above which *properly* detects this ambiguity without breaking the
 | |
| 			// listener plugin; see the check above with a commented example config
 | |
| 			if len(sblockHosts) == 0 && catchAllAP == nil {
 | |
| 				// this server block has a key with no hosts, but there is not yet
 | |
| 				// a catch-all automation policy (probably because no global options
 | |
| 				// were set), so this one becomes it
 | |
| 				catchAllAP = ap
 | |
| 			}
 | |
| 
 | |
| 			hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
 | |
| 			sort.Strings(hostsNotHTTP) // solely for deterministic test results
 | |
| 
 | |
| 			// if the we prefer wildcards and the AP is unchanged,
 | |
| 			// then we can skip this AP because it should be covered
 | |
| 			// by an AP with a wildcard
 | |
| 			if slices.Contains(autoHTTPS, "prefer_wildcard") {
 | |
| 				if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) &&
 | |
| 					reflect.DeepEqual(ap, apCopy) {
 | |
| 					continue
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// associate our new automation policy with this server block's hosts
 | |
| 			ap.SubjectsRaw = hostsNotHTTP
 | |
| 
 | |
| 			// if a combination of public and internal names were given
 | |
| 			// for this same server block and no issuer was specified, we
 | |
| 			// need to separate them out in the automation policies so
 | |
| 			// that the internal names can use the internal issuer and
 | |
| 			// the other names can use the default/public/ACME issuer
 | |
| 			var ap2 *caddytls.AutomationPolicy
 | |
| 			if len(ap.Issuers) == 0 {
 | |
| 				var internal, external []string
 | |
| 				for _, s := range ap.SubjectsRaw {
 | |
| 					// do not create Issuers for Tailscale domains; they will be given a Manager instead
 | |
| 					if isTailscaleDomain(s) {
 | |
| 						continue
 | |
| 					}
 | |
| 					if !certmagic.SubjectQualifiesForCert(s) {
 | |
| 						return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
 | |
| 					}
 | |
| 					// we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
 | |
| 					// names like *.*.tld that may not qualify for a public certificate are actually
 | |
| 					// fine when used with OnDemand, since OnDemand (currently) does not obtain
 | |
| 					// wildcards (if it ever does, there will be a separate config option to enable
 | |
| 					// it that we would need to check here) since the hostname is known at handshake;
 | |
| 					// and it is unexpected to switch to internal issuer when the user wants to get
 | |
| 					// regular certificates on-demand for a class of certs like *.*.tld.
 | |
| 					if subjectQualifiesForPublicCert(ap, s) {
 | |
| 						external = append(external, s)
 | |
| 					} else {
 | |
| 						internal = append(internal, s)
 | |
| 					}
 | |
| 				}
 | |
| 				if len(external) > 0 && len(internal) > 0 {
 | |
| 					ap.SubjectsRaw = external
 | |
| 					apCopy := *ap
 | |
| 					ap2 = &apCopy
 | |
| 					ap2.SubjectsRaw = internal
 | |
| 					ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if tlsApp.Automation == nil {
 | |
| 				tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 			}
 | |
| 			tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
 | |
| 			if ap2 != nil {
 | |
| 				tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
 | |
| 			}
 | |
| 
 | |
| 			// certificate loaders
 | |
| 			if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
 | |
| 				for _, clVal := range clVals {
 | |
| 					certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// group certificate loaders by module name, then add to config
 | |
| 	if len(certLoaders) > 0 {
 | |
| 		loadersByName := make(map[string]caddytls.CertificateLoader)
 | |
| 		for _, cl := range certLoaders {
 | |
| 			name := caddy.GetModuleName(cl)
 | |
| 			// ugh... technically, we may have multiple FileLoader and FolderLoader
 | |
| 			// modules (because the tls directive returns one per occurrence), but
 | |
| 			// the config structure expects only one instance of each kind of loader
 | |
| 			// module, so we have to combine them... instead of enumerating each
 | |
| 			// possible cert loader module in a type switch, we can use reflection,
 | |
| 			// which works on any cert loaders that are slice types
 | |
| 			if reflect.TypeOf(cl).Kind() == reflect.Slice {
 | |
| 				combined := reflect.ValueOf(loadersByName[name])
 | |
| 				if !combined.IsValid() {
 | |
| 					combined = reflect.New(reflect.TypeOf(cl)).Elem()
 | |
| 				}
 | |
| 				clVal := reflect.ValueOf(cl)
 | |
| 				for i := range clVal.Len() {
 | |
| 					combined = reflect.Append(combined, clVal.Index(i))
 | |
| 				}
 | |
| 				loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
 | |
| 			}
 | |
| 		}
 | |
| 		for certLoaderName, loaders := range loadersByName {
 | |
| 			tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// set any of the on-demand options, for if/when on-demand TLS is enabled
 | |
| 	if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok {
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.OnDemand = onDemand
 | |
| 	}
 | |
| 
 | |
| 	// set up "global" (to the TLS app) DNS provider config
 | |
| 	if globalDNS, ok := options["dns"]; ok && globalDNS != nil {
 | |
| 		tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
 | |
| 	}
 | |
| 
 | |
| 	// set up ECH from Caddyfile options
 | |
| 	if ech, ok := options["ech"].(*caddytls.ECH); ok {
 | |
| 		tlsApp.EncryptedClientHello = ech
 | |
| 
 | |
| 		// outer server names will need certificates, so make sure they're included
 | |
| 		// in an automation policy for them that applies any global options
 | |
| 		ap, err := newBaseAutomationPolicy(options, warnings, true)
 | |
| 		if err != nil {
 | |
| 			return nil, warnings, err
 | |
| 		}
 | |
| 		for _, cfg := range ech.Configs {
 | |
| 			if cfg.PublicName != "" {
 | |
| 				ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
 | |
| 			}
 | |
| 		}
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
 | |
| 	}
 | |
| 
 | |
| 	// if the storage clean interval is a boolean, then it's "off" to disable cleaning
 | |
| 	if sc, ok := options["storage_check"].(string); ok && sc == "off" {
 | |
| 		tlsApp.DisableStorageCheck = true
 | |
| 	}
 | |
| 
 | |
| 	// if the storage clean interval is a boolean, then it's "off" to disable cleaning
 | |
| 	if sci, ok := options["storage_clean_interval"].(bool); ok && !sci {
 | |
| 		tlsApp.DisableStorageClean = true
 | |
| 	}
 | |
| 
 | |
| 	// set the storage clean interval if configured
 | |
| 	if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.StorageCleanInterval = storageCleanInterval
 | |
| 	}
 | |
| 
 | |
| 	// set the expired certificates renew interval if configured
 | |
| 	if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.RenewCheckInterval = renewCheckInterval
 | |
| 	}
 | |
| 
 | |
| 	// set the OCSP check interval if configured
 | |
| 	if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
 | |
| 	}
 | |
| 
 | |
| 	// set whether OCSP stapling should be disabled for manually-managed certificates
 | |
| 	if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
 | |
| 		tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
 | |
| 	}
 | |
| 
 | |
| 	// if any hostnames appear on the same server block as a key with
 | |
| 	// no host, they will not be used with route matchers because the
 | |
| 	// hostless key matches all hosts, therefore, it wouldn't be
 | |
| 	// considered for auto-HTTPS, so we need to make sure those hosts
 | |
| 	// are manually considered for managed certificates; we also need
 | |
| 	// to make sure that any of these names which are internal-only
 | |
| 	// get internal certificates by default rather than ACME
 | |
| 	var al caddytls.AutomateLoader
 | |
| 	internalAP := &caddytls.AutomationPolicy{
 | |
| 		IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
 | |
| 	}
 | |
| 	if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
 | |
| 		for h := range httpsHostsSharedWithHostlessKey {
 | |
| 			al = append(al, h)
 | |
| 			if !certmagic.SubjectQualifiesForPublicCert(h) {
 | |
| 				internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, h)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	for name := range forcedAutomatedNames {
 | |
| 		if slices.Contains(al, name) {
 | |
| 			continue
 | |
| 		}
 | |
| 		al = append(al, name)
 | |
| 	}
 | |
| 	slices.Sort(al) // to stabilize the adapt output
 | |
| 	if len(al) > 0 {
 | |
| 		tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
 | |
| 	}
 | |
| 	if len(internalAP.SubjectsRaw) > 0 {
 | |
| 		if tlsApp.Automation == nil {
 | |
| 			tlsApp.Automation = new(caddytls.AutomationConfig)
 | |
| 		}
 | |
| 		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
 | |
| 	}
 | |
| 
 | |
| 	// if there are any global options set for issuers (ACME ones in particular), make sure they
 | |
| 	// take effect in every automation policy that does not have any issuers
 | |
| 	if tlsApp.Automation != nil {
 | |
| 		globalEmail := options["email"]
 | |
| 		globalACMECA := options["acme_ca"]
 | |
| 		globalACMECARoot := options["acme_ca_root"]
 | |
| 		_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
 | |
| 		globalACMEEAB := options["acme_eab"]
 | |
| 		globalPreferredChains := options["preferred_chains"]
 | |
| 		hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
 | |
| 		if hasGlobalACMEDefaults {
 | |
| 			for i := range tlsApp.Automation.Policies {
 | |
| 				ap := tlsApp.Automation.Policies[i]
 | |
| 				if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
 | |
| 					// for public names, create default issuers which will later be filled in with configured global defaults
 | |
| 					// (internal names will implicitly use the internal issuer at auto-https time)
 | |
| 					emailStr, _ := globalEmail.(string)
 | |
| 					ap.Issuers = caddytls.DefaultIssuers(emailStr)
 | |
| 
 | |
| 					// if a specific endpoint is configured, can't use multiple default issuers
 | |
| 					if globalACMECA != nil {
 | |
| 						ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// finalize and verify policies; do cleanup
 | |
| 	if tlsApp.Automation != nil {
 | |
| 		for i, ap := range tlsApp.Automation.Policies {
 | |
| 			// ensure all issuers have global defaults filled in
 | |
| 			for j, issuer := range ap.Issuers {
 | |
| 				err := fillInGlobalACMEDefaults(issuer, options)
 | |
| 				if err != nil {
 | |
| 					return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// encode all issuer values we created, so they will be rendered in the output
 | |
| 			if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
 | |
| 				for _, iss := range ap.Issuers {
 | |
| 					issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
 | |
| 					ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// consolidate automation policies that are the exact same
 | |
| 		tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
 | |
| 
 | |
| 		// ensure automation policies don't overlap subjects (this should be
 | |
| 		// an error at provision-time as well, but catch it in the adapt phase
 | |
| 		// for convenience)
 | |
| 		automationHostSet := make(map[string]struct{})
 | |
| 		for _, ap := range tlsApp.Automation.Policies {
 | |
| 			for _, s := range ap.SubjectsRaw {
 | |
| 				if _, ok := automationHostSet[s]; ok {
 | |
| 					return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
 | |
| 				}
 | |
| 				automationHostSet[s] = struct{}{}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// if nothing remains, remove any excess values to clean up the resulting config
 | |
| 		if len(tlsApp.Automation.Policies) == 0 {
 | |
| 			tlsApp.Automation.Policies = nil
 | |
| 		}
 | |
| 		if reflect.DeepEqual(tlsApp.Automation, new(caddytls.AutomationConfig)) {
 | |
| 			tlsApp.Automation = nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return tlsApp, warnings, nil
 | |
| }
 | |
| 
 | |
| type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
 | |
| 
 | |
| func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
 | |
| 	acmeWrapper, ok := issuer.(acmeCapable)
 | |
| 	if !ok {
 | |
| 		return nil
 | |
| 	}
 | |
| 	acmeIssuer := acmeWrapper.GetACMEIssuer()
 | |
| 	if acmeIssuer == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	globalEmail := options["email"]
 | |
| 	globalACMECA := options["acme_ca"]
 | |
| 	globalACMECARoot := options["acme_ca_root"]
 | |
| 	globalACMEDNS, globalACMEDNSok := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
 | |
| 	globalACMEEAB := options["acme_eab"]
 | |
| 	globalPreferredChains := options["preferred_chains"]
 | |
| 	globalCertLifetime := options["cert_lifetime"]
 | |
| 	globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
 | |
| 	globalDefaultBind := options["default_bind"]
 | |
| 
 | |
| 	if globalEmail != nil && acmeIssuer.Email == "" {
 | |
| 		acmeIssuer.Email = globalEmail.(string)
 | |
| 	}
 | |
| 	if globalACMECA != nil && acmeIssuer.CA == "" {
 | |
| 		acmeIssuer.CA = globalACMECA.(string)
 | |
| 	}
 | |
| 	if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
 | |
| 		acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
 | |
| 	}
 | |
| 	if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) {
 | |
| 		globalDNS := options["dns"]
 | |
| 		if globalDNS == nil && globalACMEDNS == nil {
 | |
| 			return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
 | |
| 		}
 | |
| 		if acmeIssuer.Challenges == nil {
 | |
| 			acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
 | |
| 		}
 | |
| 		if acmeIssuer.Challenges.DNS == nil {
 | |
| 			acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
 | |
| 		}
 | |
| 		// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
 | |
| 		if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
 | |
| 			// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
 | |
| 			acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
 | |
| 		}
 | |
| 	}
 | |
| 	if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
 | |
| 		acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
 | |
| 	}
 | |
| 	if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
 | |
| 		acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
 | |
| 	}
 | |
| 	// only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others)
 | |
| 	if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
 | |
| 		if acmeIssuer.Challenges == nil {
 | |
| 			acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
 | |
| 		}
 | |
| 		if acmeIssuer.Challenges.HTTP == nil {
 | |
| 			acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
 | |
| 		}
 | |
| 		acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
 | |
| 	}
 | |
| 	if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
 | |
| 		if acmeIssuer.Challenges == nil {
 | |
| 			acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
 | |
| 		}
 | |
| 		if acmeIssuer.Challenges.TLSALPN == nil {
 | |
| 			acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
 | |
| 		}
 | |
| 		acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
 | |
| 	}
 | |
| 	// If BindHost is still unset, fall back to the first default_bind address if set
 | |
| 	// This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind
 | |
| 	// In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though)
 | |
| 	// In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened
 | |
| 	if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") {
 | |
| 		if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 {
 | |
| 			if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 {
 | |
| 				if acmeIssuer.Challenges == nil {
 | |
| 					acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
 | |
| 				}
 | |
| 				acmeIssuer.Challenges.BindHost = abp.addresses[0]
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
 | |
| 		acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // newBaseAutomationPolicy returns a new TLS automation policy that gets
 | |
| // its values from the global options map. It should be used as the base
 | |
| // for any other automation policies. A nil policy (and no error) will be
 | |
| // returned if there are no default/global options. However, if always is
 | |
| // true, a non-nil value will always be returned (unless there is an error).
 | |
| func newBaseAutomationPolicy(
 | |
| 	options map[string]any,
 | |
| 	_ []caddyconfig.Warning,
 | |
| 	always bool,
 | |
| ) (*caddytls.AutomationPolicy, error) {
 | |
| 	issuers, hasIssuers := options["cert_issuer"]
 | |
| 	_, hasLocalCerts := options["local_certs"]
 | |
| 	keyType, hasKeyType := options["key_type"]
 | |
| 	ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
 | |
| 	hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
 | |
| 
 | |
| 	globalACMECA := options["acme_ca"]
 | |
| 	globalACMECARoot := options["acme_ca_root"]
 | |
| 	_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
 | |
| 	globalACMEEAB := options["acme_eab"]
 | |
| 	globalPreferredChains := options["preferred_chains"]
 | |
| 	hasGlobalACMEDefaults := globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
 | |
| 
 | |
| 	// if there are no global options related to automation policies
 | |
| 	// set, then we can just return right away
 | |
| 	if !hasGlobalAutomationOpts && !hasGlobalACMEDefaults {
 | |
| 		if always {
 | |
| 			return new(caddytls.AutomationPolicy), nil
 | |
| 		}
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	ap := new(caddytls.AutomationPolicy)
 | |
| 	if hasKeyType {
 | |
| 		ap.KeyType = keyType.(string)
 | |
| 	}
 | |
| 
 | |
| 	if hasIssuers && hasLocalCerts {
 | |
| 		return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
 | |
| 	}
 | |
| 
 | |
| 	if hasIssuers {
 | |
| 		ap.Issuers = issuers.([]certmagic.Issuer)
 | |
| 	} else if hasLocalCerts {
 | |
| 		ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
 | |
| 	}
 | |
| 
 | |
| 	if hasGlobalACMEDefaults {
 | |
| 		for i := range ap.Issuers {
 | |
| 			if err := fillInGlobalACMEDefaults(ap.Issuers[i], options); err != nil {
 | |
| 				return nil, fmt.Errorf("filling in global issuer defaults for issuer %d: %v", i, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if hasOCSPStapling {
 | |
| 		ocspConfig := ocspStapling.(certmagic.OCSPConfig)
 | |
| 		ap.DisableOCSPStapling = ocspConfig.DisableStapling
 | |
| 		ap.OCSPOverrides = ocspConfig.ResponderOverrides
 | |
| 	}
 | |
| 
 | |
| 	return ap, nil
 | |
| }
 | |
| 
 | |
| // consolidateAutomationPolicies combines automation policies that are the same,
 | |
| // for a cleaner overall output.
 | |
| func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
 | |
| 	// sort from most specific to least specific; we depend on this ordering
 | |
| 	sort.SliceStable(aps, func(i, j int) bool {
 | |
| 		if automationPolicyIsSubset(aps[i], aps[j]) {
 | |
| 			return true
 | |
| 		}
 | |
| 		if automationPolicyIsSubset(aps[j], aps[i]) {
 | |
| 			return false
 | |
| 		}
 | |
| 		return len(aps[i].SubjectsRaw) > len(aps[j].SubjectsRaw)
 | |
| 	})
 | |
| 
 | |
| 	emptyAPCount := 0
 | |
| 	origLenAPs := len(aps)
 | |
| 	// compute the number of empty policies (disregarding subjects) - see #4128
 | |
| 	emptyAP := new(caddytls.AutomationPolicy)
 | |
| 	for i := 0; i < len(aps); i++ {
 | |
| 		emptyAP.SubjectsRaw = aps[i].SubjectsRaw
 | |
| 		if reflect.DeepEqual(aps[i], emptyAP) {
 | |
| 			emptyAPCount++
 | |
| 			if !automationPolicyHasAllPublicNames(aps[i]) {
 | |
| 				// if this automation policy has internal names, we might as well remove it
 | |
| 				// so auto-https can implicitly use the internal issuer
 | |
| 				aps = slices.Delete(aps, i, i+1)
 | |
| 				i--
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	// If all policies are empty, we can return nil, as there is no need to set any policy
 | |
| 	if emptyAPCount == origLenAPs {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// remove or combine duplicate policies
 | |
| outer:
 | |
| 	for i := 0; i < len(aps); i++ {
 | |
| 		// compare only with next policies; we sorted by specificity so we must not delete earlier policies
 | |
| 		for j := i + 1; j < len(aps); j++ {
 | |
| 			// if they're exactly equal in every way, just keep one of them
 | |
| 			if reflect.DeepEqual(aps[i], aps[j]) {
 | |
| 				aps = slices.Delete(aps, j, j+1)
 | |
| 				// must re-evaluate current i against next j; can't skip it!
 | |
| 				// even if i decrements to -1, will be incremented to 0 immediately
 | |
| 				i--
 | |
| 				continue outer
 | |
| 			}
 | |
| 
 | |
| 			// if the policy is the same, we can keep just one, but we have
 | |
| 			// to be careful which one we keep; if only one has any hostnames
 | |
| 			// defined, then we need to keep the one without any hostnames,
 | |
| 			// otherwise the one without any subjects (a catch-all) would be
 | |
| 			// eaten up by the one with subjects; and if both have subjects, we
 | |
| 			// need to combine their lists
 | |
| 			if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
 | |
| 				reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
 | |
| 				bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
 | |
| 				aps[i].MustStaple == aps[j].MustStaple &&
 | |
| 				aps[i].KeyType == aps[j].KeyType &&
 | |
| 				aps[i].OnDemand == aps[j].OnDemand &&
 | |
| 				aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys &&
 | |
| 				aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
 | |
| 				if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 {
 | |
| 					// later policy (at j) has no subjects ("catch-all"), so we can
 | |
| 					// remove the identical-but-more-specific policy that comes first
 | |
| 					// AS LONG AS it is not shadowed by another policy before it; e.g.
 | |
| 					// if policy i is for example.com, policy i+1 is '*.com', and policy
 | |
| 					// j is catch-all, we cannot remove policy i because that would
 | |
| 					// cause example.com to be served by the less specific policy for
 | |
| 					// '*.com', which might be different (yes we've seen this happen)
 | |
| 					if automationPolicyShadows(i, aps) >= j {
 | |
| 						aps = slices.Delete(aps, i, i+1)
 | |
| 						i--
 | |
| 						continue outer
 | |
| 					}
 | |
| 				} else {
 | |
| 					// avoid repeated subjects
 | |
| 					for _, subj := range aps[j].SubjectsRaw {
 | |
| 						if !slices.Contains(aps[i].SubjectsRaw, subj) {
 | |
| 							aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
 | |
| 						}
 | |
| 					}
 | |
| 					aps = slices.Delete(aps, j, j+1)
 | |
| 					j--
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return aps
 | |
| }
 | |
| 
 | |
| // automationPolicyIsSubset returns true if a's subjects are a subset
 | |
| // of b's subjects.
 | |
| func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
 | |
| 	if len(b.SubjectsRaw) == 0 {
 | |
| 		return true
 | |
| 	}
 | |
| 	if len(a.SubjectsRaw) == 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 	for _, aSubj := range a.SubjectsRaw {
 | |
| 		inSuperset := slices.ContainsFunc(b.SubjectsRaw, func(bSubj string) bool {
 | |
| 			return certmagic.MatchWildcard(aSubj, bSubj)
 | |
| 		})
 | |
| 		if !inSuperset {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // automationPolicyShadows returns the index of a policy that aps[i] shadows;
 | |
| // in other words, for all policies after position i, if that policy covers
 | |
| // the same subjects but is less specific, that policy's position is returned,
 | |
| // or -1 if no shadowing is found. For example, if policy i is for
 | |
| // "foo.example.com" and policy i+2 is for "*.example.com", then i+2 will be
 | |
| // returned, since that policy is shadowed by i, which is in front.
 | |
| func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
 | |
| 	for j := i + 1; j < len(aps); j++ {
 | |
| 		if automationPolicyIsSubset(aps[i], aps[j]) {
 | |
| 			return j
 | |
| 		}
 | |
| 	}
 | |
| 	return -1
 | |
| }
 | |
| 
 | |
| // subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
 | |
| // that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
 | |
| // if the automation policy has OnDemand enabled (i.e. this function is more lenient).
 | |
| //
 | |
| // IP subjects are considered as non-qualifying for public certs. Technically, there are
 | |
| // now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
 | |
| // is used solely for implicit automation (defaults), where it gets really complicated to
 | |
| // keep track of which issuers support IP certificates in which circumstances. Currently,
 | |
| // issuers that support IP certificates are very few, and all require some sort of config
 | |
| // from the user anyway (such as an account credential). Since we cannot implicitly and
 | |
| // automatically get public IP certs without configuration from the user, we treat IPs as
 | |
| // not qualifying for public certificates. Users should expressly configure an issuer
 | |
| // that supports IP certs for that purpose.
 | |
| func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
 | |
| 	return !certmagic.SubjectIsIP(subj) &&
 | |
| 		!certmagic.SubjectIsInternal(subj) &&
 | |
| 		(strings.Count(subj, "*.") < 2 || ap.OnDemand)
 | |
| }
 | |
| 
 | |
| // automationPolicyHasAllPublicNames returns true if all the names on the policy
 | |
| // do NOT qualify for public certs OR are tailscale domains.
 | |
| func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
 | |
| 	return !slices.ContainsFunc(ap.SubjectsRaw, func(i string) bool {
 | |
| 		return !subjectQualifiesForPublicCert(ap, i) || isTailscaleDomain(i)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func isTailscaleDomain(name string) bool {
 | |
| 	return strings.HasSuffix(strings.ToLower(name), ".ts.net")
 | |
| }
 | |
| 
 | |
| func hostsCoveredByWildcard(hosts []string, wildcards []string) bool {
 | |
| 	if len(hosts) == 0 || len(wildcards) == 0 {
 | |
| 		return false
 | |
| 	}
 | |
| 	for _, host := range hosts {
 | |
| 		for _, wildcard := range wildcards {
 | |
| 			if strings.HasPrefix(host, "*.") {
 | |
| 				continue
 | |
| 			}
 | |
| 			if certmagic.MatchWildcard(host, "*."+wildcard) {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 |