pkg/controller/common/certificates/http_reconcile.go (328 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package certificates import ( "context" cryptorand "crypto/rand" "crypto/x509" "crypto/x509/pkix" "net" "reflect" "strings" "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" commonv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/common/v1" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/name" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/reconciler" "github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s" ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log" netutil "github.com/elastic/cloud-on-k8s/v3/pkg/utils/net" ) // ReconcilePublicHTTPCerts reconciles the Secret containing the HTTP Certificate currently in use, and the CA of // the certificate if available. func (r Reconciler) ReconcilePublicHTTPCerts(ctx context.Context, internalCerts *CertificatesSecret) error { nsn := PublicCertsSecretRef(r.Namer, k8s.ExtractNamespacedName(r.Owner)) expected := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: nsn.Namespace, Name: nsn.Name, Labels: r.Labels, }, Data: map[string][]byte{ CertFileName: internalCerts.CertPem(), }, } if caPem := internalCerts.CAPem(); caPem != nil { expected.Data[CAFileName] = caPem } // Don't set an ownerRef for public http certs secrets, likely to be copied into different namespaces. // See https://github.com/elastic/cloud-on-k8s/issues/3986. _, err := reconciler.ReconcileSecretNoOwnerRef(ctx, r.K8sClient, expected, r.Owner) return err } // ReconcileInternalHTTPCerts reconciles the internal resources for the HTTP certificate. func (r Reconciler) ReconcileInternalHTTPCerts(ctx context.Context, ca *CA, customCertificates *CertificatesSecret) (*CertificatesSecret, error) { log := ulog.FromContext(ctx) ownerNSN := k8s.ExtractNamespacedName(r.Owner) watchKey := CertificateWatchKey(r.Namer, ownerNSN.Name) if err := ReconcileCustomCertWatch(r.DynamicWatches, watchKey, ownerNSN, r.TLSOptions.Certificate); err != nil { return nil, err } secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: ownerNSN.Namespace, Name: InternalCertsSecretName(r.Namer, ownerNSN.Name), }, } shouldCreateSecret := false if err := r.K8sClient.Get(ctx, k8s.ExtractNamespacedName(&secret), &secret); err != nil && !apierrors.IsNotFound(err) { return nil, err } else if apierrors.IsNotFound(err) { shouldCreateSecret = true } if secret.Labels == nil { secret.Labels = make(map[string]string) } // TODO: reconcile annotations? needsUpdate := false // ensure our labels are set on the secret. for k, v := range r.Labels { if current, ok := secret.Labels[k]; !ok || current != v { secret.Labels[k] = v needsUpdate = true } } if err := controllerutil.SetControllerReference(r.Owner, &secret, scheme.Scheme); err != nil { return nil, err } // a placeholder secret may have nil entries, create them if needed if secret.Data == nil { secret.Data = make(map[string][]byte) } // by default let's assume that the CA is provided, either by the ECK internal certificate authority or by the user caCertProvided := true if customCertificates.HasLeafCertificate() { caCertProvided, needsUpdate = r.populateFromCustomCertificateContents(&secret, customCertificates, ca) } else { selfSignedNeedsUpdate, err := ensureInternalSelfSignedCertificateSecretContents( ctx, &secret, ownerNSN, r.Namer, r.TLSOptions, r.ExtraHTTPSANs, r.Services, ca, r.CertRotation, ) if err != nil { return nil, err } needsUpdate = needsUpdate || selfSignedNeedsUpdate } //nolint:nestif if needsUpdate { if shouldCreateSecret { log.Info("Creating HTTP internal certificate secret", "namespace", secret.Namespace, "secret_name", secret.Name) if err := r.K8sClient.Create(ctx, &secret); err != nil { return nil, err } } else { log.Info("Updating HTTP internal certificate secret", "namespace", secret.Namespace, "secret_name", secret.Name) if err := r.K8sClient.Update(ctx, &secret); err != nil { return nil, err } } } // The CA cert has been set in this Secret for convenience, remove it from the result in order to not propagate it. if !caCertProvided { delete(secret.Data, CAFileName) } internalCerts := CertificatesSecret{Secret: secret} return &internalCerts, nil } // populateFromCustomCertificateContents populates the secret passed as a parameter from the contents of customCertificates. Returns two // booleans: whether a CA certificate has been provided through the custom certificate secret and secondly whether data in the resulting secret // has been updated. func (r Reconciler) populateFromCustomCertificateContents(secret *corev1.Secret, customCertificates *CertificatesSecret, ca *CA) (bool, bool) { caCertProvided := true expectedSecretData := make(map[string][]byte) expectedSecretData[CertFileName] = customCertificates.CertPem() expectedSecretData[KeyFileName] = customCertificates.KeyPem() switch { case customCertificates.HasCA(): expectedSecretData[CAFileName] = customCertificates.CAPem() case r.DisableInternalCADefaulting: // NOOP default: // Ensure that the CA certificate is never empty, otherwise Elasticsearch is not able to reload the certificates. // Default to our self-signed (useless) CA if none is provided by the user. // See https://github.com/elastic/cloud-on-k8s/issues/2243 expectedSecretData[CAFileName] = EncodePEMCert(ca.Cert.Raw) // The CA has been set in the internal HTTP secret but it's only for convenience, in order to circumvent the // aforementioned issue. We need to remove it later from the result. caCertProvided = false } if !reflect.DeepEqual(secret.Data, expectedSecretData) { secret.Data = expectedSecretData return caCertProvided, true } return caCertProvided, false } // ensureInternalSelfSignedCertificateSecretContents ensures that contents of a secret containing self-signed // certificates is valid. The provided secret is updated in-place. // // Returns true if the secret was changed. func ensureInternalSelfSignedCertificateSecretContents( ctx context.Context, secret *corev1.Secret, owner types.NamespacedName, namer name.Namer, tls commonv1.TLSOptions, controllerSANs []commonv1.SubjectAlternativeName, svcs []corev1.Service, ca *CA, rotationParam RotationParams, ) (bool, error) { log := ulog.FromContext(ctx) secretWasChanged := false // verify that the secret contains a parsable and compatible private key privateKey := GetCompatiblePrivateKey(ctx, ca.PrivateKey, secret, KeyFileName) // if we need a new private key, generate it if privateKey == nil { generatedPrivateKey, err := NewPrivateKey(ca.PrivateKey) if err != nil { return false, err } encodedPEM, err := EncodePEMPrivateKey(generatedPrivateKey) if err != nil { return false, err } secret.Data[KeyFileName] = encodedPEM privateKey = generatedPrivateKey secretWasChanged = true } // check if the existing cert should be re-issued certificate := getHTTPCertificate(ctx, owner, namer, tls, controllerSANs, secret, svcs, ca, rotationParam.RotateBefore) if certificate == nil { log.Info( "Issuing new HTTP certificate", "namespace", secret.Namespace, "secret_name", secret.Name, "owner_namespace", owner.Namespace, "owner_name", owner.Name, ) csr, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{}, privateKey) if err != nil { return secretWasChanged, err } // create a cert from the csr parsedCSR, err := x509.ParseCertificateRequest(csr) if err != nil { return secretWasChanged, err } // validate the csr validatedCertificateTemplate := createValidatedHTTPCertificateTemplate( owner, namer, tls, controllerSANs, svcs, parsedCSR, rotationParam.Validity, ) // sign the certificate certificate, err = ca.CreateCertificate(*validatedCertificateTemplate) if err != nil { return secretWasChanged, err } secretWasChanged = true // store certificate and signed certificate in a secret mounted into the pod secret.Data[CAFileName] = EncodePEMCert(ca.Cert.Raw) secret.Data[CertFileName] = EncodePEMCert(certificate, ca.Cert.Raw) } // Ensure that the CA certificate is up-to-date. expectedCaPem := EncodePEMCert(ca.Cert.Raw) expectedCertPem := EncodePEMCert(certificate, ca.Cert.Raw) if !reflect.DeepEqual(secret.Data[CAFileName], expectedCaPem) || !reflect.DeepEqual(secret.Data[CertFileName], expectedCertPem) { log.Info( "Updating CA certificate", "secret_name", secret.Name, "namespace", secret.Namespace, "owner_name", owner.Name, ) secretWasChanged = true secret.Data[CAFileName] = expectedCaPem secret.Data[CertFileName] = expectedCertPem } return secretWasChanged, nil } // getHTTPCertificate returns the HTTP certificate from the provided Secret. It returns nil if the certificate does not // exist or is not valid, in which case we should issue a new HTTP certificate. // // Reasons for considering a certificate as invalid: // // - no certificate yet // - certificate has the wrong format // - certificate is invalid according to the CA or expired // - certificate SAN and IP does not match the expected ones func getHTTPCertificate( ctx context.Context, owner types.NamespacedName, namer name.Namer, tls commonv1.TLSOptions, controllerSANs []commonv1.SubjectAlternativeName, secret *corev1.Secret, svcs []corev1.Service, ca *CA, certReconcileBefore time.Duration, ) []byte { log := ulog.FromContext(ctx) validatedTemplate := createValidatedHTTPCertificateTemplate( owner, namer, tls, controllerSANs, svcs, &x509.CertificateRequest{}, certReconcileBefore, ) var certificate *x509.Certificate certData, ok := secret.Data[CertFileName] if !ok { return nil } certs, err := ParsePEMCerts(certData) if err != nil { log.Error(err, "Invalid certificate data found, issuing new certificate", "namespace", secret.Namespace, "secret_name", secret.Name) return nil } // look for the certificate based on the CommonName for _, c := range certs { if c.Subject.CommonName == validatedTemplate.Subject.CommonName { certificate = c break } } if certificate == nil { return nil } pool := x509.NewCertPool() pool.AddCert(ca.Cert) verifyOpts := x509.VerifyOptions{ DNSName: validatedTemplate.Subject.CommonName, Roots: pool, Intermediates: pool, } if _, err := certificate.Verify(verifyOpts); err != nil { log.Info( "Certificate was not valid, should issue new", "validation_failure", err, "subject", certificate.Subject, "issuer", certificate.Issuer, "current_ca_subject", ca.Cert.Subject, "secret_name", secret.Name, "namespace", secret.Namespace, "owner_name", owner.Name, ) return nil } if time.Now().After(certificate.NotAfter.Add(-certReconcileBefore)) { log.Info("Certificate soon to expire, should issue new", "namespace", secret.Namespace, "secret_name", secret.Name) return nil } if certificate.Subject.String() != validatedTemplate.Subject.String() { return nil } if !reflect.DeepEqual(certificate.IPAddresses, validatedTemplate.IPAddresses) { return nil } if !reflect.DeepEqual(certificate.DNSNames, validatedTemplate.DNSNames) { return nil } return certificate.Raw } // createValidatedHTTPCertificateTemplate validates a CSR and creates a certificate template. func createValidatedHTTPCertificateTemplate( owner types.NamespacedName, namer name.Namer, tls commonv1.TLSOptions, controllerSANs []commonv1.SubjectAlternativeName, svcs []corev1.Service, csr *x509.CertificateRequest, certValidity time.Duration, ) *ValidatedCertificateTemplate { defaultSuffixes := strings.Join(namer.DefaultSuffixes, "-") shortName := owner.Name + "-" + defaultSuffixes + "-" + string(HTTPCAType) cnNameParts := []string{ shortName, owner.Namespace, } cnNameParts = append(cnNameParts, namer.DefaultSuffixes...) // add .local to the certificate name to avoid issuing certificates signed for .es by default cnNameParts = append(cnNameParts, "local") certCommonName := strings.Join(cnNameParts, ".") dnsNames := []string{ certCommonName, // eg. clusterName-es-http.default.es.local shortName, // eg. clusterName-es-http } var ipAddresses []net.IP for _, svc := range svcs { dnsNames = append(dnsNames, k8s.GetServiceDNSName(svc)...) ipAddresses = append(ipAddresses, k8s.GetServiceIPAddresses(svc)...) } if selfSignedCerts := tls.SelfSignedCertificate; selfSignedCerts != nil { for _, san := range selfSignedCerts.SubjectAlternativeNames { if san.DNS != "" { dnsNames = append(dnsNames, san.DNS) } if san.IP != "" { ipAddresses = append(ipAddresses, netutil.IPToRFCForm(net.ParseIP(san.IP))) } } } for _, san := range controllerSANs { if san.DNS != "" { dnsNames = append(dnsNames, san.DNS) } if san.IP != "" { ipAddresses = append(ipAddresses, netutil.IPToRFCForm(net.ParseIP(san.IP))) } } certificateTemplate := ValidatedCertificateTemplate(x509.Certificate{ Subject: pkix.Name{ CommonName: certCommonName, OrganizationalUnit: []string{owner.Name}, }, DNSNames: dnsNames, IPAddresses: ipAddresses, NotBefore: time.Now().Add(-10 * time.Minute), NotAfter: time.Now().Add(certValidity), PublicKeyAlgorithm: csr.PublicKeyAlgorithm, PublicKey: csr.PublicKey, Signature: csr.Signature, SignatureAlgorithm: csr.SignatureAlgorithm, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, }) return &certificateTemplate }