@marcel91200 / caddy Mirror · read-only / commits / 57c5b921
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 18:15 UTC

caddytls: Make on-demand 'ask' permission modular (#6055)

* caddytls: Make on-demand 'ask' permission modular This makes the 'ask' endpoint a module, which means that developers can write custom plugins for granting permission for on-demand certificates. Kicking myself that we didn't do it this way at the beginning, but who coulda known... * Lint * Error on conflicting config * Fix bad merge --------- Co-authored-by: Francis Lavoie <lavofr@gmail.com>

Matt Holt committed Jan 30, 2024 at 16:11 UTC 57c5b921a4283b4efa44d2fd77dce50f3113fb5a
8 files changed +267 -137
caddyconfig/httpcaddyfile/options.go
+4 -2
@@ -335,7 +335,8 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
335 }
336
337 var ond *caddytls.OnDemandConfig
338 - for d.NextBlock(0) {
338 +
339 + for nesting := d.Nesting(); d.NextBlock(nesting); {
340 switch d.Val() {
341 case "ask":
342 if !d.NextArg() {
@@ -344,7 +345,8 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
345 if ond == nil {
346 ond = new(caddytls.OnDemandConfig)
347 }
347 - ond.Ask = d.Val()
348 + perm := caddytls.PermissionByHTTP{Endpoint: d.Val()}
349 + ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", "http", nil)
350
351 case "interval":
352 if !d.NextArg() {
caddytest/integration/caddyfile_adapt/global_options.txt
+4 -1
@@ -69,7 +69,10 @@
69 }
70 ],
71 "on_demand": {
72 - "ask": "https://example.com",
72 + "permission": {
73 + "endpoint": "https://example.com",
74 + "module": "http"
75 + },
76 "rate_limit": {
77 "interval": 30000000000,
78 "burst": 20
caddytest/integration/caddyfile_adapt/global_options_acme.txt
+4 -1
@@ -78,7 +78,10 @@
78 }
79 ],
80 "on_demand": {
81 - "ask": "https://example.com",
81 + "permission": {
82 + "endpoint": "https://example.com",
83 + "module": "http"
84 + },
85 "rate_limit": {
86 "interval": 30000000000,
87 "burst": 20
caddytest/integration/caddyfile_adapt/global_options_admin.txt
+4 -1
@@ -71,7 +71,10 @@
71 }
72 ],
73 "on_demand": {
74 - "ask": "https://example.com",
74 + "permission": {
75 + "endpoint": "https://example.com",
76 + "module": "http"
77 + },
78 "rate_limit": {
79 "interval": 30000000000,
80 "burst": 20
modules/caddytls/acmeissuer.go
-52
@@ -16,12 +16,8 @@ package caddytls
16
17 import (
18 "context"
19 - "crypto/tls"
19 "crypto/x509"
21 - "errors"
20 "fmt"
23 - "net"
24 - "net/url"
21 "os"
22 "strconv"
23 "time"
@@ -495,49 +491,6 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
491 return nil
492 }
493
498 -// onDemandAskRequest makes a request to the ask URL
499 -// to see if a certificate can be obtained for name.
500 -// The certificate request should be denied if this
501 -// returns an error.
502 -func onDemandAskRequest(ctx context.Context, logger *zap.Logger, ask string, name string) error {
503 - askURL, err := url.Parse(ask)
504 - if err != nil {
505 - return fmt.Errorf("parsing ask URL: %v", err)
506 - }
507 - qs := askURL.Query()
508 - qs.Set("domain", name)
509 - askURL.RawQuery = qs.Encode()
510 -
511 - askURLString := askURL.String()
512 - resp, err := onDemandAskClient.Get(askURLString)
513 - if err != nil {
514 - return fmt.Errorf("error checking %v to determine if certificate for hostname '%s' should be allowed: %v",
515 - ask, name, err)
516 - }
517 - resp.Body.Close()
518 -
519 - // logging out the client IP can be useful for servers that want to count
520 - // attempts from clients to detect patterns of abuse
521 - var clientIP string
522 - if hello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && hello != nil {
523 - if remote := hello.Conn.RemoteAddr(); remote != nil {
524 - clientIP, _, _ = net.SplitHostPort(remote.String())
525 - }
526 - }
527 -
528 - logger.Debug("response from ask endpoint",
529 - zap.String("client_ip", clientIP),
530 - zap.String("domain", name),
531 - zap.String("url", askURLString),
532 - zap.Int("status", resp.StatusCode))
533 -
534 - if resp.StatusCode < 200 || resp.StatusCode > 299 {
535 - return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, errAskDenied, ask, resp.StatusCode)
536 - }
537 -
538 - return nil
539 -}
540 -
494 func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPreference, error) {
495 chainPref := new(ChainPreference)
496 if d.NextArg() {
@@ -605,11 +558,6 @@ type ChainPreference struct {
558 AnyCommonName []string `json:"any_common_name,omitempty"`
559 }
560
608 -// errAskDenied is an error that should be wrapped or returned when the
609 -// configured "ask" endpoint does not allow a certificate to be issued,
610 -// to distinguish that from other errors such as connection failure.
611 -var errAskDenied = errors.New("certificate not allowed by ask endpoint")
612 -
561 // Interface guards
562 var (
563 _ certmagic.PreChecker = (*ACMEIssuer)(nil)
modules/caddytls/automation.go
+28 -60
@@ -16,12 +16,12 @@ package caddytls
16
17 import (
18 "context"
19 + "crypto/tls"
20 "encoding/json"
21 "errors"
22 "fmt"
22 - "net/http"
23 + "net"
24 "strings"
24 - "time"
25
26 "github.com/caddyserver/certmagic"
27 "github.com/mholt/acmez"
@@ -254,37 +254,52 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
254 // on-demand TLS
255 var ond *certmagic.OnDemandConfig
256 if ap.OnDemand || len(ap.Managers) > 0 {
257 - // ask endpoint is now required after a number of negligence cases causing abuse;
258 - // but is still allowed for explicit subjects (non-wildcard, non-unbounded),
259 - // for the internal issuer since it doesn't cause ACME issuer pressure
260 - if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.Ask == "") {
261 - return fmt.Errorf("on-demand TLS cannot be enabled without an 'ask' endpoint to prevent abuse; please refer to documentation for details")
257 + // permission module is now required after a number of negligence cases that allowed abuse;
258 + // but it may still be optional for explicit subjects (bounded, non-wildcard), for the
259 + // internal issuer since it doesn't cause public PKI pressure on ACME servers
260 + if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) {
261 + return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
262 }
263 ond = &certmagic.OnDemandConfig{
264 DecisionFunc: func(ctx context.Context, name string) error {
265 if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {
266 return nil
267 }
268 - if err := onDemandAskRequest(ctx, tlsApp.logger, tlsApp.Automation.OnDemand.Ask, name); err != nil {
268 +
269 + // logging the remote IP can be useful for servers that want to count
270 + // attempts from clients to detect patterns of abuse -- it should NOT be
271 + // used solely for decision making, however
272 + var remoteIP string
273 + if hello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && hello != nil {
274 + if remote := hello.Conn.RemoteAddr(); remote != nil {
275 + remoteIP, _, _ = net.SplitHostPort(remote.String())
276 + }
277 + }
278 + tlsApp.logger.Debug("asking for permission for on-demand certificate",
279 + zap.String("remote_ip", remoteIP),
280 + zap.String("domain", name))
281 +
282 + // ask the permission module if this cert is allowed
283 + if err := tlsApp.Automation.OnDemand.permission.CertificateAllowed(ctx, name); err != nil {
284 // distinguish true errors from denials, because it's important to elevate actual errors
270 - if errors.Is(err, errAskDenied) {
271 - tlsApp.logger.Debug("certificate issuance denied",
272 - zap.String("ask_endpoint", tlsApp.Automation.OnDemand.Ask),
285 + if errors.Is(err, ErrPermissionDenied) {
286 + tlsApp.logger.Debug("on-demand certificate issuance denied",
287 zap.String("domain", name),
288 zap.Error(err))
289 } else {
276 - tlsApp.logger.Error("request to 'ask' endpoint failed",
277 - zap.String("ask_endpoint", tlsApp.Automation.OnDemand.Ask),
290 + tlsApp.logger.Error("failed to get permission for on-demand certificate",
291 zap.String("domain", name),
292 zap.Error(err))
293 }
294 return err
295 }
296 +
297 // check the rate limiter last because
298 // doing so makes a reservation
299 if !onDemandRateLimiter.Allow() {
300 return fmt.Errorf("on-demand rate limit exceeded")
301 }
302 +
303 return nil
304 },
305 Managers: ap.Managers,
@@ -464,42 +479,6 @@ type DNSChallengeConfig struct {
479 solver acmez.Solver
480 }
481
467 -// OnDemandConfig configures on-demand TLS, for obtaining
468 -// needed certificates at handshake-time. Because this
469 -// feature can easily be abused, you should use this to
470 -// establish rate limits and/or an internal endpoint that
471 -// Caddy can "ask" if it should be allowed to manage
472 -// certificates for a given hostname.
473 -type OnDemandConfig struct {
474 - // REQUIRED. If Caddy needs to load a certificate from
475 - // storage or obtain/renew a certificate during a TLS
476 - // handshake, it will perform a quick HTTP request to
477 - // this URL to check if it should be allowed to try to
478 - // get a certificate for the name in the "domain" query
479 - // string parameter, like so: `?domain=example.com`.
480 - // The endpoint must return a 200 OK status if a certificate
481 - // is allowed; anything else will cause it to be denied.
482 - // Redirects are not followed.
483 - Ask string `json:"ask,omitempty"`
484 -
485 - // DEPRECATED. An optional rate limit to throttle
486 - // the checking of storage and the issuance of
487 - // certificates from handshakes if not already in
488 - // storage. WILL BE REMOVED IN A FUTURE RELEASE.
489 - RateLimit *RateLimit `json:"rate_limit,omitempty"`
490 -}
491 -
492 -// DEPRECATED. RateLimit specifies an interval with optional burst size.
493 -type RateLimit struct {
494 - // A duration value. Storage may be checked and a certificate may be
495 - // obtained 'burst' times during this interval.
496 - Interval caddy.Duration `json:"interval,omitempty"`
497 -
498 - // How many times during an interval storage can be checked or a
499 - // certificate can be obtained.
500 - Burst int `json:"burst,omitempty"`
501 -}
502 -
482 // ConfigSetter is implemented by certmagic.Issuers that
483 // need access to a parent certmagic.Config as part of
484 // their provisioning phase. For example, the ACMEIssuer
@@ -508,14 +487,3 @@ type RateLimit struct {
487 type ConfigSetter interface {
488 SetConfig(cfg *certmagic.Config)
489 }
511 -
512 -// These perpetual values are used for on-demand TLS.
513 -var (
514 - onDemandRateLimiter = certmagic.NewRateLimiter(0, 0)
515 - onDemandAskClient = &http.Client{
516 - Timeout: 10 * time.Second,
517 - CheckRedirect: func(req *http.Request, via []*http.Request) error {
518 - return fmt.Errorf("following http redirects is not allowed")
519 - },
520 - }
521 -)
modules/caddytls/ondemand.go new
+192
@@ -0,0 +1,192 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddytls
16 +
17 +import (
18 + "context"
19 + "crypto/tls"
20 + "encoding/json"
21 + "errors"
22 + "fmt"
23 + "net/http"
24 + "net/url"
25 + "time"
26 +
27 + "github.com/caddyserver/certmagic"
28 + "go.uber.org/zap"
29 +
30 + "github.com/caddyserver/caddy/v2"
31 +)
32 +
33 +func init() {
34 + caddy.RegisterModule(PermissionByHTTP{})
35 +}
36 +
37 +// OnDemandConfig configures on-demand TLS, for obtaining
38 +// needed certificates at handshake-time. Because this
39 +// feature can easily be abused, you should use this to
40 +// establish rate limits and/or an internal endpoint that
41 +// Caddy can "ask" if it should be allowed to manage
42 +// certificates for a given hostname.
43 +type OnDemandConfig struct {
44 + // DEPRECATED. WILL BE REMOVED SOON. Use 'permission' instead.
45 + Ask string `json:"ask,omitempty"`
46 +
47 + // REQUIRED. A module that will determine whether a
48 + // certificate is allowed to be loaded from storage
49 + // or obtained from an issuer on demand.
50 + PermissionRaw json.RawMessage `json:"permission,omitempty" caddy:"namespace=tls.permission inline_key=module"`
51 + permission OnDemandPermission
52 +
53 + // DEPRECATED. An optional rate limit to throttle
54 + // the checking of storage and the issuance of
55 + // certificates from handshakes if not already in
56 + // storage. WILL BE REMOVED IN A FUTURE RELEASE.
57 + RateLimit *RateLimit `json:"rate_limit,omitempty"`
58 +}
59 +
60 +// DEPRECATED. WILL LIKELY BE REMOVED SOON.
61 +// Instead of using this rate limiter, use a proper tool such as a
62 +// level 3 or 4 firewall and/or a permission module to apply rate limits.
63 +type RateLimit struct {
64 + // A duration value. Storage may be checked and a certificate may be
65 + // obtained 'burst' times during this interval.
66 + Interval caddy.Duration `json:"interval,omitempty"`
67 +
68 + // How many times during an interval storage can be checked or a
69 + // certificate can be obtained.
70 + Burst int `json:"burst,omitempty"`
71 +}
72 +
73 +// OnDemandPermission is a type that can give permission for
74 +// whether a certificate should be allowed to be obtained or
75 +// loaded from storage on-demand.
76 +// EXPERIMENTAL: This API is experimental and subject to change.
77 +type OnDemandPermission interface {
78 + // CertificateAllowed returns nil if a certificate for the given
79 + // name is allowed to be either obtained from an issuer or loaded
80 + // from storage on-demand.
81 + //
82 + // The context passed in has the associated *tls.ClientHelloInfo
83 + // value available at the certmagic.ClientHelloInfoCtxKey key.
84 + //
85 + // In the worst case, this function may be called as frequently
86 + // as every TLS handshake, so it should return as quick as possible
87 + // to reduce latency. In the normal case, this function is only
88 + // called when a certificate is needed that is not already loaded
89 + // into memory ready to serve.
90 + CertificateAllowed(ctx context.Context, name string) error
91 +}
92 +
93 +// PermissionByHTTP determines permission for a TLS certificate by
94 +// making a request to an HTTP endpoint.
95 +type PermissionByHTTP struct {
96 + // The endpoint to access. It should be a full URL.
97 + // A query string parameter "domain" will be added to it,
98 + // containing the domain (or IP) for the desired certificate,
99 + // like so: `?domain=example.com`. Generally, this endpoint
100 + // is not exposed publicly to avoid a minor information leak
101 + // (which domains are serviced by your application).
102 + //
103 + // The endpoint must return a 200 OK status if a certificate
104 + // is allowed; anything else will cause it to be denied.
105 + // Redirects are not followed.
106 + Endpoint string `json:"endpoint"`
107 +
108 + logger *zap.Logger
109 + replacer *caddy.Replacer
110 +}
111 +
112 +// CaddyModule returns the Caddy module information.
113 +func (PermissionByHTTP) CaddyModule() caddy.ModuleInfo {
114 + return caddy.ModuleInfo{
115 + ID: "tls.permission.http",
116 + New: func() caddy.Module { return new(PermissionByHTTP) },
117 + }
118 +}
119 +
120 +func (p *PermissionByHTTP) Provision(ctx caddy.Context) error {
121 + p.logger = ctx.Logger()
122 + p.replacer = caddy.NewReplacer()
123 + return nil
124 +}
125 +
126 +func (p PermissionByHTTP) CertificateAllowed(ctx context.Context, name string) error {
127 + // run replacer on endpoint URL (for environment variables) -- return errors to prevent surprises (#5036)
128 + askEndpoint, err := p.replacer.ReplaceOrErr(p.Endpoint, true, true)
129 + if err != nil {
130 + return fmt.Errorf("preparing 'ask' endpoint: %v", err)
131 + }
132 +
133 + askURL, err := url.Parse(askEndpoint)
134 + if err != nil {
135 + return fmt.Errorf("parsing ask URL: %v", err)
136 + }
137 + qs := askURL.Query()
138 + qs.Set("domain", name)
139 + askURL.RawQuery = qs.Encode()
140 + askURLString := askURL.String()
141 +
142 + var remote string
143 + if chi, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && chi != nil {
144 + remote = chi.Conn.RemoteAddr().String()
145 + }
146 +
147 + p.logger.Debug("asking permission endpoint",
148 + zap.String("remote", remote),
149 + zap.String("domain", name),
150 + zap.String("url", askURLString))
151 +
152 + resp, err := onDemandAskClient.Get(askURLString)
153 + if err != nil {
154 + return fmt.Errorf("checking %v to determine if certificate for hostname '%s' should be allowed: %v",
155 + askEndpoint, name, err)
156 + }
157 + resp.Body.Close()
158 +
159 + p.logger.Debug("response from permission endpoint",
160 + zap.String("remote", remote),
161 + zap.String("domain", name),
162 + zap.String("url", askURLString),
163 + zap.Int("status", resp.StatusCode))
164 +
165 + if resp.StatusCode < 200 || resp.StatusCode > 299 {
166 + return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, ErrPermissionDenied, askEndpoint, resp.StatusCode)
167 + }
168 +
169 + return nil
170 +}
171 +
172 +// ErrPermissionDenied is an error that should be wrapped or returned when the
173 +// configured permission module does not allow a certificate to be issued,
174 +// to distinguish that from other errors such as connection failure.
175 +var ErrPermissionDenied = errors.New("certificate not allowed by permission module")
176 +
177 +// These perpetual values are used for on-demand TLS.
178 +var (
179 + onDemandRateLimiter = certmagic.NewRateLimiter(0, 0)
180 + onDemandAskClient = &http.Client{
181 + Timeout: 10 * time.Second,
182 + CheckRedirect: func(req *http.Request, via []*http.Request) error {
183 + return fmt.Errorf("following http redirects is not allowed")
184 + },
185 + }
186 +)
187 +
188 +// Interface guards
189 +var (
190 + _ OnDemandPermission = (*PermissionByHTTP)(nil)
191 + _ caddy.Provisioner = (*PermissionByHTTP)(nil)
192 +)
modules/caddytls/tls.go
+31 -20
@@ -164,6 +164,36 @@ func (t *TLS) Provision(ctx caddy.Context) error {
164 t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader))
165 }
166
167 + // on-demand permission module
168 + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil {
169 + if t.Automation.OnDemand.Ask != "" {
170 + return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module")
171 + }
172 + val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw")
173 + if err != nil {
174 + return fmt.Errorf("loading on-demand TLS permission module: %v", err)
175 + }
176 + t.Automation.OnDemand.permission = val.(OnDemandPermission)
177 + }
178 +
179 + // on-demand rate limiting
180 + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
181 + onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
182 + onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
183 + } else {
184 + // remove any existing rate limiter
185 + onDemandRateLimiter.SetWindow(0)
186 + onDemandRateLimiter.SetMaxEvents(0)
187 + }
188 +
189 + // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036)
190 + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" {
191 + t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true)
192 + if err != nil {
193 + return fmt.Errorf("preparing 'ask' endpoint: %v", err)
194 + }
195 + }
196 +
197 // automation/management policies
198 if t.Automation == nil {
199 t.Automation = new(AutomationConfig)
@@ -204,24 +234,6 @@ func (t *TLS) Provision(ctx caddy.Context) error {
234 }
235 }
236
207 - // on-demand rate limiting
208 - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
209 - onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
210 - onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
211 - } else {
212 - // remove any existing rate limiter
213 - onDemandRateLimiter.SetWindow(0)
214 - onDemandRateLimiter.SetMaxEvents(0)
215 - }
216 -
217 - // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036)
218 - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" {
219 - t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true)
220 - if err != nil {
221 - return fmt.Errorf("preparing 'ask' endpoint: %v", err)
222 - }
223 - }
224 -
237 // load manual/static (unmanaged) certificates - we do this in
238 // provision so that other apps (such as http) can know which
239 // certificates have been manually loaded, and also so that
@@ -288,8 +300,7 @@ func (t *TLS) Validate() error {
300 // Start activates the TLS module.
301 func (t *TLS) Start() error {
302 // warn if on-demand TLS is enabled but no restrictions are in place
291 - if t.Automation.OnDemand == nil ||
292 - (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.RateLimit == nil) {
303 + if t.Automation.OnDemand == nil || (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.permission == nil) {
304 for _, ap := range t.Automation.Policies {
305 if ap.OnDemand && ap.isWildcardOrDefault() {
306 t.logger.Warn("YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place",