123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- package controlapi
- import (
- "bytes"
- "context"
- "crypto/tls"
- "crypto/x509"
- "errors"
- "net"
- "net/url"
- "time"
- "google.golang.org/grpc"
- "google.golang.org/grpc/codes"
- "github.com/cloudflare/cfssl/helpers"
- "github.com/docker/swarmkit/api"
- "github.com/docker/swarmkit/ca"
- "github.com/docker/swarmkit/log"
- )
- var minRootExpiration = 1 * helpers.OneYear
- // determines whether an api.RootCA, api.RootRotation, or api.CAConfig has a signing key (local signer)
- func hasSigningKey(a interface{}) bool {
- switch b := a.(type) {
- case *api.RootCA:
- return len(b.CAKey) > 0
- case *api.RootRotation:
- return b != nil && len(b.CAKey) > 0
- case *api.CAConfig:
- return len(b.SigningCACert) > 0 && len(b.SigningCAKey) > 0
- default:
- panic("needsExternalCAs should be called something of type *api.RootCA, *api.RootRotation, or *api.CAConfig")
- }
- }
- // Creates a cross-signed intermediate and new api.RootRotation object.
- // This function assumes that the root cert and key and the external CAs have already been validated.
- func newRootRotationObject(ctx context.Context, securityConfig *ca.SecurityConfig, apiRootCA *api.RootCA, newCARootCA ca.RootCA, extCAs []*api.ExternalCA, version uint64) (*api.RootCA, error) {
- var (
- rootCert, rootKey, crossSignedCert []byte
- newRootHasSigner bool
- err error
- )
- rootCert = newCARootCA.Certs
- if s, err := newCARootCA.Signer(); err == nil {
- rootCert, rootKey = s.Cert, s.Key
- newRootHasSigner = true
- }
- // we have to sign with the original signer, not whatever is in the SecurityConfig's RootCA (which may have an intermediate signer, if
- // a root rotation is already in progress)
- switch {
- case hasSigningKey(apiRootCA):
- var oldRootCA ca.RootCA
- oldRootCA, err = ca.NewRootCA(apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil)
- if err == nil {
- crossSignedCert, err = oldRootCA.CrossSignCACertificate(rootCert)
- }
- case !newRootHasSigner: // the original CA and the new CA both require external CAs
- return nil, grpc.Errorf(codes.InvalidArgument, "rotating from one external CA to a different external CA is not supported")
- default:
- // We need the same credentials but to connect to the original URLs (in case we are in the middle of a root rotation already)
- externalCA := securityConfig.ExternalCA().Copy()
- var urls []string
- for _, c := range extCAs {
- if c.Protocol == api.ExternalCA_CAProtocolCFSSL {
- urls = append(urls, c.URL)
- }
- }
- if len(urls) == 0 {
- return nil, grpc.Errorf(codes.InvalidArgument,
- "must provide an external CA for the current external root CA to generate a cross-signed certificate")
- }
- externalCA.UpdateURLs(urls...)
- crossSignedCert, err = externalCA.CrossSignRootCA(ctx, newCARootCA)
- }
- if err != nil {
- log.G(ctx).WithError(err).Error("unable to generate a cross-signed certificate for root rotation")
- return nil, grpc.Errorf(codes.Internal, "unable to generate a cross-signed certificate for root rotation")
- }
- copied := apiRootCA.Copy()
- copied.RootRotation = &api.RootRotation{
- CACert: rootCert,
- CAKey: rootKey,
- CrossSignedCACert: ca.NormalizePEMs(crossSignedCert),
- }
- copied.LastForcedRotation = version
- return copied, nil
- }
- // Checks that a CA URL is connectable using the credentials we have and that its server certificate is signed by the
- // root CA that we expect. This uses a TCP dialer rather than an HTTP client; because we have custom TLS configuration,
- // if we wanted to use an HTTP client we'd have to create a new transport for every connection. The docs specify that
- // Transports cache connections for future re-use, which could cause many open connections.
- func validateExternalCAURL(dialer *net.Dialer, tlsOpts *tls.Config, caURL string) error {
- parsed, err := url.Parse(caURL)
- if err != nil {
- return err
- }
- if parsed.Scheme != "https" {
- return errors.New("invalid HTTP scheme")
- }
- host, port, err := net.SplitHostPort(parsed.Host)
- if err != nil {
- // It either has no port or is otherwise invalid (e.g. too many colons). If it's otherwise invalid the dialer
- // will error later, so just assume it's no port and set the port to the default HTTPS port.
- host = parsed.Host
- port = "443"
- }
- conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(host, port), tlsOpts)
- if conn != nil {
- conn.Close()
- }
- return err
- }
- // Validates that there is at least 1 reachable, valid external CA for the given CA certificate. Returns true if there is, false otherwise.
- // Requires that the wanted cert is already normalized.
- func validateHasAtLeastOneExternalCA(ctx context.Context, externalCAs map[string][]*api.ExternalCA, securityConfig *ca.SecurityConfig,
- wantedCert []byte, desc string) ([]*api.ExternalCA, error) {
- specific, ok := externalCAs[string(wantedCert)]
- if ok {
- pool := x509.NewCertPool()
- pool.AppendCertsFromPEM(wantedCert)
- dialer := net.Dialer{Timeout: 5 * time.Second}
- opts := tls.Config{
- RootCAs: pool,
- Certificates: securityConfig.ClientTLSCreds.Config().Certificates,
- }
- for i, ca := range specific {
- if ca.Protocol == api.ExternalCA_CAProtocolCFSSL {
- if err := validateExternalCAURL(&dialer, &opts, ca.URL); err != nil {
- log.G(ctx).WithError(err).Warnf("external CA # %d is unreachable or invalid", i+1)
- } else {
- return specific, nil
- }
- }
- }
- }
- return nil, grpc.Errorf(codes.InvalidArgument, "there must be at least one valid, reachable external CA corresponding to the %s CA certificate", desc)
- }
- // validates that the list of external CAs have valid certs associated with them, and produce a mapping of subject/pubkey:external
- // for later validation of required external CAs
- func getNormalizedExtCAs(caConfig *api.CAConfig, normalizedCurrentRootCACert []byte) (map[string][]*api.ExternalCA, error) {
- extCAs := make(map[string][]*api.ExternalCA)
- for _, extCA := range caConfig.ExternalCAs {
- associatedCert := normalizedCurrentRootCACert
- // if no associated cert is provided, assume it's the current root cert
- if len(extCA.CACert) > 0 {
- associatedCert = ca.NormalizePEMs(extCA.CACert)
- }
- certKey := string(associatedCert)
- extCAs[certKey] = append(extCAs[certKey], extCA)
- }
- return extCAs, nil
- }
- // validateAndUpdateCA validates a cluster's desired CA configuration spec, and returns a RootCA value on success representing
- // current RootCA as it should be. Validation logic and return values are as follows:
- // 1. Validates that the contents are complete - e.g. a signing key is not provided without a signing cert, and that external
- // CAs are not removed if they are needed. Otherwise, returns an error.
- // 2. If no desired signing cert or key are provided, then either:
- // - we are happy with the current CA configuration (force rotation value has not changed), and we return the current RootCA
- // object as is
- // - we want to generate a new internal CA cert and key (force rotation value has changed), and we return the updated RootCA
- // object
- // 3. Signing cert and key have been provided: validate that these match (the cert and key match). Otherwise, return an error.
- // 4. Return the updated RootCA object according to the following criteria:
- // - If the desired cert is the same as the current CA cert then abort any outstanding rotations. The current signing key
- // is replaced with the desired signing key (this could lets us switch between external->internal or internal->external
- // without an actual CA rotation, which is not needed because any leaf cert issued with one CA cert can be validated using
- // the second CA certificate).
- // - 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
- // current to-be-rotated-to signing key is replaced with the desired signing key (this could lets us switch between
- // external->internal or internal->external without an actual CA rotation, which is not needed because any leaf cert
- // issued with one CA cert can be validated using the second CA certificate).
- // - Otherwise, start a new root rotation using the desired signing cert and desired signing key as the root rotation
- // signing cert and key. If a root rotation is already in progress, just replace it and start over.
- func validateCAConfig(ctx context.Context, securityConfig *ca.SecurityConfig, cluster *api.Cluster) (*api.RootCA, error) {
- newConfig := cluster.Spec.CAConfig.Copy()
- newConfig.SigningCACert = ca.NormalizePEMs(newConfig.SigningCACert) // ensure this is normalized before we use it
- if len(newConfig.SigningCAKey) > 0 && len(newConfig.SigningCACert) == 0 {
- return nil, grpc.Errorf(codes.InvalidArgument, "if a signing CA key is provided, the signing CA cert must also be provided")
- }
- normalizedRootCA := ca.NormalizePEMs(cluster.RootCA.CACert)
- extCAs, err := getNormalizedExtCAs(newConfig, normalizedRootCA) // validate that the list of external CAs is not malformed
- if err != nil {
- return nil, err
- }
- var oldCertExtCAs []*api.ExternalCA
- if !hasSigningKey(&cluster.RootCA) {
- oldCertExtCAs, err = validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, normalizedRootCA, "current")
- if err != nil {
- return nil, err
- }
- }
- // if the desired CA cert and key are not set, then we are happy with the current root CA configuration, unless
- // the ForceRotate version has changed
- if len(newConfig.SigningCACert) == 0 {
- if cluster.RootCA.LastForcedRotation != newConfig.ForceRotate {
- newRootCA, err := ca.CreateRootCA(ca.DefaultRootCN)
- if err != nil {
- return nil, grpc.Errorf(codes.Internal, err.Error())
- }
- return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
- }
- // we also need to make sure that if the current root rotation requires an external CA, those external CAs are
- // still valid
- if cluster.RootCA.RootRotation != nil && !hasSigningKey(cluster.RootCA.RootRotation) {
- _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, ca.NormalizePEMs(cluster.RootCA.RootRotation.CACert), "next")
- if err != nil {
- return nil, err
- }
- }
- return &cluster.RootCA, nil // no change, return as is
- }
- // A desired cert and maybe key were provided - we need to make sure the cert and key (if provided) match.
- var signingCert []byte
- if hasSigningKey(newConfig) {
- signingCert = newConfig.SigningCACert
- }
- newRootCA, err := ca.NewRootCA(newConfig.SigningCACert, signingCert, newConfig.SigningCAKey, ca.DefaultNodeCertExpiration, nil)
- if err != nil {
- return nil, grpc.Errorf(codes.InvalidArgument, err.Error())
- }
- if len(newRootCA.Pool.Subjects()) != 1 {
- return nil, grpc.Errorf(codes.InvalidArgument, "the desired CA certificate cannot contain multiple certificates")
- }
- parsedCert, err := helpers.ParseCertificatePEM(newConfig.SigningCACert)
- if err != nil {
- return nil, grpc.Errorf(codes.InvalidArgument, "could not parse the desired CA certificate")
- }
- // The new certificate's expiry must be at least one year away
- if parsedCert.NotAfter.Before(time.Now().Add(minRootExpiration)) {
- return nil, grpc.Errorf(codes.InvalidArgument, "CA certificate expires too soon")
- }
- if !hasSigningKey(newConfig) {
- if _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, newConfig.SigningCACert, "desired"); err != nil {
- return nil, err
- }
- }
- // check if we can abort any existing root rotations
- if bytes.Equal(normalizedRootCA, newConfig.SigningCACert) {
- copied := cluster.RootCA.Copy()
- copied.CAKey = newConfig.SigningCAKey
- copied.RootRotation = nil
- copied.LastForcedRotation = newConfig.ForceRotate
- return copied, nil
- }
- // check if this is the same desired cert as an existing root rotation
- if r := cluster.RootCA.RootRotation; r != nil && bytes.Equal(ca.NormalizePEMs(r.CACert), newConfig.SigningCACert) {
- copied := cluster.RootCA.Copy()
- copied.RootRotation.CAKey = newConfig.SigningCAKey
- copied.LastForcedRotation = newConfig.ForceRotate
- return copied, nil
- }
- // ok, everything's different; we have to begin a new root rotation which means generating a new cross-signed cert
- return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
- }
|