summaryrefslogtreecommitdiff
path: root/modules/caddytls
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2020-11-16 11:05:55 -0700
committerGitHub <noreply@github.com>2020-11-16 11:05:55 -0700
commit13781e67ab1b2553598d0dd1a7153ce3cdbd4879 (patch)
tree4c53ec6e7ebc051b7d5946a25cd4b276016b698d /modules/caddytls
parent7a3d9d81fe5836894b39d0e218193f7cffd732ff (diff)
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
Diffstat (limited to 'modules/caddytls')
-rw-r--r--modules/caddytls/acmeissuer.go9
-rw-r--r--modules/caddytls/automation.go84
-rw-r--r--modules/caddytls/tls.go20
-rw-r--r--modules/caddytls/zerosslissuer.go40
4 files changed, 85 insertions, 68 deletions
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 <api_key> {
+// ... zerossl [<api_key>] {
// ...
// }
//
// 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 (