summaryrefslogtreecommitdiff
path: root/modules/caddytls
diff options
context:
space:
mode:
authorTom Barrett <tom@tombarrett.xyz>2023-11-01 17:57:48 +0100
committerTom Barrett <tom@tombarrett.xyz>2023-11-01 18:11:33 +0100
commit240c3d1338415e5d82ef7ca0e52c4284be6441bd (patch)
tree4b0ee5d208c2cdffa78d65f1b0abe0ec85f15652 /modules/caddytls
parent73e78ab226f21e6c6c68961af88c4ab9c746f4f4 (diff)
parent0e204b730aa2b1fa0835336b1117eff8c420f713 (diff)
vbump to v2.7.5HEADcaddy-cgi
Diffstat (limited to 'modules/caddytls')
-rw-r--r--modules/caddytls/acmeissuer.go7
-rw-r--r--modules/caddytls/automation.go177
-rw-r--r--modules/caddytls/certmanagers.go20
-rw-r--r--modules/caddytls/certselection.go3
-rw-r--r--modules/caddytls/cf.go24
-rw-r--r--modules/caddytls/connpolicy.go22
-rw-r--r--modules/caddytls/distributedstek/distributedstek.go3
-rw-r--r--modules/caddytls/internalissuer.go7
-rw-r--r--modules/caddytls/matchers.go3
-rw-r--r--modules/caddytls/storageloader.go3
-rw-r--r--modules/caddytls/tls.go136
-rw-r--r--modules/caddytls/zerosslissuer.go5
12 files changed, 295 insertions, 115 deletions
diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go
index ca79981..5e79c2d 100644
--- a/modules/caddytls/acmeissuer.go
+++ b/modules/caddytls/acmeissuer.go
@@ -24,13 +24,14 @@ import (
"strconv"
"time"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig"
- "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {
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"`
}
diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go
index 1b701ab..ad26468 100644
--- a/modules/caddytls/certmanagers.go
+++ b/modules/caddytls/certmanagers.go
@@ -9,11 +9,12 @@ import (
"net/url"
"strings"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/certmagic"
"github.com/tailscale/tscert"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {
@@ -23,14 +24,6 @@ func init() {
// Tailscale is a module that can get certificates from the local Tailscale process.
type Tailscale struct {
- // If true, this module will operate in "best-effort" mode and
- // ignore "soft" errors; i.e. try Tailscale, and if it doesn't connect
- // or return a certificate, oh well. Failure to connect to Tailscale
- // results in a no-op instead of an error. Intended for the use case
- // where this module is added implicitly for convenience, even if
- // Tailscale isn't necessarily running.
- Optional bool `json:"optional,omitempty"`
-
logger *zap.Logger
}
@@ -60,16 +53,11 @@ func (ts Tailscale) GetCertificate(ctx context.Context, hello *tls.ClientHelloIn
// canHazCertificate returns true if Tailscale reports it can get a certificate for the given ClientHello.
func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (bool, error) {
- if ts.Optional && !strings.HasSuffix(strings.ToLower(hello.ServerName), tailscaleDomainAliasEnding) {
+ if !strings.HasSuffix(strings.ToLower(hello.ServerName), tailscaleDomainAliasEnding) {
return false, nil
}
status, err := tscert.GetStatus(ctx)
if err != nil {
- if ts.Optional {
- // ignore error if we don't expect/require it to work anyway, but log it for debugging
- ts.logger.Debug("error getting tailscale status", zap.Error(err), zap.String("server_name", hello.ServerName))
- return false, nil
- }
return false, err
}
for _, domain := range status.CertDomains {
diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go
index 0311f11..1bef890 100644
--- a/modules/caddytls/certselection.go
+++ b/modules/caddytls/certselection.go
@@ -58,7 +58,8 @@ nextChoice:
if len(p.SerialNumber) > 0 {
var found bool
for _, sn := range p.SerialNumber {
- if cert.Leaf.SerialNumber.Cmp(&sn.Int) == 0 {
+ snInt := sn.Int // avoid taking address of iteration variable (gosec warning)
+ if cert.Leaf.SerialNumber.Cmp(&snInt) == 0 {
found = true
break
}
diff --git a/modules/caddytls/cf.go b/modules/caddytls/cf.go
new file mode 100644
index 0000000..e61a59c
--- /dev/null
+++ b/modules/caddytls/cf.go
@@ -0,0 +1,24 @@
+//go:build cfgo
+
+package caddytls
+
+// This file adds support for X25519Kyber768Draft00, a post-quantum
+// key agreement that is currently being rolled out by Chrome [1]
+// and Cloudflare [2,3]. For more context, see the PR [4].
+//
+// [1] https://blog.chromium.org/2023/08/protecting-chrome-traffic-with-hybrid.html
+// [2] https://blog.cloudflare.com/post-quantum-for-all/
+// [3] https://blog.cloudflare.com/post-quantum-to-origins/
+// [4] https://github.com/caddyserver/caddy/pull/5852
+
+import (
+ "crypto/tls"
+)
+
+func init() {
+ SupportedCurves["X25519Kyber768Draft00"] = tls.X25519Kyber768Draft00
+ defaultCurves = append(
+ []tls.CurveID{tls.X25519Kyber768Draft00},
+ defaultCurves...,
+ )
+}
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index bce69bc..64fdd51 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -25,9 +25,10 @@ import (
"path/filepath"
"strings"
- "github.com/caddyserver/caddy/v2"
"github.com/mholt/acmez"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
)
func init() {
@@ -54,7 +55,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
}
// enable HTTP/2 by default
- if len(pol.ALPN) == 0 {
+ if pol.ALPN == nil {
pol.ALPN = append(pol.ALPN, defaultALPN...)
}
@@ -159,6 +160,18 @@ type ConnectionPolicy struct {
// is no policy configured for the empty SNI value.
DefaultSNI string `json:"default_sni,omitempty"`
+ // FallbackSNI becomes the ServerName in a ClientHello if
+ // the original ServerName doesn't match any certificates
+ // in the cache. The use cases for this are very niche;
+ // typically if a client is a CDN and passes through the
+ // ServerName of the downstream handshake but can accept
+ // a certificate with the origin's hostname instead, then
+ // you would set this to your origin's hostname. Note that
+ // Caddy must be managing a certificate for this name.
+ //
+ // This feature is EXPERIMENTAL and subject to change or removal.
+ FallbackSNI string `json:"fallback_sni,omitempty"`
+
// Also known as "SSLKEYLOGFILE", TLS secrets will be written to
// this file in NSS key log format which can then be parsed by
// Wireshark and other tools. This is INSECURE as it allows other
@@ -216,6 +229,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
cfg.CertSelection = p.CertSelection
}
cfg.DefaultServerName = p.DefaultSNI
+ cfg.FallbackServerName = p.FallbackSNI
return cfg.GetCertificate(hello)
},
MinVersion: tls.VersionTLS12,
@@ -270,7 +284,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
break
}
}
- if !alpnFound {
+ if !alpnFound && (cfg.NextProtos == nil || len(cfg.NextProtos) > 0) {
cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol)
}
@@ -303,7 +317,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
return err
}
logFile, _, err := secretsLogPool.LoadOrNew(filename, func() (caddy.Destructor, error) {
- w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
+ w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
return destructableWriter{w}, err
})
if err != nil {
diff --git a/modules/caddytls/distributedstek/distributedstek.go b/modules/caddytls/distributedstek/distributedstek.go
index 18ed694..f6d0de0 100644
--- a/modules/caddytls/distributedstek/distributedstek.go
+++ b/modules/caddytls/distributedstek/distributedstek.go
@@ -33,9 +33,10 @@ import (
"runtime/debug"
"time"
+ "github.com/caddyserver/certmagic"
+
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
- "github.com/caddyserver/certmagic"
)
func init() {
diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go
index 3dd6c35..1cf2461 100644
--- a/modules/caddytls/internalissuer.go
+++ b/modules/caddytls/internalissuer.go
@@ -21,12 +21,13 @@ import (
"encoding/pem"
"time"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
- "github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/certmagic"
"github.com/smallstep/certificates/authority/provisioner"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddypki"
)
func init() {
diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index f541220..af1f898 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -21,9 +21,10 @@ import (
"net/netip"
"strings"
- "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
)
func init() {
diff --git a/modules/caddytls/storageloader.go b/modules/caddytls/storageloader.go
index ef9d51e..ddaaa51 100644
--- a/modules/caddytls/storageloader.go
+++ b/modules/caddytls/storageloader.go
@@ -18,8 +18,9 @@ import (
"crypto/tls"
"fmt"
- "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
+
+ "github.com/caddyserver/caddy/v2"
)
func init() {
diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go
index 8051653..02d5aae 100644
--- a/modules/caddytls/tls.go
+++ b/modules/caddytls/tls.go
@@ -25,10 +25,11 @@ import (
"sync"
"time"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/modules/caddyevents"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/modules/caddyevents"
)
func init() {
@@ -36,12 +37,24 @@ func init() {
caddy.RegisterModule(AutomateLoader{})
}
+var (
+ certCache *certmagic.Cache
+ certCacheMu sync.RWMutex
+)
+
// TLS provides TLS facilities including certificate
// loading and management, client auth, and more.
type TLS struct {
- // Caches certificates in memory for quick use during
+ // Certificates to load into memory for quick recall during
// TLS handshakes. Each key is the name of a certificate
- // loader module. All loaded certificates get pooled
+ // loader module.
+ //
+ // The "automate" certificate loader module can be used to
+ // specify a list of subjects that need certificates to be
+ // managed automatically. The first matching automation
+ // policy will be applied to manage the certificate(s).
+ //
+ // All loaded certificates get pooled
// into the same cache and may be used to complete TLS
// handshakes for the relevant server names (SNI).
// Certificates loaded manually (anything other than
@@ -70,12 +83,15 @@ type TLS struct {
certificateLoaders []CertificateLoader
automateNames []string
- certCache *certmagic.Cache
ctx caddy.Context
storageCleanTicker *time.Ticker
storageCleanStop chan struct{}
logger *zap.Logger
events *caddyevents.App
+
+ // set of subjects with managed certificates,
+ // and hashes of manually-loaded certificates
+ managing, loaded map[string]struct{}
}
// CaddyModule returns the Caddy module information.
@@ -96,6 +112,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
t.ctx = ctx
t.logger = ctx.Logger()
repl := caddy.NewReplacer()
+ t.managing, t.loaded = make(map[string]struct{}), make(map[string]struct{})
// set up a new certificate cache; this (re)loads all certificates
cacheOpts := certmagic.CacheOptions{
@@ -114,7 +131,14 @@ func (t *TLS) Provision(ctx caddy.Context) error {
if cacheOpts.Capacity <= 0 {
cacheOpts.Capacity = 10000
}
- t.certCache = certmagic.NewCache(cacheOpts)
+
+ certCacheMu.Lock()
+ if certCache == nil {
+ certCache = certmagic.NewCache(cacheOpts)
+ } else {
+ certCache.SetOptions(cacheOpts)
+ }
+ certCacheMu.Unlock()
// certificate loaders
val, err := ctx.LoadModule(t, "CertificatesRaw")
@@ -126,7 +150,12 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// special case; these will be loaded in later using our automation facilities,
// which we want to avoid doing during provisioning
if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil {
- t.automateNames = []string(*automateNames)
+ repl := caddy.NewReplacer()
+ subjects := make([]string, len(*automateNames))
+ for i, sub := range *automateNames {
+ subjects[i] = repl.ReplaceAll(sub, "")
+ }
+ t.automateNames = subjects
} else {
return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface)
}
@@ -181,8 +210,8 @@ func (t *TLS) Provision(ctx caddy.Context) error {
onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
} else {
// remove any existing rate limiter
- onDemandRateLimiter.SetMaxEvents(0)
onDemandRateLimiter.SetWindow(0)
+ onDemandRateLimiter.SetMaxEvents(0)
}
// run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036)
@@ -197,7 +226,8 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// provision so that other apps (such as http) can know which
// certificates have been manually loaded, and also so that
// commands like validate can be a better test
- magic := certmagic.New(t.certCache, certmagic.Config{
+ certCacheMu.RLock()
+ magic := certmagic.New(certCache, certmagic.Config{
Storage: ctx.Storage(),
Logger: t.logger,
OnEvent: t.onEvent,
@@ -205,16 +235,18 @@ func (t *TLS) Provision(ctx caddy.Context) error {
DisableStapling: t.DisableOCSPStapling,
},
})
+ certCacheMu.RUnlock()
for _, loader := range t.certificateLoaders {
certs, err := loader.LoadCertificates()
if err != nil {
return fmt.Errorf("loading certificates: %v", err)
}
for _, cert := range certs {
- err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags)
+ hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags)
if err != nil {
return fmt.Errorf("caching unmanaged certificate: %v", err)
}
+ t.loaded[hash] = struct{}{}
}
}
@@ -231,13 +263,13 @@ func (t *TLS) Validate() error {
var hasDefault bool
hostSet := make(map[string]int)
for i, ap := range t.Automation.Policies {
- if len(ap.Subjects) == 0 {
+ if len(ap.subjects) == 0 {
if hasDefault {
return fmt.Errorf("automation policy %d is the second policy that acts as default/catch-all, but will never be used", i)
}
hasDefault = true
}
- for _, h := range ap.Subjects {
+ for _, h := range ap.subjects {
if first, ok := hostSet[h]; ok {
return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first)
}
@@ -259,7 +291,7 @@ func (t *TLS) Start() error {
if t.Automation.OnDemand == nil ||
(t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.RateLimit == nil) {
for _, ap := range t.Automation.Policies {
- if ap.OnDemand {
+ if ap.OnDemand && ap.isWildcardOrDefault() {
t.logger.Warn("YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place",
zap.String("docs", "https://caddyserver.com/docs/automatic-https#on-demand-tls"))
break
@@ -293,16 +325,44 @@ func (t *TLS) Stop() error {
// Cleanup frees up resources allocated during Provision.
func (t *TLS) Cleanup() error {
- // stop the certificate cache
- if t.certCache != nil {
- t.certCache.Stop()
- }
-
// stop the session ticket rotation goroutine
if t.SessionTickets != nil {
t.SessionTickets.stop()
}
+ // if a new TLS app was loaded, remove certificates from the cache that are no longer
+ // being managed or loaded by the new config; if there is no more TLS app running,
+ // then stop cert maintenance and let the cert cache be GC'ed
+ if nextTLS := caddy.ActiveContext().AppIfConfigured("tls"); nextTLS != nil {
+ nextTLSApp := nextTLS.(*TLS)
+
+ // compute which certificates were managed or loaded into the cert cache by this
+ // app instance (which is being stopped) that are not managed or loaded by the
+ // new app instance (which just started), and remove them from the cache
+ var noLongerManaged, noLongerLoaded []string
+ for subj := range t.managing {
+ if _, ok := nextTLSApp.managing[subj]; !ok {
+ noLongerManaged = append(noLongerManaged, subj)
+ }
+ }
+ for hash := range t.loaded {
+ if _, ok := nextTLSApp.loaded[hash]; !ok {
+ noLongerLoaded = append(noLongerLoaded, hash)
+ }
+ }
+
+ certCacheMu.RLock()
+ certCache.RemoveManaged(noLongerManaged)
+ certCache.Remove(noLongerLoaded)
+ certCacheMu.RUnlock()
+ } else {
+ // no more TLS app running, so delete in-memory cert cache
+ certCache.Stop()
+ certCacheMu.Lock()
+ certCache = nil
+ certCacheMu.Unlock()
+ }
+
return nil
}
@@ -327,6 +387,9 @@ func (t *TLS) Manage(names []string) error {
if err != nil {
return fmt.Errorf("automate: manage %v: %v", names, err)
}
+ for _, name := range names {
+ t.managing[name] = struct{}{}
+ }
}
return nil
@@ -388,8 +451,8 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error {
// first see if existing is superset of ap for all names
var otherIsSuperset bool
outer:
- for _, thisSubj := range ap.Subjects {
- for _, otherSubj := range existing.Subjects {
+ for _, thisSubj := range ap.subjects {
+ for _, otherSubj := range existing.subjects {
if certmagic.MatchWildcard(thisSubj, otherSubj) {
otherIsSuperset = true
break outer
@@ -398,7 +461,7 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error {
}
// if existing AP is a superset or if it contains fewer names (i.e. is
// more general), then new AP is more specific, so insert before it
- if otherIsSuperset || len(existing.Subjects) < len(ap.Subjects) {
+ if otherIsSuperset || len(existing.SubjectsRaw) < len(ap.SubjectsRaw) {
t.Automation.Policies = append(t.Automation.Policies[:i],
append([]*AutomationPolicy{ap}, t.Automation.Policies[i:]...)...)
return nil
@@ -420,10 +483,10 @@ func (t *TLS) getConfigForName(name string) *certmagic.Config {
// public certificate or not.
func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
for _, ap := range t.Automation.Policies {
- if len(ap.Subjects) == 0 {
+ if len(ap.subjects) == 0 {
return ap // no host filter is an automatic match
}
- for _, h := range ap.Subjects {
+ for _, h := range ap.subjects {
if certmagic.MatchWildcard(name, h) {
return ap
}
@@ -437,8 +500,27 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
// AllMatchingCertificates returns the list of all certificates in
// the cache which could be used to satisfy the given SAN.
-func (t *TLS) AllMatchingCertificates(san string) []certmagic.Certificate {
- return t.certCache.AllMatchingCertificates(san)
+func AllMatchingCertificates(san string) []certmagic.Certificate {
+ return certCache.AllMatchingCertificates(san)
+}
+
+func (t *TLS) HasCertificateForSubject(subject string) bool {
+ certCacheMu.RLock()
+ allMatchingCerts := certCache.AllMatchingCertificates(subject)
+ certCacheMu.RUnlock()
+ for _, cert := range allMatchingCerts {
+ // check if the cert is manually loaded by this config
+ if _, ok := t.loaded[cert.Hash()]; ok {
+ return true
+ }
+ // check if the cert is automatically managed by this config
+ for _, name := range cert.Names {
+ if _, ok := t.managing[name]; ok {
+ return true
+ }
+ }
+ }
+ return false
}
// keepStorageClean starts a goroutine that immediately cleans up all
@@ -552,7 +634,9 @@ type Certificate struct {
//
// Technically, this is a no-op certificate loader module that is treated as
// a special case: it uses this app's automation features to load certificates
-// for the list of hostnames, rather than loading certificates manually.
+// for the list of hostnames, rather than loading certificates manually. But
+// the end result is the same: certificates for these subject names will be
+// loaded into the in-memory cache and may then be used.
type AutomateLoader []string
// CaddyModule returns the Caddy module information.
diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go
index 0209294..697bab0 100644
--- a/modules/caddytls/zerosslissuer.go
+++ b/modules/caddytls/zerosslissuer.go
@@ -25,11 +25,12 @@ import (
"strings"
"sync"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {