github.com/hernad/nomad@v1.6.112/nomad/structs/config/tls.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package config
     5  
     6  import (
     7  	"crypto/md5"
     8  	"crypto/tls"
     9  	"encoding/hex"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"sync"
    14  )
    15  
    16  // TLSConfig provides TLS related configuration
    17  type TLSConfig struct {
    18  
    19  	// EnableHTTP enabled TLS for http traffic to the Nomad server and clients
    20  	EnableHTTP bool `hcl:"http"`
    21  
    22  	// EnableRPC enables TLS for RPC and Raft traffic to the Nomad servers
    23  	EnableRPC bool `hcl:"rpc"`
    24  
    25  	// VerifyServerHostname is used to enable hostname verification of servers. This
    26  	// ensures that the certificate presented is valid for server.<region>.nomad
    27  	// This prevents a compromised client from being restarted as a server, and then
    28  	// intercepting request traffic as well as being added as a raft peer. This should be
    29  	// enabled by default with VerifyOutgoing, but for legacy reasons we cannot break
    30  	// existing clients.
    31  	VerifyServerHostname bool `hcl:"verify_server_hostname"`
    32  
    33  	// CAFile is a path to a certificate authority file. This is used with VerifyIncoming
    34  	// or VerifyOutgoing to verify the TLS connection.
    35  	CAFile string `hcl:"ca_file"`
    36  
    37  	// CertFile is used to provide a TLS certificate that is used for serving TLS connections.
    38  	// Must be provided to serve TLS connections.
    39  	CertFile string `hcl:"cert_file"`
    40  
    41  	// KeyLoader is a helper to dynamically reload TLS configuration
    42  	KeyLoader *KeyLoader
    43  
    44  	keyloaderLock sync.Mutex
    45  
    46  	// KeyFile is used to provide a TLS key that is used for serving TLS connections.
    47  	// Must be provided to serve TLS connections.
    48  	KeyFile string `hcl:"key_file"`
    49  
    50  	// RPCUpgradeMode should be enabled when a cluster is being upgraded
    51  	// to TLS. Allows servers to accept both plaintext and TLS connections and
    52  	// should only be a temporary state.
    53  	RPCUpgradeMode bool `hcl:"rpc_upgrade_mode"`
    54  
    55  	// Verify connections to the HTTPS API
    56  	VerifyHTTPSClient bool `hcl:"verify_https_client"`
    57  
    58  	// Checksum is a MD5 hash of the certificate CA File, Certificate file, and
    59  	// key file.
    60  	Checksum string
    61  
    62  	// TLSCipherSuites are operator-defined ciphers to be used in Nomad TLS
    63  	// connections
    64  	TLSCipherSuites string `hcl:"tls_cipher_suites"`
    65  
    66  	// TLSMinVersion is used to set the minimum TLS version used for TLS
    67  	// connections. Should be either "tls10", "tls11", or "tls12".
    68  	TLSMinVersion string `hcl:"tls_min_version"`
    69  
    70  	// TLSPreferServerCipherSuites controls whether the server selects the
    71  	// client's most preferred ciphersuite, or the server's most preferred
    72  	// ciphersuite. If true then the server's preference, as expressed in
    73  	// the order of elements in CipherSuites, is used.
    74  	TLSPreferServerCipherSuites bool `hcl:"tls_prefer_server_cipher_suites"`
    75  
    76  	// ExtraKeysHCL is used by hcl to surface unexpected keys
    77  	ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
    78  }
    79  
    80  type KeyLoader struct {
    81  	cacheLock   sync.Mutex
    82  	certificate *tls.Certificate
    83  }
    84  
    85  // LoadKeyPair reloads the TLS certificate based on the specified certificate
    86  // and key file. If successful, stores the certificate for further use.
    87  func (k *KeyLoader) LoadKeyPair(certFile, keyFile string) (*tls.Certificate, error) {
    88  	k.cacheLock.Lock()
    89  	defer k.cacheLock.Unlock()
    90  
    91  	// Allow downgrading
    92  	if certFile == "" && keyFile == "" {
    93  		k.certificate = nil
    94  		return nil, nil
    95  	}
    96  
    97  	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    98  	if err != nil {
    99  		return nil, fmt.Errorf("Failed to load cert/key pair: %v", err)
   100  	}
   101  
   102  	k.certificate = &cert
   103  	return k.certificate, nil
   104  }
   105  
   106  func (k *KeyLoader) GetCertificate() *tls.Certificate {
   107  	k.cacheLock.Lock()
   108  	defer k.cacheLock.Unlock()
   109  	return k.certificate
   110  }
   111  
   112  // GetOutgoingCertificate fetches the currently-loaded certificate when
   113  // accepting a TLS connection. This currently does not consider information in
   114  // the ClientHello and only returns the certificate that was last loaded.
   115  func (k *KeyLoader) GetOutgoingCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
   116  	k.cacheLock.Lock()
   117  	defer k.cacheLock.Unlock()
   118  	return k.certificate, nil
   119  }
   120  
   121  // GetClientCertificate fetches the currently-loaded certificate when the Server
   122  // requests a certificate from the caller. This currently does not consider
   123  // information in the ClientHello and only returns the certificate that was last
   124  // loaded.
   125  func (k *KeyLoader) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
   126  	k.cacheLock.Lock()
   127  	defer k.cacheLock.Unlock()
   128  	return k.certificate, nil
   129  }
   130  
   131  // GetKeyLoader returns the keyloader for a TLSConfig object. If the keyloader
   132  // has not been initialized, it will first do so.
   133  func (t *TLSConfig) GetKeyLoader() *KeyLoader {
   134  	t.keyloaderLock.Lock()
   135  	defer t.keyloaderLock.Unlock()
   136  
   137  	// If the keyloader has not yet been initialized, do it here
   138  	if t.KeyLoader == nil {
   139  		t.KeyLoader = &KeyLoader{}
   140  	}
   141  	return t.KeyLoader
   142  }
   143  
   144  // Copy copies the fields of TLSConfig to another TLSConfig object. Required as
   145  // to not copy mutexes between objects.
   146  func (t *TLSConfig) Copy() *TLSConfig {
   147  	if t == nil {
   148  		return t
   149  	}
   150  
   151  	new := &TLSConfig{}
   152  	new.EnableHTTP = t.EnableHTTP
   153  	new.EnableRPC = t.EnableRPC
   154  	new.VerifyServerHostname = t.VerifyServerHostname
   155  	new.CAFile = t.CAFile
   156  	new.CertFile = t.CertFile
   157  
   158  	// Shallow copy the key loader as its GetOutgoingCertificate method is what
   159  	// is used by the HTTP server to retrieve the certificate. If we create a new
   160  	// KeyLoader struct, the HTTP server will still be calling the old
   161  	// GetOutgoingCertificate method.
   162  	t.keyloaderLock.Lock()
   163  	new.KeyLoader = t.KeyLoader
   164  	t.keyloaderLock.Unlock()
   165  
   166  	new.KeyFile = t.KeyFile
   167  	new.RPCUpgradeMode = t.RPCUpgradeMode
   168  	new.VerifyHTTPSClient = t.VerifyHTTPSClient
   169  
   170  	new.TLSCipherSuites = t.TLSCipherSuites
   171  	new.TLSMinVersion = t.TLSMinVersion
   172  
   173  	new.TLSPreferServerCipherSuites = t.TLSPreferServerCipherSuites
   174  
   175  	new.SetChecksum()
   176  
   177  	return new
   178  }
   179  
   180  func (t *TLSConfig) IsEmpty() bool {
   181  	if t == nil {
   182  		return true
   183  	}
   184  
   185  	return !t.EnableHTTP &&
   186  		!t.EnableRPC &&
   187  		!t.VerifyServerHostname &&
   188  		t.CAFile == "" &&
   189  		t.CertFile == "" &&
   190  		t.KeyFile == "" &&
   191  		!t.VerifyHTTPSClient
   192  }
   193  
   194  // Merge is used to merge two TLS configs together
   195  func (t *TLSConfig) Merge(b *TLSConfig) *TLSConfig {
   196  	result := t.Copy()
   197  
   198  	if b.EnableHTTP {
   199  		result.EnableHTTP = true
   200  	}
   201  	if b.EnableRPC {
   202  		result.EnableRPC = true
   203  	}
   204  	if b.VerifyServerHostname {
   205  		result.VerifyServerHostname = true
   206  	}
   207  	if b.CAFile != "" {
   208  		result.CAFile = b.CAFile
   209  	}
   210  	if b.CertFile != "" {
   211  		result.CertFile = b.CertFile
   212  	}
   213  	if b.KeyFile != "" {
   214  		result.KeyFile = b.KeyFile
   215  	}
   216  	if b.VerifyHTTPSClient {
   217  		result.VerifyHTTPSClient = true
   218  	}
   219  	if b.RPCUpgradeMode {
   220  		result.RPCUpgradeMode = true
   221  	}
   222  	if b.TLSCipherSuites != "" {
   223  		result.TLSCipherSuites = b.TLSCipherSuites
   224  	}
   225  	if b.TLSMinVersion != "" {
   226  		result.TLSMinVersion = b.TLSMinVersion
   227  	}
   228  	if b.TLSPreferServerCipherSuites {
   229  		result.TLSPreferServerCipherSuites = true
   230  	}
   231  	return result
   232  }
   233  
   234  // CertificateInfoIsEqual compares the fields of two TLS configuration objects
   235  // for the fields that are specific to configuring a TLS connection
   236  // It is possible for either the calling TLSConfig to be nil, or the TLSConfig
   237  // that it is being compared against, so we need to handle both places. See
   238  // server.go Reload for example.
   239  func (t *TLSConfig) CertificateInfoIsEqual(newConfig *TLSConfig) (bool, error) {
   240  	if t == nil || newConfig == nil {
   241  		return t == newConfig, nil
   242  	}
   243  
   244  	if t.IsEmpty() && newConfig.IsEmpty() {
   245  		return true, nil
   246  	} else if t.IsEmpty() || newConfig.IsEmpty() {
   247  		return false, nil
   248  	}
   249  
   250  	// Set the checksum if it hasn't yet been set (this should happen when the
   251  	// config is parsed but this provides safety in depth)
   252  	if newConfig.Checksum == "" {
   253  		err := newConfig.SetChecksum()
   254  		if err != nil {
   255  			return false, err
   256  		}
   257  	}
   258  
   259  	if t.Checksum == "" {
   260  		err := t.SetChecksum()
   261  		if err != nil {
   262  			return false, err
   263  		}
   264  	}
   265  
   266  	return t.Checksum == newConfig.Checksum, nil
   267  }
   268  
   269  // SetChecksum generates and sets the checksum for a TLS configuration
   270  func (t *TLSConfig) SetChecksum() error {
   271  	newCertChecksum, err := createChecksumOfFiles(t.CAFile, t.CertFile, t.KeyFile)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	t.Checksum = newCertChecksum
   277  	return nil
   278  }
   279  
   280  func getFileChecksum(filepath string) (string, error) {
   281  	f, err := os.Open(filepath)
   282  	if err != nil {
   283  		return "", err
   284  	}
   285  	defer f.Close()
   286  
   287  	h := md5.New()
   288  	if _, err := io.Copy(h, f); err != nil {
   289  		return "", err
   290  	}
   291  
   292  	return hex.EncodeToString(h.Sum(nil)), nil
   293  }
   294  
   295  func createChecksumOfFiles(inputs ...string) (string, error) {
   296  	h := md5.New()
   297  
   298  	for _, input := range inputs {
   299  		checksum, err := getFileChecksum(input)
   300  		if err != nil {
   301  			return "", err
   302  		}
   303  		io.WriteString(h, checksum)
   304  	}
   305  
   306  	return hex.EncodeToString(h.Sum(nil)), nil
   307  }