185 lines
9.2 KiB
Go
185 lines
9.2 KiB
Go
package configtypes
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// TLSConfig is a common configuration for TLS.
|
|
type TLSConfig struct {
|
|
// Enabled turns on using TLS.
|
|
Enabled bool `mapstructure:"enabled" json:"enabled" yaml:"enabled" toml:"enabled" envconfig:"enabled" doc:"Enables TLS for this entity."`
|
|
// CertPem is a PEM certificate.
|
|
CertPem PEMData `mapstructure:"cert_pem" json:"cert_pem" envconfig:"cert_pem" yaml:"cert_pem" toml:"cert_pem" expose:"full" doc:"Certificate in PEM format. May be provided as a file path, raw PEM string, or base64-encoded PEM."`
|
|
// KeyPem is a path to a file with key in PEM format.
|
|
KeyPem PEMData `mapstructure:"key_pem" json:"key_pem" envconfig:"key_pem" yaml:"key_pem" toml:"key_pem" doc:"Private key in PEM format matching the certificate. May be provided as a file path, raw PEM string, or base64-encoded PEM."`
|
|
// ServerCAPem is a server root CA certificate in PEM format.
|
|
// The client uses this certificate to verify the server's certificate during the TLS handshake.
|
|
ServerCAPem PEMData `mapstructure:"server_ca_pem" json:"server_ca_pem" envconfig:"server_ca_pem" yaml:"server_ca_pem" toml:"server_ca_pem" expose:"full" doc:"Root CA certificate in PEM format used to verify the server's certificate during the handshake (client side). May be a file path, raw PEM, or base64-encoded PEM."`
|
|
// ClientCAPem is a client CA certificate in PEM format.
|
|
// The server uses this certificate to verify the client's certificate during the TLS handshake.
|
|
ClientCAPem PEMData `mapstructure:"client_ca_pem" json:"client_ca_pem" envconfig:"client_ca_pem" yaml:"client_ca_pem" toml:"client_ca_pem" expose:"full" doc:"CA certificate in PEM format used to verify client certificates (server side). Setting it enables mutual TLS and requires clients to present a valid certificate. May be a file path, raw PEM, or base64-encoded PEM."`
|
|
// InsecureSkipVerify turns off server certificate verification.
|
|
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify" json:"insecure_skip_verify" envconfig:"insecure_skip_verify" yaml:"insecure_skip_verify" toml:"insecure_skip_verify" doc:"Disables verification of the server's certificate chain and host name. Insecure - use only for testing."`
|
|
// ServerName is used to verify the hostname on the returned certificates.
|
|
ServerName string `mapstructure:"server_name" json:"server_name" envconfig:"server_name" yaml:"server_name" toml:"server_name" expose:"full" doc:"Expected server name (SNI) used to verify the hostname on the returned certificate."`
|
|
}
|
|
|
|
func (c TLSConfig) ToGoTLSConfig(logTraceEntity string) (*tls.Config, error) {
|
|
if !c.Enabled {
|
|
return nil, nil
|
|
}
|
|
logger := log.With().Str("entity", logTraceEntity).Logger()
|
|
logger.Debug().Msg("TLS enabled")
|
|
tlsConfig, err := makeTLSConfig(c, logger, os.ReadFile, os.Stat)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error make TLS config (for %s): %w", logTraceEntity, err)
|
|
}
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// ReadFileFunc is like os.ReadFile but helps in testing.
|
|
type ReadFileFunc func(name string) ([]byte, error)
|
|
|
|
// StatFileFunc is like os.Stat but helps in testing.
|
|
type StatFileFunc func(name string) (os.FileInfo, error)
|
|
|
|
// makeTLSConfig constructs a tls.Config instance using the given configuration.
|
|
func makeTLSConfig(cfg TLSConfig, logger zerolog.Logger, readFile ReadFileFunc, statFile StatFileFunc) (*tls.Config, error) {
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
if err := loadCertificate(cfg, logger, tlsConfig, readFile, statFile); err != nil {
|
|
return nil, fmt.Errorf("error load certificate: %w", err)
|
|
}
|
|
if err := loadServerCA(cfg, logger, tlsConfig, readFile, statFile); err != nil {
|
|
return nil, fmt.Errorf("error load server CA: %w", err)
|
|
}
|
|
if err := loadClientCA(cfg, logger, tlsConfig, readFile, statFile); err != nil {
|
|
return nil, fmt.Errorf("error load client CA: %w", err)
|
|
}
|
|
tlsConfig.ServerName = cfg.ServerName
|
|
tlsConfig.InsecureSkipVerify = cfg.InsecureSkipVerify
|
|
logger.Debug().Str("server_name", cfg.ServerName).Bool("insecure_skip_verify", cfg.InsecureSkipVerify).Msg("TLS config options set")
|
|
logger.Debug().Msg("TLS config created")
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// loadCertificate loads the TLS certificate from various sources.
|
|
func loadCertificate(cfg TLSConfig, logger zerolog.Logger, tlsConfig *tls.Config, readFile ReadFileFunc, statFile StatFileFunc) error {
|
|
var certPEMBlock, keyPEMBlock []byte
|
|
var err error
|
|
|
|
switch {
|
|
case cfg.CertPem != "" && cfg.KeyPem != "":
|
|
var pemSource string
|
|
certPEMBlock, pemSource, err = cfg.CertPem.Load(statFile, readFile)
|
|
if err != nil {
|
|
return fmt.Errorf("load TLS certificate: %w", err)
|
|
}
|
|
logger.Debug().Str("pem_source", pemSource).Msg("loaded PEM certificate")
|
|
keyPEMBlock, pemSource, err = cfg.KeyPem.Load(statFile, readFile)
|
|
if err != nil {
|
|
return fmt.Errorf("load TLS key: %w", err)
|
|
}
|
|
logger.Debug().Str("pem_source", pemSource).Msg("loaded PEM key")
|
|
default:
|
|
}
|
|
|
|
if len(certPEMBlock) > 0 && len(keyPEMBlock) > 0 {
|
|
logger.Debug().Msg("create x509 key pair")
|
|
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
|
if err != nil {
|
|
return fmt.Errorf("error create x509 key pair: %w", err)
|
|
}
|
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
|
} else {
|
|
logger.Debug().Msg("no cert or key provided, skip loading x509 key pair")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadServerCA loads the root CA from various sources.
|
|
func loadServerCA(cfg TLSConfig, logger zerolog.Logger, tlsConfig *tls.Config, readFile ReadFileFunc, statFile StatFileFunc) error {
|
|
if cfg.ServerCAPem == "" {
|
|
logger.Debug().Msg("no server CA certificate provided")
|
|
return nil
|
|
}
|
|
caCert, err := loadPEMData(cfg.ServerCAPem, logger, "server CA", readFile, statFile)
|
|
if err != nil {
|
|
return fmt.Errorf("error load server CA certificate: %w", err)
|
|
}
|
|
if len(caCert) > 0 {
|
|
logger.Debug().Msg("load server CA certificate")
|
|
caCertPool, err := newCertPoolFromPEM(caCert)
|
|
if err != nil {
|
|
return fmt.Errorf("error create server CA certificate pool: %w", err)
|
|
}
|
|
tlsConfig.RootCAs = caCertPool
|
|
} else {
|
|
logger.Debug().Msg("no server CA certificate provided")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadClientCA loads the client CA from various sources.
|
|
func loadClientCA(cfg TLSConfig, logger zerolog.Logger, tlsConfig *tls.Config, readFile ReadFileFunc, statFile StatFileFunc) error {
|
|
if cfg.ClientCAPem == "" {
|
|
logger.Debug().Msg("no client CA certificate provided")
|
|
return nil
|
|
}
|
|
caCert, err := loadPEMData(cfg.ClientCAPem, logger, "client CA", readFile, statFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(caCert) > 0 {
|
|
logger.Debug().Msg("load client CA certificate")
|
|
caCertPool, err := newCertPoolFromPEM(caCert)
|
|
if err != nil {
|
|
return fmt.Errorf("error create client CA certificate pool: %w", err)
|
|
}
|
|
tlsConfig.ClientCAs = caCertPool
|
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
|
} else {
|
|
logger.Debug().Msg("no client CA certificate provided")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// loadPEMData attempts to load PEM data from a file, base64 string, or raw string.
|
|
func loadPEMData(pemData PEMData, logger zerolog.Logger, certType string, readFile ReadFileFunc, statFile StatFileFunc) ([]byte, error) {
|
|
pemBytes, pemSource, err := pemData.Load(statFile, readFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load PEM block for %s: %w", certType, err)
|
|
}
|
|
logger.Debug().Str("pem_source", pemSource).Msg("loaded PEM data")
|
|
return pemBytes, nil
|
|
}
|
|
|
|
// newCertPoolFromPEM returns certificate pool for the given PEM-encoded
|
|
// certificate bundle. Note that it currently ignores invalid blocks.
|
|
func newCertPoolFromPEM(pem []byte) (*x509.CertPool, error) {
|
|
certPool := x509.NewCertPool()
|
|
ok := certPool.AppendCertsFromPEM(pem)
|
|
if !ok {
|
|
return nil, errors.New("no valid certificates found")
|
|
}
|
|
return certPool, nil
|
|
}
|
|
|
|
type TLSAutocert struct {
|
|
Enabled bool `mapstructure:"enabled" json:"enabled" envconfig:"enabled" yaml:"enabled" toml:"enabled" doc:"Enables automatic TLS certificate provisioning via ACME (Let's Encrypt)."`
|
|
HostWhitelist []string `mapstructure:"host_whitelist" json:"host_whitelist" envconfig:"host_whitelist" yaml:"host_whitelist" toml:"host_whitelist" expose:"full" doc:"List of host names ACME certificates are allowed to be issued for. Leave empty to allow any host (not recommended in production)."`
|
|
CacheDir string `mapstructure:"cache_dir" json:"cache_dir" envconfig:"cache_dir" yaml:"cache_dir" toml:"cache_dir" expose:"full" doc:"Directory to cache issued certificates so they survive restarts and are shared between nodes."`
|
|
Email string `mapstructure:"email" json:"email" envconfig:"email" yaml:"email" toml:"email" expose:"full" doc:"Contact email sent to the ACME provider for expiry notices and account registration."`
|
|
ServerName string `mapstructure:"server_name" json:"server_name" envconfig:"server_name" yaml:"server_name" toml:"server_name" expose:"full" doc:"Server name (SNI) for which the certificate is requested."`
|
|
HTTP bool `mapstructure:"http" json:"http" envconfig:"http" yaml:"http" toml:"http" doc:"Enables an HTTP server to handle ACME http-01 challenges instead of the default tls-alpn-01."`
|
|
HTTPAddr string `mapstructure:"http_addr" json:"http_addr" envconfig:"http_addr" default:":80" yaml:"http_addr" toml:"http_addr" expose:"full" doc:"Address to bind the ACME http-01 challenge server to. Default <<:80>>."`
|
|
}
|