From 11e1e6c25b0452e6162763306930e93da97ac29f Mon Sep 17 00:00:00 2001 From: Dario Tranchitella Date: Sun, 1 Dec 2024 15:06:02 +0100 Subject: [PATCH] fix(cert): checking api server certificate SAN entries (#641) Signed-off-by: Dario Tranchitella --- internal/crypto/crypto.go | 35 ++++++++++++++++++++ internal/resources/api_server_certificate.go | 22 +++++++----- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index fa795fb..1ea5c01 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -13,9 +13,11 @@ import ( "fmt" "math/big" mathrand "math/rand" + "net" "time" "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/util/sets" ) // CheckPublicAndPrivateKeyValidity checks if the given bytes for the private and public keys are valid. @@ -37,6 +39,39 @@ func CheckPublicAndPrivateKeyValidity(publicKey []byte, privateKey []byte) (bool return checkPublicKeys(privKey.PublicKey, *pubKey), nil } +// CheckCertificateSAN checks if the Kubernetes API Server certificate matches the SAN stored in the kubeadm: +// it must check both IPs and DNS names, and returns a false if the required entry isn't available. +// In case of removal of entries, this function returns true nevertheless to avoid reloading a Control Plane uselessly. +func CheckCertificateSAN(certificateBytes []byte, certSANs []string) (bool, error) { + crt, err := ParseCertificateBytes(certificateBytes) + if err != nil { + return false, err + } + + ips := sets.New[string]() + for _, ip := range crt.IPAddresses { + ips.Insert(ip.String()) + } + + dns := sets.New[string](crt.DNSNames...) + + for _, e := range certSANs { + if ip := net.ParseIP(e); ip != nil { + if !ips.Has(ip.String()) { + return false, nil + } + + continue + } + + if !dns.Has(e) { + return false, nil + } + } + + return true, nil +} + // CheckCertificateAndPrivateKeyPairValidity checks if the certificate and private key pair are valid. func CheckCertificateAndPrivateKeyPairValidity(certificate []byte, privateKey []byte) (bool, error) { switch { diff --git a/internal/resources/api_server_certificate.go b/internal/resources/api_server_certificate.go index 076f50f..a78df5b 100644 --- a/internal/resources/api_server_certificate.go +++ b/internal/resources/api_server_certificate.go @@ -84,6 +84,14 @@ func (r *APIServerCertificate) UpdateTenantControlPlaneStatus(_ context.Context, func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn { return func() error { logger := log.FromContext(ctx, "resource", r.GetName()) + // The Kubeadm configuration must be retrieved in advance: + // this is required to check also the certificate SAN + config, kadmErr := getStoredKubeadmConfiguration(ctx, r.Client, r.TmpDirectory, tenantControlPlane) + if kadmErr != nil { + logger.Error(kadmErr, "cannot retrieve stored kubeadm configuration", "err", kadmErr.Error()) + + return fmt.Errorf("failed to generate certificate and private key: %w", kadmErr) + } // Retrieving the TenantControlPlane CA: // this is required to trigger a new generation in case of Certificate Authority rotation. namespacedName := k8stypes.NamespacedName{Namespace: tenantControlPlane.GetNamespace(), Name: tenantControlPlane.Status.Certificates.CA.SecretName} @@ -121,18 +129,16 @@ func (r *APIServerCertificate) mutate(ctx context.Context, tenantControlPlane *k logger.Info(fmt.Sprintf("%s certificate-private_key pair is not valid: %s", kubeadmconstants.APIServerCertAndKeyBaseName, err.Error())) } - if isCAValid && isCertValid { + dnsNamesMatches, dnsErr := crypto.CheckCertificateSAN(r.resource.Data[kubeadmconstants.APIServerCertName], config.InitConfiguration.APIServer.CertSANs) + if dnsErr != nil { + logger.Info(fmt.Sprintf("%s SAN check returned an error: %s", kubeadmconstants.APIServerCertAndKeyBaseName, err.Error())) + } + + if isCAValid && isCertValid && dnsNamesMatches { return nil } } - config, err := getStoredKubeadmConfiguration(ctx, r.Client, r.TmpDirectory, tenantControlPlane) - if err != nil { - logger.Error(err, "cannot generate certificate and private key in api server certificate", "details", err.Error()) - - return fmt.Errorf("failed to generate certificate and private key: %w", err) - } - ca := kubeadm.CertificatePrivateKeyPair{ Name: kubeadmconstants.CACertAndKeyBaseName, Certificate: secretCA.Data[kubeadmconstants.CACertName],