aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--caddyconfig/httpcaddyfile/httptype.go56
-rw-r--r--caddyconfig/httpcaddyfile/options.go21
-rw-r--r--caddyconfig/httpcaddyfile/tlsapp.go41
-rw-r--r--caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest106
-rw-r--r--caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest157
-rw-r--r--caddytest/integration/mockdns_test.go61
-rw-r--r--modules/caddyhttp/autohttps.go27
7 files changed, 449 insertions, 20 deletions
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index 6745969ee..4dacd9058 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -534,8 +534,8 @@ func (st *ServerType) serversFromPairings(
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
- autoHTTPS := "on"
- if ah, ok := options["auto_https"].(string); ok {
+ autoHTTPS := []string{}
+ if ah, ok := options["auto_https"].([]string); ok {
autoHTTPS = ah
}
@@ -594,17 +594,37 @@ func (st *ServerType) serversFromPairings(
}
// handle the auto_https global option
- if autoHTTPS != "on" {
- srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
- switch autoHTTPS {
+ for _, val := range autoHTTPS {
+ switch val {
case "off":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.Disabled = true
+
case "disable_redirects":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.DisableRedir = true
+
case "disable_certs":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.DisableCerts = true
+
case "ignore_loaded_certs":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.IgnoreLoadedCerts = true
+
+ case "prefer_wildcard":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
+ srv.AutoHTTPS.PreferWildcard = true
}
}
@@ -673,7 +693,7 @@ func (st *ServerType) serversFromPairings(
})
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
- autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
+ autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
@@ -757,6 +777,13 @@ func (st *ServerType) serversFromPairings(
}
}
+ wildcardHosts := []string{}
+ for _, addr := range sblock.parsedKeys {
+ if strings.HasPrefix(addr.Host, "*.") {
+ wildcardHosts = append(wildcardHosts, addr.Host[2:])
+ }
+ }
+
for _, addr := range sblock.parsedKeys {
// if server only uses HTTP port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
@@ -772,6 +799,18 @@ func (st *ServerType) serversFromPairings(
}
}
+ // If prefer wildcard is enabled, then we add hosts that are
+ // already covered by the wildcard to the skip list
+ if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" {
+ 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.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
+ }
+ }
+
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
// specifying prefix "https://"
@@ -919,7 +958,10 @@ func (st *ServerType) serversFromPairings(
if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
- srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
+ srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
+ DefaultSNI: defaultSNI,
+ FallbackSNI: fallbackSNI,
+ })
}
// tidy things up a bit
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index c14208b61..abbe8f418 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -452,15 +452,22 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
- if !d.Next() {
- return "", d.ArgErr()
- }
- val := d.Val()
- if d.Next() {
+ val := d.RemainingArgs()
+ if len(val) == 0 {
return "", d.ArgErr()
}
- if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
- return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
+ for _, v := range val {
+ switch v {
+ case "off":
+ case "disable_redirects":
+ case "disable_certs":
+ case "ignore_loaded_certs":
+ case "prefer_wildcard":
+ break
+
+ default:
+ return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
+ }
}
return val, nil
}
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index c6ff81b2b..ed708524d 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -45,8 +45,8 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
- autoHTTPS := "on"
- if ah, ok := options["auto_https"].(string); ok {
+ autoHTTPS := []string{}
+ if ah, ok := options["auto_https"].([]string); ok {
autoHTTPS = ah
}
@@ -54,13 +54,14 @@ func (st ServerType) buildTLSApp(
// 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 autoHTTPS != "off" {
+ 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 {
@@ -350,7 +351,7 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
- if autoHTTPS != "off" && autoHTTPS != "disable_certs" {
+ if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
@@ -417,7 +418,10 @@ func (st ServerType) buildTLSApp(
}
// consolidate automation policies that are the exact same
- tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
+ tlsApp.Automation.Policies = consolidateAutomationPolicies(
+ tlsApp.Automation.Policies,
+ slices.Contains(autoHTTPS, "prefer_wildcard"),
+ )
// ensure automation policies don't overlap subjects (this should be
// an error at provision-time as well, but catch it in the adapt phase
@@ -563,7 +567,7 @@ func newBaseAutomationPolicy(
// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
-func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
+func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*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]) {
@@ -648,6 +652,31 @@ outer:
j--
}
}
+
+ if preferWildcard {
+ // remove subjects from i if they're covered by a wildcard in j
+ iSubjs := aps[i].SubjectsRaw
+ for iSubj := 0; iSubj < len(iSubjs); iSubj++ {
+ for jSubj := range aps[j].SubjectsRaw {
+ if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") {
+ continue
+ }
+ if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) {
+ iSubjs = slices.Delete(iSubjs, iSubj, iSubj+1)
+ iSubj--
+ break
+ }
+ }
+ }
+ aps[i].SubjectsRaw = iSubjs
+
+ // remove i if it has no subjects left
+ if len(aps[i].SubjectsRaw) == 0 {
+ aps = slices.Delete(aps, i, i+1)
+ i--
+ continue outer
+ }
+ }
}
}
diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
new file mode 100644
index 000000000..8880d71ae
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
@@ -0,0 +1,106 @@
+{
+ 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": {
+ "prefer_wildcard": true
+ }
+ }
+ }
+ },
+ "tls": {
+ "automation": {
+ "policies": [
+ {
+ "subjects": [
+ "*.example.com"
+ ],
+ "issuers": [
+ {
+ "challenges": {
+ "dns": {
+ "provider": {
+ "name": "mock"
+ }
+ }
+ },
+ "module": "acme"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest b/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
new file mode 100644
index 000000000..1a9ccea74
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
@@ -0,0 +1,157 @@
+*.example.com {
+ dns mock
+ }
+
+ @foo host foo.example.com
+ handle @foo {
+ respond "Foo!"
+ }
+
+ @bar host bar.example.com
+ handle @bar {
+ respond "Bar!"
+ }
+
+ # Fallback for otherwise unhandled domains
+ handle {
+ abort
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "group": "group3",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Foo!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "host": [
+ "foo.example.com"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "group3",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Bar!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "host": [
+ "bar.example.com"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "group3",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "abort": true,
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ },
+ "tls": {
+ "automation": {
+ "policies": [
+ {
+ "subjects": [
+ "*.example.com"
+ ],
+ "issuers": [
+ {
+ "challenges": {
+ "dns": {
+ "provider": {
+ "name": "mock"
+ }
+ }
+ },
+ "email": "[email protected]",
+ "module": "acme"
+ },
+ {
+ "ca": "https://acme.zerossl.com/v2/DV90",
+ "challenges": {
+ "dns": {
+ "provider": {
+ "name": "mock"
+ }
+ }
+ },
+ "email": "[email protected]",
+ "module": "acme"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/mockdns_test.go b/caddytest/integration/mockdns_test.go
new file mode 100644
index 000000000..1b2efb653
--- /dev/null
+++ b/caddytest/integration/mockdns_test.go
@@ -0,0 +1,61 @@
+package integration
+
+import (
+ "context"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/certmagic"
+ "github.com/libdns/libdns"
+)
+
+func init() {
+ caddy.RegisterModule(MockDNSProvider{})
+}
+
+// MockDNSProvider is a mock DNS provider, for testing config with DNS modules.
+type MockDNSProvider struct{}
+
+// CaddyModule returns the Caddy module information.
+func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "dns.providers.mock",
+ New: func() caddy.Module { return new(MockDNSProvider) },
+ }
+}
+
+// Provision sets up the module.
+func (MockDNSProvider) Provision(ctx caddy.Context) error {
+ return nil
+}
+
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
+func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ return nil
+}
+
+// AppendsRecords appends DNS records to the zone.
+func (MockDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// DeleteRecords deletes DNS records from the zone.
+func (MockDNSProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// GetRecords gets DNS records from the zone.
+func (MockDNSProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// SetRecords sets DNS records in the zone.
+func (MockDNSProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// Interface guard
+var _ caddyfile.Unmarshaler = (*MockDNSProvider)(nil)
+var _ certmagic.DNSProvider = (*MockDNSProvider)(nil)
+var _ caddy.Provisioner = (*MockDNSProvider)(nil)
+var _ caddy.Module = (*MockDNSProvider)(nil)
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go
index 79fdfd6ec..ccb610327 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -65,6 +65,12 @@ type AutoHTTPSConfig struct {
// enabled. To force automated certificate management
// regardless of loaded certificates, set this to true.
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
@@ -157,6 +163,27 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}
}
+ 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)
+ }
+ }
+ }
+
// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
// if there is at least one domain but no TLS conn policy (F&&T), we'll