From 0e3ae9c89fc6e3f93585ed4779bc6c325471da16 Mon Sep 17 00:00:00 2001 From: Furkat Gofurov Date: Thu, 9 Jan 2025 12:33:13 +0200 Subject: [PATCH] Add kubeconfig cert rotation tests Signed-off-by: Furkat Gofurov --- .../rke2controlplane_controller.go | 2 +- .../rke2controlplane_controller_test.go | 136 +++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/controlplane/internal/controllers/rke2controlplane_controller.go b/controlplane/internal/controllers/rke2controlplane_controller.go index d9aed085..9ec60f87 100644 --- a/controlplane/internal/controllers/rke2controlplane_controller.go +++ b/controlplane/internal/controllers/rke2controlplane_controller.go @@ -468,7 +468,7 @@ func (r *RKE2ControlPlaneReconciler) reconcileNormal( conditions.MarkFalse( rcp, controlplanev1.CertificatesAvailableCondition, controlplanev1.CertificatesGenerationFailedReason, - clusterv1.ConditionSeverityWarning, err.Error()) + clusterv1.ConditionSeverityWarning, "%s", err.Error()) return ctrl.Result{}, err } diff --git a/controlplane/internal/controllers/rke2controlplane_controller_test.go b/controlplane/internal/controllers/rke2controlplane_controller_test.go index 2ae2d730..f0dd5523 100644 --- a/controlplane/internal/controllers/rke2controlplane_controller_test.go +++ b/controlplane/internal/controllers/rke2controlplane_controller_test.go @@ -1,18 +1,25 @@ package controllers import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "math/big" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" bootstrapv1 "github.com/rancher/cluster-api-provider-rke2/bootstrap/api/v1beta1" controlplanev1 "github.com/rancher/cluster-api-provider-rke2/controlplane/api/v1beta1" - - // "github.com/rancher/cluster-api-provider-rke2/pkg/kubeconfig" "github.com/rancher/cluster-api-provider-rke2/pkg/rke2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/certs" "sigs.k8s.io/cluster-api/util/collections" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/kubeconfig" @@ -251,4 +258,129 @@ var _ = Describe("Reconcile control plane conditions", func() { Expect(conditions.GetMessage(rcp, controlplanev1.ControlPlaneComponentsHealthyCondition)).To(Equal( "Control plane node missing-machine does not have a corresponding machine")) }) + + It("should rotate kubeconfig secret if needed", func() { + r := &RKE2ControlPlaneReconciler{ + Client: testEnv.GetClient(), + Scheme: testEnv.GetScheme(), + managementCluster: &rke2.Management{Client: testEnv.GetClient(), SecretCachingClient: testEnv.GetClient()}, + managementClusterUncached: &rke2.Management{Client: testEnv.GetClient()}, + } + clusterName := client.ObjectKey{Namespace: ns.Name, Name: "test"} + endpoint := clusterv1.APIEndpoint{Host: "1.2.3.4", Port: 6443} + + // Create kubeconfig secret with short expiry + shortExpiryDate := time.Now().Add(24 * time.Hour) // 1 day from now + secret, err := createKubeconfigSecret(ns.Name, shortExpiryDate) + Expect(err).ToNot(HaveOccurred()) + Expect(testEnv.Create(ctx, secret)).To(Succeed()) + + // Check that rotation is needed + needsRotation, err := kubeconfig.NeedsClientCertRotation(secret, certs.ClientCertificateRenewalDuration) + Expect(err).ToNot(HaveOccurred()) + Expect(needsRotation).To(BeTrue()) + + // Rotate kubeconfig secret + _, err = r.reconcileKubeconfig(ctx, clusterName, endpoint, rcp) + Expect(err).ToNot(HaveOccurred()) + + Expect(testEnv.Get(ctx, client.ObjectKey{Namespace: ns.Name, Name: secret.Name}, secret)).To(Succeed()) + }) + + It("should not rotate kubeconfig secret if not needed", func() { + r := &RKE2ControlPlaneReconciler{ + Client: testEnv.GetClient(), + Scheme: testEnv.GetScheme(), + managementCluster: &rke2.Management{Client: testEnv.GetClient(), SecretCachingClient: testEnv.GetClient()}, + managementClusterUncached: &rke2.Management{Client: testEnv.GetClient()}, + } + clusterName := client.ObjectKey{Namespace: ns.Name, Name: "test"} + endpoint := clusterv1.APIEndpoint{Host: "1.2.3.4", Port: 6443} + + // Create kubeconfig secret with long expiry + longExpiryDate := time.Now().Add(365 * 24 * time.Hour) // 1 year from now + secret, err := createKubeconfigSecret(ns.Name, longExpiryDate) + Expect(err).ToNot(HaveOccurred()) + Expect(testEnv.Create(ctx, secret)).To(Succeed()) + + // Check that no rotation is needed + needsRotation, err := kubeconfig.NeedsClientCertRotation(secret, certs.ClientCertificateRenewalDuration) + Expect(err).ToNot(HaveOccurred()) + Expect(needsRotation).To(BeFalse()) + + // Ensure no rotation occurs + _, err = r.reconcileKubeconfig(ctx, clusterName, endpoint, rcp) + Expect(err).ToNot(HaveOccurred()) + + Expect(testEnv.Get(ctx, client.ObjectKey{Namespace: ns.Name, Name: secret.Name}, secret)).To(Succeed()) + }) }) + +// generateCertAndKey generates a self-signed certificate and private key. +func generateCertAndKey(expiryDate time.Time) ([]byte, []byte, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test Org"}, + }, + NotBefore: time.Now(), + NotAfter: expiryDate, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + + return certPEM, keyPEM, nil +} + +// createKubeconfigSecret creates a Kubernetes secret with a kubeconfig containing a client certificate and key. +func createKubeconfigSecret(namespace string, expiryDate time.Time) (*corev1.Secret, error) { + certPEM, keyPEM, err := generateCertAndKey(expiryDate) + if err != nil { + return nil, err + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-kubeconfig-secret", + Namespace: namespace, + }, + Data: map[string][]byte{ + "value": []byte(fmt.Sprintf(` +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://1.2.3.4:6443 + name: test-cluster +contexts: +- context: + cluster: test-cluster + user: test-user + name: test-context +current-context: test-context +users: +- name: test-user + user: + client-certificate-data: %s + client-key-data: %s +`, base64.StdEncoding.EncodeToString(certPEM), base64.StdEncoding.EncodeToString(keyPEM))), + }, + } + + return secret, nil +}