mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	caddytls: Prefer managed wildcard certs over individual subdomain certs (#6959)
* caddytls: Prefer managed wildcard certs over individual subdomain certs * Repurpose force_automate as no_wildcard * Fix a couple bugs * Restore force_automate and use automate loader as wildcard override
This commit is contained in:
		
							parent
							
								
									35c8c2d92d
								
							
						
					
					
						commit
						1bfa111552
					
				| @ -633,12 +633,6 @@ func (st *ServerType) serversFromPairings( | |||||||
| 					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) | 					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) | ||||||
| 				} | 				} | ||||||
| 				srv.AutoHTTPS.IgnoreLoadedCerts = true | 				srv.AutoHTTPS.IgnoreLoadedCerts = true | ||||||
| 
 |  | ||||||
| 			case "prefer_wildcard": |  | ||||||
| 				if srv.AutoHTTPS == nil { |  | ||||||
| 					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) |  | ||||||
| 				} |  | ||||||
| 				srv.AutoHTTPS.PreferWildcard = true |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @ -706,16 +700,6 @@ func (st *ServerType) serversFromPairings( | |||||||
| 			return specificity(iLongestHost) > specificity(jLongestHost) | 			return specificity(iLongestHost) > specificity(jLongestHost) | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		// collect all hosts that have a wildcard in them |  | ||||||
| 		wildcardHosts := []string{} |  | ||||||
| 		for _, sblock := range p.serverBlocks { |  | ||||||
| 			for _, addr := range sblock.parsedKeys { |  | ||||||
| 				if strings.HasPrefix(addr.Host, "*.") { |  | ||||||
| 					wildcardHosts = append(wildcardHosts, addr.Host[2:]) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool | 		var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool | ||||||
| 		autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled | 		autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled | ||||||
| 
 | 
 | ||||||
| @ -801,7 +785,13 @@ func (st *ServerType) serversFromPairings( | |||||||
| 						cp.FallbackSNI = fallbackSNI | 						cp.FallbackSNI = fallbackSNI | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					// only append this policy if it actually changes something | 					// only append this policy if it actually changes something, | ||||||
|  | 					// or if the configuration explicitly automates certs for | ||||||
|  | 					// these names (this is necessary to hoist a connection policy | ||||||
|  | 					// above one that may manually load a wildcard cert that would | ||||||
|  | 					// otherwise clobber the automated one; the code that appends | ||||||
|  | 					// policies that manually load certs comes later, so they're | ||||||
|  | 					// lower in the list) | ||||||
| 					if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) { | 					if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) { | ||||||
| 						srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) | 						srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) | ||||||
| 						hasCatchAllTLSConnPolicy = len(hosts) == 0 | 						hasCatchAllTLSConnPolicy = len(hosts) == 0 | ||||||
| @ -841,18 +831,6 @@ func (st *ServerType) serversFromPairings( | |||||||
| 					addressQualifiesForTLS = true | 					addressQualifiesForTLS = true | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				// If prefer wildcard is enabled, then we add hosts that are |  | ||||||
| 				// already covered by the wildcard to the skip list |  | ||||||
| 				if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard { |  | ||||||
| 					baseDomain := addr.Host |  | ||||||
| 					if idx := strings.Index(baseDomain, "."); idx != -1 { |  | ||||||
| 						baseDomain = baseDomain[idx+1:] |  | ||||||
| 					} |  | ||||||
| 					if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) { |  | ||||||
| 						srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host) |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				// predict whether auto-HTTPS will add the conn policy for us; if so, we | 				// predict whether auto-HTTPS will add the conn policy for us; if so, we | ||||||
| 				// may not need to add one for this server | 				// may not need to add one for this server | ||||||
| 				autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy && | 				autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy && | ||||||
| @ -1083,11 +1061,40 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti | |||||||
| 
 | 
 | ||||||
| 			// if they're exactly equal in every way, just keep one of them | 			// if they're exactly equal in every way, just keep one of them | ||||||
| 			if reflect.DeepEqual(cps[i], cps[j]) { | 			if reflect.DeepEqual(cps[i], cps[j]) { | ||||||
| 				cps = append(cps[:j], cps[j+1:]...) | 				cps = slices.Delete(cps, j, j+1) | ||||||
| 				i-- | 				i-- | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			// as a special case, if there are adjacent TLS conn policies that are identical except | ||||||
|  | 			// by their matchers, and the matchers are specifically just ServerName ("sni") matchers | ||||||
|  | 			// (by far the most common), we can combine them into a single policy | ||||||
|  | 			if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 { | ||||||
|  | 				if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok { | ||||||
|  | 					if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok { | ||||||
|  | 						// position of policies and the matcher criteria check out; if settings are | ||||||
|  | 						// the same, then we can combine the policies; we have to unmarshal and | ||||||
|  | 						// remarshal the matchers though | ||||||
|  | 						if cps[i].SettingsEqual(*cps[j]) { | ||||||
|  | 							var iSNIMatcher caddytls.MatchServerName | ||||||
|  | 							if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil { | ||||||
|  | 								var jSNIMatcher caddytls.MatchServerName | ||||||
|  | 								if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil { | ||||||
|  | 									iSNIMatcher = append(iSNIMatcher, jSNIMatcher...) | ||||||
|  | 									cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher) | ||||||
|  | 									if err != nil { | ||||||
|  | 										return nil, fmt.Errorf("recombining SNI matchers: %v", err) | ||||||
|  | 									} | ||||||
|  | 									cps = slices.Delete(cps, j, j+1) | ||||||
|  | 									i-- | ||||||
|  | 									break | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			// if they have the same matcher, try to reconcile each field: either they must | 			// if they have the same matcher, try to reconcile each field: either they must | ||||||
| 			// be identical, or we have to be able to combine them safely | 			// be identical, or we have to be able to combine them safely | ||||||
| 			if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) { | 			if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) { | ||||||
| @ -1189,12 +1196,13 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti | |||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				cps = append(cps[:j], cps[j+1:]...) | 				cps = slices.Delete(cps, j, j+1) | ||||||
| 				i-- | 				i-- | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	return cps, nil | 	return cps, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -92,11 +92,9 @@ func (st ServerType) buildTLSApp( | |||||||
| 		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) | 		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// collect all hosts that have a wildcard in them, and arent HTTP | 	var wildcardHosts []string                        // collect all hosts that have a wildcard in them, and aren't HTTP | ||||||
| 	wildcardHosts := []string{} | 	forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard | ||||||
| 	// hosts that have been explicitly marked to be automated, | 
 | ||||||
| 	// even if covered by another wildcard |  | ||||||
| 	forcedAutomatedNames := make(map[string]struct{}) |  | ||||||
| 	for _, p := range pairings { | 	for _, p := range pairings { | ||||||
| 		var addresses []string | 		var addresses []string | ||||||
| 		for _, addressWithProtocols := range p.addressesWithProtocols { | 		for _, addressWithProtocols := range p.addressesWithProtocols { | ||||||
| @ -153,7 +151,7 @@ func (st ServerType) buildTLSApp( | |||||||
| 				ap.OnDemand = true | 				ap.OnDemand = true | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// collect hosts that are forced to be automated | 			// collect hosts that are forced to have certs automated for their specific name | ||||||
| 			if _, ok := sblock.pile["tls.force_automate"]; ok { | 			if _, ok := sblock.pile["tls.force_automate"]; ok { | ||||||
| 				for _, host := range sblockHosts { | 				for _, host := range sblockHosts { | ||||||
| 					forcedAutomatedNames[host] = struct{}{} | 					forcedAutomatedNames[host] = struct{}{} | ||||||
| @ -375,8 +373,10 @@ func (st ServerType) buildTLSApp( | |||||||
| 			return nil, warnings, err | 			return nil, warnings, err | ||||||
| 		} | 		} | ||||||
| 		for _, cfg := range ech.Configs { | 		for _, cfg := range ech.Configs { | ||||||
|  | 			if cfg.PublicName != "" { | ||||||
| 				ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) | 				ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 		if tlsApp.Automation == nil { | 		if tlsApp.Automation == nil { | ||||||
| 			tlsApp.Automation = new(caddytls.AutomationConfig) | 			tlsApp.Automation = new(caddytls.AutomationConfig) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -1,109 +0,0 @@ | |||||||
| { |  | ||||||
| 	auto_https prefer_wildcard |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| *.example.com { |  | ||||||
| 	tls { |  | ||||||
| 		dns mock |  | ||||||
| 	} |  | ||||||
| 	respond "fallback" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| foo.example.com { |  | ||||||
| 	respond "foo" |  | ||||||
| } |  | ||||||
| ---------- |  | ||||||
| { |  | ||||||
| 	"apps": { |  | ||||||
| 		"http": { |  | ||||||
| 			"servers": { |  | ||||||
| 				"srv0": { |  | ||||||
| 					"listen": [ |  | ||||||
| 						":443" |  | ||||||
| 					], |  | ||||||
| 					"routes": [ |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"foo.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "foo", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						}, |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"*.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "fallback", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						} |  | ||||||
| 					], |  | ||||||
| 					"automatic_https": { |  | ||||||
| 						"skip_certificates": [ |  | ||||||
| 							"foo.example.com" |  | ||||||
| 						], |  | ||||||
| 						"prefer_wildcard": true |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		"tls": { |  | ||||||
| 			"automation": { |  | ||||||
| 				"policies": [ |  | ||||||
| 					{ |  | ||||||
| 						"subjects": [ |  | ||||||
| 							"*.example.com" |  | ||||||
| 						], |  | ||||||
| 						"issuers": [ |  | ||||||
| 							{ |  | ||||||
| 								"challenges": { |  | ||||||
| 									"dns": { |  | ||||||
| 										"provider": { |  | ||||||
| 											"name": "mock" |  | ||||||
| 										} |  | ||||||
| 									} |  | ||||||
| 								}, |  | ||||||
| 								"module": "acme" |  | ||||||
| 							} |  | ||||||
| 						] |  | ||||||
| 					} |  | ||||||
| 				] |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,268 +0,0 @@ | |||||||
| { |  | ||||||
| 	auto_https prefer_wildcard |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # Covers two domains |  | ||||||
| *.one.example.com { |  | ||||||
| 	tls { |  | ||||||
| 		dns mock |  | ||||||
| 	} |  | ||||||
| 	respond "one fallback" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # Is covered, should not get its own AP |  | ||||||
| foo.one.example.com { |  | ||||||
| 	respond "foo one" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # This one has its own tls config so it doesn't get covered (escape hatch) |  | ||||||
| bar.one.example.com { |  | ||||||
| 	respond "bar one" |  | ||||||
| 	tls bar@bar.com |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # Covers nothing but AP gets consolidated with the first |  | ||||||
| *.two.example.com { |  | ||||||
| 	tls { |  | ||||||
| 		dns mock |  | ||||||
| 	} |  | ||||||
| 	respond "two fallback" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # Is HTTP so it should not cover |  | ||||||
| http://*.three.example.com { |  | ||||||
| 	respond "three fallback" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| # Has no wildcard coverage so it gets an AP |  | ||||||
| foo.three.example.com { |  | ||||||
| 	respond "foo three" |  | ||||||
| } |  | ||||||
| ---------- |  | ||||||
| { |  | ||||||
| 	"apps": { |  | ||||||
| 		"http": { |  | ||||||
| 			"servers": { |  | ||||||
| 				"srv0": { |  | ||||||
| 					"listen": [ |  | ||||||
| 						":443" |  | ||||||
| 					], |  | ||||||
| 					"routes": [ |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"foo.three.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "foo three", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						}, |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"foo.one.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "foo one", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						}, |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"bar.one.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "bar one", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						}, |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"*.one.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "one fallback", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						}, |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"*.two.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "two fallback", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						} |  | ||||||
| 					], |  | ||||||
| 					"automatic_https": { |  | ||||||
| 						"skip_certificates": [ |  | ||||||
| 							"foo.one.example.com", |  | ||||||
| 							"bar.one.example.com" |  | ||||||
| 						], |  | ||||||
| 						"prefer_wildcard": true |  | ||||||
| 					} |  | ||||||
| 				}, |  | ||||||
| 				"srv1": { |  | ||||||
| 					"listen": [ |  | ||||||
| 						":80" |  | ||||||
| 					], |  | ||||||
| 					"routes": [ |  | ||||||
| 						{ |  | ||||||
| 							"match": [ |  | ||||||
| 								{ |  | ||||||
| 									"host": [ |  | ||||||
| 										"*.three.example.com" |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"handle": [ |  | ||||||
| 								{ |  | ||||||
| 									"handler": "subroute", |  | ||||||
| 									"routes": [ |  | ||||||
| 										{ |  | ||||||
| 											"handle": [ |  | ||||||
| 												{ |  | ||||||
| 													"body": "three fallback", |  | ||||||
| 													"handler": "static_response" |  | ||||||
| 												} |  | ||||||
| 											] |  | ||||||
| 										} |  | ||||||
| 									] |  | ||||||
| 								} |  | ||||||
| 							], |  | ||||||
| 							"terminal": true |  | ||||||
| 						} |  | ||||||
| 					], |  | ||||||
| 					"automatic_https": { |  | ||||||
| 						"prefer_wildcard": true |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		"tls": { |  | ||||||
| 			"automation": { |  | ||||||
| 				"policies": [ |  | ||||||
| 					{ |  | ||||||
| 						"subjects": [ |  | ||||||
| 							"foo.three.example.com" |  | ||||||
| 						] |  | ||||||
| 					}, |  | ||||||
| 					{ |  | ||||||
| 						"subjects": [ |  | ||||||
| 							"bar.one.example.com" |  | ||||||
| 						], |  | ||||||
| 						"issuers": [ |  | ||||||
| 							{ |  | ||||||
| 								"email": "bar@bar.com", |  | ||||||
| 								"module": "acme" |  | ||||||
| 							}, |  | ||||||
| 							{ |  | ||||||
| 								"ca": "https://acme.zerossl.com/v2/DV90", |  | ||||||
| 								"email": "bar@bar.com", |  | ||||||
| 								"module": "acme" |  | ||||||
| 							} |  | ||||||
| 						] |  | ||||||
| 					}, |  | ||||||
| 					{ |  | ||||||
| 						"subjects": [ |  | ||||||
| 							"*.one.example.com", |  | ||||||
| 							"*.two.example.com" |  | ||||||
| 						], |  | ||||||
| 						"issuers": [ |  | ||||||
| 							{ |  | ||||||
| 								"challenges": { |  | ||||||
| 									"dns": { |  | ||||||
| 										"provider": { |  | ||||||
| 											"name": "mock" |  | ||||||
| 										} |  | ||||||
| 									} |  | ||||||
| 								}, |  | ||||||
| 								"module": "acme" |  | ||||||
| 							} |  | ||||||
| 						] |  | ||||||
| 					} |  | ||||||
| 				] |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -131,13 +131,7 @@ shadowed.example.com { | |||||||
| 						{ | 						{ | ||||||
| 							"match": { | 							"match": { | ||||||
| 								"sni": [ | 								"sni": [ | ||||||
| 									"automated1.example.com" | 									"automated1.example.com", | ||||||
| 								] |  | ||||||
| 							} |  | ||||||
| 						}, |  | ||||||
| 						{ |  | ||||||
| 							"match": { |  | ||||||
| 								"sni": [ |  | ||||||
| 									"automated2.example.com" | 									"automated2.example.com" | ||||||
| 								] | 								] | ||||||
| 							} | 							} | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								internal/logs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								internal/logs.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | package internal | ||||||
|  | 
 | ||||||
|  | import "fmt" | ||||||
|  | 
 | ||||||
|  | // MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length | ||||||
|  | // maxToDisplay. It is useful for logging domains being managed, for example, since a | ||||||
|  | // map is typically needed for quick lookup, but a slice is needed for logging, and this | ||||||
|  | // can be quite a doozy since there may be a huge amount (hundreds of thousands). | ||||||
|  | func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string { | ||||||
|  | 	numberOfNamesToDisplay := min(len(subjects), maxToDisplay) | ||||||
|  | 	domainsToDisplay := make([]string, 0, numberOfNamesToDisplay) | ||||||
|  | 	for domain := range subjects { | ||||||
|  | 		domainsToDisplay = append(domainsToDisplay, domain) | ||||||
|  | 		if len(domainsToDisplay) >= numberOfNamesToDisplay { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(subjects) > maxToDisplay { | ||||||
|  | 		domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay)) | ||||||
|  | 	} | ||||||
|  | 	return domainsToDisplay | ||||||
|  | } | ||||||
| @ -20,6 +20,7 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| @ -490,12 +491,10 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error { | |||||||
| 	if len(cl.Include) > 0 && len(cl.Exclude) > 0 { | 	if len(cl.Include) > 0 && len(cl.Exclude) > 0 { | ||||||
| 		// prevent intersections | 		// prevent intersections | ||||||
| 		for _, allow := range cl.Include { | 		for _, allow := range cl.Include { | ||||||
| 			for _, deny := range cl.Exclude { | 			if slices.Contains(cl.Exclude, allow) { | ||||||
| 				if allow == deny { |  | ||||||
| 				return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) | 				return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		// ensure namespaces are nested | 		// ensure namespaces are nested | ||||||
| 	outer: | 	outer: | ||||||
|  | |||||||
| @ -152,7 +152,7 @@ type App struct { | |||||||
| 	tlsApp *caddytls.TLS | 	tlsApp *caddytls.TLS | ||||||
| 
 | 
 | ||||||
| 	// used temporarily between phases 1 and 2 of auto HTTPS | 	// used temporarily between phases 1 and 2 of auto HTTPS | ||||||
| 	allCertDomains []string | 	allCertDomains map[string]struct{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CaddyModule returns the Caddy module information. | // CaddyModule returns the Caddy module information. | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ import ( | |||||||
| 	"go.uber.org/zap" | 	"go.uber.org/zap" | ||||||
| 
 | 
 | ||||||
| 	"github.com/caddyserver/caddy/v2" | 	"github.com/caddyserver/caddy/v2" | ||||||
|  | 	"github.com/caddyserver/caddy/v2/internal" | ||||||
| 	"github.com/caddyserver/caddy/v2/modules/caddytls" | 	"github.com/caddyserver/caddy/v2/modules/caddytls" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -65,12 +66,6 @@ type AutoHTTPSConfig struct { | |||||||
| 	// enabled. To force automated certificate management | 	// enabled. To force automated certificate management | ||||||
| 	// regardless of loaded certificates, set this to true. | 	// regardless of loaded certificates, set this to true. | ||||||
| 	IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` | 	IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` | ||||||
| 
 |  | ||||||
| 	// If true, automatic HTTPS will prefer wildcard names |  | ||||||
| 	// and ignore non-wildcard names if both are available. |  | ||||||
| 	// This allows for writing a config with top-level host |  | ||||||
| 	// matchers without having those names produce certificates. |  | ||||||
| 	PreferWildcard bool `json:"prefer_wildcard,omitempty"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // automaticHTTPSPhase1 provisions all route matchers, determines | // automaticHTTPSPhase1 provisions all route matchers, determines | ||||||
| @ -163,33 +158,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// trim the list of domains covered by wildcards, if configured |  | ||||||
| 		if srv.AutoHTTPS.PreferWildcard { |  | ||||||
| 			wildcards := make(map[string]struct{}) |  | ||||||
| 			for d := range serverDomainSet { |  | ||||||
| 				if strings.HasPrefix(d, "*.") { |  | ||||||
| 					wildcards[d[2:]] = struct{}{} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			for d := range serverDomainSet { |  | ||||||
| 				if strings.HasPrefix(d, "*.") { |  | ||||||
| 					continue |  | ||||||
| 				} |  | ||||||
| 				base := d |  | ||||||
| 				if idx := strings.Index(d, "."); idx != -1 { |  | ||||||
| 					base = d[idx+1:] |  | ||||||
| 				} |  | ||||||
| 				if _, ok := wildcards[base]; ok { |  | ||||||
| 					delete(serverDomainSet, d) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// build the list of domains that could be used with ECH (if enabled) | 		// build the list of domains that could be used with ECH (if enabled) | ||||||
| 		// so the TLS app can know to publish ECH configs for them; we do this | 		// so the TLS app can know to publish ECH configs for them | ||||||
| 		// after trimming domains covered by wildcards because, presumably, |  | ||||||
| 		// if the user wants to use wildcard certs, they also want to use the |  | ||||||
| 		// wildcard for ECH, rather than individual subdomains |  | ||||||
| 		echDomains := make([]string, 0, len(serverDomainSet)) | 		echDomains := make([]string, 0, len(serverDomainSet)) | ||||||
| 		for d := range serverDomainSet { | 		for d := range serverDomainSet { | ||||||
| 			echDomains = append(echDomains, d) | 			echDomains = append(echDomains, d) | ||||||
| @ -295,19 +265,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// we now have a list of all the unique names for which we need certs; | 	// we now have a list of all the unique names for which we need certs | ||||||
| 	// turn the set into a slice so that phase 2 can use it |  | ||||||
| 	app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) |  | ||||||
| 	var internal, tailscale []string | 	var internal, tailscale []string | ||||||
| uniqueDomainsLoop: | uniqueDomainsLoop: | ||||||
| 	for d := range uniqueDomainsForCerts { | 	for d := range uniqueDomainsForCerts { | ||||||
| 		if !isTailscaleDomain(d) { |  | ||||||
| 			// whether or not there is already an automation policy for this |  | ||||||
| 			// name, we should add it to the list to manage a cert for it, |  | ||||||
| 			// unless it's a Tailscale domain, because we don't manage those |  | ||||||
| 			app.allCertDomains = append(app.allCertDomains, d) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// some names we've found might already have automation policies | 		// some names we've found might already have automation policies | ||||||
| 		// explicitly specified for them; we should exclude those from | 		// explicitly specified for them; we should exclude those from | ||||||
| 		// our hidden/implicit policy, since applying a name to more than | 		// our hidden/implicit policy, since applying a name to more than | ||||||
| @ -346,6 +307,7 @@ uniqueDomainsLoop: | |||||||
| 		} | 		} | ||||||
| 		if isTailscaleDomain(d) { | 		if isTailscaleDomain(d) { | ||||||
| 			tailscale = append(tailscale, d) | 			tailscale = append(tailscale, d) | ||||||
|  | 			delete(uniqueDomainsForCerts, d) // not managed by us; handled separately | ||||||
| 		} else if shouldUseInternal(d) { | 		} else if shouldUseInternal(d) { | ||||||
| 			internal = append(internal, d) | 			internal = append(internal, d) | ||||||
| 		} | 		} | ||||||
| @ -475,6 +437,9 @@ redirServersLoop: | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// persist the domains/IPs we're managing certs for through provisioning/startup | ||||||
|  | 	app.allCertDomains = uniqueDomainsForCerts | ||||||
|  | 
 | ||||||
| 	logger.Debug("adjusted config", | 	logger.Debug("adjusted config", | ||||||
| 		zap.Reflect("tls", app.tlsApp), | 		zap.Reflect("tls", app.tlsApp), | ||||||
| 		zap.Reflect("http", app)) | 		zap.Reflect("http", app)) | ||||||
| @ -777,7 +742,7 @@ func (app *App) automaticHTTPSPhase2() error { | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	app.logger.Info("enabling automatic TLS certificate management", | 	app.logger.Info("enabling automatic TLS certificate management", | ||||||
| 		zap.Strings("domains", app.allCertDomains), | 		zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)), | ||||||
| 	) | 	) | ||||||
| 	err := app.tlsApp.Manage(app.allCertDomains) | 	err := app.tlsApp.Manage(app.allCertDomains) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | |||||||
| @ -652,7 +652,7 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) | |||||||
| 			return nil, fmt.Errorf("getting tls app: %v", err) | 			return nil, fmt.Errorf("getting tls app: %v", err) | ||||||
| 		} | 		} | ||||||
| 		tlsApp := tlsAppIface.(*caddytls.TLS) | 		tlsApp := tlsAppIface.(*caddytls.TLS) | ||||||
| 		err = tlsApp.Manage([]string{t.ClientCertificateAutomate}) | 		err = tlsApp.Manage(map[string]struct{}{t.ClientCertificateAutomate: {}}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("managing client certificate: %v", err) | 			return nil, fmt.Errorf("managing client certificate: %v", err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/acmez/v3" | 	"github.com/mholt/acmez/v3" | ||||||
| @ -461,6 +462,14 @@ func (p ConnectionPolicy) SettingsEmpty() bool { | |||||||
| 		p.InsecureSecretsLog == "" | 		p.InsecureSecretsLog == "" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // SettingsEmpty returns true if p's settings (fields | ||||||
|  | // except the matchers) are the same as q. | ||||||
|  | func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool { | ||||||
|  | 	p.MatchersRaw = nil | ||||||
|  | 	q.MatchersRaw = nil | ||||||
|  | 	return reflect.DeepEqual(p, q) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: | // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: | ||||||
| // | // | ||||||
| //	connection_policy { | //	connection_policy { | ||||||
|  | |||||||
| @ -138,7 +138,6 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { | |||||||
| 	// all existing configs are now loaded; see if we need to make any new ones | 	// all existing configs are now loaded; see if we need to make any new ones | ||||||
| 	// based on the input configuration, and also mark the most recent one(s) as | 	// based on the input configuration, and also mark the most recent one(s) as | ||||||
| 	// current/active, so they can be used for ECH retries | 	// current/active, so they can be used for ECH retries | ||||||
| 
 |  | ||||||
| 	for _, cfg := range ech.Configs { | 	for _, cfg := range ech.Configs { | ||||||
| 		publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) | 		publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -33,6 +33,7 @@ import ( | |||||||
| 	"go.uber.org/zap/zapcore" | 	"go.uber.org/zap/zapcore" | ||||||
| 
 | 
 | ||||||
| 	"github.com/caddyserver/caddy/v2" | 	"github.com/caddyserver/caddy/v2" | ||||||
|  | 	"github.com/caddyserver/caddy/v2/internal" | ||||||
| 	"github.com/caddyserver/caddy/v2/modules/caddyevents" | 	"github.com/caddyserver/caddy/v2/modules/caddyevents" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -55,8 +56,10 @@ type TLS struct { | |||||||
| 	// | 	// | ||||||
| 	// The "automate" certificate loader module can be used to | 	// The "automate" certificate loader module can be used to | ||||||
| 	// specify a list of subjects that need certificates to be | 	// specify a list of subjects that need certificates to be | ||||||
| 	// managed automatically. The first matching automation | 	// managed automatically, including subdomains that may | ||||||
| 	// policy will be applied to manage the certificate(s). | 	// already be covered by a managed wildcard certificate. | ||||||
|  | 	// The first matching automation policy will be used | ||||||
|  | 	// to manage automated certificate(s). | ||||||
| 	// | 	// | ||||||
| 	// All loaded certificates get pooled | 	// All loaded certificates get pooled | ||||||
| 	// into the same cache and may be used to complete TLS | 	// into the same cache and may be used to complete TLS | ||||||
| @ -123,7 +126,7 @@ type TLS struct { | |||||||
| 	dns    any             // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) | 	dns    any             // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) | ||||||
| 
 | 
 | ||||||
| 	certificateLoaders []CertificateLoader | 	certificateLoaders []CertificateLoader | ||||||
| 	automateNames      []string | 	automateNames      map[string]struct{} | ||||||
| 	ctx                caddy.Context | 	ctx                caddy.Context | ||||||
| 	storageCleanTicker *time.Ticker | 	storageCleanTicker *time.Ticker | ||||||
| 	storageCleanStop   chan struct{} | 	storageCleanStop   chan struct{} | ||||||
| @ -218,12 +221,13 @@ func (t *TLS) Provision(ctx caddy.Context) error { | |||||||
| 			// special case; these will be loaded in later using our automation facilities, | 			// special case; these will be loaded in later using our automation facilities, | ||||||
| 			// which we want to avoid doing during provisioning | 			// which we want to avoid doing during provisioning | ||||||
| 			if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil { | 			if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil { | ||||||
| 				repl := caddy.NewReplacer() | 				if t.automateNames == nil { | ||||||
| 				subjects := make([]string, len(*automateNames)) | 					t.automateNames = make(map[string]struct{}) | ||||||
| 				for i, sub := range *automateNames { | 				} | ||||||
| 					subjects[i] = repl.ReplaceAll(sub, "") | 				repl := caddy.NewReplacer() | ||||||
|  | 				for _, sub := range *automateNames { | ||||||
|  | 					t.automateNames[repl.ReplaceAll(sub, "")] = struct{}{} | ||||||
| 				} | 				} | ||||||
| 				t.automateNames = append(t.automateNames, subjects...) |  | ||||||
| 			} else { | 			} else { | ||||||
| 				return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) | 				return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) | ||||||
| 			} | 			} | ||||||
| @ -283,7 +287,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("provisioning default public automation policy: %v", err) | 		return fmt.Errorf("provisioning default public automation policy: %v", err) | ||||||
| 	} | 	} | ||||||
| 	for _, n := range t.automateNames { | 	for n := range t.automateNames { | ||||||
| 		// if any names specified by the "automate" loader do not qualify for a public | 		// if any names specified by the "automate" loader do not qualify for a public | ||||||
| 		// certificate, we should initialize a default internal automation policy | 		// certificate, we should initialize a default internal automation policy | ||||||
| 		// (but we don't want to do this unnecessarily, since it may prompt for password!) | 		// (but we don't want to do this unnecessarily, since it may prompt for password!) | ||||||
| @ -339,8 +343,14 @@ func (t *TLS) Provision(ctx caddy.Context) error { | |||||||
| 
 | 
 | ||||||
| 		// outer names should have certificates to reduce client brittleness | 		// outer names should have certificates to reduce client brittleness | ||||||
| 		for _, outerName := range outerNames { | 		for _, outerName := range outerNames { | ||||||
|  | 			if outerName == "" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			if !t.HasCertificateForSubject(outerName) { | 			if !t.HasCertificateForSubject(outerName) { | ||||||
| 				t.automateNames = append(t.automateNames, outerNames...) | 				if t.automateNames == nil { | ||||||
|  | 					t.automateNames = make(map[string]struct{}) | ||||||
|  | 				} | ||||||
|  | 				t.automateNames[outerName] = struct{}{} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -449,7 +459,8 @@ func (t *TLS) Cleanup() error { | |||||||
| 		// app instance (which is being stopped) that are not managed or loaded by the | 		// app instance (which is being stopped) that are not managed or loaded by the | ||||||
| 		// new app instance (which just started), and remove them from the cache | 		// new app instance (which just started), and remove them from the cache | ||||||
| 		var noLongerManaged []certmagic.SubjectIssuer | 		var noLongerManaged []certmagic.SubjectIssuer | ||||||
| 		var reManage, noLongerLoaded []string | 		var noLongerLoaded []string | ||||||
|  | 		reManage := make(map[string]struct{}) | ||||||
| 		for subj, currentIssuerKey := range t.managing { | 		for subj, currentIssuerKey := range t.managing { | ||||||
| 			// It's a bit nuanced: managed certs can sometimes be different enough that we have to | 			// It's a bit nuanced: managed certs can sometimes be different enough that we have to | ||||||
| 			// swap them out for a different one, even if they are for the same subject/domain. | 			// swap them out for a different one, even if they are for the same subject/domain. | ||||||
| @ -467,7 +478,7 @@ func (t *TLS) Cleanup() error { | |||||||
| 
 | 
 | ||||||
| 				// then, if the next app is managing a cert for this name, but with a different issuer, re-manage it | 				// then, if the next app is managing a cert for this name, but with a different issuer, re-manage it | ||||||
| 				if ok && nextIssuerKey != currentIssuerKey { | 				if ok && nextIssuerKey != currentIssuerKey { | ||||||
| 					reManage = append(reManage, subj) | 					reManage[subj] = struct{}{} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -488,7 +499,7 @@ func (t *TLS) Cleanup() error { | |||||||
| 		if err := nextTLSApp.Manage(reManage); err != nil { | 		if err := nextTLSApp.Manage(reManage); err != nil { | ||||||
| 			if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil { | 			if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil { | ||||||
| 				c.Write( | 				c.Write( | ||||||
| 					zap.Strings("subjects", reManage), | 					zap.Strings("subjects", internal.MaxSizeSubjectsListForLog(reManage, 1000)), | ||||||
| 					zap.Error(err), | 					zap.Error(err), | ||||||
| 				) | 				) | ||||||
| 			} | 			} | ||||||
| @ -509,17 +520,31 @@ func (t *TLS) Cleanup() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Manage immediately begins managing names according to the | // Manage immediately begins managing subjects according to the | ||||||
| // matching automation policy. | // matching automation policy. The subjects are given in a map | ||||||
| func (t *TLS) Manage(names []string) error { | // to prevent duplication and also because quick lookups are | ||||||
|  | // needed to assess wildcard coverage, if any, depending on | ||||||
|  | // certain config parameters (with lots of subjects, computing | ||||||
|  | // wildcard coverage over a slice can be highly inefficient). | ||||||
|  | func (t *TLS) Manage(subjects map[string]struct{}) error { | ||||||
| 	// for a large number of names, we can be more memory-efficient | 	// for a large number of names, we can be more memory-efficient | ||||||
| 	// by making only one certmagic.Config for all the names that | 	// by making only one certmagic.Config for all the names that | ||||||
| 	// use that config, rather than calling ManageAsync once for | 	// use that config, rather than calling ManageAsync once for | ||||||
| 	// every name; so first, bin names by AutomationPolicy | 	// every name; so first, bin names by AutomationPolicy | ||||||
| 	policyToNames := make(map[*AutomationPolicy][]string) | 	policyToNames := make(map[*AutomationPolicy][]string) | ||||||
| 	for _, name := range names { | 	for subj := range subjects { | ||||||
| 		ap := t.getAutomationPolicyForName(name) | 		ap := t.getAutomationPolicyForName(subj) | ||||||
| 		policyToNames[ap] = append(policyToNames[ap], name) | 		// by default, if a wildcard that covers the subj is also being | ||||||
|  | 		// managed, either by a previous call to Manage or by this one, | ||||||
|  | 		// prefer using that over individual certs for its subdomains; | ||||||
|  | 		// but users can disable this and force getting a certificate for | ||||||
|  | 		// subdomains by adding the name to the 'automate' cert loader | ||||||
|  | 		if t.managingWildcardFor(subj, subjects) { | ||||||
|  | 			if _, ok := t.automateNames[subj]; !ok { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		policyToNames[ap] = append(policyToNames[ap], subj) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// now that names are grouped by policy, we can simply make one | 	// now that names are grouped by policy, we can simply make one | ||||||
| @ -530,7 +555,7 @@ func (t *TLS) Manage(names []string) error { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			const maxNamesToDisplay = 100 | 			const maxNamesToDisplay = 100 | ||||||
| 			if len(names) > maxNamesToDisplay { | 			if len(names) > maxNamesToDisplay { | ||||||
| 				names = append(names[:maxNamesToDisplay], fmt.Sprintf("(%d more...)", len(names)-maxNamesToDisplay)) | 				names = append(names[:maxNamesToDisplay], fmt.Sprintf("(and %d more...)", len(names)-maxNamesToDisplay)) | ||||||
| 			} | 			} | ||||||
| 			return fmt.Errorf("automate: manage %v: %v", names, err) | 			return fmt.Errorf("automate: manage %v: %v", names, err) | ||||||
| 		} | 		} | ||||||
| @ -555,6 +580,43 @@ func (t *TLS) Manage(names []string) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // managingWildcardFor returns true if the app is managing a certificate that covers that | ||||||
|  | // subject name (including consideration of wildcards), either from its internal list of | ||||||
|  | // names that it IS managing certs for, or from the otherSubjsToManage which includes names | ||||||
|  | // that WILL be managed. | ||||||
|  | func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool { | ||||||
|  | 	// TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(), | ||||||
|  | 	// but that does not account for how manually-loaded certs may be restricted as to which | ||||||
|  | 	// hostnames or ClientHellos they can be used with by tags, etc; I don't *think* anyone | ||||||
|  | 	// necessarily wants this anyway, but I thought I'd note this here for now (if we did | ||||||
|  | 	// consider manually-loaded certs, we'd probably want to rename the method since it | ||||||
|  | 	// wouldn't be just about managed certs anymore) | ||||||
|  | 
 | ||||||
|  | 	// IP addresses must match exactly | ||||||
|  | 	if ip := net.ParseIP(subj); ip != nil { | ||||||
|  | 		_, managing := t.managing[subj] | ||||||
|  | 		return managing | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// replace labels of the domain with wildcards until we get a match | ||||||
|  | 	labels := strings.Split(subj, ".") | ||||||
|  | 	for i := range labels { | ||||||
|  | 		if labels[i] == "*" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		labels[i] = "*" | ||||||
|  | 		candidate := strings.Join(labels, ".") | ||||||
|  | 		if _, ok := t.managing[candidate]; ok { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 		if _, ok := otherSubjsToManage[candidate]; ok { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // RegisterServerNames registers the provided DNS names with the TLS app. | // RegisterServerNames registers the provided DNS names with the TLS app. | ||||||
| // This is currently used to auto-publish Encrypted ClientHello (ECH) | // This is currently used to auto-publish Encrypted ClientHello (ECH) | ||||||
| // configurations, if enabled. Use of this function by apps using the TLS | // configurations, if enabled. Use of this function by apps using the TLS | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user