@marcel91200 / caddy Mirror · read-only / commits / ab80ff4f
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 14, 05:15 UTC

admin: Identity management, remote admin, config loaders (#3994)

This commits dds 3 separate, but very related features: 1. Automated server identity management How do you know you're connecting to the server you think you are? How do you know the server connecting to you is the server instance you think it is? Mutually-authenticated TLS (mTLS) answers both of these questions. Using TLS to authenticate requires a public/private key pair (and the peer must trust the certificate you present to it). Fortunately, Caddy is really good at managing certificates by now. We tap into that power to make it possible for Caddy to obtain and renew its own identity credentials, or in other words, a certificate that can be used for both server verification when clients connect to it, and client verification when it connects to other servers. Its associated private key is essentially its identity, and TLS takes care of possession proofs. This configuration is simply a list of identifiers and an optional list of custom certificate issuers. Identifiers are things like IP addresses or DNS names that can be used to access the Caddy instance. The default issuers are ZeroSSL and Let's Encrypt, but these are public CAs, so they won't issue certs for private identifiers. Caddy will simply manage credentials for these, which other parts of Caddy can use, for example: remote administration or dynamic config loading (described below). 2. Remote administration over secure connection This feature adds generic remote admin functionality that is safe to expose on a public interface. - The "remote" (or "secure") endpoint is optional. It does not affect the standard/local/plaintext endpoint. - It's the same as the [API endpoint on localhost:2019](https://caddyserver.com/docs/api), but over TLS. - TLS cannot be disabled on this endpoint. - TLS mutual auth is required, and cannot be disabled. - The server's certificate _must_ be obtained and renewed via automated means, such as ACME. It cannot be manually loaded. - The TLS server takes care of verifying the client. - The admin handler takes care of application-layer permissions (methods and paths that each client is allowed to use).\ - Sensible defaults are still WIP. - Config fields subject to change/renaming. 3. Dyanmic config loading at startup Since this feature was planned in tandem with remote admin, and depends on its changes, I am combining them into one PR. Dynamic config loading is where you tell Caddy how to load its config, and then it loads and runs that. First, it will load the config you give it (and persist that so it can be optionally resumed later). Then, it will try pulling its _actual_ config using the module you've specified (dynamically loaded configs are _not_ persisted to storage, since resuming them doesn't make sense). This PR comes with a standard config loader module called `caddy.config_loaders.http`. Caddyfile config for all of this can probably be added later. COMMITS: * admin: Secure socket for remote management Functional, but still WIP. Optional secure socket for the admin endpoint is designed for remote management, i.e. to be exposed on a public port. It enforces TLS mutual authentication which cannot be disabled. The default port for this is :2021. The server certificate cannot be specified manually, it MUST be obtained from a certificate issuer (i.e. ACME). More polish and sensible defaults are still in development. Also cleaned up and consolidated the code related to quitting the process. * Happy lint * Implement dynamic config loading; HTTP config loader module This allows Caddy to load a dynamic config when it starts. Dynamically-loaded configs are intentionally not persisted to storage. Includes an implementation of the standard config loader, HTTPLoader. Can be used to download configs over HTTP(S). * Refactor and cleanup; prevent recursive config pulls Identity management is now separated from remote administration. There is no need to enable remote administration if all you want is identity management, but you will need to configure identity management if you want remote administration. * Fix lint warnings * Rename identities->identifiers for consistency

Matt Holt committed Jan 27, 2021 at 16:16 UTC ab80ff4fd2911afc394b9dbceeb9f71c7a0b7ec1
11 files changed +862 -214
admin.go
+451 -112
@@ -17,6 +17,10 @@ package caddy
17 import (
18 "bytes"
19 "context"
20 + "crypto"
21 + "crypto/tls"
22 + "crypto/x509"
23 + "encoding/base64"
24 "encoding/json"
25 "errors"
26 "expvar"
@@ -35,12 +39,11 @@ import (
39 "sync"
40 "time"
41
42 + "github.com/caddyserver/certmagic"
43 "github.com/prometheus/client_golang/prometheus"
44 "go.uber.org/zap"
45 )
46
42 -// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
43 -
47 // AdminConfig configures Caddy's API endpoint, which is used
48 // to manage Caddy while it is running.
49 type AdminConfig struct {
@@ -58,54 +61,131 @@ type AdminConfig struct {
61 // If true, CORS headers will be emitted, and requests to the
62 // API will be rejected if their `Host` and `Origin` headers
63 // do not match the expected value(s). Use `origins` to
61 - // customize which origins/hosts are allowed.If `origins` is
64 + // customize which origins/hosts are allowed. If `origins` is
65 // not set, the listen address is the only value allowed by
63 - // default.
66 + // default. Enforced only on local (plaintext) endpoint.
67 EnforceOrigin bool `json:"enforce_origin,omitempty"`
68
69 // The list of allowed origins/hosts for API requests. Only needed
70 // if accessing the admin endpoint from a host different from the
71 // socket's network interface or if `enforce_origin` is true. If not
72 // set, the listener address will be the default value. If set but
70 - // empty, no origins will be allowed.
73 + // empty, no origins will be allowed. Enforced only on local
74 + // (plaintext) endpoint.
75 Origins []string `json:"origins,omitempty"`
76
73 - // Options related to configuration management.
77 + // Options pertaining to configuration management.
78 Config *ConfigSettings `json:"config,omitempty"`
79 +
80 + // Options that establish this server's identity. Identity refers to
81 + // credentials which can be used to uniquely identify and authenticate
82 + // this server instance. This is required if remote administration is
83 + // enabled (but does not require remote administration to be enabled).
84 + // Default: no identity management.
85 + Identity *IdentityConfig `json:"identity,omitempty"`
86 +
87 + // Options pertaining to remote administration. By default, remote
88 + // administration is disabled. If enabled, identity management must
89 + // also be configured, as that is how the endpoint is secured.
90 + // See the neighboring "identity" object.
91 + //
92 + // EXPERIMENTAL: This feature is subject to change.
93 + Remote *RemoteAdmin `json:"remote,omitempty"`
94 }
95
77 -// ConfigSettings configures the, uh, configuration... and
78 -// management thereof.
96 +// ConfigSettings configures the management of configuration.
97 type ConfigSettings struct {
98 // Whether to keep a copy of the active config on disk. Default is true.
99 + // Note that "pulled" dynamic configs (using the neighboring "load" module)
100 + // are not persisted; only configs that are pushed to Caddy get persisted.
101 Persist *bool `json:"persist,omitempty"`
102 +
103 + // Loads a configuration to use. This is helpful if your configs are
104 + // managed elsewhere, and you want Caddy to pull its config dynamically
105 + // when it starts. The pulled config completely replaces the current
106 + // one, just like any other config load. It is an error if a pulled
107 + // config is configured to pull another config.
108 + //
109 + // EXPERIMENTAL: Subject to change.
110 + LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
111 }
112
84 -// listenAddr extracts a singular listen address from ac.Listen,
85 -// returning the network and the address of the listener.
86 -func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
87 - input := admin.Listen
88 - if input == "" {
89 - input = DefaultAdminListen
90 - }
91 - listenAddr, err := ParseNetworkAddress(input)
92 - if err != nil {
93 - return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
94 - }
95 - if listenAddr.PortRangeSize() != 1 {
96 - return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
97 - }
98 - return listenAddr, nil
113 +// IdentityConfig configures management of this server's identity. An identity
114 +// consists of credentials that uniquely verify this instance; for example,
115 +// TLS certificates (public + private key pairs).
116 +type IdentityConfig struct {
117 + // List of names or IP addresses which refer to this server.
118 + // Certificates will be obtained for these identifiers so
119 + // secure TLS connections can be made using them.
120 + Identifiers []string `json:"identifiers,omitempty"`
121 +
122 + // Issuers that can provide this admin endpoint its identity
123 + // certificate(s). Default: ACME issuers configured for
124 + // ZeroSSL and Let's Encrypt. Be sure to change this if you
125 + // require credentials for private identifiers.
126 + IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
127 +
128 + issuers []certmagic.Issuer
129 +}
130 +
131 +// RemoteAdmin enables and configures remote administration. If enabled,
132 +// a secure listener enforcing mutual TLS authentication will be started
133 +// on a different port from the standard plaintext admin server.
134 +//
135 +// This endpoint is secured using identity management, which must be
136 +// configured separately (because identity management does not depend
137 +// on remote administration). See the admin/identity config struct.
138 +//
139 +// EXPERIMENTAL: Subject to change.
140 +type RemoteAdmin struct {
141 + // The address on which to start the secure listener.
142 + // Default: :2021
143 + Listen string `json:"listen,omitempty"`
144 +
145 + // List of access controls for this secure admin endpoint.
146 + // This configures TLS mutual authentication (i.e. authorized
147 + // client certificates), but also application-layer permissions
148 + // like which paths and methods each identity is authorized for.
149 + AccessControl []*AdminAccess `json:"access_control,omitempty"`
150 +}
151 +
152 +// AdminAccess specifies what permissions an identity or group
153 +// of identities are granted.
154 +type AdminAccess struct {
155 + // Base64-encoded DER certificates containing public keys to accept.
156 + // (The contents of PEM certificate blocks are base64-encoded DER.)
157 + // Any of these public keys can appear in any part of a verified chain.
158 + PublicKeys []string `json:"public_keys,omitempty"`
159 +
160 + // Limits what the associated identities are allowed to do.
161 + // If unspecified, all permissions are granted.
162 + Permissions []AdminPermissions `json:"permissions,omitempty"`
163 +
164 + publicKeys []crypto.PublicKey
165 +}
166 +
167 +// AdminPermissions specifies what kinds of requests are allowed
168 +// to be made to the admin endpoint.
169 +type AdminPermissions struct {
170 + // The API paths allowed. Paths are simple prefix matches.
171 + // Any subpath of the specified paths will be allowed.
172 + Paths []string `json:"paths,omitempty"`
173 +
174 + // The HTTP methods allowed for the given paths.
175 + Methods []string `json:"methods,omitempty"`
176 }
177
178 // newAdminHandler reads admin's config and returns an http.Handler suitable
179 // for use in an admin endpoint server, which will be listening on listenAddr.
103 -func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
104 - muxWrap := adminHandler{
105 - enforceOrigin: admin.EnforceOrigin,
106 - enforceHost: !addr.isWildcardInterface(),
107 - allowedOrigins: admin.allowedOrigins(addr),
108 - mux: http.NewServeMux(),
180 +func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
181 + muxWrap := adminHandler{mux: http.NewServeMux()}
182 +
183 + // secure the local or remote endpoint respectively
184 + if remote {
185 + muxWrap.remoteControl = admin.Remote
186 + } else {
187 + muxWrap.enforceHost = !addr.isWildcardInterface()
188 + muxWrap.allowedOrigins = admin.allowedOrigins(addr)
189 }
190
191 addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
@@ -197,18 +277,18 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
277 return allowed
278 }
279
200 -// replaceAdmin replaces the running admin server according
201 -// to the relevant configuration in cfg. If no configuration
202 -// for the admin endpoint exists in cfg, a default one is
203 -// used, so that there is always an admin server (unless it
204 -// is explicitly configured to be disabled).
205 -func replaceAdmin(cfg *Config) error {
280 +// replaceLocalAdminServer replaces the running local admin server
281 +// according to the relevant configuration in cfg. If no configuration
282 +// for the admin endpoint exists in cfg, a default one is used, so
283 +// that there is always an admin server (unless it is explicitly
284 +// configured to be disabled).
285 +func replaceLocalAdminServer(cfg *Config) error {
286 // always be sure to close down the old admin endpoint
287 // as gracefully as possible, even if the new one is
288 // disabled -- careful to use reference to the current
289 // (old) admin endpoint since it will be different
290 // when the function returns
211 - oldAdminServer := adminServer
291 + oldAdminServer := localAdminServer
292 defer func() {
293 // do the shutdown asynchronously so that any
294 // current API request gets a response; this
@@ -236,19 +316,20 @@ func replaceAdmin(cfg *Config) error {
316 }
317
318 // extract a singular listener address
239 - addr, err := adminConfig.listenAddr()
319 + addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
320 if err != nil {
321 return err
322 }
323
244 - handler := adminConfig.newAdminHandler(addr)
324 + handler := adminConfig.newAdminHandler(addr, false)
325
326 ln, err := Listen(addr.Network, addr.JoinHostPort(0))
327 if err != nil {
328 return err
329 }
330
251 - adminServer = &http.Server{
331 + localAdminServer = &http.Server{
332 + Addr: addr.String(), // for logging purposes only
333 Handler: handler,
334 ReadTimeout: 10 * time.Second,
335 ReadHeaderTimeout: 5 * time.Second,
@@ -258,7 +339,7 @@ func replaceAdmin(cfg *Config) error {
339
340 adminLogger := Log().Named("admin")
341 go func() {
261 - if err := adminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
342 + if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
343 adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
344 }
345 }()
@@ -276,6 +357,252 @@ func replaceAdmin(cfg *Config) error {
357 return nil
358 }
359
360 +// manageIdentity sets up automated identity management for this server.
361 +func manageIdentity(ctx Context, cfg *Config) error {
362 + if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil {
363 + return nil
364 + }
365 +
366 + oldIdentityCertCache := identityCertCache
367 + if oldIdentityCertCache != nil {
368 + defer oldIdentityCertCache.Stop()
369 + }
370 +
371 + // set default issuers; this is pretty hacky because we can't
372 + // import the caddytls package -- but it works
373 + if cfg.Admin.Identity.IssuersRaw == nil {
374 + cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
375 + json.RawMessage(`{"module": "zerossl"}`),
376 + json.RawMessage(`{"module": "acme"}`),
377 + }
378 + }
379 +
380 + // load and provision issuer modules
381 + if cfg.Admin.Identity.IssuersRaw != nil {
382 + val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw")
383 + if err != nil {
384 + return fmt.Errorf("loading identity issuer modules: %s", err)
385 + }
386 + for _, issVal := range val.([]interface{}) {
387 + cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
388 + }
389 + }
390 +
391 + logger := Log().Named("admin.identity")
392 + cmCfg := cfg.Admin.Identity.certmagicConfig(logger)
393 +
394 + // issuers have circular dependencies with the configs because,
395 + // as explained in the caddytls package, they need access to the
396 + // correct storage and cache to solve ACME challenges
397 + for _, issuer := range cfg.Admin.Identity.issuers {
398 + // avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
399 + if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok {
400 + annoying.SetConfig(cmCfg)
401 + }
402 + }
403 +
404 + // obtain and renew server identity certificate(s)
405 + return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers)
406 +}
407 +
408 +// replaceRemoteAdminServer replaces the running remote admin server
409 +// according to the relevant configuration in cfg. It stops any previous
410 +// remote admin server and only starts a new one if configured.
411 +func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
412 + if cfg == nil {
413 + return nil
414 + }
415 +
416 + remoteLogger := Log().Named("admin.remote")
417 +
418 + oldAdminServer := remoteAdminServer
419 + defer func() {
420 + if oldAdminServer != nil {
421 + go func(oldAdminServer *http.Server) {
422 + err := stopAdminServer(oldAdminServer)
423 + if err != nil {
424 + Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err))
425 + }
426 + }(oldAdminServer)
427 + }
428 + }()
429 +
430 + if cfg.Admin == nil || cfg.Admin.Remote == nil {
431 + return nil
432 + }
433 +
434 + addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen)
435 + if err != nil {
436 + return err
437 + }
438 +
439 + // make the HTTP handler but disable Host/Origin enforcement
440 + // because we are using TLS authentication instead
441 + handler := cfg.Admin.newAdminHandler(addr, true)
442 +
443 + // create client certificate pool for TLS mutual auth, and extract public keys
444 + // so that we can enforce access controls at the application layer
445 + clientCertPool := x509.NewCertPool()
446 + for i, accessControl := range cfg.Admin.Remote.AccessControl {
447 + for j, certBase64 := range accessControl.PublicKeys {
448 + cert, err := decodeBase64DERCert(certBase64)
449 + if err != nil {
450 + return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err)
451 + }
452 + accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey)
453 + clientCertPool.AddCert(cert)
454 + }
455 + }
456 +
457 + // create TLS config that will enforce mutual authentication
458 + cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger)
459 + tlsConfig := cmCfg.TLSConfig()
460 + tlsConfig.NextProtos = nil // this server does not solve ACME challenges
461 + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
462 + tlsConfig.ClientCAs = clientCertPool
463 +
464 + // convert logger to stdlib so it can be used by HTTP server
465 + serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel)
466 + if err != nil {
467 + return err
468 + }
469 +
470 + // create secure HTTP server
471 + remoteAdminServer = &http.Server{
472 + Addr: addr.String(), // for logging purposes only
473 + Handler: handler,
474 + TLSConfig: tlsConfig,
475 + ReadTimeout: 10 * time.Second,
476 + ReadHeaderTimeout: 5 * time.Second,
477 + IdleTimeout: 60 * time.Second,
478 + MaxHeaderBytes: 1024 * 64,
479 + ErrorLog: serverLogger,
480 + }
481 +
482 + // start listener
483 + ln, err := Listen(addr.Network, addr.JoinHostPort(0))
484 + if err != nil {
485 + return err
486 + }
487 + ln = tls.NewListener(ln, tlsConfig)
488 +
489 + go func() {
490 + if err := remoteAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
491 + remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
492 + }
493 + }()
494 +
495 + remoteLogger.Info("secure admin remote control endpoint started",
496 + zap.String("address", addr.String()))
497 +
498 + return nil
499 +}
500 +
501 +func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Config {
502 + if ident == nil {
503 + // user might not have configured identity; that's OK, we can still make a
504 + // certmagic config, although it'll be mostly useless for remote management
505 + ident = new(IdentityConfig)
506 + }
507 + cmCfg := &certmagic.Config{
508 + Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
509 + Logger: logger,
510 + Issuers: ident.issuers,
511 + }
512 + if identityCertCache == nil {
513 + identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
514 + GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
515 + return cmCfg, nil
516 + },
517 + })
518 + }
519 + return certmagic.New(identityCertCache, *cmCfg)
520 +}
521 +
522 +// IdentityCredentials returns this instance's configured, managed identity credentials
523 +// that can be used in TLS client authentication.
524 +func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) {
525 + if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil {
526 + return nil, fmt.Errorf("no server identity configured")
527 + }
528 + ident := ctx.cfg.Admin.Identity
529 + if len(ident.Identifiers) == 0 {
530 + return nil, fmt.Errorf("no identifiers configured")
531 + }
532 + if logger == nil {
533 + logger = Log()
534 + }
535 + magic := ident.certmagicConfig(logger)
536 + return magic.ClientCredentials(ctx, ident.Identifiers)
537 +}
538 +
539 +// enforceAccessControls enforces application-layer access controls for r based on remote.
540 +// It expects that the TLS server has already established at least one verified chain of
541 +// trust, and then looks for a matching, authorized public key that is allowed to access
542 +// the defined path(s) using the defined method(s).
543 +func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
544 + for _, chain := range r.TLS.VerifiedChains {
545 + for _, peerCert := range chain {
546 + for _, adminAccess := range remote.AccessControl {
547 + for _, allowedKey := range adminAccess.publicKeys {
548 + // see if we found a matching public key; the TLS server already verified the chain
549 + // so we know the client possesses the associated private key; this handy interface
550 + // doesn't appear to be defined anywhere in the std lib, but was implemented here:
551 + // https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
552 + comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
553 + if !ok || !comparer.Equal(allowedKey) {
554 + continue
555 + }
556 +
557 + // key recognized; make sure its HTTP request is permitted
558 + for _, accessPerm := range adminAccess.Permissions {
559 + // verify method
560 + methodFound := accessPerm.Methods == nil
561 + for _, method := range accessPerm.Methods {
562 + if method == r.Method {
563 + methodFound = true
564 + break
565 + }
566 + }
567 + if !methodFound {
568 + return APIError{
569 + HTTPStatus: http.StatusForbidden,
570 + Message: "not authorized to use this method",
571 + }
572 + }
573 +
574 + // verify path
575 + pathFound := accessPerm.Paths == nil
576 + for _, allowedPath := range accessPerm.Paths {
577 + if strings.HasPrefix(r.URL.Path, allowedPath) {
578 + pathFound = true
579 + break
580 + }
581 + }
582 + if !pathFound {
583 + return APIError{
584 + HTTPStatus: http.StatusForbidden,
585 + Message: "not authorized to access this path",
586 + }
587 + }
588 + }
589 +
590 + // public key authorized, method and path allowed
591 + return nil
592 + }
593 + }
594 + }
595 + }
596 +
597 + // in theory, this should never happen; with an unverified chain, the TLS server
598 + // should not accept the connection in the first place, and the acceptable cert
599 + // pool is configured using the same list of public keys we verify against
600 + return APIError{
601 + HTTPStatus: http.StatusUnauthorized,
602 + Message: "client identity not authorized",
603 + }
604 +}
605 +
606 func stopAdminServer(srv *http.Server) error {
607 if srv == nil {
608 return fmt.Errorf("no admin server")
@@ -286,7 +613,7 @@ func stopAdminServer(srv *http.Server) error {
613 if err != nil {
614 return fmt.Errorf("shutting down admin server: %v", err)
615 }
289 - Log().Named("admin").Info("stopped previous server")
616 + Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
617 return nil
618 }
619
@@ -302,10 +629,15 @@ type AdminRoute struct {
629 }
630
631 type adminHandler struct {
632 + mux *http.ServeMux
633 +
634 + // security for local/plaintext) endpoint, on by default
635 enforceOrigin bool
636 enforceHost bool
637 allowedOrigins []string
308 - mux *http.ServeMux
638 +
639 + // security for remote/encrypted endpoint
640 + remoteControl *RemoteAdmin
641 }
642
643 // ServeHTTP is the external entry point for API requests.
@@ -318,6 +650,12 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
650 zap.String("remote_addr", r.RemoteAddr),
651 zap.Reflect("headers", r.Header),
652 )
653 + if r.TLS != nil {
654 + log = log.With(
655 + zap.Bool("secure", true),
656 + zap.Int("verified_chains", len(r.TLS.VerifiedChains)),
657 + )
658 + }
659 if r.RequestURI == "/metrics" {
660 log.Debug("received request")
661 } else {
@@ -330,6 +668,14 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
668 // be called more than once per request, for example if a request
669 // is rewritten (i.e. internal redirect).
670 func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
671 + if h.remoteControl != nil {
672 + // enforce access controls on secure endpoint
673 + if err := h.remoteControl.enforceAccessControls(r); err != nil {
674 + h.handleError(w, r, err)
675 + return
676 + }
677 + }
678 +
679 if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
680 // I've never been able demonstrate a vulnerability myself, but apparently
681 // WebSocket connections originating from browsers aren't subject to CORS
@@ -363,8 +709,6 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
709 w.Header().Set("Access-Control-Allow-Origin", origin)
710 }
711
366 - // TODO: authentication & authorization, if configured
367 -
712 h.mux.ServeHTTP(w, r)
713 }
714
@@ -372,20 +716,16 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
716 if err == nil {
717 return
718 }
375 - if err == ErrInternalRedir {
376 - h.serveHTTP(w, r)
377 - return
378 - }
719
720 apiErr, ok := err.(APIError)
721 if !ok {
722 apiErr = APIError{
383 - Code: http.StatusInternalServerError,
384 - Err: err,
723 + HTTPStatus: http.StatusInternalServerError,
724 + Err: err,
725 }
726 }
387 - if apiErr.Code == 0 {
388 - apiErr.Code = http.StatusInternalServerError
727 + if apiErr.HTTPStatus == 0 {
728 + apiErr.HTTPStatus = http.StatusInternalServerError
729 }
730 if apiErr.Message == "" && apiErr.Err != nil {
731 apiErr.Message = apiErr.Err.Error()
@@ -393,11 +733,11 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
733
734 Log().Named("admin.api").Error("request error",
735 zap.Error(err),
396 - zap.Int("status_code", apiErr.Code),
736 + zap.Int("status_code", apiErr.HTTPStatus),
737 )
738
739 w.Header().Set("Content-Type", "application/json")
400 - w.WriteHeader(apiErr.Code)
740 + w.WriteHeader(apiErr.HTTPStatus)
741 encErr := json.NewEncoder(w).Encode(apiErr)
742 if encErr != nil {
743 Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
@@ -418,8 +758,8 @@ func (h adminHandler) checkHost(r *http.Request) error {
758 }
759 if !allowed {
760 return APIError{
421 - Code: http.StatusForbidden,
422 - Err: fmt.Errorf("host not allowed: %s", r.Host),
761 + HTTPStatus: http.StatusForbidden,
762 + Err: fmt.Errorf("host not allowed: %s", r.Host),
763 }
764 }
765 return nil
@@ -433,14 +773,14 @@ func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
773 origin := h.getOriginHost(r)
774 if origin == "" {
775 return origin, APIError{
436 - Code: http.StatusForbidden,
437 - Err: fmt.Errorf("missing required Origin header"),
776 + HTTPStatus: http.StatusForbidden,
777 + Err: fmt.Errorf("missing required Origin header"),
778 }
779 }
780 if !h.originAllowed(origin) {
781 return origin, APIError{
442 - Code: http.StatusForbidden,
443 - Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
782 + HTTPStatus: http.StatusForbidden,
783 + Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
784 }
785 }
786 return origin, nil
@@ -480,7 +820,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
820
821 err := readConfig(r.URL.Path, w)
822 if err != nil {
483 - return APIError{Code: http.StatusBadRequest, Err: err}
823 + return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
824 }
825
826 return nil
@@ -495,8 +835,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
835 if r.Method != http.MethodDelete {
836 if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
837 return APIError{
498 - Code: http.StatusBadRequest,
499 - Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
838 + HTTPStatus: http.StatusBadRequest,
839 + Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
840 }
841 }
842
@@ -507,8 +847,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
847 _, err := io.Copy(buf, r.Body)
848 if err != nil {
849 return APIError{
510 - Code: http.StatusBadRequest,
511 - Err: fmt.Errorf("reading request body: %v", err),
850 + HTTPStatus: http.StatusBadRequest,
851 + Err: fmt.Errorf("reading request body: %v", err),
852 }
853 }
854 body = buf.Bytes()
@@ -523,8 +863,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
863
864 default:
865 return APIError{
526 - Code: http.StatusMethodNotAllowed,
527 - Err: fmt.Errorf("method %s not allowed", r.Method),
866 + HTTPStatus: http.StatusMethodNotAllowed,
867 + Err: fmt.Errorf("method %s not allowed", r.Method),
868 }
869 }
870
@@ -555,46 +895,17 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
895 parts = append([]string{expanded}, parts[3:]...)
896 r.URL.Path = path.Join(parts...)
897
558 - return ErrInternalRedir
559 -}
560 -
561 -func handleStop(w http.ResponseWriter, r *http.Request) error {
562 - err := handleUnload(w, r)
563 - if err != nil {
564 - Log().Named("admin.api").Error("unload error", zap.Error(err))
565 - }
566 - if adminServer != nil {
567 - // use goroutine so that we can finish responding to API request
568 - go func() {
569 - err := stopAdminServer(adminServer)
570 - var exitCode int
571 - if err != nil {
572 - exitCode = ExitCodeFailedQuit
573 - Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
574 - }
575 - Log().Named("admin.api").Info("stopping now, bye!! 👋")
576 - os.Exit(exitCode)
577 - }()
578 - }
898 return nil
899 }
900
582 -// handleUnload stops the current configuration that is running.
583 -// Note that doing this can also be accomplished with DELETE /config/
584 -// but we leave this function because handleStop uses it.
585 -func handleUnload(w http.ResponseWriter, r *http.Request) error {
901 +func handleStop(w http.ResponseWriter, r *http.Request) error {
902 if r.Method != http.MethodPost {
903 return APIError{
588 - Code: http.StatusMethodNotAllowed,
589 - Err: fmt.Errorf("method not allowed"),
904 + HTTPStatus: http.StatusMethodNotAllowed,
905 + Err: fmt.Errorf("method not allowed"),
906 }
907 }
592 - Log().Named("admin.api").Info("unloading")
593 - if err := stopAndCleanup(); err != nil {
594 - Log().Named("admin.api").Error("error unloading", zap.Error(err))
595 - } else {
596 - Log().Named("admin.api").Info("unloading completed")
597 - }
908 + exitProcess(Log().Named("admin.api"))
909 return nil
910 }
911
@@ -806,9 +1117,9 @@ func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) erro
1117 // and client responses. If Message is unset, then
1118 // Err.Error() will be serialized in its place.
1119 type APIError struct {
809 - Code int `json:"-"`
810 - Err error `json:"-"`
811 - Message string `json:"error"`
1120 + HTTPStatus int `json:"-"`
1121 + Err error `json:"-"`
1122 + Message string `json:"error"`
1123 }
1124
1125 func (e APIError) Error() string {
@@ -818,20 +1129,44 @@ func (e APIError) Error() string {
1129 return e.Message
1130 }
1131
1132 +// parseAdminListenAddr extracts a singular listen address from either addr
1133 +// or defaultAddr, returning the network and the address of the listener.
1134 +func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
1135 + input := addr
1136 + if input == "" {
1137 + input = defaultAddr
1138 + }
1139 + listenAddr, err := ParseNetworkAddress(input)
1140 + if err != nil {
1141 + return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err)
1142 + }
1143 + if listenAddr.PortRangeSize() != 1 {
1144 + return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr)
1145 + }
1146 + return listenAddr, nil
1147 +}
1148 +
1149 +// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
1150 +func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
1151 + derBytes, err := base64.StdEncoding.DecodeString(certStr)
1152 + if err != nil {
1153 + return nil, err
1154 + }
1155 + return x509.ParseCertificate(derBytes)
1156 +}
1157 +
1158 var (
822 - // DefaultAdminListen is the address for the admin
1159 + // DefaultAdminListen is the address for the local admin
1160 // listener, if none is specified at startup.
1161 DefaultAdminListen = "localhost:2019"
1162
826 - // ErrInternalRedir indicates an internal redirect
827 - // and is useful when admin API handlers rewrite
828 - // the request; in that case, authentication and
829 - // authorization needs to happen again for the
830 - // rewritten request.
831 - ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
1163 + // DefaultRemoteAdminListen is the address for the remote
1164 + // (TLS-authenticated) admin listener, if enabled and not
1165 + // specified otherwise.
1166 + DefaultRemoteAdminListen = ":2021"
1167
1168 // DefaultAdminConfig is the default configuration
834 - // for the administration endpoint.
1169 + // for the local administration endpoint.
1170 DefaultAdminConfig = &AdminConfig{
1171 Listen: DefaultAdminListen,
1172 }
@@ -869,4 +1204,8 @@ var bufPool = sync.Pool{
1204 },
1205 }
1206
872 -var adminServer *http.Server
1207 +// keep a reference to admin endpoint singletons while they're active
1208 +var (
1209 + localAdminServer, remoteAdminServer *http.Server
1210 + identityCertCache *certmagic.Cache
1211 +)
caddy.go
+151 -25
@@ -130,8 +130,8 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
130 newCfg, err := json.Marshal(rawCfg[rawConfigKey])
131 if err != nil {
132 return APIError{
133 - Code: http.StatusBadRequest,
134 - Err: fmt.Errorf("encoding new config: %v", err),
133 + HTTPStatus: http.StatusBadRequest,
134 + Err: fmt.Errorf("encoding new config: %v", err),
135 }
136 }
137
@@ -146,14 +146,14 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
146 err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
147 if err != nil {
148 return APIError{
149 - Code: http.StatusInternalServerError,
150 - Err: fmt.Errorf("indexing config: %v", err),
149 + HTTPStatus: http.StatusInternalServerError,
150 + Err: fmt.Errorf("indexing config: %v", err),
151 }
152 }
153
154 // load this new config; if it fails, we need to revert to
155 // our old representation of caddy's actual config
156 - err = unsyncedDecodeAndRun(newCfg)
156 + err = unsyncedDecodeAndRun(newCfg, true)
157 if err != nil {
158 if len(rawCfgJSON) > 0 {
159 // restore old config state to keep it consistent
@@ -233,8 +233,10 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
233 // it as the new config, replacing any other current config.
234 // It does NOT update the raw config state, as this is a
235 // lower-level function; most callers will want to use Load
236 -// instead. A write lock on currentCfgMu is required!
237 -func unsyncedDecodeAndRun(cfgJSON []byte) error {
236 +// instead. A write lock on currentCfgMu is required! If
237 +// allowPersist is false, it will not be persisted to disk,
238 +// even if it is configured to.
239 +func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
240 // remove any @id fields from the JSON, which would cause
241 // loading to break since the field wouldn't be recognized
242 strippedCfgJSON := RemoveMetaFields(cfgJSON)
@@ -245,6 +247,19 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
247 return err
248 }
249
250 + // prevent recursive config loads; that is a user error, and
251 + // although frequent config loads should be safe, we cannot
252 + // guarantee that in the presence of third party plugins, nor
253 + // do we want this error to go unnoticed (we assume it was a
254 + // pulled config if we're not allowed to persist it)
255 + if !allowPersist &&
256 + newCfg != nil &&
257 + newCfg.Admin != nil &&
258 + newCfg.Admin.Config != nil &&
259 + newCfg.Admin.Config.LoadRaw != nil {
260 + return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs")
261 + }
262 +
263 // run the new config and start all its apps
264 err = run(newCfg, true)
265 if err != nil {
@@ -259,7 +274,8 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
274 unsyncedStop(oldCfg)
275
276 // autosave a non-nil config, if not disabled
262 - if newCfg != nil &&
277 + if allowPersist &&
278 + newCfg != nil &&
279 (newCfg.Admin == nil ||
280 newCfg.Admin.Config == nil ||
281 newCfg.Admin.Config.Persist == nil ||
@@ -311,14 +327,14 @@ func run(newCfg *Config, start bool) error {
327
328 // start the admin endpoint (and stop any prior one)
329 if start {
314 - err = replaceAdmin(newCfg)
330 + err = replaceLocalAdminServer(newCfg)
331 if err != nil {
332 return fmt.Errorf("starting caddy administration endpoint: %v", err)
333 }
334 }
335
336 if newCfg == nil {
321 - return nil
337 + newCfg = new(Config)
338 }
339
340 // prepare the new config for use
@@ -400,7 +416,7 @@ func run(newCfg *Config, start bool) error {
416 }
417
418 // Start
403 - return func() error {
419 + err = func() error {
420 var started []string
421 for name, a := range newCfg.apps {
422 err := a.Start()
@@ -420,6 +436,64 @@ func run(newCfg *Config, start bool) error {
436 }
437 return nil
438 }()
439 + if err != nil {
440 + return err
441 + }
442 +
443 + // now that the user's config is running, finish setting up anything else,
444 + // such as remote admin endpoint, config loader, etc.
445 + return finishSettingUp(ctx, newCfg)
446 +}
447 +
448 +// finishSettingUp should be run after all apps have successfully started.
449 +func finishSettingUp(ctx Context, cfg *Config) error {
450 + // establish this server's identity (only after apps are loaded
451 + // so that cert management of this endpoint doesn't prevent user's
452 + // servers from starting which likely also use HTTP/HTTPS ports;
453 + // but before remote management which may depend on these creds)
454 + err := manageIdentity(ctx, cfg)
455 + if err != nil {
456 + return fmt.Errorf("provisioning remote admin endpoint: %v", err)
457 + }
458 +
459 + // replace any remote admin endpoint
460 + err = replaceRemoteAdminServer(ctx, cfg)
461 + if err != nil {
462 + return fmt.Errorf("provisioning remote admin endpoint: %v", err)
463 + }
464 +
465 + // if dynamic config is requested, set that up and run it
466 + if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
467 + val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
468 + if err != nil {
469 + return fmt.Errorf("loading config loader module: %s", err)
470 + }
471 + loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
472 + if err != nil {
473 + return fmt.Errorf("loading dynamic config from %T: %v", val, err)
474 + }
475 +
476 + // do this in a goroutine so current config can finish being loaded; otherwise deadlock
477 + go func() {
478 + Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()))
479 + currentCfgMu.Lock()
480 + err := unsyncedDecodeAndRun(loadedConfig, false)
481 + currentCfgMu.Unlock()
482 + if err == nil {
483 + Log().Info("dynamically-loaded config applied successfully")
484 + } else {
485 + Log().Error("running dynamically-loaded config failed", zap.Error(err))
486 + }
487 + }()
488 + }
489 +
490 + return nil
491 +}
492 +
493 +// ConfigLoader is a type that can load a Caddy config. The
494 +// returned config must be valid Caddy JSON.
495 +type ConfigLoader interface {
496 + LoadConfig(Context) ([]byte, error)
497 }
498
499 // Stop stops running the current configuration.
@@ -462,20 +536,6 @@ func unsyncedStop(cfg *Config) {
536 cfg.cancelFunc()
537 }
538
465 -// stopAndCleanup calls stop and cleans up anything
466 -// else that is expedient. This should only be used
467 -// when stopping and not replacing with a new config.
468 -func stopAndCleanup() error {
469 - if err := Stop(); err != nil {
470 - return err
471 - }
472 - certmagic.CleanUpOwnLocks()
473 - if pidfile != "" {
474 - return os.Remove(pidfile)
475 - }
476 - return nil
477 -}
478 -
539 // Validate loads, provisions, and validates
540 // cfg, but does not start running it.
541 func Validate(cfg *Config) error {
@@ -486,6 +546,72 @@ func Validate(cfg *Config) error {
546 return err
547 }
548
549 +// exitProcess exits the process as gracefully as possible,
550 +// but it always exits, even if there are errors doing so.
551 +// It stops all apps, cleans up external locks, removes any
552 +// PID file, and shuts down admin endpoint(s) in a goroutine.
553 +// Errors are logged along the way, and an appropriate exit
554 +// code is emitted.
555 +func exitProcess(logger *zap.Logger) {
556 + if logger == nil {
557 + logger = Log()
558 + }
559 + logger.Warn("exiting; byeee!! 👋")
560 +
561 + exitCode := ExitCodeSuccess
562 +
563 + // stop all apps
564 + if err := Stop(); err != nil {
565 + logger.Error("failed to stop apps", zap.Error(err))
566 + exitCode = ExitCodeFailedQuit
567 + }
568 +
569 + // clean up certmagic locks
570 + certmagic.CleanUpOwnLocks(logger)
571 +
572 + // remove pidfile
573 + if pidfile != "" {
574 + err := os.Remove(pidfile)
575 + if err != nil {
576 + logger.Error("cleaning up PID file:",
577 + zap.String("pidfile", pidfile),
578 + zap.Error(err))
579 + exitCode = ExitCodeFailedQuit
580 + }
581 + }
582 +
583 + // shut down admin endpoint(s) in goroutines so that
584 + // if this function was called from an admin handler,
585 + // it has a chance to return gracefully
586 + // use goroutine so that we can finish responding to API request
587 + go func() {
588 + defer func() {
589 + logger = logger.With(zap.Int("exit_code", exitCode))
590 + if exitCode == ExitCodeSuccess {
591 + logger.Info("shutdown complete")
592 + } else {
593 + logger.Error("unclean shutdown")
594 + }
595 + os.Exit(exitCode)
596 + }()
597 +
598 + if remoteAdminServer != nil {
599 + err := stopAdminServer(remoteAdminServer)
600 + if err != nil {
601 + exitCode = ExitCodeFailedQuit
602 + logger.Error("failed to stop remote admin server gracefully", zap.Error(err))
603 + }
604 + }
605 + if localAdminServer != nil {
606 + err := stopAdminServer(localAdminServer)
607 + if err != nil {
608 + exitCode = ExitCodeFailedQuit
609 + logger.Error("failed to stop local admin server gracefully", zap.Error(err))
610 + }
611 + }
612 + }()
613 +}
614 +
615 // Duration can be an integer or a string. An integer is
616 // interpreted as nanoseconds. If a string, it is a Go
617 // time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
caddyconfig/configadapters.go
+8
@@ -35,6 +35,14 @@ type Warning struct {
35 Message string `json:"message,omitempty"`
36 }
37
38 +func (w Warning) String() string {
39 + var directive string
40 + if w.Directive != "" {
41 + directive = fmt.Sprintf(" (%s)", w.Directive)
42 + }
43 + return fmt.Sprintf("%s:%d%s: %s", w.File, w.Line, directive, w.Message)
44 +}
45 +
46 // JSON encodes val as JSON, returning it as a json.RawMessage. Any
47 // marshaling errors (which are highly unlikely with correct code)
48 // are converted to warnings. This is convenient when filling config
caddyconfig/httploader.go new
+151
@@ -0,0 +1,151 @@
1 +package caddyconfig
2 +
3 +import (
4 + "crypto/tls"
5 + "crypto/x509"
6 + "fmt"
7 + "io/ioutil"
8 + "net/http"
9 + "time"
10 +
11 + "github.com/caddyserver/caddy/v2"
12 +)
13 +
14 +func init() {
15 + caddy.RegisterModule(HTTPLoader{})
16 +}
17 +
18 +// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
19 +// based on the Content-Type header of the HTTP response.
20 +type HTTPLoader struct {
21 + // The method for the request. Default: GET
22 + Method string `json:"method,omitempty"`
23 +
24 + // The URL of the request.
25 + URL string `json:"url,omitempty"`
26 +
27 + // HTTP headers to add to the request.
28 + Headers http.Header `json:"header,omitempty"`
29 +
30 + // Maximum time allowed for a complete connection and request.
31 + Timeout caddy.Duration `json:"timeout,omitempty"`
32 +
33 + TLS *struct {
34 + // Present this instance's managed remote identity credentials to the server.
35 + UseServerIdentity bool `json:"use_server_identity,omitempty"`
36 +
37 + // PEM-encoded client certificate filename to present to the server.
38 + ClientCertificateFile string `json:"client_certificate_file,omitempty"`
39 +
40 + // PEM-encoded key to use with the client certificate.
41 + ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
42 +
43 + // List of PEM-encoded CA certificate files to add to the same trust
44 + // store as RootCAPool (or root_ca_pool in the JSON).
45 + RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
46 + } `json:"tls,omitempty"`
47 +}
48 +
49 +// CaddyModule returns the Caddy module information.
50 +func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
51 + return caddy.ModuleInfo{
52 + ID: "caddy.config_loaders.http",
53 + New: func() caddy.Module { return new(HTTPLoader) },
54 + }
55 +}
56 +
57 +// LoadConfig loads a Caddy config.
58 +func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
59 + client, err := hl.makeClient(ctx)
60 + if err != nil {
61 + return nil, err
62 + }
63 +
64 + method := hl.Method
65 + if method == "" {
66 + method = http.MethodGet
67 + }
68 +
69 + req, err := http.NewRequest(method, hl.URL, nil)
70 + if err != nil {
71 + return nil, err
72 + }
73 + req.Header = hl.Headers
74 +
75 + resp, err := client.Do(req)
76 + if err != nil {
77 + return nil, err
78 + }
79 + defer resp.Body.Close()
80 + if resp.StatusCode >= 400 {
81 + return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
82 + }
83 +
84 + body, err := ioutil.ReadAll(resp.Body)
85 + if err != nil {
86 + return nil, err
87 + }
88 +
89 + result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
90 + if err != nil {
91 + return nil, err
92 + }
93 + for _, warn := range warnings {
94 + ctx.Logger(hl).Warn(warn.String())
95 + }
96 +
97 + return result, nil
98 +}
99 +
100 +func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
101 + client := &http.Client{
102 + Timeout: time.Duration(hl.Timeout),
103 + }
104 +
105 + if hl.TLS != nil {
106 + var tlsConfig *tls.Config
107 +
108 + // client authentication
109 + if hl.TLS.UseServerIdentity {
110 + certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
111 + if err != nil {
112 + return nil, fmt.Errorf("getting server identity credentials: %v", err)
113 + }
114 + if tlsConfig == nil {
115 + tlsConfig = new(tls.Config)
116 + }
117 + tlsConfig.Certificates = certs
118 + } else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
119 + cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
120 + if err != nil {
121 + return nil, err
122 + }
123 + if tlsConfig == nil {
124 + tlsConfig = new(tls.Config)
125 + }
126 + tlsConfig.Certificates = []tls.Certificate{cert}
127 + }
128 +
129 + // trusted server certs
130 + if len(hl.TLS.RootCAPEMFiles) > 0 {
131 + rootPool := x509.NewCertPool()
132 + for _, pemFile := range hl.TLS.RootCAPEMFiles {
133 + pemData, err := ioutil.ReadFile(pemFile)
134 + if err != nil {
135 + return nil, fmt.Errorf("failed reading ca cert: %v", err)
136 + }
137 + rootPool.AppendCertsFromPEM(pemData)
138 + }
139 + if tlsConfig == nil {
140 + tlsConfig = new(tls.Config)
141 + }
142 + tlsConfig.RootCAs = rootPool
143 + }
144 +
145 + client.Transport = &http.Transport{TLSClientConfig: tlsConfig}
146 + }
147 +
148 + return client, nil
149 +}
150 +
151 +var _ caddy.ConfigLoader = (*HTTPLoader)(nil)
caddyconfig/load.go
+55 -38
@@ -69,8 +69,8 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
69 func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
70 if r.Method != http.MethodPost {
71 return caddy.APIError{
72 - Code: http.StatusMethodNotAllowed,
73 - Err: fmt.Errorf("method not allowed"),
72 + HTTPStatus: http.StatusMethodNotAllowed,
73 + Err: fmt.Errorf("method not allowed"),
74 }
75 }
76
@@ -81,8 +81,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
81 _, err := io.Copy(buf, r.Body)
82 if err != nil {
83 return caddy.APIError{
84 - Code: http.StatusBadRequest,
85 - Err: fmt.Errorf("reading request body: %v", err),
84 + HTTPStatus: http.StatusBadRequest,
85 + Err: fmt.Errorf("reading request body: %v", err),
86 }
87 }
88 body := buf.Bytes()
@@ -90,45 +90,21 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
90 // if the config is formatted other than Caddy's native
91 // JSON, we need to adapt it before loading it
92 if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
93 - ct, _, err := mime.ParseMediaType(ctHeader)
93 + result, warnings, err := adaptByContentType(ctHeader, body)
94 if err != nil {
95 return caddy.APIError{
96 - Code: http.StatusBadRequest,
97 - Err: fmt.Errorf("invalid Content-Type: %v", err),
96 + HTTPStatus: http.StatusBadRequest,
97 + Err: err,
98 }
99 }
100 - if !strings.HasSuffix(ct, "/json") {
101 - slashIdx := strings.Index(ct, "/")
102 - if slashIdx < 0 {
103 - return caddy.APIError{
104 - Code: http.StatusBadRequest,
105 - Err: fmt.Errorf("malformed Content-Type"),
106 - }
107 - }
108 - adapterName := ct[slashIdx+1:]
109 - cfgAdapter := GetAdapter(adapterName)
110 - if cfgAdapter == nil {
111 - return caddy.APIError{
112 - Code: http.StatusBadRequest,
113 - Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
114 - }
115 - }
116 - result, warnings, err := cfgAdapter.Adapt(body, nil)
100 + if len(warnings) > 0 {
101 + respBody, err := json.Marshal(warnings)
102 if err != nil {
118 - return caddy.APIError{
119 - Code: http.StatusBadRequest,
120 - Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
121 - }
122 - }
123 - if len(warnings) > 0 {
124 - respBody, err := json.Marshal(warnings)
125 - if err != nil {
126 - caddy.Log().Named("admin.api.load").Error(err.Error())
127 - }
128 - _, _ = w.Write(respBody)
103 + caddy.Log().Named("admin.api.load").Error(err.Error())
104 }
130 - body = result
105 + _, _ = w.Write(respBody)
106 }
107 + body = result
108 }
109
110 forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
@@ -136,8 +112,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
112 err = caddy.Load(body, forceReload)
113 if err != nil {
114 return caddy.APIError{
139 - Code: http.StatusBadRequest,
140 - Err: fmt.Errorf("loading config: %v", err),
115 + HTTPStatus: http.StatusBadRequest,
116 + Err: fmt.Errorf("loading config: %v", err),
117 }
118 }
119
@@ -146,6 +122,47 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
122 return nil
123 }
124
125 +// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
126 +// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
127 +func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
128 + // assume JSON as the default
129 + if contentType == "" {
130 + return body, nil, nil
131 + }
132 +
133 + ct, _, err := mime.ParseMediaType(contentType)
134 + if err != nil {
135 + return nil, nil, caddy.APIError{
136 + HTTPStatus: http.StatusBadRequest,
137 + Err: fmt.Errorf("invalid Content-Type: %v", err),
138 + }
139 + }
140 +
141 + // if already JSON, no need to adapt
142 + if strings.HasSuffix(ct, "/json") {
143 + return body, nil, nil
144 + }
145 +
146 + // adapter name should be suffix of MIME type
147 + slashIdx := strings.Index(ct, "/")
148 + if slashIdx < 0 {
149 + return nil, nil, fmt.Errorf("malformed Content-Type")
150 + }
151 +
152 + adapterName := ct[slashIdx+1:]
153 + cfgAdapter := GetAdapter(adapterName)
154 + if cfgAdapter == nil {
155 + return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
156 + }
157 +
158 + result, warnings, err := cfgAdapter.Adapt(body, nil)
159 + if err != nil {
160 + return nil, nil, fmt.Errorf("adapting config using %s adapter: %v", adapterName, err)
161 + }
162 +
163 + return result, warnings, nil
164 +}
165 +
166 var bufPool = sync.Pool{
167 New: func() interface{} {
168 return new(bytes.Buffer)
go.mod
+2 -2
@@ -6,14 +6,14 @@ require (
6 github.com/Masterminds/sprig/v3 v3.1.0
7 github.com/alecthomas/chroma v0.8.2
8 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
9 - github.com/caddyserver/certmagic v0.12.1-0.20210107224522-725b69d53d57
9 + github.com/caddyserver/certmagic v0.12.1-0.20210126230115-267fdad76a0f
10 github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
11 github.com/go-chi/chi v4.1.2+incompatible
12 github.com/google/cel-go v0.6.0
13 github.com/klauspost/compress v1.11.3
14 github.com/klauspost/cpuid/v2 v2.0.1
15 github.com/lucas-clemente/quic-go v0.19.3
16 - github.com/mholt/acmez v0.1.2
16 + github.com/mholt/acmez v0.1.3
17 github.com/naoina/go-stringutil v0.1.0 // indirect
18 github.com/naoina/toml v0.1.1
19 github.com/prometheus/client_golang v1.9.0
go.sum
+5 -7
@@ -99,8 +99,8 @@ github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTK
99 github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
100 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
101 github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
102 -github.com/caddyserver/certmagic v0.12.1-0.20210107224522-725b69d53d57 h1:eslWGgoQlVAzOGMUfK3ncoHnONjCUVOPTGRD9JG3gAY=
103 -github.com/caddyserver/certmagic v0.12.1-0.20210107224522-725b69d53d57/go.mod h1:yHMCSjG2eOFdI/Jx0+CCzr2DLw+UQu42KbaOVBx7LwA=
102 +github.com/caddyserver/certmagic v0.12.1-0.20210126230115-267fdad76a0f h1:uJoft/gLxPvKq+ojfq3k7w8deji/xt/1RSWN7OAk6Ng=
103 +github.com/caddyserver/certmagic v0.12.1-0.20210126230115-267fdad76a0f/go.mod h1:CUPfwomVXGCyV77EQbR3v7H4tGJ4pX16HATeR55rqws=
104 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
105 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
106 github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -175,6 +175,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
175 github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
176 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
177 github.com/fatih/color v1.8.0/go.mod h1:3l45GVGkyrnYNl9HoIjnp2NnNWvh6hLAqD8yTfGjnw8=
178 +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
179 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
180 github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
181 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
@@ -379,8 +380,6 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
380 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
381 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
382 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
382 -github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
383 -github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
383 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
384 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
385 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
@@ -459,8 +458,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
458 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
459 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
460 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
462 -github.com/mholt/acmez v0.1.2 h1:26ncYNBt59D+59cMUHuGa/Fzjmu6FFrBm6kk/8hdXt0=
463 -github.com/mholt/acmez v0.1.2/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
461 +github.com/mholt/acmez v0.1.3 h1:J7MmNIk4Qf9b8mAGqAh4XkNeowv3f1zW816yf4zt7Qk=
462 +github.com/mholt/acmez v0.1.3/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
463 github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
464 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
465 github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
@@ -785,7 +784,6 @@ go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
784 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
785 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
786 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
788 -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
787 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
788 go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
789 go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
modules/caddytls/acmeissuer.go
+20 -1
@@ -59,6 +59,13 @@ type ACMEIssuer struct {
59 // other than ACME transactions.
60 Email string `json:"email,omitempty"`
61
62 + // If you have an existing account with the ACME server, put
63 + // the private key here in PEM format. The ACME client will
64 + // look up your account information with this key first before
65 + // trying to create a new one. You can use placeholders here,
66 + // for example if you have it in an environment variable.
67 + AccountKey string `json:"account_key,omitempty"`
68 +
69 // If using an ACME CA that requires an external account
70 // binding, specify the CA-provided credentials here.
71 ExternalAccount *acme.EAB `json:"external_account,omitempty"`
@@ -98,15 +105,26 @@ func (ACMEIssuer) CaddyModule() caddy.ModuleInfo {
105 func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
106 iss.logger = ctx.Logger(iss)
107
108 + repl := caddy.NewReplacer()
109 +
110 // expand email address, if non-empty
111 if iss.Email != "" {
103 - email, err := caddy.NewReplacer().ReplaceOrErr(iss.Email, true, true)
112 + email, err := repl.ReplaceOrErr(iss.Email, true, true)
113 if err != nil {
114 return fmt.Errorf("expanding email address '%s': %v", iss.Email, err)
115 }
116 iss.Email = email
117 }
118
119 + // expand account key, if non-empty
120 + if iss.AccountKey != "" {
121 + accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true)
122 + if err != nil {
123 + return fmt.Errorf("expanding account key PEM '%s': %v", iss.AccountKey, err)
124 + }
125 + iss.AccountKey = accountKey
126 + }
127 +
128 // DNS providers
129 if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil {
130 val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw")
@@ -161,6 +179,7 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEManager, error) {
179 CA: iss.CA,
180 TestCA: iss.TestCA,
181 Email: iss.Email,
182 + AccountKeyPEM: iss.AccountKey,
183 CertObtainTimeout: time.Duration(iss.ACMETimeout),
184 TrustedRoots: iss.rootPool,
185 ExternalAccount: iss.ExternalAccount,
modules/caddytls/tls.go
+12
@@ -306,9 +306,11 @@ func (t *TLS) Manage(names []string) error {
306 // requires that the automation policy for r.Host has an issuer of type
307 // *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()).
308 func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
309 + // no-op if it's not an ACME challenge request
310 if !certmagic.LooksLikeHTTPChallenge(r) {
311 return false
312 }
313 +
314 // try all the issuers until we find the one that initiated the challenge
315 ap := t.getAutomationPolicyForName(r.Host)
316 type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
@@ -320,6 +322,16 @@ func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
322 }
323 }
324 }
325 +
326 + // it's possible another server in this process initiated the challenge;
327 + // users have requested that Caddy only handle HTTP challenges it initiated,
328 + // so that users can proxy the others through to their backends; but we
329 + // might not have an automation policy for all identifiers that are trying
330 + // to get certificates (e.g. the admin endpoint), so we do this manual check
331 + if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
332 + return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
333 + }
334 +
335 return false
336 }
337
sigtrap.go
+5 -27
@@ -47,37 +47,15 @@ func trapSignalsCrossPlatform() {
47 }
48
49 Log().Info("shutting down", zap.String("signal", "SIGINT"))
50 - go gracefulStop("SIGINT")
50 + go exitProcessFromSignal("SIGINT")
51 }
52 }()
53 }
54
55 -// gracefulStop exits the process as gracefully as possible.
56 -// It always exits, even if there are errors shutting down.
57 -func gracefulStop(sigName string) {
58 - exitCode := ExitCodeSuccess
59 - defer func() {
60 - Log().Info("shutdown done", zap.String("signal", sigName))
61 - os.Exit(exitCode)
62 - }()
63 -
64 - err := stopAndCleanup()
65 - if err != nil {
66 - Log().Error("stopping config",
67 - zap.String("signal", sigName),
68 - zap.Error(err))
69 - exitCode = ExitCodeFailedQuit
70 - }
71 -
72 - if adminServer != nil {
73 - err = stopAdminServer(adminServer)
74 - if err != nil {
75 - Log().Error("stopping admin endpoint",
76 - zap.String("signal", sigName),
77 - zap.Error(err))
78 - exitCode = ExitCodeFailedQuit
79 - }
80 - }
55 +// exitProcessFromSignal exits the process from a system signal.
56 +func exitProcessFromSignal(sigName string) {
57 + logger := Log().With(zap.String("signal", sigName))
58 + exitProcess(logger)
59 }
60
61 // Exit codes. Generally, you should NOT
sigtrap_posix.go
+2 -2
@@ -35,12 +35,12 @@ func trapSignalsPosix() {
35 switch sig {
36 case syscall.SIGQUIT:
37 Log().Info("quitting process immediately", zap.String("signal", "SIGQUIT"))
38 - certmagic.CleanUpOwnLocks() // try to clean up locks anyway, it's important
38 + certmagic.CleanUpOwnLocks(Log()) // try to clean up locks anyway, it's important
39 os.Exit(ExitCodeForceQuit)
40
41 case syscall.SIGTERM:
42 Log().Info("shutting down apps then terminating", zap.String("signal", "SIGTERM"))
43 - gracefulStop("SIGTERM")
43 + exitProcessFromSignal("SIGTERM")
44
45 case syscall.SIGUSR1:
46 Log().Info("not implemented", zap.String("signal", "SIGUSR1"))