diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go index 72eb77063ae..5d26c8b9ffa 100644 --- a/modules/caddytls/capools.go +++ b/modules/caddytls/capools.go @@ -1,10 +1,14 @@ package caddytls import ( + "crypto/tls" "crypto/x509" "encoding/json" "fmt" + "io" + "net/http" "os" + "reflect" "github.com/caddyserver/certmagic" @@ -19,10 +23,13 @@ func init() { caddy.RegisterModule(StoragePool{}) } +// The interface to be implemented by all guest modules part of +// the namespace 'tls.ca_pool.source.' type CA interface { CertPool() *x509.CertPool } +// FileCAPool generates trusted root certificates pool from the designated DER and PEM file type FileCAPool struct { // A list of base64 DER-encoded CA certificates // against which to validate client certificates. @@ -42,13 +49,14 @@ type FileCAPool struct { // CaddyModule implements caddy.Module. func (FileCAPool) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "tls.client_auth.ca.file", + ID: "tls.ca_pool.source.file", New: func() caddy.Module { return new(FileCAPool) }, } } +// Loads and decodes the DER and pem files to generate the certificate pool func (f *FileCAPool) Provision(ctx caddy.Context) error { caPool := x509.NewCertPool() for _, clientCAString := range f.TrustedCACerts { @@ -73,8 +81,11 @@ func (f FileCAPool) CertPool() *x509.CertPool { return f.pool } +// PKIRootCAPool extracts the trusted root certificates from Caddy's native 'pki' app type PKIRootCAPool struct { - CA []string `json:"ca,omitempty"` + // List of the CA names that are configured in the `pki` app whose root certificates are trusted + CA []string `json:"ca,omitempty"` + ca []*caddypki.CA pool *x509.CertPool } @@ -82,14 +93,14 @@ type PKIRootCAPool struct { // CaddyModule implements caddy.Module. func (PKIRootCAPool) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "tls.client_auth.ca.pki_root", + ID: "tls.ca_pool.source.pki_root", New: func() caddy.Module { return new(PKIRootCAPool) }, } } -// Provision implements caddy.Provisioner. +// Loads the PKI app and load the root certificates into the certificate pool func (p *PKIRootCAPool) Provision(ctx caddy.Context) error { pkiApp := ctx.AppIfConfigured("pki") if pkiApp == nil { @@ -113,12 +124,16 @@ func (p *PKIRootCAPool) Provision(ctx caddy.Context) error { return nil } +// return the certificate pool generated with root certificates from the PKI app func (p PKIRootCAPool) CertPool() *x509.CertPool { return p.pool } +// PKIIntermediateCAPool extracts the trusted intermediate certificates from Caddy's native 'pki' app type PKIIntermediateCAPool struct { - CA []string `json:"ca,omitempty"` + // List of the CA names that are configured in the `pki` app whose intermediate certificates are trusted + CA []string `json:"ca,omitempty"` + ca []*caddypki.CA pool *x509.CertPool } @@ -126,14 +141,14 @@ type PKIIntermediateCAPool struct { // CaddyModule implements caddy.Module. func (PKIIntermediateCAPool) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "tls.client_auth.ca.pki_intermediate", + ID: "tls.ca_pool.source.pki_intermediate", New: func() caddy.Module { return new(PKIIntermediateCAPool) }, } } -// Provision implements caddy.Provisioner. +// Loads the PKI app and load the intermediate certificates into the certificate pool func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error { pkiApp := ctx.AppIfConfigured("pki") if pkiApp == nil { @@ -157,21 +172,28 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error { return nil } +// return the certificate pool generated with intermediate certificates from the PKI app func (p PKIIntermediateCAPool) CertPool() *x509.CertPool { return p.pool } +// StoragePool extracts the trusted certificates root from Caddy storage type StoragePool struct { + // The storage module where the trusted root certificates are stored. Absent + // explicit storage implies the use of Caddy default storage. StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` - PEMKeys []string `json:"pem_keys,omitempty"` - storage certmagic.Storage - pool *x509.CertPool + + // The storage key/index to the location of the certificates + PEMKeys []string `json:"pem_keys,omitempty"` + + storage certmagic.Storage + pool *x509.CertPool } // CaddyModule implements caddy.Module. func (StoragePool) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "tls.client_auth.ca.storage", + ID: "tls.ca_pool.source.storage", New: func() caddy.Module { return new(StoragePool) }, @@ -214,6 +236,211 @@ func (p StoragePool) CertPool() *x509.CertPool { return p.pool } +// TLSConfig holds configuration related to the TLS configuration for the +// transport/client. +// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go +type TLSConfig struct { + // Provides the guest module that provides the trusted certificate authority (CA) certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + + // If true, TLS verification of server certificates will be disabled. + // This is insecure and may be removed in the future. Do not use this + // option except in testing or local development environments. + InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"` + + // The duration to allow a TLS handshake to a server. Default: No timeout. + HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"` + + // The server name used when verifying the certificate received in the TLS + // handshake. By default, this will use the upstream address' host part. + // You only need to override this if your upstream address does not match the + // certificate the upstream is likely to use. For example if the upstream + // address is an IP address, then you would need to configure this to the + // hostname being served by the upstream server. Currently, this does not + // support placeholders because the TLS config is not provisioned on each + // connection, so a static value must be used. + ServerName string `json:"server_name,omitempty"` + + // TLS renegotiation level. TLS renegotiation is the act of performing + // subsequent handshakes on a connection after the first. + // The level can be: + // - "never": (the default) disables renegotiation. + // - "once": allows a remote server to request renegotiation once per connection. + // - "freely": allows a remote server to repeatedly request renegotiation. + Renegotiation string `json:"renegotiation,omitempty"` +} + +// MakeTLSClientConfig returns a tls.Config usable by a client to a backend. +// If there is no custom TLS configuration, a nil config may be returned. +// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go +func (t TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { + cfg := new(tls.Config) + + if t.CARaw != nil { + caRaw, err := ctx.LoadModule(t, "CARaw") + if err != nil { + return nil, err + } + ca := caRaw.(CA) + cfg.RootCAs = ca.CertPool() + } + + // Renegotiation + switch t.Renegotiation { + case "never", "": + cfg.Renegotiation = tls.RenegotiateNever + case "once": + cfg.Renegotiation = tls.RenegotiateOnceAsClient + case "freely": + cfg.Renegotiation = tls.RenegotiateFreelyAsClient + default: + return nil, fmt.Errorf("invalid TLS renegotiation level: %v", t.Renegotiation) + } + + // override for the server name used verify the TLS handshake + cfg.ServerName = t.ServerName + + // throw all security out the window + cfg.InsecureSkipVerify = t.InsecureSkipVerify + + // only return a config if it's not empty + if reflect.DeepEqual(cfg, new(tls.Config)) { + return nil, nil + } + + return cfg, nil +} + +// The HTTPCertPool fetches the trusted root certificates from HTTP(S) +// endpoints. The TLS connection properties can be customized, including custom +// trusted root certificate. One example usage of this module is to get the trusted +// certificates from another Caddy instance that is running the PKI app and ACME server. +type HTTPCertPool struct { + // the list of URLs that respond with PEM-encoded certificates to trust. + Endpoints []string `json:"endpoints,omitempty"` + + // Customize the TLS connection knobs to used during the HTTP call + TLS *TLSConfig `json:"tls,omitempty"` + + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (*HTTPCertPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.http", + New: func() caddy.Module { + return new(StoragePool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error { + caPool := x509.NewCertPool() + + customTransport := http.DefaultTransport.(*http.Transport).Clone() + if hcp.TLS != nil { + tlsConfig, err := hcp.TLS.makeTLSClientConfig(ctx) + if err != nil { + return err + } + customTransport.TLSClientConfig = tlsConfig + } + + var httpClient *http.Client + *httpClient = *http.DefaultClient + httpClient.Transport = customTransport + + for _, uri := range hcp.Endpoints { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return err + } + res, err := httpClient.Do(req) + if err != nil { + return err + } + pembs, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return err + } + if !caPool.AppendCertsFromPEM(pembs) { + return fmt.Errorf("failed to add certs from URL: %s", uri) + } + } + hcp.pool = caPool + return nil +} + +// CertPool return the certificate pool generated from the HTTP responses +func (hcp HTTPCertPool) CertPool() *x509.CertPool { + return hcp.pool +} + +// LazyCertPool defers the generation of the certificate pool from the +// guest module to demand-time rather than at provisionig time. The gain of the +// lazy load adds a risk of failure to load the certificates at demand time +// because the validation that's typically done at provisioning is deferred. +// The validation can be enforced to run before runtime by setting +// `EagerValidation`/`eager_validation` to `true`. It is the operator's responsibility +// to ensure the resources are available if `EagerValidation`/`eager_validation` +// is set to `true`. The module also incurs performance cost at every demand. +type LazyCertPool struct { + // Provides the guest module that provides the trusted certificate authority (CA) certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + + // Whether the validation step should try to load and provision the guest module to validate + // the correctness of the configuration. Depeneding on the type of the guest module, + // the resources may not be available at validation time. It is the + // operator's responsibility to ensure the resources are available if `EagerValidation`/`eager_validation` + // is set to `true`. + EagerValidation bool `json:"eager_validation,omitempty"` + + ctx caddy.Context +} + +// CaddyModule implements caddy.Module. +func (*LazyCertPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.lazy", + New: func() caddy.Module { + return new(LazyCertPool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (lcp *LazyCertPool) Provision(ctx caddy.Context) error { + lcp.ctx = ctx + return nil +} + +// If EagerValidation is `true`, it attempts to load and provision the guest module +// to ensure the guesst module's configuration is correct. Depeneding on the type of the +// guest module, the resources may not be available at validation time. It is the +// operator's responsibility to ensure the resources are available if `EagerValidation` is +// set to `true`. +func (lcp *LazyCertPool) Validate() error { + if lcp.EagerValidation { + _, err := lcp.ctx.LoadModule(lcp, "CARaw") + return err + } + return nil +} + +// CertPool loads the guest module and returns the CertPool from there +// TODO: Cache? +func (lcp *LazyCertPool) CertPool() *x509.CertPool { + caRaw, err := lcp.ctx.LoadModule(lcp, "CARaw") + if err != nil { + return nil + } + ca := caRaw.(CA) + return ca.CertPool() +} + var ( _ caddy.Module = (*FileCAPool)(nil) _ caddy.Provisioner = (*FileCAPool)(nil) @@ -230,4 +457,13 @@ var ( _ caddy.Module = (*StoragePool)(nil) _ caddy.Provisioner = (*StoragePool)(nil) _ CA = (*StoragePool)(nil) + + _ caddy.Module = (*HTTPCertPool)(nil) + _ caddy.Provisioner = (*HTTPCertPool)(nil) + _ CA = (*HTTPCertPool)(nil) + + _ caddy.Module = (*LazyCertPool)(nil) + _ caddy.Provisioner = (*LazyCertPool)(nil) + _ caddy.Validator = (*LazyCertPool)(nil) + _ CA = (*LazyCertPool)(nil) ) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index d0bb15555e2..4d714e233df 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -356,13 +356,15 @@ func (p ConnectionPolicy) SettingsEmpty() bool { // ClientAuthentication configures TLS client auth. type ClientAuthentication struct { - CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.client_auth.ca inline_key=provider"` + // Certificate authority module which provides the certificate pool of trusted certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` ca CA // A list of base64 DER-encoded CA certificates // against which to validate client certificates. // Client certs which are not signed by any of // these CAs will be rejected. + // Deprecated: Use the `ca` field. TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` // TrustedCACertPEMFiles is a list of PEM file names