diff options
Diffstat (limited to 'modules/caddytls/automation.go')
-rw-r--r-- | modules/caddytls/automation.go | 177 |
1 files changed, 120 insertions, 57 deletions
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 7f216d5..ee25400 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -19,12 +19,14 @@ import ( "errors" "fmt" "net/http" + "strings" "time" - "github.com/caddyserver/caddy/v2" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" ) // AutomationConfig governs the automated management of TLS certificates. @@ -84,7 +86,14 @@ type AutomationConfig struct { // TLS app to properly provision a new policy. type AutomationPolicy struct { // Which subjects (hostnames or IP addresses) this policy applies to. - Subjects []string `json:"subjects,omitempty"` + // + // This list is a filter, not a command. In other words, it is used + // only to filter whether this policy should apply to a subject that + // needs a certificate; it does NOT command the TLS app to manage a + // certificate for that subject. To have Caddy automate a certificate + // or specific subjects, use the "automate" certificate loader module + // of the TLS app. + SubjectsRaw []string `json:"subjects,omitempty"` // The modules that may issue certificates. Default: internal if all // subjects do not qualify for public certificates; othewise acme and @@ -94,9 +103,11 @@ type AutomationPolicy struct { // Modules that can get a custom certificate to use for any // given TLS handshake at handshake-time. Custom certificates // can be useful if another entity is managing certificates - // and Caddy need only get it and serve it. + // and Caddy need only get it and serve it. Specifying a Manager + // enables on-demand TLS, i.e. it has the side-effect of setting + // the on_demand parameter to `true`. // - // TODO: This is an EXPERIMENTAL feature. It is subject to change or removal. + // TODO: This is an EXPERIMENTAL feature. Subject to change or removal. ManagersRaw []json.RawMessage `json:"get_certificate,omitempty" caddy:"namespace=tls.get_certificate inline_key=via"` // If true, certificates will be requested with MustStaple. Not all @@ -146,12 +157,21 @@ type AutomationPolicy struct { Issuers []certmagic.Issuer `json:"-"` Managers []certmagic.Manager `json:"-"` - magic *certmagic.Config - storage certmagic.Storage + subjects []string + magic *certmagic.Config + storage certmagic.Storage } // Provision sets up ap and builds its underlying CertMagic config. func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { + // replace placeholders in subjects to allow environment variables + repl := caddy.NewReplacer() + subjects := make([]string, len(ap.SubjectsRaw)) + for i, sub := range ap.SubjectsRaw { + subjects[i] = repl.ReplaceAll(sub, "") + } + ap.subjects = subjects + // policy-specific storage implementation if ap.StorageRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "StorageRaw") @@ -165,36 +185,6 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { ap.storage = cmStorage } - // on-demand TLS - var ond *certmagic.OnDemandConfig - if ap.OnDemand { - ond = &certmagic.OnDemandConfig{ - DecisionFunc: func(name string) error { - // if an "ask" endpoint was defined, consult it first - if tlsApp.Automation != nil && - tlsApp.Automation.OnDemand != nil && - tlsApp.Automation.OnDemand.Ask != "" { - if err := onDemandAskRequest(tlsApp.logger, tlsApp.Automation.OnDemand.Ask, name); err != nil { - // distinguish true errors from denials, because it's important to log actual errors - if !errors.Is(err, errAskDenied) { - tlsApp.logger.Error("request to 'ask' endpoint failed", - zap.Error(err), - zap.String("endpoint", tlsApp.Automation.OnDemand.Ask), - zap.String("domain", name)) - } - return err - } - } - // check the rate limiter last because - // doing so makes a reservation - if !onDemandRateLimiter.Allow() { - return fmt.Errorf("on-demand rate limit exceeded") - } - return nil - }, - } - } - // we don't store loaded modules directly in the certmagic config since // policy provisioning may happen more than once (during auto-HTTPS) and // loading a module clears its config bytes; thus, load the module and @@ -251,6 +241,46 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { storage = tlsApp.ctx.Storage() } + // on-demand TLS + var ond *certmagic.OnDemandConfig + if ap.OnDemand || len(ap.Managers) > 0 { + // ask endpoint is now required after a number of negligence cases causing abuse; + // but is still allowed for explicit subjects (non-wildcard, non-unbounded), + // for the internal issuer since it doesn't cause ACME issuer pressure + if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.Ask == "") { + return fmt.Errorf("on-demand TLS cannot be enabled without an 'ask' endpoint to prevent abuse; please refer to documentation for details") + } + ond = &certmagic.OnDemandConfig{ + DecisionFunc: func(name string) error { + if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil { + return nil + } + if err := onDemandAskRequest(tlsApp.logger, tlsApp.Automation.OnDemand.Ask, name); err != nil { + // distinguish true errors from denials, because it's important to elevate actual errors + if errors.Is(err, errAskDenied) { + tlsApp.logger.Debug("certificate issuance denied", + zap.String("ask_endpoint", tlsApp.Automation.OnDemand.Ask), + zap.String("domain", name), + zap.Error(err)) + } else { + tlsApp.logger.Error("request to 'ask' endpoint failed", + zap.String("ask_endpoint", tlsApp.Automation.OnDemand.Ask), + zap.String("domain", name), + zap.Error(err)) + } + return err + } + // check the rate limiter last because + // doing so makes a reservation + if !onDemandRateLimiter.Allow() { + return fmt.Errorf("on-demand rate limit exceeded") + } + return nil + }, + Managers: ap.Managers, + } + } + template := certmagic.Config{ MustStaple: ap.MustStaple, RenewalWindowRatio: ap.RenewalWindowRatio, @@ -261,12 +291,13 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { DisableStapling: ap.DisableOCSPStapling, ResponderOverrides: ap.OCSPOverrides, }, - Storage: storage, - Issuers: issuers, - Managers: ap.Managers, - Logger: tlsApp.logger, + Storage: storage, + Issuers: issuers, + Logger: tlsApp.logger, } - ap.magic = certmagic.New(tlsApp.certCache, template) + certCacheMu.RLock() + ap.magic = certmagic.New(certCache, template) + certCacheMu.RUnlock() // sometimes issuers may need the parent certmagic.Config in // order to function properly (for example, ACMEIssuer needs @@ -282,6 +313,35 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { return nil } +// Subjects returns the list of subjects with all placeholders replaced. +func (ap *AutomationPolicy) Subjects() []string { + return ap.subjects +} + +func (ap *AutomationPolicy) onlyInternalIssuer() bool { + if len(ap.Issuers) != 1 { + return false + } + _, ok := ap.Issuers[0].(*InternalIssuer) + return ok +} + +// isWildcardOrDefault determines if the subjects include any wildcard domains, +// or is the "default" policy (i.e. no subjects) which is unbounded. +func (ap *AutomationPolicy) isWildcardOrDefault() bool { + isWildcardOrDefault := false + if len(ap.subjects) == 0 { + isWildcardOrDefault = true + } + for _, sub := range ap.subjects { + if strings.HasPrefix(sub, "*") { + isWildcardOrDefault = true + break + } + } + return isWildcardOrDefault +} + // DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults. // This function is experimental and has no compatibility promises. func DefaultIssuers() []certmagic.Issuer { @@ -400,29 +460,32 @@ type DNSChallengeConfig struct { // Caddy can "ask" if it should be allowed to manage // certificates for a given hostname. type OnDemandConfig struct { - // An optional rate limit to throttle the - // issuance of certificates from handshakes. - RateLimit *RateLimit `json:"rate_limit,omitempty"` - - // If Caddy needs to obtain or renew a certificate - // during a TLS handshake, it will perform a quick - // HTTP request to this URL to check if it should be - // allowed to try to get a certificate for the name - // in the "domain" query string parameter, like so: - // `?domain=example.com`. The endpoint must return a - // 200 OK status if a certificate is allowed; - // anything else will cause it to be denied. + // REQUIRED. If Caddy needs to load a certificate from + // storage or obtain/renew a certificate during a TLS + // handshake, it will perform a quick HTTP request to + // this URL to check if it should be allowed to try to + // get a certificate for the name in the "domain" query + // string parameter, like so: `?domain=example.com`. + // The endpoint must return a 200 OK status if a certificate + // is allowed; anything else will cause it to be denied. // Redirects are not followed. Ask string `json:"ask,omitempty"` + + // DEPRECATED. An optional rate limit to throttle + // the checking of storage and the issuance of + // certificates from handshakes if not already in + // storage. WILL BE REMOVED IN A FUTURE RELEASE. + RateLimit *RateLimit `json:"rate_limit,omitempty"` } -// RateLimit specifies an interval with optional burst size. +// DEPRECATED. RateLimit specifies an interval with optional burst size. type RateLimit struct { - // A duration value. A certificate may be obtained 'burst' - // times during this interval. + // A duration value. Storage may be checked and a certificate may be + // obtained 'burst' times during this interval. Interval caddy.Duration `json:"interval,omitempty"` - // How many times during an interval a certificate can be obtained. + // How many times during an interval storage can be checked or a + // certificate can be obtained. Burst int `json:"burst,omitempty"` } |