github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/manager/controlapi/ca_rotation.go (about)

     1  package controlapi
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/tls"
     7  	"crypto/x509"
     8  	"errors"
     9  	"net"
    10  	"net/url"
    11  	"time"
    12  
    13  	"github.com/cloudflare/cfssl/helpers"
    14  	"github.com/docker/swarmkit/api"
    15  	"github.com/docker/swarmkit/ca"
    16  	"github.com/docker/swarmkit/log"
    17  	"google.golang.org/grpc/codes"
    18  	"google.golang.org/grpc/status"
    19  )
    20  
    21  var minRootExpiration = 1 * helpers.OneYear
    22  
    23  // determines whether an api.RootCA, api.RootRotation, or api.CAConfig has a signing key (local signer)
    24  func hasSigningKey(a interface{}) bool {
    25  	switch b := a.(type) {
    26  	case *api.RootCA:
    27  		return len(b.CAKey) > 0
    28  	case *api.RootRotation:
    29  		return b != nil && len(b.CAKey) > 0
    30  	case *api.CAConfig:
    31  		return len(b.SigningCACert) > 0 && len(b.SigningCAKey) > 0
    32  	default:
    33  		panic("needsExternalCAs should be called something of type *api.RootCA, *api.RootRotation, or *api.CAConfig")
    34  	}
    35  }
    36  
    37  // Creates a cross-signed intermediate and new api.RootRotation object.
    38  // This function assumes that the root cert and key and the external CAs have already been validated.
    39  func newRootRotationObject(ctx context.Context, securityConfig *ca.SecurityConfig, apiRootCA *api.RootCA, newCARootCA ca.RootCA, extCAs []*api.ExternalCA, version uint64) (*api.RootCA, error) {
    40  	var (
    41  		rootCert, rootKey, crossSignedCert []byte
    42  		newRootHasSigner                   bool
    43  		err                                error
    44  	)
    45  
    46  	rootCert = newCARootCA.Certs
    47  	if s, err := newCARootCA.Signer(); err == nil {
    48  		rootCert, rootKey = s.Cert, s.Key
    49  		newRootHasSigner = true
    50  	}
    51  
    52  	// we have to sign with the original signer, not whatever is in the SecurityConfig's RootCA (which may have an intermediate signer, if
    53  	// a root rotation is already in progress)
    54  	switch {
    55  	case hasSigningKey(apiRootCA):
    56  		var oldRootCA ca.RootCA
    57  		oldRootCA, err = ca.NewRootCA(apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil)
    58  		if err == nil {
    59  			crossSignedCert, err = oldRootCA.CrossSignCACertificate(rootCert)
    60  		}
    61  	case !newRootHasSigner: // the original CA and the new CA both require external CAs
    62  		return nil, status.Errorf(codes.InvalidArgument, "rotating from one external CA to a different external CA is not supported")
    63  	default:
    64  		// We need the same credentials but to connect to the original URLs (in case we are in the middle of a root rotation already)
    65  		var urls []string
    66  		for _, c := range extCAs {
    67  			if c.Protocol == api.ExternalCA_CAProtocolCFSSL {
    68  				urls = append(urls, c.URL)
    69  			}
    70  		}
    71  		if len(urls) == 0 {
    72  			return nil, status.Errorf(codes.InvalidArgument,
    73  				"must provide an external CA for the current external root CA to generate a cross-signed certificate")
    74  		}
    75  		rootPool := x509.NewCertPool()
    76  		rootPool.AppendCertsFromPEM(apiRootCA.CACert)
    77  
    78  		externalCAConfig := ca.NewExternalCATLSConfig(securityConfig.ClientTLSCreds.Config().Certificates, rootPool)
    79  		externalCA := ca.NewExternalCA(nil, externalCAConfig, urls...)
    80  		crossSignedCert, err = externalCA.CrossSignRootCA(ctx, newCARootCA)
    81  	}
    82  
    83  	if err != nil {
    84  		log.G(ctx).WithError(err).Error("unable to generate a cross-signed certificate for root rotation")
    85  		return nil, status.Errorf(codes.Internal, "unable to generate a cross-signed certificate for root rotation")
    86  	}
    87  
    88  	copied := apiRootCA.Copy()
    89  	copied.RootRotation = &api.RootRotation{
    90  		CACert:            rootCert,
    91  		CAKey:             rootKey,
    92  		CrossSignedCACert: ca.NormalizePEMs(crossSignedCert),
    93  	}
    94  	copied.LastForcedRotation = version
    95  	return copied, nil
    96  }
    97  
    98  // Checks that a CA URL is connectable using the credentials we have and that its server certificate is signed by the
    99  // root CA that we expect.  This uses a TCP dialer rather than an HTTP client; because we have custom TLS configuration,
   100  // if we wanted to use an HTTP client we'd have to create a new transport for every connection.  The docs specify that
   101  // Transports cache connections for future re-use, which could cause many open connections.
   102  func validateExternalCAURL(dialer *net.Dialer, tlsOpts *tls.Config, caURL string) error {
   103  	parsed, err := url.Parse(caURL)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	if parsed.Scheme != "https" {
   108  		return errors.New("invalid HTTP scheme")
   109  	}
   110  	host, port, err := net.SplitHostPort(parsed.Host)
   111  	if err != nil {
   112  		// It either has no port or is otherwise invalid (e.g. too many colons).  If it's otherwise invalid the dialer
   113  		// will error later, so just assume it's no port and set the port to the default HTTPS port.
   114  		host = parsed.Host
   115  		port = "443"
   116  	}
   117  
   118  	conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(host, port), tlsOpts)
   119  	if conn != nil {
   120  		conn.Close()
   121  	}
   122  	return err
   123  }
   124  
   125  // Validates that there is at least 1 reachable, valid external CA for the given CA certificate.  Returns true if there is, false otherwise.
   126  // Requires that the wanted cert is already normalized.
   127  func validateHasAtLeastOneExternalCA(ctx context.Context, externalCAs map[string][]*api.ExternalCA, securityConfig *ca.SecurityConfig,
   128  	wantedCert []byte, desc string) ([]*api.ExternalCA, error) {
   129  	specific, ok := externalCAs[string(wantedCert)]
   130  	if ok {
   131  		pool := x509.NewCertPool()
   132  		pool.AppendCertsFromPEM(wantedCert)
   133  		dialer := net.Dialer{Timeout: 5 * time.Second}
   134  		opts := tls.Config{
   135  			RootCAs:      pool,
   136  			Certificates: securityConfig.ClientTLSCreds.Config().Certificates,
   137  		}
   138  		for i, ca := range specific {
   139  			if ca.Protocol == api.ExternalCA_CAProtocolCFSSL {
   140  				if err := validateExternalCAURL(&dialer, &opts, ca.URL); err != nil {
   141  					log.G(ctx).WithError(err).Warnf("external CA # %d is unreachable or invalid", i+1)
   142  				} else {
   143  					return specific, nil
   144  				}
   145  			}
   146  		}
   147  	}
   148  	return nil, status.Errorf(codes.InvalidArgument, "there must be at least one valid, reachable external CA corresponding to the %s CA certificate", desc)
   149  }
   150  
   151  // validates that the list of external CAs have valid certs associated with them, and produce a mapping of subject/pubkey:external
   152  // for later validation of required external CAs
   153  func getNormalizedExtCAs(caConfig *api.CAConfig, normalizedCurrentRootCACert []byte) (map[string][]*api.ExternalCA, error) {
   154  	extCAs := make(map[string][]*api.ExternalCA)
   155  
   156  	for _, extCA := range caConfig.ExternalCAs {
   157  		associatedCert := normalizedCurrentRootCACert
   158  		// if no associated cert is provided, assume it's the current root cert
   159  		if len(extCA.CACert) > 0 {
   160  			associatedCert = ca.NormalizePEMs(extCA.CACert)
   161  		}
   162  		certKey := string(associatedCert)
   163  		extCAs[certKey] = append(extCAs[certKey], extCA)
   164  	}
   165  
   166  	return extCAs, nil
   167  }
   168  
   169  // validateAndUpdateCA validates a cluster's desired CA configuration spec, and returns a RootCA value on success representing
   170  // current RootCA as it should be.  Validation logic and return values are as follows:
   171  // 1. Validates that the contents are complete - e.g. a signing key is not provided without a signing cert, and that external
   172  //    CAs are not removed if they are needed.  Otherwise, returns an error.
   173  // 2. If no desired signing cert or key are provided, then either:
   174  //    - we are happy with the current CA configuration (force rotation value has not changed), and we return the current RootCA
   175  //      object as is
   176  //    - we want to generate a new internal CA cert and key (force rotation value has changed), and we return the updated RootCA
   177  //      object
   178  // 3. Signing cert and key have been provided: validate that these match (the cert and key match). Otherwise, return an error.
   179  // 4. Return the updated RootCA object according to the following criteria:
   180  //    - If the desired cert is the same as the current CA cert then abort any outstanding rotations. The current signing key
   181  //      is replaced with the desired signing key (this could lets us switch between external->internal or internal->external
   182  //      without an actual CA rotation, which is not needed because any leaf cert issued with one CA cert can be validated using
   183  //       the second CA certificate).
   184  //    - If the desired cert is the same as the current to-be-rotated-to CA cert then a new root rotation is not needed. The
   185  //      current to-be-rotated-to signing key is replaced with the desired signing key (this could lets us switch between
   186  //      external->internal or internal->external without an actual CA rotation, which is not needed because any leaf cert
   187  //      issued with one CA cert can be validated using the second CA certificate).
   188  //    - Otherwise, start a new root rotation using the desired signing cert and desired signing key as the root rotation
   189  //      signing cert and key.  If a root rotation is already in progress, just replace it and start over.
   190  func validateCAConfig(ctx context.Context, securityConfig *ca.SecurityConfig, cluster *api.Cluster) (*api.RootCA, error) {
   191  	newConfig := cluster.Spec.CAConfig.Copy()
   192  	newConfig.SigningCACert = ca.NormalizePEMs(newConfig.SigningCACert) // ensure this is normalized before we use it
   193  
   194  	if len(newConfig.SigningCAKey) > 0 && len(newConfig.SigningCACert) == 0 {
   195  		return nil, status.Errorf(codes.InvalidArgument, "if a signing CA key is provided, the signing CA cert must also be provided")
   196  	}
   197  
   198  	normalizedRootCA := ca.NormalizePEMs(cluster.RootCA.CACert)
   199  	extCAs, err := getNormalizedExtCAs(newConfig, normalizedRootCA) // validate that the list of external CAs is not malformed
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	var oldCertExtCAs []*api.ExternalCA
   205  	if !hasSigningKey(&cluster.RootCA) {
   206  
   207  		// If we are going from external -> internal, but providing the external CA's signing key,
   208  		// then we don't need to validate any external CAs.  We can in fact abort any outstanding root
   209  		// rotations if we are just adding a key.  Because we have a key, we don't care if there are
   210  		// no external CAs matching the certificate.
   211  		if bytes.Equal(normalizedRootCA, newConfig.SigningCACert) && hasSigningKey(newConfig) {
   212  			// validate that the key and cert indeed match - if they don't then just fail now rather
   213  			// than go through all the external CA URLs, which is a more expensive operation
   214  			if _, err := ca.NewRootCA(newConfig.SigningCACert, newConfig.SigningCACert, newConfig.SigningCAKey, ca.DefaultNodeCertExpiration, nil); err != nil {
   215  				return nil, err
   216  			}
   217  			copied := cluster.RootCA.Copy()
   218  			copied.CAKey = newConfig.SigningCAKey
   219  			copied.RootRotation = nil
   220  			copied.LastForcedRotation = newConfig.ForceRotate
   221  			return copied, nil
   222  		}
   223  
   224  		oldCertExtCAs, err = validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, normalizedRootCA, "current")
   225  		if err != nil {
   226  			return nil, err
   227  		}
   228  	}
   229  
   230  	// if the desired CA cert and key are not set, then we are happy with the current root CA configuration, unless
   231  	// the ForceRotate version has changed
   232  	if len(newConfig.SigningCACert) == 0 {
   233  		if cluster.RootCA.LastForcedRotation != newConfig.ForceRotate {
   234  			newRootCA, err := ca.CreateRootCA(ca.DefaultRootCN)
   235  			if err != nil {
   236  				return nil, status.Errorf(codes.Internal, err.Error())
   237  			}
   238  			return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
   239  		}
   240  
   241  		// we also need to make sure that if the current root rotation requires an external CA, those external CAs are
   242  		// still valid
   243  		if cluster.RootCA.RootRotation != nil && !hasSigningKey(cluster.RootCA.RootRotation) {
   244  			_, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, ca.NormalizePEMs(cluster.RootCA.RootRotation.CACert), "next")
   245  			if err != nil {
   246  				return nil, err
   247  			}
   248  		}
   249  
   250  		return &cluster.RootCA, nil // no change, return as is
   251  	}
   252  
   253  	// A desired cert and maybe key were provided - we need to make sure the cert and key (if provided) match.
   254  	var signingCert []byte
   255  	if hasSigningKey(newConfig) {
   256  		signingCert = newConfig.SigningCACert
   257  	}
   258  	newRootCA, err := ca.NewRootCA(newConfig.SigningCACert, signingCert, newConfig.SigningCAKey, ca.DefaultNodeCertExpiration, nil)
   259  	if err != nil {
   260  		return nil, status.Errorf(codes.InvalidArgument, err.Error())
   261  	}
   262  
   263  	if len(newRootCA.Pool.Subjects()) != 1 {
   264  		return nil, status.Errorf(codes.InvalidArgument, "the desired CA certificate cannot contain multiple certificates")
   265  	}
   266  
   267  	parsedCert, err := helpers.ParseCertificatePEM(newConfig.SigningCACert)
   268  	if err != nil {
   269  		return nil, status.Errorf(codes.InvalidArgument, "could not parse the desired CA certificate")
   270  	}
   271  
   272  	// The new certificate's expiry must be at least one year away
   273  	if parsedCert.NotAfter.Before(time.Now().Add(minRootExpiration)) {
   274  		return nil, status.Errorf(codes.InvalidArgument, "CA certificate expires too soon")
   275  	}
   276  
   277  	if !hasSigningKey(newConfig) {
   278  		if _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, newConfig.SigningCACert, "desired"); err != nil {
   279  			return nil, err
   280  		}
   281  	}
   282  
   283  	// check if we can abort any existing root rotations
   284  	if bytes.Equal(normalizedRootCA, newConfig.SigningCACert) {
   285  		copied := cluster.RootCA.Copy()
   286  		copied.CAKey = newConfig.SigningCAKey
   287  		copied.RootRotation = nil
   288  		copied.LastForcedRotation = newConfig.ForceRotate
   289  		return copied, nil
   290  	}
   291  
   292  	// check if this is the same desired cert as an existing root rotation
   293  	if r := cluster.RootCA.RootRotation; r != nil && bytes.Equal(ca.NormalizePEMs(r.CACert), newConfig.SigningCACert) {
   294  		copied := cluster.RootCA.Copy()
   295  		copied.RootRotation.CAKey = newConfig.SigningCAKey
   296  		copied.LastForcedRotation = newConfig.ForceRotate
   297  		return copied, nil
   298  	}
   299  
   300  	// ok, everything's different; we have to begin a new root rotation which means generating a new cross-signed cert
   301  	return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
   302  }