istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/ca/ca.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ca 16 17 import ( 18 "context" 19 "crypto/elliptic" 20 "crypto/x509" 21 "encoding/pem" 22 "fmt" 23 "os" 24 "time" 25 26 v1 "k8s.io/api/core/v1" 27 apierror "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 30 31 "istio.io/istio/pkg/backoff" 32 "istio.io/istio/pkg/log" 33 "istio.io/istio/security/pkg/cmd" 34 caerror "istio.io/istio/security/pkg/pki/error" 35 "istio.io/istio/security/pkg/pki/util" 36 certutil "istio.io/istio/security/pkg/util" 37 ) 38 39 const ( 40 // istioCASecretType is the Istio secret annotation type. 41 istioCASecretType = "istio.io/ca-root" 42 43 // CACertFile is the CA certificate chain file. 44 CACertFile = "ca-cert.pem" 45 // CAPrivateKeyFile is the private key file of CA. 46 CAPrivateKeyFile = "ca-key.pem" 47 // CASecret stores the key/cert of self-signed CA for persistency purpose. 48 CASecret = "istio-ca-secret" 49 // CertChainFile is the ID/name for the certificate chain file. 50 CertChainFile = "cert-chain.pem" 51 // PrivateKeyFile is the ID/name for the private key file. 52 PrivateKeyFile = "key.pem" 53 // RootCertFile is the ID/name for the CA root certificate file. 54 RootCertFile = "root-cert.pem" 55 // TLSSecretCACertFile is the CA certificate file name as it exists in tls type k8s secret. 56 TLSSecretCACertFile = "tls.crt" 57 // TLSSecretCAPrivateKeyFile is the CA certificate key file name as it exists in tls type k8s secret. 58 TLSSecretCAPrivateKeyFile = "tls.key" 59 // TLSSecretRootCertFile is the root cert file name as it exists in tls type k8s secret. 60 TLSSecretRootCertFile = "ca.crt" 61 // The standard key size to use when generating an RSA private key 62 rsaKeySize = 2048 63 // CACertsSecret stores the plugin CA certificates, in external istiod scenario, the secret can be in the config cluster. 64 CACertsSecret = "cacerts" 65 // IstioGenerated is the key indicating the secret is generated by Istio. 66 IstioGenerated = "istio-generated" 67 ) 68 69 // SigningCAFileBundle locations of the files used for the signing CA 70 type SigningCAFileBundle struct { 71 RootCertFile string 72 CertChainFiles []string 73 SigningCertFile string 74 SigningKeyFile string 75 } 76 77 var pkiCaLog = log.RegisterScope("pkica", "Citadel CA log") 78 79 // caTypes is the enum for the CA type. 80 type caTypes int 81 82 type CertOpts struct { 83 // SubjectIDs are used for building the SAN extension for the certificate. 84 SubjectIDs []string 85 86 // TTL is the requested lifetime (Time to live) to be applied in the certificate. 87 TTL time.Duration 88 89 // ForCA indicates whether the signed certificate if for CA. 90 // If true, the signed certificate is a CA certificate, otherwise, it is a workload certificate. 91 ForCA bool 92 93 // Cert Signer info 94 CertSigner string 95 } 96 97 const ( 98 // selfSignedCA means the Istio CA uses a self signed certificate. 99 selfSignedCA caTypes = iota 100 // pluggedCertCA means the Istio CA uses a operator-specified key/cert. 101 pluggedCertCA 102 ) 103 104 // IstioCAOptions holds the configurations for creating an Istio CA. 105 type IstioCAOptions struct { 106 CAType caTypes 107 108 DefaultCertTTL time.Duration 109 MaxCertTTL time.Duration 110 CARSAKeySize int 111 112 KeyCertBundle *util.KeyCertBundle 113 114 // Config for creating self-signed root cert rotator. 115 RotatorConfig *SelfSignedCARootCertRotatorConfig 116 117 // OnRootCertUpdate is the cb which can only be called by self-signed root cert rotator 118 OnRootCertUpdate func() error 119 } 120 121 type RootCertUpdateFunc func() error 122 123 // NewSelfSignedIstioCAOptions returns a new IstioCAOptions instance using self-signed certificate. 124 func NewSelfSignedIstioCAOptions(ctx context.Context, 125 rootCertGracePeriodPercentile int, caCertTTL, rootCertCheckInverval, defaultCertTTL, 126 maxCertTTL time.Duration, org string, useCacertsSecretName, dualUse bool, namespace string, client corev1.CoreV1Interface, 127 rootCertFile string, enableJitter bool, caRSAKeySize int, 128 ) (caOpts *IstioCAOptions, err error) { 129 caOpts = &IstioCAOptions{ 130 CAType: selfSignedCA, 131 DefaultCertTTL: defaultCertTTL, 132 MaxCertTTL: maxCertTTL, 133 RotatorConfig: &SelfSignedCARootCertRotatorConfig{ 134 CheckInterval: rootCertCheckInverval, 135 caCertTTL: caCertTTL, 136 retryInterval: cmd.ReadSigningCertRetryInterval, 137 retryMax: cmd.ReadSigningCertRetryMax, 138 certInspector: certutil.NewCertUtil(rootCertGracePeriodPercentile), 139 caStorageNamespace: namespace, 140 dualUse: dualUse, 141 org: org, 142 rootCertFile: rootCertFile, 143 enableJitter: enableJitter, 144 client: client, 145 }, 146 } 147 148 // always use ``istio-ca-secret` in priority, otherwise fall back to `cacerts` 149 var caCertName string 150 b := backoff.NewExponentialBackOff(backoff.DefaultOption()) 151 err = b.RetryWithContext(ctx, func() error { 152 caCertName = CASecret 153 // 1. fetch `istio-ca-secret` in priority 154 err := loadSelfSignedCaSecret(client, namespace, caCertName, rootCertFile, caOpts) 155 if err == nil { 156 return nil 157 } else if apierror.IsNotFound(err) { 158 // 2. if `istio-ca-secret` not exist and use cacerts enabled, fallback to fetch `cacerts` 159 if useCacertsSecretName { 160 caCertName = CACertsSecret 161 err := loadSelfSignedCaSecret(client, namespace, caCertName, rootCertFile, caOpts) 162 if err == nil { 163 return nil 164 } else if apierror.IsNotFound(err) { // if neither `istio-ca-secret` nor `cacerts` exists, we create a `cacerts` 165 // continue to create `cacerts` 166 } else { 167 return err 168 } 169 } 170 171 // 3. if use cacerts disabled, create `istio-ca-secret`, otherwise create `cacerts`. 172 pkiCaLog.Infof("CASecret %s not found, will create one", caCertName) 173 options := util.CertOptions{ 174 TTL: caCertTTL, 175 Org: org, 176 IsCA: true, 177 IsSelfSigned: true, 178 RSAKeySize: caRSAKeySize, 179 IsDualUse: dualUse, 180 } 181 pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options) 182 if ckErr != nil { 183 pkiCaLog.Warnf("unable to generate CA cert and key for self-signed CA (%v)", ckErr) 184 return fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr) 185 } 186 187 rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile) 188 if err != nil { 189 pkiCaLog.Warnf("failed to append root certificates (%v)", err) 190 return fmt.Errorf("failed to append root certificates (%v)", err) 191 } 192 if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil { 193 pkiCaLog.Warnf("failed to create CA KeyCertBundle (%v)", err) 194 return fmt.Errorf("failed to create CA KeyCertBundle (%v)", err) 195 } 196 // Write the key/cert back to secret, so they will be persistent when CA restarts. 197 secret := BuildSecret(caCertName, namespace, nil, nil, pemCert, pemCert, pemKey, istioCASecretType) 198 _, err = client.Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) 199 if err != nil { 200 pkiCaLog.Warnf("Failed to create secret %s (%v)", caCertName, err) 201 return err 202 } 203 pkiCaLog.Infof("Using self-generated public key: %v", string(rootCerts)) 204 return nil 205 } 206 return err 207 }) 208 pkiCaLog.Infof("Set secret name for self-signed CA cert rotator to %s", caCertName) 209 caOpts.RotatorConfig.secretName = caCertName 210 return caOpts, err 211 } 212 213 func loadSelfSignedCaSecret(client corev1.CoreV1Interface, namespace string, caCertName string, rootCertFile string, caOpts *IstioCAOptions) error { 214 caSecret, err := client.Secrets(namespace).Get(context.TODO(), caCertName, metav1.GetOptions{}) 215 if err == nil { 216 pkiCaLog.Infof("Load signing key and cert from existing secret %s/%s", caSecret.Namespace, caSecret.Name) 217 rootCerts, err := util.AppendRootCerts(caSecret.Data[CACertFile], rootCertFile) 218 if err != nil { 219 return fmt.Errorf("failed to append root certificates (%v)", err) 220 } 221 if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(caSecret.Data[CACertFile], 222 caSecret.Data[CAPrivateKeyFile], nil, rootCerts); err != nil { 223 return fmt.Errorf("failed to create CA KeyCertBundle (%v)", err) 224 } 225 pkiCaLog.Infof("Using existing public key: %v", string(rootCerts)) 226 } 227 return err 228 } 229 230 // NewSelfSignedDebugIstioCAOptions returns a new IstioCAOptions instance using self-signed certificate produced by in-memory CA, 231 // which runs without K8s, and no local ca key file presented. 232 func NewSelfSignedDebugIstioCAOptions(rootCertFile string, caCertTTL, defaultCertTTL, maxCertTTL time.Duration, 233 org string, caRSAKeySize int, 234 ) (caOpts *IstioCAOptions, err error) { 235 caOpts = &IstioCAOptions{ 236 CAType: selfSignedCA, 237 DefaultCertTTL: defaultCertTTL, 238 MaxCertTTL: maxCertTTL, 239 CARSAKeySize: caRSAKeySize, 240 } 241 242 options := util.CertOptions{ 243 TTL: caCertTTL, 244 Org: org, 245 IsCA: true, 246 IsSelfSigned: true, 247 RSAKeySize: caRSAKeySize, 248 IsDualUse: true, // hardcoded to true for K8S as well 249 } 250 pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options) 251 if ckErr != nil { 252 return nil, fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr) 253 } 254 255 rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile) 256 if err != nil { 257 return nil, fmt.Errorf("failed to append root certificates (%v)", err) 258 } 259 260 if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil { 261 return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err) 262 } 263 264 return caOpts, nil 265 } 266 267 // NewPluggedCertIstioCAOptions returns a new IstioCAOptions instance using given certificate. 268 func NewPluggedCertIstioCAOptions(fileBundle SigningCAFileBundle, 269 defaultCertTTL, maxCertTTL time.Duration, caRSAKeySize int, 270 ) (caOpts *IstioCAOptions, err error) { 271 caOpts = &IstioCAOptions{ 272 CAType: pluggedCertCA, 273 DefaultCertTTL: defaultCertTTL, 274 MaxCertTTL: maxCertTTL, 275 CARSAKeySize: caRSAKeySize, 276 } 277 278 if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromFile( 279 fileBundle.SigningCertFile, fileBundle.SigningKeyFile, fileBundle.CertChainFiles, fileBundle.RootCertFile); err != nil { 280 return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err) 281 } 282 283 // Validate that the passed in signing cert can be used as CA. 284 // The check can't be done inside `KeyCertBundle`, since bundle could also be used to 285 // validate workload certificates (i.e., where the leaf certificate is not a CA). 286 b, err := os.ReadFile(fileBundle.SigningCertFile) 287 if err != nil { 288 return nil, err 289 } 290 block, _ := pem.Decode(b) 291 if block == nil { 292 return nil, fmt.Errorf("invalid PEM encoded certificate") 293 } 294 cert, err := x509.ParseCertificate(block.Bytes) 295 if err != nil { 296 return nil, fmt.Errorf("failed to parse X.509 certificate") 297 } 298 if !cert.IsCA { 299 return nil, fmt.Errorf("certificate is not authorized to sign other certificates") 300 } 301 302 return caOpts, nil 303 } 304 305 // BuildSecret returns a secret struct, contents of which are filled with parameters passed in. 306 // Adds the "istio-generated" key if the secret name is `cacerts`. 307 func BuildSecret(scrtName, namespace string, certChain, privateKey, rootCert, caCert, caPrivateKey []byte, secretType v1.SecretType) *v1.Secret { 308 secret := &v1.Secret{ 309 Data: map[string][]byte{ 310 CertChainFile: certChain, 311 PrivateKeyFile: privateKey, 312 RootCertFile: rootCert, 313 CACertFile: caCert, 314 CAPrivateKeyFile: caPrivateKey, 315 }, 316 ObjectMeta: metav1.ObjectMeta{ 317 Name: scrtName, 318 Namespace: namespace, 319 }, 320 Type: secretType, 321 } 322 323 if scrtName == CACertsSecret { 324 secret.Data[IstioGenerated] = []byte("") 325 } 326 327 return secret 328 } 329 330 // IstioCA generates keys and certificates for Istio identities. 331 type IstioCA struct { 332 defaultCertTTL time.Duration 333 maxCertTTL time.Duration 334 caRSAKeySize int 335 336 keyCertBundle *util.KeyCertBundle 337 338 // rootCertRotator periodically rotates self-signed root cert for CA. It is nil 339 // if CA is not self-signed CA. 340 rootCertRotator *SelfSignedCARootCertRotator 341 } 342 343 // NewIstioCA returns a new IstioCA instance. 344 func NewIstioCA(opts *IstioCAOptions) (*IstioCA, error) { 345 ca := &IstioCA{ 346 maxCertTTL: opts.MaxCertTTL, 347 keyCertBundle: opts.KeyCertBundle, 348 caRSAKeySize: opts.CARSAKeySize, 349 } 350 351 if opts.CAType == selfSignedCA && opts.RotatorConfig != nil && opts.RotatorConfig.CheckInterval > time.Duration(0) { 352 ca.rootCertRotator = NewSelfSignedCARootCertRotator(opts.RotatorConfig, ca, opts.OnRootCertUpdate) 353 } 354 355 // if CA cert becomes invalid before workload cert it's going to cause workload cert to be invalid too, 356 // however citatel won't rotate if that happens, this function will prevent that using cert chain TTL as 357 // the workload TTL 358 defaultCertTTL, err := ca.minTTL(opts.DefaultCertTTL) 359 if err != nil { 360 return ca, fmt.Errorf("failed to get default cert TTL %s", err.Error()) 361 } 362 ca.defaultCertTTL = defaultCertTTL 363 364 return ca, nil 365 } 366 367 func (ca *IstioCA) Run(stopChan chan struct{}) { 368 if ca.rootCertRotator != nil { 369 // Start root cert rotator in a separate goroutine. 370 go ca.rootCertRotator.Run(stopChan) 371 } 372 } 373 374 // Sign takes a PEM-encoded CSR and cert opts, and returns a signed certificate. 375 func (ca *IstioCA) Sign(csrPEM []byte, certOpts CertOpts) ( 376 []byte, error, 377 ) { 378 return ca.sign(csrPEM, certOpts.SubjectIDs, certOpts.TTL, true, certOpts.ForCA) 379 } 380 381 // SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain. 382 func (ca *IstioCA) SignWithCertChain(csrPEM []byte, certOpts CertOpts) ( 383 []string, error, 384 ) { 385 cert, err := ca.signWithCertChain(csrPEM, certOpts.SubjectIDs, certOpts.TTL, true, certOpts.ForCA) 386 if err != nil { 387 return nil, err 388 } 389 return []string{string(cert)}, nil 390 } 391 392 // GetCAKeyCertBundle returns the KeyCertBundle for the CA. 393 func (ca *IstioCA) GetCAKeyCertBundle() *util.KeyCertBundle { 394 return ca.keyCertBundle 395 } 396 397 // GenKeyCert generates a certificate signed by the CA, 398 // returns the certificate chain and the private key. 399 func (ca *IstioCA) GenKeyCert(hostnames []string, certTTL time.Duration, checkLifetime bool) ([]byte, []byte, error) { 400 opts := util.CertOptions{ 401 RSAKeySize: rsaKeySize, 402 } 403 404 // use the type of private key the CA uses to generate an intermediate CA of that type (e.g. CA cert using RSA will 405 // cause intermediate CAs using RSA to be generated) 406 _, signingKey, _, _ := ca.keyCertBundle.GetAll() 407 curve, err := util.GetEllipticCurve(signingKey) 408 if err == nil { 409 opts.ECSigAlg = util.EcdsaSigAlg 410 switch curve { 411 case elliptic.P384(): 412 opts.ECCCurve = util.P384Curve 413 default: 414 opts.ECCCurve = util.P256Curve 415 } 416 } 417 418 csrPEM, privPEM, err := util.GenCSR(opts) 419 if err != nil { 420 return nil, nil, err 421 } 422 423 certPEM, err := ca.signWithCertChain(csrPEM, hostnames, certTTL, checkLifetime, false) 424 if err != nil { 425 return nil, nil, err 426 } 427 428 return certPEM, privPEM, nil 429 } 430 431 func (ca *IstioCA) minTTL(defaultCertTTL time.Duration) (time.Duration, error) { 432 certChainPem := ca.keyCertBundle.GetCertChainPem() 433 if len(certChainPem) == 0 { 434 return defaultCertTTL, nil 435 } 436 437 certChainExpiration, err := util.TimeBeforeCertExpires(certChainPem, time.Now()) 438 if err != nil { 439 return 0, fmt.Errorf("failed to get cert chain TTL %s", err.Error()) 440 } 441 442 if certChainExpiration.Seconds() <= 0 { 443 return 0, fmt.Errorf("cert chain has expired") 444 } 445 446 if defaultCertTTL.Seconds() > certChainExpiration.Seconds() { 447 return certChainExpiration, nil 448 } 449 450 return defaultCertTTL, nil 451 } 452 453 func (ca *IstioCA) sign(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, checkLifetime, forCA bool) ([]byte, error) { 454 signingCert, signingKey, _, _ := ca.keyCertBundle.GetAll() 455 if signingCert == nil { 456 return nil, caerror.NewError(caerror.CANotReady, fmt.Errorf("Istio CA is not ready")) // nolint 457 } 458 459 csr, err := util.ParsePemEncodedCSR(csrPEM) 460 if err != nil { 461 return nil, caerror.NewError(caerror.CSRError, err) 462 } 463 464 if err := csr.CheckSignature(); err != nil { 465 return nil, caerror.NewError(caerror.CSRError, err) 466 } 467 468 lifetime := requestedLifetime 469 // If the requested requestedLifetime is non-positive, apply the default TTL. 470 if requestedLifetime.Seconds() <= 0 { 471 lifetime = ca.defaultCertTTL 472 } 473 // If checkLifetime is set and the requested TTL is greater than maxCertTTL, return an error 474 if checkLifetime && requestedLifetime.Seconds() > ca.maxCertTTL.Seconds() { 475 return nil, caerror.NewError(caerror.TTLError, fmt.Errorf( 476 "requested TTL %s is greater than the max allowed TTL %s", requestedLifetime, ca.maxCertTTL)) 477 } 478 479 certBytes, err := util.GenCertFromCSR(csr, signingCert, csr.PublicKey, *signingKey, subjectIDs, lifetime, forCA) 480 if err != nil { 481 return nil, caerror.NewError(caerror.CertGenError, err) 482 } 483 484 block := &pem.Block{ 485 Type: "CERTIFICATE", 486 Bytes: certBytes, 487 } 488 cert := pem.EncodeToMemory(block) 489 490 return cert, nil 491 } 492 493 func (ca *IstioCA) signWithCertChain(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, lifetimeCheck, 494 forCA bool, 495 ) ([]byte, error) { 496 cert, err := ca.sign(csrPEM, subjectIDs, requestedLifetime, lifetimeCheck, forCA) 497 if err != nil { 498 return nil, err 499 } 500 501 chainPem := ca.GetCAKeyCertBundle().GetCertChainPem() 502 if len(chainPem) > 0 { 503 cert = append(cert, chainPem...) 504 } 505 return cert, nil 506 }