From 0080764b4cdf96597e551b3a33d8df8df46ca2c0 Mon Sep 17 00:00:00 2001 From: Kaleemullah Siddiqui Date: Wed, 24 Jun 2026 16:18:52 +0530 Subject: [PATCH] PKI config tests Tests for kube-apiserver and service-ca added Signed-off-by: Kaleemullah Siddiqui --- pkg/testsuites/standard_suites.go | 16 + test/extended/include.go | 1 + test/extended/pki/helpers.go | 317 +++++++++++++++++++ test/extended/pki/pki_kube_apiserver.go | 384 ++++++++++++++++++++++++ test/extended/pki/pki_service_ca.go | 384 ++++++++++++++++++++++++ 5 files changed, 1102 insertions(+) create mode 100644 test/extended/pki/helpers.go create mode 100644 test/extended/pki/pki_kube_apiserver.go create mode 100644 test/extended/pki/pki_service_ca.go diff --git a/pkg/testsuites/standard_suites.go b/pkg/testsuites/standard_suites.go index 557d7e28ddd8..56efa885016d 100644 --- a/pkg/testsuites/standard_suites.go +++ b/pkg/testsuites/standard_suites.go @@ -500,6 +500,22 @@ var staticSuites = []ginkgo.TestSuite{ TestTimeout: 90 * time.Minute, ClusterStabilityDuringTest: ginkgo.Disruptive, }, + { + Name: "openshift/pkiconfig", + Description: templates.LongDesc(` + Tests that verify PKI configuration is properly applied to cluster certificates. + This includes validation of certificate key algorithms (RSA, ECDSA) and key sizes + for service-ca and kube-apiserver certificates. The tests reconfigure PKI settings + and trigger certificate regeneration, which causes temporary cluster disruption as + apiserver pods restart to apply new certificates. + `), + Qualifiers: []string{ + withStandardEarlyOrLateTests(`name.contains("[Suite:openshift/pkiconfig]")`), + }, + Parallelism: 1, + TestTimeout: 90 * time.Minute, + ClusterStabilityDuringTest: ginkgo.Disruptive, + }, } // mergeParentQualifiers appends each suite's qualifiers to its declared parent diff --git a/test/extended/include.go b/test/extended/include.go index 654f27379f8b..d2e9b88adba8 100644 --- a/test/extended/include.go +++ b/test/extended/include.go @@ -49,6 +49,7 @@ import ( _ "github.com/openshift/origin/test/extended/oauth" _ "github.com/openshift/origin/test/extended/olm" _ "github.com/openshift/origin/test/extended/operators" + _ "github.com/openshift/origin/test/extended/pki" _ "github.com/openshift/origin/test/extended/poddisruptionbudget" _ "github.com/openshift/origin/test/extended/pods" _ "github.com/openshift/origin/test/extended/project" diff --git a/test/extended/pki/helpers.go b/test/extended/pki/helpers.go new file mode 100644 index 000000000000..1d7f58051e77 --- /dev/null +++ b/test/extended/pki/helpers.go @@ -0,0 +1,317 @@ +package pki + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + e2e "k8s.io/kubernetes/test/e2e/framework" + + configv1alpha1 "github.com/openshift/api/config/v1alpha1" + configclient "github.com/openshift/client-go/config/clientset/versioned" +) + +// certInfo contains parsed certificate information +type certInfo struct { + Algorithm string + KeySize int + Curve string +} + +// pkiTestConfig defines a PKI test configuration +type pkiTestConfig struct { + name string + algorithm configv1alpha1.KeyAlgorithm + rsaSize int32 + ecdsaCurve configv1alpha1.ECDSACurve + signerOverride *keyOverride + servingOverride *keyOverride +} + +// keyOverride specifies key configuration override for a certificate type +type keyOverride struct { + algorithm configv1alpha1.KeyAlgorithm + rsaSize int32 + ecdsaCurve configv1alpha1.ECDSACurve +} + +// mixedPKITestConfig defines a mixed PKI test configuration with different settings per category +type mixedPKITestConfig struct { + name string + signerAlgorithm configv1alpha1.KeyAlgorithm + signerRSASize int32 + signerECDSACurve configv1alpha1.ECDSACurve + servingAlgorithm configv1alpha1.KeyAlgorithm + servingRSASize int32 + servingECDSACurve configv1alpha1.ECDSACurve + clientAlgorithm configv1alpha1.KeyAlgorithm + clientRSASize int32 + clientECDSACurve configv1alpha1.ECDSACurve +} + +// operatorCertificate represents a certificate managed by an operator +type operatorCertificate struct { + Namespace string + SecretName string + CertKey string // Key in the secret containing the certificate (e.g., "tls.crt") + Category string // "signer", "serving", or "client" + OperatorName string // For metrics verification +} + +// waitForPKICRD waits for the PKI CRD to become available +func waitForPKICRD(ctx context.Context, configClient configclient.Interface, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, 5*time.Second, timeout, true, func(ctx context.Context) (bool, error) { + _, err := configClient.ConfigV1alpha1().PKIs().List(ctx, metav1.ListOptions{Limit: 1}) + if err != nil { + // CRD not available yet + return false, nil + } + return true, nil + }) +} + +// buildKeyConfig creates a KeyConfig from algorithm and key parameters +func buildKeyConfig(algorithm configv1alpha1.KeyAlgorithm, rsaSize int32, ecdsaCurve configv1alpha1.ECDSACurve) configv1alpha1.KeyConfig { + keyConfig := configv1alpha1.KeyConfig{ + Algorithm: algorithm, + } + + if algorithm == configv1alpha1.KeyAlgorithmRSA { + keyConfig.RSA = configv1alpha1.RSAKeyConfig{ + KeySize: rsaSize, + } + } else if algorithm == configv1alpha1.KeyAlgorithmECDSA { + keyConfig.ECDSA = configv1alpha1.ECDSAKeyConfig{ + Curve: ecdsaCurve, + } + } + + return keyConfig +} + +// applyPKIConfig applies a PKI configuration based on the test config +func applyPKIConfig(ctx context.Context, configClient configclient.Interface, tc pkiTestConfig) error { + // Build default key config (used for serving certs unless overridden) + defaultKeyConfig := buildKeyConfig(tc.algorithm, tc.rsaSize, tc.ecdsaCurve) + + pkiProfile := configv1alpha1.PKIProfile{ + Defaults: configv1alpha1.DefaultCertificateConfig{ + Key: defaultKeyConfig, + }, + } + + // Add signer certificate override if specified + if tc.signerOverride != nil { + signerKeyConfig := buildKeyConfig(tc.signerOverride.algorithm, tc.signerOverride.rsaSize, tc.signerOverride.ecdsaCurve) + pkiProfile.SignerCertificates = configv1alpha1.CertificateConfig{ + Key: signerKeyConfig, + } + } + + // Add serving certificate override if specified + if tc.servingOverride != nil { + servingKeyConfig := buildKeyConfig(tc.servingOverride.algorithm, tc.servingOverride.rsaSize, tc.servingOverride.ecdsaCurve) + pkiProfile.ServingCertificates = configv1alpha1.CertificateConfig{ + Key: servingKeyConfig, + } + } + + pki := &configv1alpha1.PKI{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: configv1alpha1.PKISpec{ + CertificateManagement: configv1alpha1.PKICertificateManagement{ + Mode: configv1alpha1.PKICertificateManagementModeCustom, + Custom: configv1alpha1.CustomPKIPolicy{ + PKIProfile: pkiProfile, + }, + }, + }, + } + + // Try to create or update + existing, err := configClient.ConfigV1alpha1().PKIs().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // Create new + _, err = configClient.ConfigV1alpha1().PKIs().Create(ctx, pki, metav1.CreateOptions{}) + return err + } + // Return other errors (transient, permission, etc.) + return err + } + + // Update existing + existing.Spec = pki.Spec + _, err = configClient.ConfigV1alpha1().PKIs().Update(ctx, existing, metav1.UpdateOptions{}) + return err +} + +// applyMixedPKIConfig applies a mixed PKI configuration with different settings per category +func applyMixedPKIConfig(ctx context.Context, configClient configclient.Interface, tc mixedPKITestConfig) error { + // Build default key config (we'll use signer as default) + defaultKeyConfig := buildKeyConfig(tc.signerAlgorithm, tc.signerRSASize, tc.signerECDSACurve) + + // Build override configs for serving and client + servingKeyConfig := buildKeyConfig(tc.servingAlgorithm, tc.servingRSASize, tc.servingECDSACurve) + clientKeyConfig := buildKeyConfig(tc.clientAlgorithm, tc.clientRSASize, tc.clientECDSACurve) + + pki := &configv1alpha1.PKI{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: configv1alpha1.PKISpec{ + CertificateManagement: configv1alpha1.PKICertificateManagement{ + Mode: configv1alpha1.PKICertificateManagementModeCustom, + Custom: configv1alpha1.CustomPKIPolicy{ + PKIProfile: configv1alpha1.PKIProfile{ + Defaults: configv1alpha1.DefaultCertificateConfig{ + Key: defaultKeyConfig, + }, + SignerCertificates: configv1alpha1.CertificateConfig{ + Key: defaultKeyConfig, + }, + ServingCertificates: configv1alpha1.CertificateConfig{ + Key: servingKeyConfig, + }, + ClientCertificates: configv1alpha1.CertificateConfig{ + Key: clientKeyConfig, + }, + }, + }, + }, + }, + } + + // Try to create or update + existing, err := configClient.ConfigV1alpha1().PKIs().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // Create new + _, err = configClient.ConfigV1alpha1().PKIs().Create(ctx, pki, metav1.CreateOptions{}) + return err + } + // Return other errors (transient, permission, etc.) + return err + } + + // Update existing + existing.Spec = pki.Spec + _, err = configClient.ConfigV1alpha1().PKIs().Update(ctx, existing, metav1.UpdateOptions{}) + return err +} + +// getCertificateFromSecret retrieves and parses a certificate from a secret +func getCertificateFromSecret(ctx context.Context, kubeClient *kubernetes.Clientset, namespace, secretName, certKey string) (*certInfo, error) { + secret, err := kubeClient.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get secret %s/%s: %w", namespace, secretName, err) + } + + certData, ok := secret.Data[certKey] + if !ok { + return nil, fmt.Errorf("certificate key %q not found in secret %s/%s", certKey, namespace, secretName) + } + + return parseCertificate(certData) +} + +// parseCertificate parses PEM-encoded certificate data +func parseCertificate(certPEM []byte) (*certInfo, error) { + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + info := &certInfo{} + + switch pub := cert.PublicKey.(type) { + case *rsa.PublicKey: + info.Algorithm = "RSA" + info.KeySize = pub.N.BitLen() + case *ecdsa.PublicKey: + info.Algorithm = "ECDSA" + switch pub.Curve { + case elliptic.P256(): + info.Curve = "P256" + case elliptic.P384(): + info.Curve = "P384" + case elliptic.P521(): + info.Curve = "P521" + default: + info.Curve = "Unknown" + } + default: + return nil, fmt.Errorf("unsupported public key type: %T", pub) + } + + return info, nil +} + +// waitForSecretRegeneration waits for a secret to be recreated with new UID and populated cert data +// oldUID is the UID of the secret before deletion; certKey is the data key to verify (e.g., "tls.crt") +func waitForSecretRegeneration(ctx context.Context, kubeClient *kubernetes.Clientset, namespace, secretName, certKey string, oldUID string, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, 5*time.Second, timeout, true, func(ctx context.Context) (bool, error) { + s, err := kubeClient.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + return false, nil // Secret doesn't exist yet or error + } + if string(s.UID) == oldUID { + return false, nil // Still the old secret (delete not propagated yet) + } + // Verify the cert data is populated + _, ok := s.Data[certKey] + return ok, nil // Return true only if new secret with populated cert data + }) +} + +// deleteCertificateSecret deletes a certificate secret to trigger rotation/regeneration +func deleteCertificateSecret(ctx context.Context, kubeClient *kubernetes.Clientset, namespace, secretName string) error { + return kubeClient.CoreV1().Secrets(namespace).Delete(ctx, secretName, metav1.DeleteOptions{}) +} + +// cleanupPKIConfiguration resets the PKI configuration to default (Unmanaged) +// NOTE: Does NOT disable the feature gate - feature gate lifecycle is managed by CI job config +func cleanupPKIConfiguration(ctx context.Context, configClient configclient.Interface) { + e2e.Logf("Starting PKI cleanup...") + + // Reset PKI cluster resource to default (unmanaged) configuration + e2e.Logf("Resetting PKI cluster resource to default configuration...") + pki, err := configClient.ConfigV1alpha1().PKIs().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + if !apierrors.IsNotFound(err) { + e2e.Logf("Warning: error getting PKI resource: %v", err) + } else { + e2e.Logf("PKI resource not found, skipping reset") + } + } else { + // Reset to default/unmanaged mode + // Note: custom field must be empty when mode is Unmanaged + pki.Spec.CertificateManagement.Mode = configv1alpha1.PKICertificateManagementModeUnmanaged + pki.Spec.CertificateManagement.Custom = configv1alpha1.CustomPKIPolicy{} + + _, err = configClient.ConfigV1alpha1().PKIs().Update(ctx, pki, metav1.UpdateOptions{}) + if err != nil { + e2e.Logf("Warning: error resetting PKI resource: %v", err) + } else { + e2e.Logf("✓ PKI cluster resource reset to Unmanaged mode successfully") + } + } + + e2e.Logf("PKI cleanup completed") +} diff --git a/test/extended/pki/pki_kube_apiserver.go b/test/extended/pki/pki_kube_apiserver.go new file mode 100644 index 000000000000..692710b67b37 --- /dev/null +++ b/test/extended/pki/pki_kube_apiserver.go @@ -0,0 +1,384 @@ +package pki + +import ( + "context" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + e2e "k8s.io/kubernetes/test/e2e/framework" + + configv1alpha1 "github.com/openshift/api/config/v1alpha1" + configclient "github.com/openshift/client-go/config/clientset/versioned" + exutil "github.com/openshift/origin/test/extended/util" +) + +const ( + kubeAPIServerOperatorNamespace = "openshift-kube-apiserver-operator" + kubeAPIServerNamespace = "openshift-kube-apiserver" +) + +var _ = g.Describe("[sig-kube-apiserver][OCPFeatureGate:ConfigurablePKI][Serial][Disruptive][Suite:openshift/pkiconfig] PKI Configuration", g.Ordered, func() { + oc := exutil.NewCLIWithoutNamespace("kube-apiserver-pki") + + var kubeClient *kubernetes.Clientset + var configClient configclient.Interface + + g.BeforeAll(func(ctx context.Context) { + kubeClient = oc.AdminKubeClient().(*kubernetes.Clientset) + configClient = oc.AdminConfigClient() + + // Register cleanup to run even if tests fail + g.DeferCleanup(func() { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Minute) + defer cancel() + cleanupPKIConfiguration(cleanupCtx, configClient) + }) + }) + + g.It("should validate uniform PKI configurations and certificate regeneration [apigroup:config.openshift.io][Skipped:MicroShift]", func() { + testUniformPKIConfigurations(context.TODO(), kubeClient, configClient) + }) + + g.It("should validate mixed PKI configurations and certificate regeneration [apigroup:config.openshift.io][Skipped:MicroShift]", func() { + testMixedPKIConfigurations(context.TODO(), kubeClient, configClient) + }) +}) + +// testUniformPKIConfigurations tests uniform PKI configurations (same algorithm for all cert categories) +func testUniformPKIConfigurations(ctx context.Context, kubeClient *kubernetes.Clientset, configClient configclient.Interface) { + e2e.Logf("Testing uniform PKI configurations for kube-apiserver...") + + // Define test configurations + testConfigs := []pkiTestConfig{ + { + name: "RSA-2048", + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 2048, + }, + { + name: "RSA-4096", + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 4096, + }, + { + name: "RSA-8192", + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 8192, + }, + { + name: "ECDSA-P256", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP256, + }, + { + name: "ECDSA-P384", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP384, + }, + { + name: "ECDSA-P521", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP521, + }, + } + + for _, tc := range testConfigs { + e2e.Logf("\n=== Testing configuration: %s ===", tc.name) + + // Apply the PKI configuration + err := applyPKIConfig(ctx, configClient, tc) + o.Expect(err).NotTo(o.HaveOccurred(), "error applying PKI config %s", tc.name) + + e2e.Logf("PKI configuration %s applied successfully", tc.name) + + // Wait a moment for operators to process the configuration + time.Sleep(10 * time.Second) + + // Test certificate regeneration for kube-apiserver certificates + e2e.Logf("Testing kube-apiserver certificate regeneration with %s...", tc.name) + testKubeAPIServerCertificates(ctx, kubeClient, tc) + + e2e.Logf("Configuration %s tested successfully", tc.name) + } + + e2e.Logf("\nAll uniform PKI configuration tests passed successfully") +} + +// testMixedPKIConfigurations tests mixed PKI configurations (different algorithms per cert category) +func testMixedPKIConfigurations(ctx context.Context, kubeClient *kubernetes.Clientset, configClient configclient.Interface) { + e2e.Logf("Testing mixed PKI configurations (different key types per certificate category)...") + + // Define mixed test configurations + // Format: signer-serving-client (algorithm-size/curve) + mixedConfigs := []mixedPKITestConfig{ + { + name: "RSA4096-P256-P521", + signerAlgorithm: configv1alpha1.KeyAlgorithmRSA, + signerRSASize: 4096, + servingAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + servingECDSACurve: configv1alpha1.ECDSACurveP256, + clientAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + clientECDSACurve: configv1alpha1.ECDSACurveP521, + }, + { + name: "RSA2048-RSA4096-P384", + signerAlgorithm: configv1alpha1.KeyAlgorithmRSA, + signerRSASize: 2048, + servingAlgorithm: configv1alpha1.KeyAlgorithmRSA, + servingRSASize: 4096, + clientAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + clientECDSACurve: configv1alpha1.ECDSACurveP384, + }, + { + name: "P256-RSA8192-RSA2048", + signerAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + signerECDSACurve: configv1alpha1.ECDSACurveP256, + servingAlgorithm: configv1alpha1.KeyAlgorithmRSA, + servingRSASize: 8192, + clientAlgorithm: configv1alpha1.KeyAlgorithmRSA, + clientRSASize: 2048, + }, + { + name: "P384-P256-RSA4096", + signerAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + signerECDSACurve: configv1alpha1.ECDSACurveP384, + servingAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + servingECDSACurve: configv1alpha1.ECDSACurveP256, + clientAlgorithm: configv1alpha1.KeyAlgorithmRSA, + clientRSASize: 4096, + }, + { + name: "P521-RSA2048-P256", + signerAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + signerECDSACurve: configv1alpha1.ECDSACurveP521, + servingAlgorithm: configv1alpha1.KeyAlgorithmRSA, + servingRSASize: 2048, + clientAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + clientECDSACurve: configv1alpha1.ECDSACurveP256, + }, + { + name: "RSA8192-P384-P521", + signerAlgorithm: configv1alpha1.KeyAlgorithmRSA, + signerRSASize: 8192, + servingAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + servingECDSACurve: configv1alpha1.ECDSACurveP384, + clientAlgorithm: configv1alpha1.KeyAlgorithmECDSA, + clientECDSACurve: configv1alpha1.ECDSACurveP521, + }, + } + + for _, tc := range mixedConfigs { + e2e.Logf("\n=== Testing mixed configuration: %s ===", tc.name) + + // Apply the mixed PKI configuration + err := applyMixedPKIConfig(ctx, configClient, tc) + o.Expect(err).NotTo(o.HaveOccurred(), "error applying mixed PKI config %s", tc.name) + + e2e.Logf("Mixed PKI configuration %s applied successfully", tc.name) + + // Wait a moment for operators to process the configuration + time.Sleep(10 * time.Second) + + // Test certificate regeneration with mixed configuration + e2e.Logf("Testing kube-apiserver certificate regeneration with mixed config %s...", tc.name) + testMixedKubeAPIServerCertificates(ctx, kubeClient, tc) + + e2e.Logf("Mixed configuration %s tested successfully", tc.name) + } + + e2e.Logf("\nAll mixed PKI configuration tests passed successfully") +} + +// testKubeAPIServerCertificates tests certificate regeneration for kube-apiserver +func testKubeAPIServerCertificates(ctx context.Context, kubeClient *kubernetes.Clientset, tc pkiTestConfig) { + // Test a subset of certificates to avoid excessive test time + // Pick one from each category (signer, serving, client) + testCerts := []operatorCertificate{ + // One signer from operator namespace + { + Namespace: kubeAPIServerOperatorNamespace, + SecretName: "aggregator-client-signer", + CertKey: "tls.crt", + Category: "signer", + OperatorName: "kube-apiserver-operator", + }, + // One serving cert from apiserver namespace + { + Namespace: kubeAPIServerNamespace, + SecretName: "external-loadbalancer-serving-certkey", + CertKey: "tls.crt", + Category: "serving", + OperatorName: "kube-apiserver", + }, + // One client cert from apiserver namespace + { + Namespace: kubeAPIServerNamespace, + SecretName: "aggregator-client", + CertKey: "tls.crt", + Category: "client", + OperatorName: "kube-apiserver", + }, + } + + verifiedCount := 0 + for _, cert := range testCerts { + e2e.Logf(" Testing %s certificate: %s/%s", cert.Category, cert.Namespace, cert.SecretName) + + // Get the current UID before deletion + oldSecret, err := kubeClient.CoreV1().Secrets(cert.Namespace).Get(ctx, cert.SecretName, metav1.GetOptions{}) + if err != nil { + o.Expect(err).NotTo(o.HaveOccurred(), "certificate %s/%s must exist before deletion", cert.Namespace, cert.SecretName) + } + oldUID := string(oldSecret.UID) + + // Delete the certificate to trigger regeneration + err = deleteCertificateSecret(ctx, kubeClient, cert.Namespace, cert.SecretName) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to delete certificate %s/%s", cert.Namespace, cert.SecretName) + e2e.Logf(" Certificate deleted") + + // Wait for regeneration with appropriate timeout + // RSA-8192 requires much longer timeout due to computational cost + certTimeout := 3 * time.Minute + if tc.algorithm == configv1alpha1.KeyAlgorithmRSA && tc.rsaSize == 8192 { + certTimeout = 20 * time.Minute + e2e.Logf(" Waiting for certificate regeneration (RSA-8192 may take several minutes)...") + } else { + e2e.Logf(" Waiting for certificate regeneration...") + } + + err = waitForSecretRegeneration(ctx, kubeClient, cert.Namespace, cert.SecretName, cert.CertKey, oldUID, certTimeout) + o.Expect(err).NotTo(o.HaveOccurred(), "error waiting for certificate %s/%s regeneration", cert.Namespace, cert.SecretName) + e2e.Logf(" Certificate regenerated") + + // Verify the regenerated certificate matches expected config + newCert, err := getCertificateFromSecret(ctx, kubeClient, cert.Namespace, cert.SecretName, cert.CertKey) + o.Expect(err).NotTo(o.HaveOccurred(), "error getting regenerated certificate %s/%s", cert.Namespace, cert.SecretName) + + // Verify algorithm and key parameters + if tc.algorithm == configv1alpha1.KeyAlgorithmRSA { + o.Expect(newCert.Algorithm).To(o.Equal("RSA"), "expected RSA algorithm for %s/%s", cert.Namespace, cert.SecretName) + o.Expect(int32(newCert.KeySize)).To(o.Equal(tc.rsaSize), "expected RSA key size %d for %s/%s", tc.rsaSize, cert.Namespace, cert.SecretName) + e2e.Logf(" Certificate verified: RSA-%d", newCert.KeySize) + } else if tc.algorithm == configv1alpha1.KeyAlgorithmECDSA { + o.Expect(newCert.Algorithm).To(o.Equal("ECDSA"), "expected ECDSA algorithm for %s/%s", cert.Namespace, cert.SecretName) + expectedCurve := string(tc.ecdsaCurve) + o.Expect(newCert.Curve).To(o.Equal(expectedCurve), "expected ECDSA curve %s for %s/%s", expectedCurve, cert.Namespace, cert.SecretName) + e2e.Logf(" Certificate verified: ECDSA-%s", newCert.Curve) + } + + verifiedCount++ + + // Small delay between deletions to avoid overwhelming the operators + time.Sleep(5 * time.Second) + } + + o.Expect(verifiedCount).To(o.BeNumerically(">", 0), "at least one certificate must be verified") + e2e.Logf(" Configuration test completed: %d certificates verified", verifiedCount) +} + +// testMixedKubeAPIServerCertificates tests certificate regeneration with mixed key configurations +func testMixedKubeAPIServerCertificates(ctx context.Context, kubeClient *kubernetes.Clientset, tc mixedPKITestConfig) { + // Test one cert from each category to verify different key types + testCerts := []struct { + cert operatorCertificate + expectedAlgorithm configv1alpha1.KeyAlgorithm + expectedRSASize int32 + expectedECDSACurve configv1alpha1.ECDSACurve + }{ + { + cert: operatorCertificate{ + Namespace: kubeAPIServerOperatorNamespace, + SecretName: "aggregator-client-signer", + CertKey: "tls.crt", + Category: "signer", + OperatorName: "kube-apiserver-operator", + }, + expectedAlgorithm: tc.signerAlgorithm, + expectedRSASize: tc.signerRSASize, + expectedECDSACurve: tc.signerECDSACurve, + }, + { + cert: operatorCertificate{ + Namespace: kubeAPIServerNamespace, + SecretName: "external-loadbalancer-serving-certkey", + CertKey: "tls.crt", + Category: "serving", + OperatorName: "kube-apiserver", + }, + expectedAlgorithm: tc.servingAlgorithm, + expectedRSASize: tc.servingRSASize, + expectedECDSACurve: tc.servingECDSACurve, + }, + { + cert: operatorCertificate{ + Namespace: kubeAPIServerNamespace, + SecretName: "aggregator-client", + CertKey: "tls.crt", + Category: "client", + OperatorName: "kube-apiserver", + }, + expectedAlgorithm: tc.clientAlgorithm, + expectedRSASize: tc.clientRSASize, + expectedECDSACurve: tc.clientECDSACurve, + }, + } + + verifiedCount := 0 + for _, testCase := range testCerts { + cert := testCase.cert + e2e.Logf(" Testing %s certificate: %s/%s", cert.Category, cert.Namespace, cert.SecretName) + + // Get the current UID before deletion + oldSecret, err := kubeClient.CoreV1().Secrets(cert.Namespace).Get(ctx, cert.SecretName, metav1.GetOptions{}) + if err != nil { + o.Expect(err).NotTo(o.HaveOccurred(), "certificate %s/%s must exist before deletion", cert.Namespace, cert.SecretName) + } + oldUID := string(oldSecret.UID) + + // Delete the certificate to trigger regeneration + err = deleteCertificateSecret(ctx, kubeClient, cert.Namespace, cert.SecretName) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to delete certificate %s/%s", cert.Namespace, cert.SecretName) + e2e.Logf(" Certificate deleted") + + // Determine timeout based on algorithm and size + certTimeout := 3 * time.Minute + if testCase.expectedAlgorithm == configv1alpha1.KeyAlgorithmRSA && testCase.expectedRSASize == 8192 { + certTimeout = 20 * time.Minute + e2e.Logf(" Waiting for certificate regeneration (RSA-8192 may take several minutes)...") + } else { + e2e.Logf(" Waiting for certificate regeneration...") + } + + err = waitForSecretRegeneration(ctx, kubeClient, cert.Namespace, cert.SecretName, cert.CertKey, oldUID, certTimeout) + o.Expect(err).NotTo(o.HaveOccurred(), "error waiting for certificate %s/%s regeneration", cert.Namespace, cert.SecretName) + e2e.Logf(" Certificate regenerated") + + // Verify the regenerated certificate matches expected config + newCert, err := getCertificateFromSecret(ctx, kubeClient, cert.Namespace, cert.SecretName, cert.CertKey) + o.Expect(err).NotTo(o.HaveOccurred(), "error getting regenerated certificate %s/%s", cert.Namespace, cert.SecretName) + + // Verify algorithm and key parameters + if testCase.expectedAlgorithm == configv1alpha1.KeyAlgorithmRSA { + o.Expect(newCert.Algorithm).To(o.Equal("RSA"), "expected RSA algorithm for %s certificate %s/%s", cert.Category, cert.Namespace, cert.SecretName) + o.Expect(int32(newCert.KeySize)).To(o.Equal(testCase.expectedRSASize), "expected RSA key size %d for %s certificate %s/%s", testCase.expectedRSASize, cert.Category, cert.Namespace, cert.SecretName) + e2e.Logf(" %s certificate verified: RSA-%d", cert.Category, newCert.KeySize) + } else if testCase.expectedAlgorithm == configv1alpha1.KeyAlgorithmECDSA { + o.Expect(newCert.Algorithm).To(o.Equal("ECDSA"), "expected ECDSA algorithm for %s certificate %s/%s", cert.Category, cert.Namespace, cert.SecretName) + expectedCurve := string(testCase.expectedECDSACurve) + o.Expect(newCert.Curve).To(o.Equal(expectedCurve), "expected ECDSA curve %s for %s certificate %s/%s", expectedCurve, cert.Category, cert.Namespace, cert.SecretName) + e2e.Logf(" %s certificate verified: ECDSA-%s", cert.Category, newCert.Curve) + } + + verifiedCount++ + + // Small delay between deletions + time.Sleep(5 * time.Second) + } + + o.Expect(verifiedCount).To(o.BeNumerically(">", 0), "at least one certificate must be verified") + e2e.Logf(" Configuration test completed: %d certificates verified", verifiedCount) +} diff --git a/test/extended/pki/pki_service_ca.go b/test/extended/pki/pki_service_ca.go new file mode 100644 index 000000000000..a3af8bfe3478 --- /dev/null +++ b/test/extended/pki/pki_service_ca.go @@ -0,0 +1,384 @@ +package pki + +import ( + "context" + "fmt" + "strings" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + e2e "k8s.io/kubernetes/test/e2e/framework" + + configv1alpha1 "github.com/openshift/api/config/v1alpha1" + configclient "github.com/openshift/client-go/config/clientset/versioned" + exutil "github.com/openshift/origin/test/extended/util" +) + +var _ = g.Describe("[sig-service-ca][OCPFeatureGate:ConfigurablePKI][Serial][Disruptive][Suite:openshift/pkiconfig] PKI Configuration", g.Ordered, func() { + oc := exutil.NewCLIWithoutNamespace("service-ca-pki") + + var configClient configclient.Interface + + g.BeforeAll(func(ctx context.Context) { + configClient = oc.AdminConfigClient() + + // Register cleanup to reset PKI configuration even if tests fail + g.DeferCleanup(func() { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Minute) + defer cancel() + cleanupPKIConfiguration(cleanupCtx, configClient) + }) + }) + + g.It("should validate PKI config and regenerate signing cert [apigroup:config.openshift.io][Skipped:MicroShift]", func() { + testServiceCAPKIConfiguration(oc) + }) +}) + +// testServiceCAPKIConfiguration tests the PKI configuration flow: +// 1. Verify ConfigurablePKI feature gate is enabled +// 2. Test multiple PKI configurations (RSA and ECDSA) +// 3. For each config, test CA signing cert and service cert generation +// +// NOTE: This test assumes ConfigurablePKI feature gate is already enabled via +// FEATURE_SET=TechPreviewNoUpgrade in the CI job configuration. +func testServiceCAPKIConfiguration(oc *exutil.CLI) { + kubeClient := oc.AdminKubeClient().(*kubernetes.Clientset) + configClient := oc.AdminConfigClient() + + ctx := context.TODO() + + // Verify PKI CRD is available (ConfigurablePKI feature gate is enabled by FEATURE_SET=TechPreviewNoUpgrade) + e2e.Logf("Verifying PKI CRD is available...") + err := waitForPKICRD(ctx, configClient, 30*time.Second) + o.Expect(err).NotTo(o.HaveOccurred()) + e2e.Logf("PKI CRD is available") + + // Test multiple PKI configurations + e2e.Logf("Testing multiple PKI configurations...") + + // Define test configurations + // NOTE: RSA-8192 is NOT tested here for the following reasons: + // + // 1. Excessive Time: Each RSA-8192 certificate takes ~80 seconds to generate. + // With ~78 services in a typical cluster, a full CA rotation takes 60-90+ minutes. + // + // 2. Cluster Disruption: Deleting the signing-key CA triggers: + // - Service-CA controller restart (can't mount deleted secret) + // - Regeneration of ALL ~78 service certificates cluster-wide + // - Rolling updates of multiple operators (kube-controller-manager, kube-scheduler, + // kube-apiserver, monitoring, etc.) as their serving-cert secrets change + // + // 3. Limited Test Value: If PKI configuration works for RSA-2048, RSA-4096, and ECDSA + // variants, the same code path will work for RSA-8192. The key generation logic is + // in library-go and doesn't vary by key size. + // + // 4. Production Impact: This test is disruptive and should only run on dedicated test + // clusters. Adding RSA-8192 would multiply the disruption by 6 CA rotations instead of 5. + // + // RSA-8192 support can be manually verified if needed, but is excluded from automated tests. + testConfigs := []pkiTestConfig{ + { + name: "RSA-4096", + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 4096, + }, + { + name: "ECDSA-P256", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP256, + }, + { + name: "ECDSA-P384", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP384, + }, + { + name: "ECDSA-P521", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP521, + }, + // Mixed configurations: different key sizes/algorithms for signer vs serving certs + // Stronger signer (CA), weaker serving certs - typical security practice + { + name: "Mixed-RSA-4096-signer-RSA-2048-serving", + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 2048, // Default for serving + signerOverride: &keyOverride{ + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 4096, + }, + }, + { + name: "Mixed-ECDSA-P384-signer-ECDSA-P256-serving", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP256, // Default for serving + signerOverride: &keyOverride{ + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP384, + }, + }, + // Cross-algorithm configurations: RSA and ECDSA mixed + // Uses servingOverride to test stronger serving certs (applies to ALL serving certs) + { + name: "Mixed-RSA-4096-CA-ECDSA-P384-serving", + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP256, // Default (not used due to overrides) + signerOverride: &keyOverride{ + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 4096, + }, + servingOverride: &keyOverride{ + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP384, // All serving certs use P384 + }, + }, + { + name: "Mixed-ECDSA-P384-CA-RSA-4096-serving", + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 2048, // Default (not used due to overrides) + signerOverride: &keyOverride{ + algorithm: configv1alpha1.KeyAlgorithmECDSA, + ecdsaCurve: configv1alpha1.ECDSACurveP384, + }, + servingOverride: &keyOverride{ + algorithm: configv1alpha1.KeyAlgorithmRSA, + rsaSize: 4096, // All serving certs use RSA-4096 + }, + }, + } + + for _, tc := range testConfigs { + e2e.Logf("\n=== Testing configuration: %s ===", tc.name) + + // Apply the PKI configuration + err = applyPKIConfig(ctx, configClient, tc) + o.Expect(err).NotTo(o.HaveOccurred(), "error applying PKI config %s", tc.name) + + // Verify the configuration was applied + pki, err := configClient.ConfigV1alpha1().PKIs().Get(ctx, "cluster", metav1.GetOptions{}) + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(pki.Spec.CertificateManagement.Mode).To(o.Equal(configv1alpha1.PKICertificateManagementModeCustom)) + + e2e.Logf("PKI configuration %s applied successfully", tc.name) + + // Wait for operator to reconcile the PKI config change + // Check ClusterOperator status to ensure it's fully reconciled + e2e.Logf("Waiting for operator to reconcile PKI config...") + err = exutil.WaitForOperatorProgressingFalse(ctx, configClient, "service-ca") + if err != nil { + e2e.Logf("warning waiting for operator to reconcile PKI config: %v", err) + } + e2e.Logf("Operator has reconciled PKI configuration") + + // Wait for controller deployment to be updated with the new PKI config + // The operator updates the controller deployment with PKI config via command-line args + // The controller is what generates serving certs, so it must have the new config + e2e.Logf("Waiting for controller deployment to be updated with PKI config...") + err = exutil.WaitForDeploymentReadyWithTimeout(oc, "service-ca", "openshift-service-ca", -1, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + e2e.Logf("Controller deployment has been updated with PKI configuration") + + // Test CA signing certificate regeneration + e2e.Logf("Testing CA signing certificate regeneration with %s...", tc.name) + testSigningCertRegeneration(ctx, kubeClient, tc) + + // Test service certificate generation + e2e.Logf("Testing service certificate generation with %s...", tc.name) + testServiceCertGeneration(ctx, kubeClient, tc) + + e2e.Logf("Configuration %s tested successfully", tc.name) + } + + e2e.Logf("\nAll PKI configuration tests passed successfully") +} + +// testSigningCertRegeneration tests CA signing certificate regeneration +func testSigningCertRegeneration(ctx context.Context, kubeClient *kubernetes.Clientset, tc pkiTestConfig) { + // Get the current UID of the signing-key secret before deletion + signingKeySecret, err := kubeClient.CoreV1().Secrets("openshift-service-ca").Get(ctx, "signing-key", metav1.GetOptions{}) + o.Expect(err).NotTo(o.HaveOccurred()) + oldSigningKeyUID := string(signingKeySecret.UID) + + // Get the current UID of the operator serving cert before deletion (may not exist) + var oldServingCertUID string + servingCertSecret, err := kubeClient.CoreV1().Secrets("openshift-service-ca-operator").Get(ctx, "serving-cert", metav1.GetOptions{}) + if err == nil { + oldServingCertUID = string(servingCertSecret.UID) + } + + // Delete the signing certificate to trigger regeneration + err = kubeClient.CoreV1().Secrets("openshift-service-ca").Delete(ctx, "signing-key", metav1.DeleteOptions{}) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Also delete the operator's serving cert to ensure full regeneration with new PKI config + err = kubeClient.CoreV1().Secrets("openshift-service-ca-operator").Delete(ctx, "serving-cert", metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + o.Expect(err).NotTo(o.HaveOccurred()) + } + + e2e.Logf(" Deleted CA signing certificate and operator serving cert, waiting for regeneration...") + + // Wait for regeneration (increased timeout to handle operator exhaustion after multiple reconfigurations) + err = waitForSecretRegeneration(ctx, kubeClient, "openshift-service-ca", "signing-key", "tls.crt", oldSigningKeyUID, 10*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Verify new certificate matches expected config + // For signing cert, use signerOverride if present, otherwise use default + newCert, err := getCertificateFromSecret(ctx, kubeClient, "openshift-service-ca", "signing-key", "tls.crt") + o.Expect(err).NotTo(o.HaveOccurred()) + + // Determine expected values for signing cert (CA) + expectedAlgo := tc.algorithm + expectedRSASize := tc.rsaSize + expectedCurve := tc.ecdsaCurve + if tc.signerOverride != nil { + expectedAlgo = tc.signerOverride.algorithm + expectedRSASize = tc.signerOverride.rsaSize + expectedCurve = tc.signerOverride.ecdsaCurve + } + + // Verify algorithm and key parameters + if expectedAlgo == configv1alpha1.KeyAlgorithmRSA { + o.Expect(newCert.Algorithm).To(o.Equal("RSA")) + o.Expect(int32(newCert.KeySize)).To(o.Equal(expectedRSASize)) + e2e.Logf(" CA signing cert: RSA-%d", newCert.KeySize) + } else if expectedAlgo == configv1alpha1.KeyAlgorithmECDSA { + o.Expect(newCert.Algorithm).To(o.Equal("ECDSA")) + expectedCurveStr := string(expectedCurve) + o.Expect(newCert.Curve).To(o.Equal(expectedCurveStr)) + e2e.Logf(" CA signing cert: ECDSA-%s", newCert.Curve) + } + + // Wait for controller to be ready after signing-key regeneration + // The controller pod will restart when signing-key is deleted/recreated + e2e.Logf(" Waiting for controller to be ready...") + _, err = exutil.WaitForPods(kubeClient.CoreV1().Pods("openshift-service-ca"), exutil.ParseLabelsOrDie("app=service-ca"), exutil.CheckPodIsReady, 1, 5*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Wait for operator's serving cert to be regenerated (increased timeout for operator exhaustion) + err = waitForSecretRegeneration(ctx, kubeClient, "openshift-service-ca-operator", "serving-cert", "tls.crt", oldServingCertUID, 15*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Verify operator's serving cert also uses the new PKI config + operatorCert, err := getCertificateFromSecret(ctx, kubeClient, "openshift-service-ca-operator", "serving-cert", "tls.crt") + o.Expect(err).NotTo(o.HaveOccurred()) + + // Determine expected values for operator serving cert + // Use servingOverride if present, otherwise use default + expectedServingAlgo := tc.algorithm + expectedServingRSASize := tc.rsaSize + expectedServingCurve := tc.ecdsaCurve + if tc.servingOverride != nil { + expectedServingAlgo = tc.servingOverride.algorithm + expectedServingRSASize = tc.servingOverride.rsaSize + expectedServingCurve = tc.servingOverride.ecdsaCurve + } + + if expectedServingAlgo == configv1alpha1.KeyAlgorithmRSA { + o.Expect(operatorCert.Algorithm).To(o.Equal("RSA")) + o.Expect(int32(operatorCert.KeySize)).To(o.Equal(expectedServingRSASize)) + e2e.Logf(" Operator serving cert: RSA-%d", operatorCert.KeySize) + } else if expectedServingAlgo == configv1alpha1.KeyAlgorithmECDSA { + o.Expect(operatorCert.Algorithm).To(o.Equal("ECDSA")) + expectedCurveStr := string(expectedServingCurve) + o.Expect(operatorCert.Curve).To(o.Equal(expectedCurveStr)) + e2e.Logf(" Operator serving cert: ECDSA-%s", operatorCert.Curve) + } +} + +// testServiceCertGeneration tests service certificate generation +func testServiceCertGeneration(ctx context.Context, kubeClient *kubernetes.Clientset, tc pkiTestConfig) { + // Create a unique test namespace (timestamp suffix to avoid conflicts with previous runs) + // Use hash for long names to stay under 63 character Kubernetes limit + configName := strings.ToLower(tc.name) + if len(configName) > 30 { + // For long names, use first 20 chars + hash of full name + // This ensures uniqueness while keeping under the limit + hash := fmt.Sprintf("%x", time.Now().UnixNano()%0xFFFFFF) // 6 hex chars + configName = configName[:20] + "-" + hash + } + testNS := fmt.Sprintf("test-pki-%s-%d", configName, time.Now().Unix()) + + // Create the namespace + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + }, + } + _, err := kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Cleanup namespace at the end + defer func() { + err := kubeClient.CoreV1().Namespaces().Delete(ctx, testNS, metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + e2e.Logf("Warning: failed to delete test namespace %s: %v", testNS, err) + } + }() + + // Create a test service with serving-cert annotation + testSvc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: testNS, + Annotations: map[string]string{ + "service.beta.openshift.io/serving-cert-secret-name": "test-service-cert", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "https", + Port: 443, + Protocol: corev1.ProtocolTCP, + }, + }, + Selector: map[string]string{ + "app": "test", + }, + }, + } + + _, err = kubeClient.CoreV1().Services(testNS).Create(ctx, testSvc, metav1.CreateOptions{}) + o.Expect(err).NotTo(o.HaveOccurred()) + + e2e.Logf(" Created test service in namespace %s", testNS) + + // Wait for the service certificate to be generated + e2e.Logf(" Waiting for service certificate to be generated...") + err = waitForSecretRegeneration(ctx, kubeClient, testNS, "test-service-cert", "tls.crt", "", 3*time.Minute) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Verify the service certificate + serviceCert, err := getCertificateFromSecret(ctx, kubeClient, testNS, "test-service-cert", "tls.crt") + o.Expect(err).NotTo(o.HaveOccurred()) + + // Verify algorithm and key parameters match expected config + // Use servingOverride if present, otherwise use default + expectedServingAlgo := tc.algorithm + expectedServingRSASize := tc.rsaSize + expectedServingCurve := tc.ecdsaCurve + if tc.servingOverride != nil { + expectedServingAlgo = tc.servingOverride.algorithm + expectedServingRSASize = tc.servingOverride.rsaSize + expectedServingCurve = tc.servingOverride.ecdsaCurve + } + + if expectedServingAlgo == configv1alpha1.KeyAlgorithmRSA { + o.Expect(serviceCert.Algorithm).To(o.Equal("RSA")) + o.Expect(int32(serviceCert.KeySize)).To(o.Equal(expectedServingRSASize)) + e2e.Logf(" Service cert: RSA-%d", serviceCert.KeySize) + } else if expectedServingAlgo == configv1alpha1.KeyAlgorithmECDSA { + o.Expect(serviceCert.Algorithm).To(o.Equal("ECDSA")) + expectedCurveStr := string(expectedServingCurve) + o.Expect(serviceCert.Curve).To(o.Equal(expectedCurveStr)) + e2e.Logf(" Service cert: ECDSA-%s", serviceCert.Curve) + } +}