Skip to content

Commit

Permalink
Add kubeconfig cert rotation tests
Browse files Browse the repository at this point in the history
Signed-off-by: Furkat Gofurov <furkat.gofurov@suse.com>
  • Loading branch information
furkatgofurov7 committed Jan 9, 2025
1 parent 7c0bc0b commit 0e3ae9c
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
136 changes: 134 additions & 2 deletions controlplane/internal/controllers/rke2controlplane_controller_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}

0 comments on commit 0e3ae9c

Please sign in to comment.