From 13781e67ab1b2553598d0dd1a7153ce3cdbd4879 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Mon, 16 Nov 2020 11:05:55 -0700 Subject: caddytls: Support multiple issuers (#3862) * caddytls: Support multiple issuers Defaults are Let's Encrypt and ZeroSSL. There are probably bugs. * Commit updated integration tests, d'oh * Update go.mod --- caddyconfig/httpcaddyfile/builtins.go | 43 ++--- caddyconfig/httpcaddyfile/tlsapp.go | 212 ++++++++++++--------- .../integration/caddyfile_adapt/global_options.txt | 8 +- .../caddyfile_adapt/global_options_acme.txt | 26 +-- .../caddyfile_adapt/global_options_admin.txt | 8 +- .../caddyfile_adapt/tls_automation_policies.txt | 24 ++- go.mod | 2 +- go.sum | 4 +- modules/caddyhttp/autohttps.go | 61 ++++-- modules/caddytls/acmeissuer.go | 9 + modules/caddytls/automation.go | 84 ++++---- modules/caddytls/tls.go | 20 +- modules/caddytls/zerosslissuer.go | 40 ++-- 13 files changed, 307 insertions(+), 234 deletions(-) diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 5ba35aa..f8b5e2c 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -88,7 +88,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var certSelector caddytls.CustomCertSelectionPolicy var acmeIssuer *caddytls.ACMEIssuer var internalIssuer *caddytls.InternalIssuer - var issuer certmagic.Issuer + var issuers []certmagic.Issuer var onDemand bool for h.Next() { @@ -297,10 +297,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if err != nil { return nil, err } - issuer, ok = unm.(certmagic.Issuer) + issuer, ok := unm.(certmagic.Issuer) if !ok { return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID) } + issuers = append(issuers, issuer) case "dns": if !h.NextArg() { @@ -371,42 +372,28 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } - // issuer - if acmeIssuer != nil && internalIssuer != nil { - // the logic to support this would be complex - return nil, h.Err("cannot use both ACME and internal issuers in same server block") + if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) { + // some tls subdirectives are shortcuts that implicitly configure issuers, and the + // user can also configure issuers explicitly using the issuer subdirective; the + // logic to support both would likely be complex, or at least unintuitive + return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)") } - if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) { - // similarly, the logic to support this would be complex - return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives") - } - switch { - case issuer != nil: + for _, issuer := range issuers { configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", Value: issuer, }) - - case internalIssuer != nil: + } + if acmeIssuer != nil { configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", - Value: internalIssuer, + Value: disambiguateACMEIssuer(acmeIssuer), }) - - case acmeIssuer != nil: - // fill in global defaults, if configured - if email := h.Option("email"); email != nil && acmeIssuer.Email == "" { - acmeIssuer.Email = email.(string) - } - if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" { - acmeIssuer.CA = acmeCA.(string) - } - if caPemFile := h.Option("acme_ca_root"); caPemFile != nil { - acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string)) - } + } + if internalIssuer != nil { configVals = append(configVals, ConfigValue{ Class: "tls.cert_issuer", - Value: disambiguateACMEIssuer(acmeIssuer), + Value: internalIssuer, }) } diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 0ac862e..fe4c1b1 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -110,47 +110,43 @@ func (st ServerType) buildTLSApp( // certificate issuers if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok { + var issuers []certmagic.Issuer for _, issuerVal := range issuerVals { - issuer := issuerVal.Value.(certmagic.Issuer) - if ap == catchAllAP && !reflect.DeepEqual(ap.Issuer, issuer) { - 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.Issuer, issuer) - } - ap.Issuer = issuer + ap.Issuers = append(ap.Issuers, issuerVal.Value.(certmagic.Issuer)) + } + if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) { + 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) } } // custom bind host for _, cfgVal := range sblock.pile["bind"] { - // if an issuer was already configured and it is NOT an ACME - // issuer, skip, since we intend to adjust only ACME issuers - var acmeIssuer *caddytls.ACMEIssuer - if ap.Issuer != nil { - // ensure we include any issuer that embeds/wraps an underlying ACME issuer - type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } - if acmeWrapper, ok := ap.Issuer.(acmeCapable); ok { + 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() - } else { - break } - } + if acmeIssuer == nil { + continue + } - // proceed to configure the ACME issuer's bind host, without - // overwriting any existing settings - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.BindHost == "" { - // only binding to one host is supported - var bindHost string - if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 { - bindHost = bindHosts[0] + // 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 bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 { + bindHost = bindHosts[0] + } + acmeIssuer.Challenges.BindHost = bindHost } - acmeIssuer.Challenges.BindHost = bindHost } - ap.Issuer = acmeIssuer // we'll encode it later } // first make sure this block is allowed to create an automation policy; @@ -188,7 +184,7 @@ func (st ServerType) buildTLSApp( // 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 ap.Issuer == nil { + if len(ap.Issuers) == 0 { var internal, external []string for _, s := range ap.Subjects { if !certmagic.SubjectQualifiesForCert(s) { @@ -212,7 +208,7 @@ func (st ServerType) buildTLSApp( apCopy := *ap ap2 = &apCopy ap2.Subjects = internal - ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings) + ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)} } } if tlsApp.Automation == nil { @@ -277,7 +273,7 @@ func (st ServerType) buildTLSApp( // get internal certificates by default rather than ACME var al caddytls.AutomateLoader internalAP := &caddytls.AutomationPolicy{ - IssuerRaw: json.RawMessage(`{"module":"internal"}`), + IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, } for h := range hostsSharedWithHostlessKey { al = append(al, h) @@ -295,14 +291,48 @@ func (st ServerType) buildTLSApp( 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"] + globalACMEEAB := options["acme_eab"] + hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil + if hasGlobalACMEDefaults { + for _, ap := range tlsApp.Automation.Policies { + if len(ap.Issuers) == 0 { + acme, zerosslACME := new(caddytls.ACMEIssuer), new(caddytls.ACMEIssuer) + zerossl := &caddytls.ZeroSSLIssuer{ACMEIssuer: zerosslACME} + ap.Issuers = []certmagic.Issuer{acme, zerossl} // TODO: keep this in sync with Caddy's other issuer defaults elsewhere, like in caddytls/automation.go (DefaultIssuers). + + // if a non-ZeroSSL endpoint is specified, we assume we can't use the ZeroSSL issuer successfully + if globalACMECA != nil && !strings.Contains(globalACMECA.(string), "zerossl") { + ap.Issuers = []certmagic.Issuer{acme} + } + } + } + } + } + // finalize and verify policies; do cleanup if tlsApp.Automation != nil { - // encode any issuer values we created, so they will be rendered in the output - for _, ap := range tlsApp.Automation.Policies { - if ap.Issuer != nil && ap.IssuerRaw == nil { - // encode issuer now that it's all set up - issuerName := ap.Issuer.(caddy.Module).CaddyModule().ID.Name() - ap.IssuerRaw = caddyconfig.JSONModuleObject(ap.Issuer, "module", issuerName, &warnings) + 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)) + } } } @@ -334,6 +364,51 @@ func (st ServerType) buildTLSApp( return tlsApp, warnings, nil } +type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } + +func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) 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 := options["acme_dns"] + globalACMEEAB := options["acme_eab"] + + if globalEmail != nil && acmeIssuer.Email == "" { + acmeIssuer.Email = globalEmail.(string) + } + if globalACMECA != nil && acmeIssuer.CA == "" { + acmeIssuer.CA = globalACMECA.(string) + } + if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) { + acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) + } + if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) { + provName := globalACMEDNS.(string) + dnsProvModule, err := caddy.GetModule("dns.providers." + provName) + if err != nil { + return fmt.Errorf("getting DNS provider module named '%s': %v", provName, err) + } + acmeIssuer.Challenges = &caddytls.ChallengesConfig{ + DNS: &caddytls.DNSChallengeConfig{ + ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, nil), + }, + } + } + if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil { + acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB) + } + 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 @@ -341,17 +416,10 @@ func (st ServerType) buildTLSApp( // true, a non-nil value will always be returned (unless there is an error). func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) { issuer, hasIssuer := options["cert_issuer"] - - acmeCA, hasACMECA := options["acme_ca"] - acmeCARoot, hasACMECARoot := options["acme_ca_root"] - acmeDNS, hasACMEDNS := options["acme_dns"] - acmeEAB, hasACMEEAB := options["acme_eab"] - - email, hasEmail := options["email"] - localCerts, hasLocalCerts := options["local_certs"] + _, hasLocalCerts := options["local_certs"] keyType, hasKeyType := options["key_type"] - hasGlobalAutomationOpts := hasIssuer || hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType + hasGlobalAutomationOpts := hasIssuer || hasLocalCerts || hasKeyType // if there are no global options related to automation policies // set, then we can just return right away @@ -363,48 +431,18 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon } ap := new(caddytls.AutomationPolicy) - if keyType != nil { + if hasKeyType { ap.KeyType = keyType.(string) } + if hasIssuer && 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 hasIssuer { - if hasACMECA || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts { - return nil, fmt.Errorf("global options are ambiguous: cert_issuer is confusing when combined with acme_*, email, or local_certs options") - } - ap.Issuer = issuer.(certmagic.Issuer) - } else if localCerts != nil { - // internal issuer enabled trumps any ACME configurations; useful in testing - ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later - } else { - if acmeCA == nil { - acmeCA = "" - } - if email == nil { - email = "" - } - mgr := &caddytls.ACMEIssuer{ - CA: acmeCA.(string), - Email: email.(string), - } - if acmeDNS != nil { - provName := acmeDNS.(string) - dnsProvModule, err := caddy.GetModule("dns.providers." + provName) - if err != nil { - return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err) - } - mgr.Challenges = &caddytls.ChallengesConfig{ - DNS: &caddytls.DNSChallengeConfig{ - ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings), - }, - } - } - if acmeCARoot != nil { - mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)} - } - if acmeEAB != nil { - mgr.ExternalAccount = acmeEAB.(*acme.EAB) - } - ap.Issuer = disambiguateACMEIssuer(mgr) // we'll encode it later + ap.Issuers = []certmagic.Issuer{issuer.(certmagic.Issuer)} + } else if hasLocalCerts { + ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)} } return ap, nil @@ -463,7 +501,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls // 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 bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) && + if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && aps[i].MustStaple == aps[j].MustStaple && aps[i].KeyType == aps[j].KeyType && diff --git a/caddytest/integration/caddyfile_adapt/global_options.txt b/caddytest/integration/caddyfile_adapt/global_options.txt index 6562824..a0a7f0b 100644 --- a/caddytest/integration/caddyfile_adapt/global_options.txt +++ b/caddytest/integration/caddyfile_adapt/global_options.txt @@ -54,9 +54,11 @@ "automation": { "policies": [ { - "issuer": { - "module": "internal" - }, + "issuers": [ + { + "module": "internal" + } + ], "key_type": "ed25519" } ], diff --git a/caddytest/integration/caddyfile_adapt/global_options_acme.txt b/caddytest/integration/caddyfile_adapt/global_options_acme.txt index 500a723..0f1d78f 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_acme.txt +++ b/caddytest/integration/caddyfile_adapt/global_options_acme.txt @@ -57,18 +57,20 @@ "automation": { "policies": [ { - "issuer": { - "ca": "https://example.com", - "email": "test@example.com", - "external_account": { - "key_id": "4K2scIVbBpNd-78scadB2g", - "mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh" - }, - "module": "acme", - "trusted_roots_pem_files": [ - "/path/to/ca.crt" - ] - }, + "issuers": [ + { + "ca": "https://example.com", + "email": "test@example.com", + "external_account": { + "key_id": "4K2scIVbBpNd-78scadB2g", + "mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh" + }, + "module": "acme", + "trusted_roots_pem_files": [ + "/path/to/ca.crt" + ] + } + ], "key_type": "ed25519" } ], diff --git a/caddytest/integration/caddyfile_adapt/global_options_admin.txt b/caddytest/integration/caddyfile_adapt/global_options_admin.txt index 9d0fb27..67cf5ad 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_admin.txt +++ b/caddytest/integration/caddyfile_adapt/global_options_admin.txt @@ -62,9 +62,11 @@ "automation": { "policies": [ { - "issuer": { - "module": "internal" - }, + "issuers": [ + { + "module": "internal" + } + ], "key_type": "ed25519" } ], diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies.txt index 0a90e4a..c3fd489 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_policies.txt +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies.txt @@ -54,24 +54,30 @@ foo.tld, www.foo.tld { "foo.tld", "www.foo.tld" ], - "issuer": { - "module": "internal" - } + "issuers": [ + { + "module": "internal" + } + ] }, { "subjects": [ "*.*.tld", "*.tld" ], - "issuer": { - "module": "internal" - }, + "issuers": [ + { + "module": "internal" + } + ], "on_demand": true }, { - "issuer": { - "module": "internal" - } + "issuers": [ + { + "module": "internal" + } + ] } ] } diff --git a/go.mod b/go.mod index 8a0d4f2..3e55d38 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.1.0 github.com/alecthomas/chroma v0.8.0 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a - github.com/caddyserver/certmagic v0.12.1-0.20201112220015-12df2be5a021 + github.com/caddyserver/certmagic v0.12.1-0.20201116175341-0f8a9f688760 github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/go-chi/chi v4.1.2+incompatible github.com/google/cel-go v0.5.1 diff --git a/go.sum b/go.sum index 294eccc..1070c73 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,8 @@ github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTK github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/caddyserver/certmagic v0.12.1-0.20201112220015-12df2be5a021 h1:VY4JahIicW/GT84KoIm+TSKwMHj08NyPJhsGypTWJn4= -github.com/caddyserver/certmagic v0.12.1-0.20201112220015-12df2be5a021/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U= +github.com/caddyserver/certmagic v0.12.1-0.20201116175341-0f8a9f688760 h1:h7KGtOb9TAfZp2/KwPd9iyqiLVZMWbpx5Mu0Her2iRw= +github.com/caddyserver/certmagic v0.12.1-0.20201116175341-0f8a9f688760/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 0780981..805a37c 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -241,7 +241,7 @@ 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; // turn the set into a slice so that phase 2 can use it app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) - var internal, external []string + var internal []string uniqueDomainsLoop: for d := range uniqueDomainsForCerts { // whether or not there is already an automation policy for this @@ -264,15 +264,13 @@ uniqueDomainsLoop: // if no automation policy exists for the name yet, we // will associate it with an implicit one - if certmagic.SubjectQualifiesForPublicCert(d) { - external = append(external, d) - } else { + if !certmagic.SubjectQualifiesForPublicCert(d) { internal = append(internal, d) } } // ensure there is an automation policy to handle these certs - err := app.createAutomationPolicies(ctx, external, internal) + err := app.createAutomationPolicies(ctx, internal) if err != nil { return err } @@ -430,7 +428,7 @@ redirServersLoop: // automation policy exists, it will be shallow-copied and used as the // base for the new ones (this is important for preserving behavior the // user intends to be "defaults"). -func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error { +func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []string) error { // before we begin, loop through the existing automation policies // and, for any ACMEIssuers we find, make sure they're filled in // with default values that might be specified in our HTTP app; also @@ -447,16 +445,23 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna // set up default issuer -- honestly, this is only // really necessary because the HTTP app is opinionated // and has settings which could be inferred as new - // defaults for the ACMEIssuer in the TLS app - if ap.Issuer == nil { - ap.Issuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer, ok := ap.Issuer.(acmeCapable); ok { - err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer()) + // defaults for the ACMEIssuer in the TLS app (such as + // what the HTTP and HTTPS ports are) + if ap.Issuers == nil { + var err error + ap.Issuers, err = caddytls.DefaultIssuers(ctx) if err != nil { return err } } + for _, iss := range ap.Issuers { + if acmeIssuer, ok := iss.(acmeCapable); ok { + err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer()) + if err != nil { + return err + } + } + } // while we're here, is this the catch-all/base policy? if !foundBasePolicy && len(ap.Subjects) == 0 { @@ -471,11 +476,14 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna } // if the basePolicy has an existing ACMEIssuer (particularly to - // include any type that embeds/wraps an ACMEIssuer), let's use it, - // otherwise we'll make one + // include any type that embeds/wraps an ACMEIssuer), let's use it + // (I guess we just use the first one?), otherwise we'll make one var baseACMEIssuer *caddytls.ACMEIssuer - if acmeWrapper, ok := basePolicy.Issuer.(acmeCapable); ok { - baseACMEIssuer = acmeWrapper.GetACMEIssuer() + for _, iss := range basePolicy.Issuers { + if acmeWrapper, ok := iss.(acmeCapable); ok { + baseACMEIssuer = acmeWrapper.GetACMEIssuer() + break + } } if baseACMEIssuer == nil { // note that this happens if basePolicy.Issuer is nil @@ -485,7 +493,7 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna // if there was a base policy to begin with, we already // filled in its issuer's defaults; if there wasn't, we - // stil need to do that + // still need to do that if !foundBasePolicy { err := app.fillInACMEIssuer(baseACMEIssuer) if err != nil { @@ -494,8 +502,20 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna } // never overwrite any other issuer that might already be configured - if basePolicy.Issuer == nil { - basePolicy.Issuer = baseACMEIssuer + if basePolicy.Issuers == nil { + var err error + basePolicy.Issuers, err = caddytls.DefaultIssuers(ctx) + if err != nil { + return err + } + for _, iss := range basePolicy.Issuers { + if acmeIssuer, ok := iss.(acmeCapable); ok { + err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer()) + if err != nil { + return err + } + } + } } if !foundBasePolicy { @@ -549,8 +569,7 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna // of names that would normally use the production API; // anyway, that gets into the weeds a bit... newPolicy.Subjects = internalNames - newPolicy.Issuer = internalIssuer - + newPolicy.Issuers = []certmagic.Issuer{internalIssuer} err := app.tlsApp.AddAutomationPolicy(newPolicy) if err != nil { return err diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 6466229..7c79c7e 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -97,6 +97,15 @@ func (ACMEIssuer) CaddyModule() caddy.ModuleInfo { func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.logger = ctx.Logger(iss) + // expand email address, if non-empty + if iss.Email != "" { + email, err := caddy.NewReplacer().ReplaceOrErr(iss.Email, true, true) + if err != nil { + return fmt.Errorf("expanding email address '%s': %v", iss.Email, err) + } + iss.Email = email + } + // DNS providers if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil { val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 1612391..509ad6e 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -23,7 +23,6 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" - "go.uber.org/zap" ) // AutomationConfig governs the automated management of TLS certificates. @@ -72,8 +71,13 @@ type AutomationPolicy struct { // Which subjects (hostnames or IP addresses) this policy applies to. Subjects []string `json:"subjects,omitempty"` - // The module that will issue certificates. Default: internal if all - // subjects do not qualify for public certificates; othewise acme. + // The modules that may issue certificates. Default: internal if all + // subjects do not qualify for public certificates; othewise acme and + // zerossl. + IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"` + + // DEPRECATED: Use `issuers` instead (November 2020). This field will + // be removed in the future. IssuerRaw json.RawMessage `json:"issuer,omitempty" caddy:"namespace=tls.issuance inline_key=module"` // If true, certificates will be requested with MustStaple. Not all @@ -103,10 +107,10 @@ type AutomationPolicy struct { // load. OnDemand bool `json:"on_demand,omitempty"` - // Issuer stores the decoded issuer parameters. This is only - // used to populate an underlying certmagic.Config's Issuer + // Issuers stores the decoded issuer parameters. This is only + // used to populate an underlying certmagic.Config's Issuers // field; it is not referenced thereafter. - Issuer certmagic.Issuer `json:"-"` + Issuers []certmagic.Issuer `json:"-"` magic *certmagic.Config storage certmagic.Storage @@ -150,34 +154,30 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { } } - // if this automation policy has no Issuer defined, and - // none of the subjects qualify for a public certificate, - // set the issuer to internal so that these names can all - // get certificates; critically, we can only do this if an - // issuer is not explicitly configured (IssuerRaw, vs. just - // Issuer) AND if the list of subjects is non-empty - if ap.IssuerRaw == nil && len(ap.Subjects) > 0 { - var anyPublic bool - for _, s := range ap.Subjects { - if certmagic.SubjectQualifiesForPublicCert(s) { - anyPublic = true - break - } + // TODO: IssuerRaw field deprecated as of November 2020 - remove this shim after deprecation is complete + if ap.IssuerRaw != nil { + tlsApp.logger.Warn("the 'issuer' field is deprecated and will be removed in the future; use 'issuers' instead; your issuer has been appended automatically for now") + ap.IssuersRaw = append(ap.IssuersRaw, ap.IssuerRaw) + } + + // load and provision any explicitly-configured issuer modules + if ap.IssuersRaw != nil { + val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw") + if err != nil { + return fmt.Errorf("loading TLS automation management module: %s", err) } - if !anyPublic { - tlsApp.logger.Info("setting internal issuer for automation policy that has only internal subjects but no issuer configured", - zap.Strings("subjects", ap.Subjects)) - ap.IssuerRaw = json.RawMessage(`{"module":"internal"}`) + for _, issVal := range val.([]interface{}) { + ap.Issuers = append(ap.Issuers, issVal.(certmagic.Issuer)) } } - // load and provision any explicitly-configured issuer module - if ap.IssuerRaw != nil { - val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw") + issuers := ap.Issuers + if len(issuers) == 0 { + var err error + issuers, err = DefaultIssuers(tlsApp.ctx) if err != nil { - return fmt.Errorf("loading TLS automation management module: %s", err) + return err } - ap.Issuer = val.(certmagic.Issuer) } keyType := ap.KeyType @@ -206,12 +206,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { KeySource: keySource, OnDemand: ond, Storage: storage, - Issuer: ap.Issuer, // if nil, certmagic.New() will create one + Issuers: issuers, Logger: tlsApp.logger, } - if rev, ok := ap.Issuer.(certmagic.Revoker); ok { - template.Revoker = rev - } ap.magic = certmagic.New(tlsApp.certCache, template) // sometimes issuers may need the parent certmagic.Config in @@ -219,13 +216,32 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // access to the correct storage and cache so it can solve // ACME challenges -- it's an annoying, inelegant circular // dependency that I don't know how to resolve nicely!) - if annoying, ok := ap.Issuer.(ConfigSetter); ok { - annoying.SetConfig(ap.magic) + for _, issuer := range ap.magic.Issuers { + if annoying, ok := issuer.(ConfigSetter); ok { + annoying.SetConfig(ap.magic) + } } return nil } +// DefaultIssuers returns empty but provisioned default Issuers. +// This function is experimental and has no compatibility promises. +func DefaultIssuers(ctx caddy.Context) ([]certmagic.Issuer, error) { + acme := new(ACMEIssuer) + err := acme.Provision(ctx) + if err != nil { + return nil, err + } + zerossl := new(ZeroSSLIssuer) + err = zerossl.Provision(ctx) + if err != nil { + return nil, err + } + // TODO: eventually, insert ZeroSSL into first position in the slice -- see also httpcaddyfile/tlsapp.go for where similar defaults are configured + return []certmagic.Issuer{acme, zerossl}, nil +} + // ChallengesConfig configures the ACME challenges. type ChallengesConfig struct { // HTTP configures the ACME HTTP challenge. This diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 12d25ad..146eed4 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -137,7 +137,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { continue } t.Automation.defaultInternalAutomationPolicy = &AutomationPolicy{ - IssuerRaw: json.RawMessage(`{"module":"internal"}`), + IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, } err = t.Automation.defaultInternalAutomationPolicy.Provision(t) if err != nil { @@ -303,20 +303,22 @@ func (t *TLS) Manage(names []string) error { // HandleHTTPChallenge ensures that the HTTP challenge is handled for the // certificate named by r.Host, if it is an HTTP challenge request. It -// requires that the automation policy for r.Host has an issue of type -// *certmagic.ACMEManager. +// requires that the automation policy for r.Host has an issuer of type +// *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()). func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { if !certmagic.LooksLikeHTTPChallenge(r) { return false } + // try all the issuers until we find the one that initiated the challenge ap := t.getAutomationPolicyForName(r.Host) - if ap.magic.Issuer == nil { - return false - } type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer } - if am, ok := ap.magic.Issuer.(acmeCapable); ok { - iss := am.GetACMEIssuer() - return certmagic.NewACMEManager(iss.magic, iss.template).HandleHTTPChallenge(w, r) + for _, iss := range ap.magic.Issuers { + if am, ok := iss.(acmeCapable); ok { + iss := am.GetACMEIssuer() + if certmagic.NewACMEManager(iss.magic, iss.template).HandleHTTPChallenge(w, r) { + return true + } + } } return false } diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go index d0f4950..4680d1b 100644 --- a/modules/caddytls/zerosslissuer.go +++ b/modules/caddytls/zerosslissuer.go @@ -59,16 +59,13 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo { // Provision sets up iss. func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error { iss.logger = ctx.Logger(iss) - if iss.ACMEIssuer == nil { iss.ACMEIssuer = new(ACMEIssuer) } - err := iss.ACMEIssuer.Provision(ctx) - if err != nil { - return err + if iss.ACMEIssuer.CA == "" { + iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA } - - return nil + return iss.ACMEIssuer.Provision(ctx) } func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, am *certmagic.ACMEManager, _ acme.Account) error { @@ -86,26 +83,22 @@ func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context) (*acme.EAB // there are two ways to generate EAB credentials: authenticated with // their API key, or unauthenticated with their email address - switch { - case iss.APIKey != "": + if iss.APIKey != "" { apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "") if apiKey == "" { return nil, fmt.Errorf("missing API key: '%v'", iss.APIKey) } qs := url.Values{"access_key": []string{apiKey}} endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode()) - - case iss.Email != "": - email := caddy.NewReplacer().ReplaceAll(iss.Email, "") + } else { + email := iss.Email if email == "" { - return nil, fmt.Errorf("missing email: '%v'", iss.Email) + iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time") + email = "caddy@zerossl.com" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well } endpoint = zerosslAPIBase + "/eab-credentials-email" form := url.Values{"email": []string{email}} body = strings.NewReader(form.Encode()) - - default: - return nil, fmt.Errorf("must configure either an API key or email address to use ZeroSSL without explicit EAB") } req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) @@ -161,9 +154,6 @@ func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context) (*acme.EAB func (iss *ZeroSSLIssuer) initialize() { iss.mu.Lock() defer iss.mu.Unlock() - if iss.template.CA == "" { - iss.template.CA = zerosslACMEDirectory - } if iss.template.NewAccountFunc == nil { iss.template.NewAccountFunc = iss.newAccountCallback } @@ -195,15 +185,18 @@ func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.Certificate // UnmarshalCaddyfile deserializes Caddyfile tokens into iss. // -// ... zerossl { +// ... zerossl [] { // ... // } // // Any of the subdirectives for the ACME issuer can be used in the block. func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { - if !d.AllArgs(&iss.APIKey) { - return d.ArgErr() + if d.NextArg() { + iss.APIKey = d.Val() + if d.NextArg() { + return d.ArgErr() + } } if iss.ACMEIssuer == nil { @@ -217,10 +210,7 @@ func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -const ( - zerosslACMEDirectory = "https://acme.zerossl.com/v2/DV90" - zerosslAPIBase = "https://api.zerossl.com/acme" -) +const zerosslAPIBase = "https://api.zerossl.com/acme" // Interface guards var ( -- cgit v1.2.3