diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index ddec0b941d7..4029259cc06 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -703,9 +703,11 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls 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 + if !automationPolicyHasAllPublicNames(aps[i]) || + automationPolicyCoveredByWildcard(aps[i], aps[i+1:]) { + // remove this empty policy if it has internal names (so auto-https + // can implicitly use the internal issuer), or if every subject is + // covered by a later wildcard policy with managers (#7559) aps = slices.Delete(aps, i, i+1) i-- } @@ -836,6 +838,36 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { }) } +// automationPolicyCoveredByWildcard returns true if every subject in ap is +// covered by a wildcard subject in one of the later policies that has +// managers (get_certificate). +func automationPolicyCoveredByWildcard(ap *caddytls.AutomationPolicy, laterPolicies []*caddytls.AutomationPolicy) bool { + if len(ap.SubjectsRaw) == 0 { + return false + } + for _, subj := range ap.SubjectsRaw { + covered := false + for _, other := range laterPolicies { + if len(other.ManagersRaw) == 0 { + continue + } + for _, otherSubj := range other.SubjectsRaw { + if certmagic.MatchWildcard(subj, otherSubj) { + covered = true + break + } + } + if covered { + break + } + } + if !covered { + return false + } + } + return true +} + func isTailscaleDomain(name string) bool { return strings.HasSuffix(strings.ToLower(name), ".ts.net") } diff --git a/caddyconfig/httpcaddyfile/tlsapp_test.go b/caddyconfig/httpcaddyfile/tlsapp_test.go index d8edbdf9b19..c8c21648ae6 100644 --- a/caddyconfig/httpcaddyfile/tlsapp_test.go +++ b/caddyconfig/httpcaddyfile/tlsapp_test.go @@ -1,6 +1,7 @@ package httpcaddyfile import ( + "encoding/json" "testing" "github.com/caddyserver/caddy/v2/modules/caddytls" @@ -54,3 +55,41 @@ func TestAutomationPolicyIsSubset(t *testing.T) { } } } + +func TestConsolidateAutomationPoliciesWildcardManager(t *testing.T) { + httpManager := json.RawMessage(`{"via":"http"}`) + + for i, test := range []struct { + policies []*caddytls.AutomationPolicy + expect int // expected number of policies after consolidation; -1 means nil + }{ + { + // empty subdomain policy should be removed when covered by + // a wildcard policy with get_certificate (#7559) + policies: []*caddytls.AutomationPolicy{ + {SubjectsRaw: []string{"foo.example.com"}}, + {SubjectsRaw: []string{"*.example.com"}, ManagersRaw: []json.RawMessage{httpManager}}, + }, + expect: 1, + }, + { + // empty policy with no wildcard coverage should be kept + policies: []*caddytls.AutomationPolicy{ + {SubjectsRaw: []string{"example.com"}}, + {SubjectsRaw: []string{"*.other.com"}, ManagersRaw: []json.RawMessage{httpManager}}, + }, + expect: 2, + }, + } { + result := consolidateAutomationPolicies(test.policies) + var got int + if result == nil { + got = -1 + } else { + got = len(result) + } + if got != test.expect { + t.Errorf("Test %d: Expected %d policies but got %d", i, test.expect, got) + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_get_certificate.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_get_certificate.caddyfiletest new file mode 100644 index 00000000000..b9f0f4a7241 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_get_certificate.caddyfiletest @@ -0,0 +1,93 @@ +*.example.com { + tls { + get_certificate http http://localhost:9000/certs + } + respond "Wildcard" +} + +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": "Wildcard", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "*.example.com" + ], + "get_certificate": [ + { + "url": "http://localhost:9000/certs", + "via": "http" + } + ] + } + ] + } + } + } +}