summaryrefslogtreecommitdiff
path: root/modules/caddytls/automation.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddytls/automation.go')
-rw-r--r--modules/caddytls/automation.go177
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"`
}