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 }