From 3cac34ca44a1b5fe37f88bd2cd2da3d0b68b7ee4 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 13:39:51 +0200 Subject: [PATCH 01/29] Use CA in cloudprofile for STACKIT clients Signed-off-by: Niclas Schad --- .../templates/deployment.yaml | 4 -- .../templates/secret.yaml | 9 --- .../values.yaml | 3 - .../deployment-csi-driver-controller.yaml | 22 +++----- .../templates/secret.yaml | 9 +++ .../values.yaml | 1 + .../templates/secret.yaml | 9 +++ .../stackit-cloud-controller-manager.yaml | 6 +- .../values.yaml | 8 ++- pkg/controller/controlplane/valuesprovider.go | 12 ++++ pkg/stackit/client/dns.go | 8 ++- pkg/stackit/client/factory.go | 54 +++++++++++++++++- pkg/stackit/client/factory_test.go | 56 +++++++++++++++++++ pkg/stackit/client/iaas.go | 8 ++- pkg/stackit/client/loadbalancing.go | 8 ++- .../stackit/infrastructure_test.go | 2 +- .../selfhostedshootexposure_test.go | 4 +- 17 files changed, 177 insertions(+), 46 deletions(-) delete mode 100644 charts/gardener-extension-provider-stackit/templates/secret.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml create mode 100644 pkg/stackit/client/factory_test.go diff --git a/charts/gardener-extension-provider-stackit/templates/deployment.yaml b/charts/gardener-extension-provider-stackit/templates/deployment.yaml index 73882df6..b3ce43bb 100644 --- a/charts/gardener-extension-provider-stackit/templates/deployment.yaml +++ b/charts/gardener-extension-provider-stackit/templates/deployment.yaml @@ -144,10 +144,6 @@ spec: configMap: name: {{ include "name" . }}-configmap defaultMode: 420 - - name: custom-certs - secret: - secretName: {{ include "name" . }}-ca - optional: true {{- if .Values.imageVectorOverwrite }} - name: imagevector-overwrite configMap: diff --git a/charts/gardener-extension-provider-stackit/templates/secret.yaml b/charts/gardener-extension-provider-stackit/templates/secret.yaml deleted file mode 100644 index 1f159aad..00000000 --- a/charts/gardener-extension-provider-stackit/templates/secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.config.stackitAPIEndpoints.caBundle }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "name" . }}-ca - namespace: {{ .Release.Namespace }} -data: - customca.crt: {{ .Values.config.stackitAPIEndpoints.caBundle }} -{{- end }} diff --git a/charts/gardener-extension-provider-stackit/values.yaml b/charts/gardener-extension-provider-stackit/values.yaml index f72ae03f..fb60b55d 100644 --- a/charts/gardener-extension-provider-stackit/values.yaml +++ b/charts/gardener-extension-provider-stackit/values.yaml @@ -75,9 +75,6 @@ config: capacity: 25Gi provisioner: block-storage.csi.stackit.cloud volumeBindingMode: WaitForFirstConsumer - stackitAPIEndpoints: - # Must be base64 encoded - caBundle: "" registryCaches: # - server: reg.example.com # cache: reg-cache.example.com diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml index df9581f8..203557c9 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml @@ -89,13 +89,9 @@ spec: mountPath: {{ .Values.socketPath }} - name: cloud-provider-config mountPath: /etc/config - # Host certificates are mounted to accommodate OpenStack endpoints that might be served with a certificate - # signed by a CA that is not globally trusted. - - mountPath: /usr/share/ca-certificates - name: usr-share-ca-certificates - readOnly: true - - mountPath: /etc/ssl - name: etc-ssl + - mountPath: /etc/ssl/certs/stackit-ca.crt + name: stackit-ca-cert + subPath: stackit-ca.crt readOnly: true securityContext: allowPrivilegeEscalation: false @@ -344,15 +340,11 @@ spec: path: token name: shoot-access-csi-resizer optional: false - - name: usr-share-ca-certificates - hostPath: - path: /usr/share/ca-certificates - name: cloud-provider-config configMap: defaultMode: 420 name: {{ .Values.prefix }}-cloud-provider-config - # Host certificates are mounted to accommodate OpenStack endpoints that might be served with a certificate - # signed by a CA that is not globally trusted. - - name: etc-ssl - hostPath: - path: /etc/ssl + - name: stackit-ca-cert + secret: + optional: true + secretName: {{ .Values.prefix}}-stackit-ca diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml new file mode 100644 index 00000000..aa81c30b --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml @@ -0,0 +1,9 @@ +{{ if .Values.stackitEndpoints.caCert }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.prefix}}-stackit-ca + namespace: {{ .Release.Namespace }} +data: + stackit-ca.crt: {{ .Values.stackitEndpoints.caCert | b64enc }} +{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml index 60ff609a..2e83206b 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml @@ -22,6 +22,7 @@ userAgentHeaders: [] stackitEndpoints: tokenUrl: "" iaasUrl: "" + caCert: "" csiSnapshotController: replicas: 1 diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml new file mode 100644 index 00000000..7502f20a --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml @@ -0,0 +1,9 @@ +{{ if .Values.config.caCert }} +apiVersion: v1 +kind: Secret +metadata: + name: cloud-config-extra-ca + namespace: {{ .Release.Namespace }} +data: + stackit-ca.crt: {{ .Values.config.caCert | b64enc }} +{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml index 5099c90e..0469208c 100644 --- a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml @@ -89,9 +89,9 @@ spec: name: config-volume - mountPath: /etc/serviceaccount name: sa-secret - - mountPath: /etc/ssl/certs/keystone-ca.crt + - mountPath: /etc/ssl/certs/stackit-ca.crt name: cloud-config-ca - subPath: keystone-ca.crt + subPath: stackit-ca.crt readOnly: true securityContext: allowPrivilegeEscalation: false @@ -113,7 +113,7 @@ spec: path: "serviceaccount.json" - name: cloud-config-ca secret: - secretName: cloud-provider-config + secretName: cloud-config-extra-ca optional: true - name: kubeconfig projected: diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml index b4ed7ae0..497a5ed8 100644 --- a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml @@ -5,9 +5,11 @@ config: stackitProjectID: foo stackitNetworkID: foo stackitRegion: foo - #loadBalancerApiUrl: foo.bar.example.com/v1 - #iaasApiUrl: foo.bar.example.com/v1 - #tokenUrl: foo.bar.example.com/v1 + loadBalancerApiUrl: "" + iaasApiUrl: "" + tokenUrl: "" + caCert: "" + loadBalancerEmergencyToken: "" port: 10258 metricsPort: 9090 podAnnotations: {} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 180ac234..f1abd133 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -851,6 +851,13 @@ func getSTACKITCCMChartValues( return nil, fmt.Errorf("no STACKIT credentials are provided in cluster %s", cluster.Shoot.Name) } + var caBundle string + if cloudProfileConfig, err := helper.CloudProfileConfigFromCluster(cluster); err == nil { + if cloudProfileConfig.CABundle != nil { + caBundle = ptr.Deref(cloudProfileConfig.CABundle, "") + } + } + ccmConfig := map[string]any{ "stackitNetworkID": infra.Networks.ID, "stackitRegion": stackitRegion, @@ -865,6 +872,11 @@ func getSTACKITCCMChartValues( "customLabelDomain": customLabelDomain, } + // Add extra CA to the pod's filesystem + if caBundle != "" { + ccmConfig["caCert"] = caBundle + } + if credentials.LoadBalancerAPIEmergencyToken != "" { ccmConfig["loadBalancerEmergencyToken"] = credentials.LoadBalancerAPIEmergencyToken } diff --git a/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index e28689de..f8bef734 100644 --- a/pkg/stackit/client/dns.go +++ b/pkg/stackit/client/dns.go @@ -13,7 +13,7 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" ) -func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) (DNSClient, error) { +func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (DNSClient, error) { options := clientOptions(endpoints, credentials) if endpoints.DNS != nil { @@ -24,6 +24,12 @@ func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, cre if err != nil { return nil, err } + if caBundle != "" { + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + if err != nil { + return nil, err + } + } return &dnsClient{ api: apiClient.DefaultAPI, projectID: credentials.ProjectID, diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 543123a9..da77a505 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -2,6 +2,10 @@ package client import ( "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" @@ -33,18 +37,24 @@ type Factory interface { type factory struct { StackitRegion string StackitAPIEndpoints stackitv1alpha1.APIEndpoints + CABundle string } func New(region string, cluster *extensionscontroller.Cluster) Factory { var apiEndpoints stackitv1alpha1.APIEndpoints + var caBundle string if cloudProfileConfig, err := helper.CloudProfileConfigFromCluster(cluster); err == nil { apiEndpoints = ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) + if cloudProfileConfig.CABundle != nil { + caBundle = ptr.Deref(cloudProfileConfig.CABundle, "") + } } return &factory{ StackitRegion: region, StackitAPIEndpoints: apiEndpoints, + CABundle: caBundle, } } @@ -54,7 +64,7 @@ func (f factory) LoadBalancing(ctx context.Context, c client.Client, secretRef c return nil, err } - return NewLoadBalancingClient(ctx, f.StackitRegion, f.StackitAPIEndpoints, credentials) + return NewLoadBalancingClient(ctx, f.StackitRegion, f.StackitAPIEndpoints, credentials, f.CABundle) } func (f factory) IaaS(ctx context.Context, c client.Client, secretRef corev1.SecretReference) (IaaSClient, error) { @@ -63,7 +73,7 @@ func (f factory) IaaS(ctx context.Context, c client.Client, secretRef corev1.Sec return nil, err } - return NewIaaSClient(f.StackitRegion, f.StackitAPIEndpoints, credentials) + return NewIaaSClient(f.StackitRegion, f.StackitAPIEndpoints, credentials, f.CABundle) } func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.SecretReference) (DNSClient, error) { @@ -72,9 +82,47 @@ func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.Secr return nil, err } - return NewDNSClient(ctx, f.StackitAPIEndpoints, credentials) + return NewDNSClient(ctx, f.StackitAPIEndpoints, credentials, f.CABundle) } +// InjectCAIntoHTTPClient injects a CABundle into an existing http.Client +func InjectCAIntoHTTPClient(client *http.Client, caBundle string) error { + caCertPool, err := x509.SystemCertPool() + if err != nil { + // we could also fall back here and use an empty pool via x509.NewCertPool() + return err + } + if ok := caCertPool.AppendCertsFromPEM([]byte(caBundle)); !ok { + return fmt.Errorf("failed to append CA bundle to cert pool") + } + var transport *http.Transport + if client.Transport != nil { + var ok bool + transport, ok = client.Transport.(*http.Transport) + if !ok { + return fmt.Errorf("client.Transport is not an *http.Transport") + } + // Clone it to avoid race conditions if the transport is shared + transport = transport.Clone() + } else { + // Explicitly clone the default transport + // The client should already have transport. Should never happen + transport = http.DefaultTransport.(*http.Transport).Clone() + } + + // Inject the custom TLS configuration + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } else { + transport.TLSClientConfig = transport.TLSClientConfig.Clone() + } + + transport.TLSClientConfig.RootCAs = caCertPool + + // Re-assign the modified transport back to the client + client.Transport = transport + return nil +} func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) []sdkconfig.ConfigurationOption { result := []sdkconfig.ConfigurationOption{ sdkconfig.WithUserAgent(UserAgent), diff --git a/pkg/stackit/client/factory_test.go b/pkg/stackit/client/factory_test.go new file mode 100644 index 00000000..1da0c729 --- /dev/null +++ b/pkg/stackit/client/factory_test.go @@ -0,0 +1,56 @@ +package client + +import ( + "crypto/tls" + "encoding/pem" + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("InjectCAIntoHTTPClient", func() { + var ( + testServer *httptest.Server + httpClient *http.Client + ) + + BeforeEach(func() { + testServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{}, + }, + } + }) + + AfterEach(func() { + testServer.Close() + }) + + Context("when the custom CA is injected into the HTTP client", func() { + It("should successfully connect to the TLS server without certificate errors", func() { + serverCert := testServer.Certificate() + + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: serverCert.Raw, + } + serverCertPEM := pem.EncodeToMemory(pemBlock) + + err := InjectCAIntoHTTPClient(httpClient, string(serverCertPEM)) + Expect(err).NotTo(HaveOccurred(), "InjectCAIntoHTTPClient should not return an error") + + resp, err := httpClient.Get(testServer.URL) + + Expect(err).NotTo(HaveOccurred(), "The client should trust the server's certificate") + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }) + }) +}) diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index f941ac5c..583114c7 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -126,7 +126,7 @@ func (c iaasClient) GetNetworkByName(ctx context.Context, name string) ([]iaas.N return filteredNetworks, nil } -func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) (IaaSClient, error) { +func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (IaaSClient, error) { options := clientOptions(endpoints, credentials) if endpoints.IaaS != nil { @@ -141,6 +141,12 @@ func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, creden if err != nil { return nil, err } + if caBundle != "" { + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + if err != nil { + return nil, err + } + } return &iaasClient{ Client: apiClient.DefaultAPI, projectID: credentials.ProjectID, diff --git a/pkg/stackit/client/loadbalancing.go b/pkg/stackit/client/loadbalancing.go index 66bb7e68..338cd1df 100644 --- a/pkg/stackit/client/loadbalancing.go +++ b/pkg/stackit/client/loadbalancing.go @@ -26,7 +26,7 @@ type loadBalancingClient struct { region string } -func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) (LoadBalancingClient, error) { +func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (LoadBalancingClient, error) { options := clientOptions(endpoints, credentials) if endpoints.LoadBalancer != nil { @@ -37,6 +37,12 @@ func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv if err != nil { return nil, err } + if caBundle != "" { + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + if err != nil { + return nil, err + } + } return &loadBalancingClient{ Client: apiClient.DefaultAPI, projectID: credentials.ProjectID, diff --git a/test/integration/infrastructure/stackit/infrastructure_test.go b/test/integration/infrastructure/stackit/infrastructure_test.go index c861c827..569db7db 100644 --- a/test/integration/infrastructure/stackit/infrastructure_test.go +++ b/test/integration/infrastructure/stackit/infrastructure_test.go @@ -132,7 +132,7 @@ var _ = BeforeSuite(func() { // TODO: Consider creating manual STACKIT NLB to ensure stackit NLB deletion works DeferCleanup(testutils.WithFeatureGate(feature.MutableGate, feature.EnsureSTACKITLBDeletion, false)) - iaasClient, err = stackitclient.NewIaaSClient(*region, endpoints, credentials) + iaasClient, err = stackitclient.NewIaaSClient(*region, endpoints, credentials, "") Expect(err).NotTo(HaveOccurred()) repoRoot := filepath.Join("..", "..", "..", "..") diff --git a/test/integration/selfhostedshootexposure/selfhostedshootexposure_test.go b/test/integration/selfhostedshootexposure/selfhostedshootexposure_test.go index d40e7082..2eafc29f 100644 --- a/test/integration/selfhostedshootexposure/selfhostedshootexposure_test.go +++ b/test/integration/selfhostedshootexposure/selfhostedshootexposure_test.go @@ -110,10 +110,10 @@ var _ = BeforeSuite(func() { Expect(*region).NotTo(BeEmpty()) Expect(validateEnvs()).To(Succeed()) - iaasClient, err = stackitclient.NewIaaSClient(*region, endpoints, credentials) + iaasClient, err = stackitclient.NewIaaSClient(*region, endpoints, credentials, "") Expect(err).NotTo(HaveOccurred()) - lbClient, err = stackitclient.NewLoadBalancingClient(ctx, *region, endpoints, credentials) + lbClient, err = stackitclient.NewLoadBalancingClient(ctx, *region, endpoints, credentials, "") Expect(err).NotTo(HaveOccurred()) repoRoot := filepath.Join("..", "..", "..") From 1d346d4ea3555257f7926d67666ed44deb9e5b33 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 5 Jun 2026 14:05:16 +0200 Subject: [PATCH 02/29] Add CABundle to mcm-provider-stackit if specified Signed-off-by: Niclas Schad --- .../templates/stackit-ca-bundle.yaml | 12 +++++++++ .../cloud-provider-config/values.yaml | 1 + pkg/controller/controlplane/valuesprovider.go | 7 ++++++ pkg/webhook/controlplane/ensurer.go | 25 ++++++++++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml new file mode 100644 index 00000000..5c8fb15f --- /dev/null +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -0,0 +1,12 @@ +{{- if .Values.stackitCaCert }} +apiVersion: v1 +kind: Secret +metadata: + name: stackit-ca-bundle + namespace: {{ .Release.Namespace }} +type: Opaque +data: + {{- if .Values.stackitCaCert }} + stackit-ca.crt: {{ .Values.stackitCaCert | b64enc }} + {{- end }} +{{- end }} diff --git a/charts/internal/cloud-provider-config/values.yaml b/charts/internal/cloud-provider-config/values.yaml index 2f537842..776c9ed2 100644 --- a/charts/internal/cloud-provider-config/values.yaml +++ b/charts/internal/cloud-provider-config/values.yaml @@ -1,4 +1,5 @@ stackitOnly: false +stackitCaCert: "" # [Global] authUrl: fooURL domainName: fooDomain diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index f1abd133..f04d231b 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -681,6 +681,13 @@ func getConfigChartValues( } } + if feature.UseStackitMachineControllerManager(cluster) { + if cloudProfileConfig.CABundle != nil { + caBundle := ptr.Deref(cloudProfileConfig.CABundle, "") + values["stackitCaCert"] = caBundle + } + } + return values, nil } diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 2a1a2adc..5a045111 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -54,7 +54,9 @@ type ensurer struct { var ImageVector = imagevector.ImageVector() // EnsureMachineControllerManagerDeployment ensures that the machine-controller-manager deployment conforms to the provider requirements. -func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, gctx gcontext.GardenContext, newObj, _ *appsv1.Deployment) error { +func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, gctx gcontext.GardenContext, newObj, machineDeployment *appsv1.Deployment) error { + var caBundle string + cluster, err := gctx.GetCluster(ctx) if err != nil { return fmt.Errorf("failed reading Cluster: %w", err) @@ -81,8 +83,29 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, return err } + if cloudProfileConfig.CABundle != nil { + caBundle = ptr.Deref(cloudProfileConfig.CABundle, "") + } + apiEndpoints := ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) + if caBundle != "" { + machineDeployment.Spec.Template.Spec.Volumes = append(machineDeployment.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: "stackit-ca-bundle", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "stackit-ca-bundle", + }, + }, + }) + sidecarContainer.VolumeMounts = append(sidecarContainer.VolumeMounts, corev1.VolumeMount{ + Name: "stackit-ca-bundle", + MountPath: "/etc/ssl/certs/stackit-ca.crt", + SubPath: "stackit-ca.crt", + ReadOnly: true, + }) + } + sidecarContainer.Env = []corev1.EnvVar{} if apiEndpoints.IaaS != nil { sidecarContainer.Env = append(sidecarContainer.Env, corev1.EnvVar{ From 3fc27c4b13bf35ac80aa011fbcb6caf19a5598f1 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 5 Jun 2026 15:08:38 +0200 Subject: [PATCH 03/29] deduplicate CABundle for CSI/CCM Signed-off-by: Niclas Schad --- .../templates/deployment-csi-driver-controller.yaml | 6 +++--- .../templates/secret.yaml | 9 --------- .../templates/stackit-cloud-controller-manager.yaml | 6 +++--- 3 files changed, 6 insertions(+), 15 deletions(-) delete mode 100644 charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml index 203557c9..7703d6f8 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml @@ -90,7 +90,7 @@ spec: - name: cloud-provider-config mountPath: /etc/config - mountPath: /etc/ssl/certs/stackit-ca.crt - name: stackit-ca-cert + name: stackit-ca subPath: stackit-ca.crt readOnly: true securityContext: @@ -344,7 +344,7 @@ spec: configMap: defaultMode: 420 name: {{ .Values.prefix }}-cloud-provider-config - - name: stackit-ca-cert + - name: stackit-ca secret: optional: true - secretName: {{ .Values.prefix}}-stackit-ca + secretName: stackit-ca-bundle diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml deleted file mode 100644 index aa81c30b..00000000 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{ if .Values.stackitEndpoints.caCert }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.prefix}}-stackit-ca - namespace: {{ .Release.Namespace }} -data: - stackit-ca.crt: {{ .Values.stackitEndpoints.caCert | b64enc }} -{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml index 0469208c..d805851b 100644 --- a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml @@ -90,7 +90,7 @@ spec: - mountPath: /etc/serviceaccount name: sa-secret - mountPath: /etc/ssl/certs/stackit-ca.crt - name: cloud-config-ca + name: stackit-ca subPath: stackit-ca.crt readOnly: true securityContext: @@ -111,9 +111,9 @@ spec: items: - key: "serviceaccount.json" path: "serviceaccount.json" - - name: cloud-config-ca + - name: stackit-ca secret: - secretName: cloud-config-extra-ca + secretName: stackit-ca-bundle optional: true - name: kubeconfig projected: From 3951f6dabe37e7be79f06dfbb381e1ec1ab5e94f Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 5 Jun 2026 15:12:35 +0200 Subject: [PATCH 04/29] align volumeName with CSI/CCM Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 5a045111..6712dd6c 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -91,7 +91,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, if caBundle != "" { machineDeployment.Spec.Template.Spec.Volumes = append(machineDeployment.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "stackit-ca-bundle", + Name: "stackit-ca", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: "stackit-ca-bundle", @@ -99,7 +99,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, }, }) sidecarContainer.VolumeMounts = append(sidecarContainer.VolumeMounts, corev1.VolumeMount{ - Name: "stackit-ca-bundle", + Name: "stackit-ca", MountPath: "/etc/ssl/certs/stackit-ca.crt", SubPath: "stackit-ca.crt", ReadOnly: true, From 1247bc87196890775f23aaf6cb67ff0fb6efaf20 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 09:46:38 +0200 Subject: [PATCH 05/29] fix some lint errors Signed-off-by: Niclas Schad --- .../cloud-provider-config/templates/stackit-ca-bundle.yaml | 6 +++--- charts/internal/cloud-provider-config/values.yaml | 2 +- pkg/controller/controlplane/valuesprovider.go | 7 +++++-- pkg/webhook/controlplane/ensurer.go | 7 ++++++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index 5c8fb15f..4ea2166b 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -1,4 +1,4 @@ -{{- if .Values.stackitCaCert }} +{{- if .Values.cloudProfileCABundle }} apiVersion: v1 kind: Secret metadata: @@ -6,7 +6,7 @@ metadata: namespace: {{ .Release.Namespace }} type: Opaque data: - {{- if .Values.stackitCaCert }} - stackit-ca.crt: {{ .Values.stackitCaCert | b64enc }} + {{- if .Values.cloudProfileCABundle }} + stackit-ca.crt: {{ .Values.cloudProfileCABundle | b64enc }} {{- end }} {{- end }} diff --git a/charts/internal/cloud-provider-config/values.yaml b/charts/internal/cloud-provider-config/values.yaml index 776c9ed2..35629903 100644 --- a/charts/internal/cloud-provider-config/values.yaml +++ b/charts/internal/cloud-provider-config/values.yaml @@ -1,5 +1,5 @@ stackitOnly: false -stackitCaCert: "" +cloudProfileCABundle: "" # [Global] authUrl: fooURL domainName: fooDomain diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index f04d231b..e4d7b8a8 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -681,10 +681,13 @@ func getConfigChartValues( } } - if feature.UseStackitMachineControllerManager(cluster) { + // Deploy the stackit-ca-bundle when at least one of stackit-mcm, stackit-csi or stackit-ccm is deployed + if feature.UseStackitMachineControllerManager(cluster) || + getCSIDriver(controlPlaneConfig) == stackitv1alpha1.STACKIT || + getCCMController(controlPlaneConfig) == stackitv1alpha1.STACKIT { if cloudProfileConfig.CABundle != nil { caBundle := ptr.Deref(cloudProfileConfig.CABundle, "") - values["stackitCaCert"] = caBundle + values["cloudProfileCABundle"] = caBundle } } diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 6712dd6c..07ef3863 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -34,6 +34,11 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/webhook/controlplane/registrycache" ) +const ( + //nolint:gosec // These are not a credentials. Just the name of the kubernetes Secret + CASecretName = "stackit-ca-bundle" +) + // NewEnsurer creates a new controlplane ensurer. func NewEnsurer(regCaches []config.RegistryCacheConfiguration, logger logr.Logger) genericmutator.Ensurer { return &ensurer{ @@ -94,7 +99,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, Name: "stackit-ca", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: "stackit-ca-bundle", + SecretName: CASecretName, }, }, }) From 155c48e8da1345afd0e4392c66021651ea58e043 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 11:33:03 +0200 Subject: [PATCH 06/29] drop costuom-certs in provider-stackit Signed-off-by: Niclas Schad --- .../templates/deployment.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/charts/gardener-extension-provider-stackit/templates/deployment.yaml b/charts/gardener-extension-provider-stackit/templates/deployment.yaml index b3ce43bb..3f13b58e 100644 --- a/charts/gardener-extension-provider-stackit/templates/deployment.yaml +++ b/charts/gardener-extension-provider-stackit/templates/deployment.yaml @@ -130,10 +130,6 @@ spec: volumeMounts: - name: config mountPath: /etc/{{ include "name" . }}/config - - name: custom-certs - mountPath: /etc/ssl/certs/customca.crt - readOnly: true - subPath: customca.crt {{- if .Values.imageVectorOverwrite }} - name: imagevector-overwrite mountPath: /charts_overwrite/ From 402ffcd289dd360d9de178da413a5e6fc9c0ce81 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 14:18:55 +0200 Subject: [PATCH 07/29] do not double-encode cloudprofile caBundle Signed-off-by: Niclas Schad --- .../templates/stackit-ca-bundle.yaml | 4 ++-- .../cloud-provider-config/values.yaml | 2 +- .../templates/secret.yaml | 9 --------- .../values.yaml | 1 - pkg/controller/controlplane/valuesprovider.go | 20 +++++++------------ pkg/utils/utils.go | 13 ++++++++++++ pkg/webhook/controlplane/ensurer.go | 8 +------- 7 files changed, 24 insertions(+), 33 deletions(-) delete mode 100644 charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index 4ea2166b..590f49e0 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -6,7 +6,7 @@ metadata: namespace: {{ .Release.Namespace }} type: Opaque data: - {{- if .Values.cloudProfileCABundle }} - stackit-ca.crt: {{ .Values.cloudProfileCABundle | b64enc }} + {{- if .Values.cloudProfileCABundleB64 }} + stackit-ca.crt: {{ .Values.cloudProfileCABundleB64 }} {{- end }} {{- end }} diff --git a/charts/internal/cloud-provider-config/values.yaml b/charts/internal/cloud-provider-config/values.yaml index 35629903..655e83be 100644 --- a/charts/internal/cloud-provider-config/values.yaml +++ b/charts/internal/cloud-provider-config/values.yaml @@ -1,5 +1,5 @@ stackitOnly: false -cloudProfileCABundle: "" +cloudProfileCABundleB64: "" # [Global] authUrl: fooURL domainName: fooDomain diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml deleted file mode 100644 index 7502f20a..00000000 --- a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{ if .Values.config.caCert }} -apiVersion: v1 -kind: Secret -metadata: - name: cloud-config-extra-ca - namespace: {{ .Release.Namespace }} -data: - stackit-ca.crt: {{ .Values.config.caCert | b64enc }} -{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml index 497a5ed8..4f60b830 100644 --- a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/values.yaml @@ -8,7 +8,6 @@ config: loadBalancerApiUrl: "" iaasApiUrl: "" tokenUrl: "" - caCert: "" loadBalancerEmergencyToken: "" port: 10258 metricsPort: 9090 diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index e4d7b8a8..238e62be 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -54,6 +54,7 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/feature" "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" ) const ( @@ -686,8 +687,13 @@ func getConfigChartValues( getCSIDriver(controlPlaneConfig) == stackitv1alpha1.STACKIT || getCCMController(controlPlaneConfig) == stackitv1alpha1.STACKIT { if cloudProfileConfig.CABundle != nil { + // caBundle is already in base64 format caBundle := ptr.Deref(cloudProfileConfig.CABundle, "") - values["cloudProfileCABundle"] = caBundle + if !utils.IsBase64(caBundle) { + return nil, fmt.Errorf("CA bundle is not base64 encoded." + + " Please ensure that caBundle in the CloudProfile is base64 encoded") + } + values["cloudProfileCABundleB64"] = caBundle } } @@ -861,13 +867,6 @@ func getSTACKITCCMChartValues( return nil, fmt.Errorf("no STACKIT credentials are provided in cluster %s", cluster.Shoot.Name) } - var caBundle string - if cloudProfileConfig, err := helper.CloudProfileConfigFromCluster(cluster); err == nil { - if cloudProfileConfig.CABundle != nil { - caBundle = ptr.Deref(cloudProfileConfig.CABundle, "") - } - } - ccmConfig := map[string]any{ "stackitNetworkID": infra.Networks.ID, "stackitRegion": stackitRegion, @@ -882,11 +881,6 @@ func getSTACKITCCMChartValues( "customLabelDomain": customLabelDomain, } - // Add extra CA to the pod's filesystem - if caBundle != "" { - ccmConfig["caCert"] = caBundle - } - if credentials.LoadBalancerAPIEmergencyToken != "" { ccmConfig["loadBalancerEmergencyToken"] = credentials.LoadBalancerAPIEmergencyToken } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index f1308182..a610d669 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,6 +5,7 @@ package utils import ( + "encoding/base64" "fmt" "strings" @@ -71,6 +72,18 @@ func ComputeEgressCIDRs(ips []string) []string { return result } +func IsBase64(s string) bool { + // Support padded encoding + if _, err := base64.StdEncoding.DecodeString(s); err == nil { + return true + } + // Support unpadded encodings + if _, err := base64.RawStdEncoding.DecodeString(s); err == nil { + return true + } + return false +} + // BuildLabelKey constructs a label key from a custom domain and suffix. // If customDomain is empty, it defaults to "kubernetes.io". // Example: BuildLabelKey("ske.stackit.cloud", "cluster") returns "ske.stackit.cloud/cluster" diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 07ef3863..7ba61829 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -60,8 +60,6 @@ var ImageVector = imagevector.ImageVector() // EnsureMachineControllerManagerDeployment ensures that the machine-controller-manager deployment conforms to the provider requirements. func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, gctx gcontext.GardenContext, newObj, machineDeployment *appsv1.Deployment) error { - var caBundle string - cluster, err := gctx.GetCluster(ctx) if err != nil { return fmt.Errorf("failed reading Cluster: %w", err) @@ -88,13 +86,9 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, return err } - if cloudProfileConfig.CABundle != nil { - caBundle = ptr.Deref(cloudProfileConfig.CABundle, "") - } - apiEndpoints := ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) - if caBundle != "" { + if cloudProfileConfig.CABundle != nil { machineDeployment.Spec.Template.Spec.Volumes = append(machineDeployment.Spec.Template.Spec.Volumes, corev1.Volume{ Name: "stackit-ca", VolumeSource: corev1.VolumeSource{ From 36d70d0ea9676433f953c2583150cd9f08f645a2 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 14:31:11 +0200 Subject: [PATCH 08/29] fix secret chart Signed-off-by: Niclas Schad --- .../cloud-provider-config/templates/stackit-ca-bundle.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index 590f49e0..f5c3ee1f 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -1,4 +1,4 @@ -{{- if .Values.cloudProfileCABundle }} +{{- if .Values.cloudProfileCABundleB64 }} apiVersion: v1 kind: Secret metadata: From 0e8fe9579e3fcd7fa2d6f209432c93d403ed8cc9 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 15:05:05 +0200 Subject: [PATCH 09/29] decode cloudProfile.caBundle before using it Signed-off-by: Niclas Schad --- pkg/stackit/client/dns.go | 7 ++++++- pkg/stackit/client/factory.go | 14 +++++++------- pkg/stackit/client/factory_test.go | 2 +- pkg/stackit/client/iaas.go | 7 ++++++- pkg/stackit/client/loadbalancing.go | 7 ++++++- pkg/utils/utils.go | 15 +++++++++++++++ 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index f8bef734..7e9b3deb 100644 --- a/pkg/stackit/client/dns.go +++ b/pkg/stackit/client/dns.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" "k8s.io/utils/set" @@ -25,7 +26,11 @@ func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, cre return nil, err } if caBundle != "" { - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + var ca []byte + if ca, err = utils.DecodeCloudProfileCABundle(caBundle); err != nil { + return nil, err + } + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, ca) if err != nil { return nil, err } diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index da77a505..47bb919d 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -37,7 +37,7 @@ type Factory interface { type factory struct { StackitRegion string StackitAPIEndpoints stackitv1alpha1.APIEndpoints - CABundle string + CABundleB64 string } func New(region string, cluster *extensionscontroller.Cluster) Factory { @@ -54,7 +54,7 @@ func New(region string, cluster *extensionscontroller.Cluster) Factory { return &factory{ StackitRegion: region, StackitAPIEndpoints: apiEndpoints, - CABundle: caBundle, + CABundleB64: caBundle, } } @@ -64,7 +64,7 @@ func (f factory) LoadBalancing(ctx context.Context, c client.Client, secretRef c return nil, err } - return NewLoadBalancingClient(ctx, f.StackitRegion, f.StackitAPIEndpoints, credentials, f.CABundle) + return NewLoadBalancingClient(ctx, f.StackitRegion, f.StackitAPIEndpoints, credentials, f.CABundleB64) } func (f factory) IaaS(ctx context.Context, c client.Client, secretRef corev1.SecretReference) (IaaSClient, error) { @@ -73,7 +73,7 @@ func (f factory) IaaS(ctx context.Context, c client.Client, secretRef corev1.Sec return nil, err } - return NewIaaSClient(f.StackitRegion, f.StackitAPIEndpoints, credentials, f.CABundle) + return NewIaaSClient(f.StackitRegion, f.StackitAPIEndpoints, credentials, f.CABundleB64) } func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.SecretReference) (DNSClient, error) { @@ -82,17 +82,17 @@ func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.Secr return nil, err } - return NewDNSClient(ctx, f.StackitAPIEndpoints, credentials, f.CABundle) + return NewDNSClient(ctx, f.StackitAPIEndpoints, credentials, f.CABundleB64) } // InjectCAIntoHTTPClient injects a CABundle into an existing http.Client -func InjectCAIntoHTTPClient(client *http.Client, caBundle string) error { +func InjectCAIntoHTTPClient(client *http.Client, caBundle []byte) error { caCertPool, err := x509.SystemCertPool() if err != nil { // we could also fall back here and use an empty pool via x509.NewCertPool() return err } - if ok := caCertPool.AppendCertsFromPEM([]byte(caBundle)); !ok { + if ok := caCertPool.AppendCertsFromPEM(caBundle); !ok { return fmt.Errorf("failed to append CA bundle to cert pool") } var transport *http.Transport diff --git a/pkg/stackit/client/factory_test.go b/pkg/stackit/client/factory_test.go index 1da0c729..73e712e3 100644 --- a/pkg/stackit/client/factory_test.go +++ b/pkg/stackit/client/factory_test.go @@ -42,7 +42,7 @@ var _ = Describe("InjectCAIntoHTTPClient", func() { } serverCertPEM := pem.EncodeToMemory(pemBlock) - err := InjectCAIntoHTTPClient(httpClient, string(serverCertPEM)) + err := InjectCAIntoHTTPClient(httpClient, serverCertPEM) Expect(err).NotTo(HaveOccurred(), "InjectCAIntoHTTPClient should not return an error") resp, err := httpClient.Get(testServer.URL) diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index 583114c7..3947cb2b 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -8,6 +8,7 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" @@ -142,7 +143,11 @@ func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, creden return nil, err } if caBundle != "" { - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + var ca []byte + if ca, err = utils.DecodeCloudProfileCABundle(caBundle); err != nil { + return nil, err + } + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, ca) if err != nil { return nil, err } diff --git a/pkg/stackit/client/loadbalancing.go b/pkg/stackit/client/loadbalancing.go index 338cd1df..5527ec2a 100644 --- a/pkg/stackit/client/loadbalancing.go +++ b/pkg/stackit/client/loadbalancing.go @@ -3,6 +3,7 @@ package client import ( "context" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" @@ -38,7 +39,11 @@ func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv return nil, err } if caBundle != "" { - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + var ca []byte + if ca, err = utils.DecodeCloudProfileCABundle(caBundle); err != nil { + return nil, err + } + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, ca) if err != nil { return nil, err } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a610d669..2ced4455 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + gardenerutils "github.com/gardener/gardener/pkg/utils" utilsnet "k8s.io/utils/net" ) @@ -84,6 +85,20 @@ func IsBase64(s string) bool { return false } +// DecodeCloudProfileCABundle returns the decoded data from gardener CloudProfile.caBundle +func DecodeCloudProfileCABundle(data string) ([]byte, error) { + if !IsBase64(data) { + return nil, fmt.Errorf("caBundle must be base64 encoded") + } + + cert, err := gardenerutils.DecodeBase64(data) + if err != nil { + return nil, err + } + + return cert, nil +} + // BuildLabelKey constructs a label key from a custom domain and suffix. // If customDomain is empty, it defaults to "kubernetes.io". // Example: BuildLabelKey("ske.stackit.cloud", "cluster") returns "ske.stackit.cloud/cluster" From 521eb0691e875c4bba8347edd5aa416ad0eaa5f5 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 15:46:29 +0200 Subject: [PATCH 10/29] use stackit SDK WithCustomHTTPClient for custom Transport with modified Root CA's Signed-off-by: Niclas Schad --- pkg/stackit/client/dns.go | 17 +++------ pkg/stackit/client/factory.go | 55 +++++++++++++--------------- pkg/stackit/client/factory_test.go | 56 ----------------------------- pkg/stackit/client/iaas.go | 18 +++------- pkg/stackit/client/loadbalancing.go | 17 +++------ 5 files changed, 39 insertions(+), 124 deletions(-) delete mode 100644 pkg/stackit/client/factory_test.go diff --git a/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index 7e9b3deb..ea786e98 100644 --- a/pkg/stackit/client/dns.go +++ b/pkg/stackit/client/dns.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" "k8s.io/utils/set" @@ -15,7 +14,10 @@ import ( ) func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (DNSClient, error) { - options := clientOptions(endpoints, credentials) + options, err := clientOptions(endpoints, credentials, caBundle) + if err != nil { + return nil, err + } if endpoints.DNS != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.DNS)) @@ -25,16 +27,7 @@ func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, cre if err != nil { return nil, err } - if caBundle != "" { - var ca []byte - if ca, err = utils.DecodeCloudProfileCABundle(caBundle); err != nil { - return nil, err - } - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, ca) - if err != nil { - return nil, err - } - } + return &dnsClient{ api: apiClient.DefaultAPI, projectID: credentials.ProjectID, diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 47bb919d..0fce3e4e 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -8,6 +8,7 @@ import ( "net/http" extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" @@ -86,44 +87,25 @@ func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.Secr } // InjectCAIntoHTTPClient injects a CABundle into an existing http.Client -func InjectCAIntoHTTPClient(client *http.Client, caBundle []byte) error { +func InjectCAIntoHTTPClient(caBundle []byte) (*http.Client, error) { caCertPool, err := x509.SystemCertPool() if err != nil { // we could also fall back here and use an empty pool via x509.NewCertPool() - return err + return nil, err } if ok := caCertPool.AppendCertsFromPEM(caBundle); !ok { - return fmt.Errorf("failed to append CA bundle to cert pool") - } - var transport *http.Transport - if client.Transport != nil { - var ok bool - transport, ok = client.Transport.(*http.Transport) - if !ok { - return fmt.Errorf("client.Transport is not an *http.Transport") - } - // Clone it to avoid race conditions if the transport is shared - transport = transport.Clone() - } else { - // Explicitly clone the default transport - // The client should already have transport. Should never happen - transport = http.DefaultTransport.(*http.Transport).Clone() + return nil, fmt.Errorf("failed to append CA bundle to cert pool") } - // Inject the custom TLS configuration - if transport.TLSClientConfig == nil { - transport.TLSClientConfig = &tls.Config{} - } else { - transport.TLSClientConfig = transport.TLSClientConfig.Clone() + httpClient := http.Client{} + httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: caCertPool, + }, } - - transport.TLSClientConfig.RootCAs = caCertPool - - // Re-assign the modified transport back to the client - client.Transport = transport - return nil + return &httpClient, nil } -func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) []sdkconfig.ConfigurationOption { +func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) ([]sdkconfig.ConfigurationOption, error) { result := []sdkconfig.ConfigurationOption{ sdkconfig.WithUserAgent(UserAgent), sdkconfig.WithServiceAccountKey(credentials.SaKeyJSON), @@ -133,5 +115,18 @@ func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit. result = append(result, sdkconfig.WithTokenEndpoint(*endpoints.TokenEndpoint)) } - return result + if caBundle != "" { + var ca []byte + ca, err := utils.DecodeCloudProfileCABundle(caBundle) + if err != nil { + return nil, err + } + customHttpClient, err := InjectCAIntoHTTPClient(ca) + if err != nil { + return nil, err + } + result = append(result, sdkconfig.WithHTTPClient(customHttpClient)) + } + + return result, nil } diff --git a/pkg/stackit/client/factory_test.go b/pkg/stackit/client/factory_test.go deleted file mode 100644 index 73e712e3..00000000 --- a/pkg/stackit/client/factory_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package client - -import ( - "crypto/tls" - "encoding/pem" - "net/http" - "net/http/httptest" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("InjectCAIntoHTTPClient", func() { - var ( - testServer *httptest.Server - httpClient *http.Client - ) - - BeforeEach(func() { - testServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - httpClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{}, - }, - } - }) - - AfterEach(func() { - testServer.Close() - }) - - Context("when the custom CA is injected into the HTTP client", func() { - It("should successfully connect to the TLS server without certificate errors", func() { - serverCert := testServer.Certificate() - - pemBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: serverCert.Raw, - } - serverCertPEM := pem.EncodeToMemory(pemBlock) - - err := InjectCAIntoHTTPClient(httpClient, serverCertPEM) - Expect(err).NotTo(HaveOccurred(), "InjectCAIntoHTTPClient should not return an error") - - resp, err := httpClient.Get(testServer.URL) - - Expect(err).NotTo(HaveOccurred(), "The client should trust the server's certificate") - defer resp.Body.Close() - - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - }) - }) -}) diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index 3947cb2b..628f1a96 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -8,7 +8,6 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" @@ -128,8 +127,10 @@ func (c iaasClient) GetNetworkByName(ctx context.Context, name string) ([]iaas.N } func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (IaaSClient, error) { - options := clientOptions(endpoints, credentials) - + options, err := clientOptions(endpoints, credentials, caBundle) + if err != nil { + return nil, err + } if endpoints.IaaS != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.IaaS)) } @@ -142,16 +143,7 @@ func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, creden if err != nil { return nil, err } - if caBundle != "" { - var ca []byte - if ca, err = utils.DecodeCloudProfileCABundle(caBundle); err != nil { - return nil, err - } - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, ca) - if err != nil { - return nil, err - } - } + return &iaasClient{ Client: apiClient.DefaultAPI, projectID: credentials.ProjectID, diff --git a/pkg/stackit/client/loadbalancing.go b/pkg/stackit/client/loadbalancing.go index 5527ec2a..c25f7477 100644 --- a/pkg/stackit/client/loadbalancing.go +++ b/pkg/stackit/client/loadbalancing.go @@ -3,7 +3,6 @@ package client import ( "context" - "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" @@ -28,8 +27,10 @@ type loadBalancingClient struct { } func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (LoadBalancingClient, error) { - options := clientOptions(endpoints, credentials) - + options, err := clientOptions(endpoints, credentials, caBundle) + if err != nil { + return nil, err + } if endpoints.LoadBalancer != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.LoadBalancer)) } @@ -38,16 +39,6 @@ func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv if err != nil { return nil, err } - if caBundle != "" { - var ca []byte - if ca, err = utils.DecodeCloudProfileCABundle(caBundle); err != nil { - return nil, err - } - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, ca) - if err != nil { - return nil, err - } - } return &loadBalancingClient{ Client: apiClient.DefaultAPI, projectID: credentials.ProjectID, From 0cb5e9c81ef0e9d0d5004ef7c72267493a15ef07 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 15:56:32 +0200 Subject: [PATCH 11/29] format and simplify code Signed-off-by: Niclas Schad --- pkg/stackit/client/factory.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 0fce3e4e..559ef82e 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -8,7 +8,6 @@ import ( "net/http" extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" - "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" @@ -17,6 +16,7 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/helper" stackitv1alpha1 "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/v1alpha1" "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" ) const ( @@ -90,20 +90,18 @@ func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.Secr func InjectCAIntoHTTPClient(caBundle []byte) (*http.Client, error) { caCertPool, err := x509.SystemCertPool() if err != nil { - // we could also fall back here and use an empty pool via x509.NewCertPool() + // we could also fall back here and use an empty pool via x509.NewCertPool() return nil, err } if ok := caCertPool.AppendCertsFromPEM(caBundle); !ok { return nil, fmt.Errorf("failed to append CA bundle to cert pool") } - httpClient := http.Client{} - httpClient.Transport = &http.Transport{ + return &http.Client{Transport: &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: caCertPool, }, - } - return &httpClient, nil + }}, nil } func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) ([]sdkconfig.ConfigurationOption, error) { result := []sdkconfig.ConfigurationOption{ From 5b01a22613fcb572c877c5095d9a68151d4bbf2f Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 15:58:42 +0200 Subject: [PATCH 12/29] remove unused helm value Signed-off-by: Niclas Schad --- .../charts/stackit-blockstorage-csi-driver/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml index 2e83206b..60ff609a 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml @@ -22,7 +22,6 @@ userAgentHeaders: [] stackitEndpoints: tokenUrl: "" iaasUrl: "" - caCert: "" csiSnapshotController: replicas: 1 From 980bdbf8119930c782a0bc7367f8602406fc757c Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 8 Jun 2026 16:06:37 +0200 Subject: [PATCH 13/29] rename InjectCAIntoHTTPClient => newHTTPClientWithCustomCA Signed-off-by: Niclas Schad --- pkg/stackit/client/factory.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 559ef82e..156f4187 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -86,8 +86,8 @@ func (f factory) DNS(ctx context.Context, c client.Client, secretRef corev1.Secr return NewDNSClient(ctx, f.StackitAPIEndpoints, credentials, f.CABundleB64) } -// InjectCAIntoHTTPClient injects a CABundle into an existing http.Client -func InjectCAIntoHTTPClient(caBundle []byte) (*http.Client, error) { +// newHTTPClientWithCustomCA creates an http.Client with a custom CA +func newHTTPClientWithCustomCA(caBundle []byte) (*http.Client, error) { caCertPool, err := x509.SystemCertPool() if err != nil { // we could also fall back here and use an empty pool via x509.NewCertPool() @@ -103,6 +103,7 @@ func InjectCAIntoHTTPClient(caBundle []byte) (*http.Client, error) { }, }}, nil } + func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) ([]sdkconfig.ConfigurationOption, error) { result := []sdkconfig.ConfigurationOption{ sdkconfig.WithUserAgent(UserAgent), @@ -119,7 +120,7 @@ func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit. if err != nil { return nil, err } - customHttpClient, err := InjectCAIntoHTTPClient(ca) + customHttpClient, err := newHTTPClientWithCustomCA(ca) if err != nil { return nil, err } From 74fff99267235230a918bce7bf8647f8c2af74c3 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 10:18:24 +0200 Subject: [PATCH 14/29] fix: use newObj instead of old machineDeployment Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 7ba61829..a0a489de 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -59,7 +59,7 @@ type ensurer struct { var ImageVector = imagevector.ImageVector() // EnsureMachineControllerManagerDeployment ensures that the machine-controller-manager deployment conforms to the provider requirements. -func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, gctx gcontext.GardenContext, newObj, machineDeployment *appsv1.Deployment) error { +func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, gctx gcontext.GardenContext, newObj, _ *appsv1.Deployment) error { cluster, err := gctx.GetCluster(ctx) if err != nil { return fmt.Errorf("failed reading Cluster: %w", err) @@ -89,7 +89,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, apiEndpoints := ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) if cloudProfileConfig.CABundle != nil { - machineDeployment.Spec.Template.Spec.Volumes = append(machineDeployment.Spec.Template.Spec.Volumes, corev1.Volume{ + newObj.Spec.Template.Spec.Volumes = append(newObj.Spec.Template.Spec.Volumes, corev1.Volume{ Name: "stackit-ca", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ From 5b75cf3c7c25c8b8cbc2fe26b859cb4ffc6baaad Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 10:46:16 +0200 Subject: [PATCH 15/29] add unit test for webhook Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer_test.go | 89 ++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/pkg/webhook/controlplane/ensurer_test.go b/pkg/webhook/controlplane/ensurer_test.go index fd55dbc9..2c7c5691 100644 --- a/pkg/webhook/controlplane/ensurer_test.go +++ b/pkg/webhook/controlplane/ensurer_test.go @@ -6,9 +6,17 @@ package controlplane import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" "encoding/json" + "encoding/pem" + "math/big" "strings" "testing" + "time" "github.com/Masterminds/semver/v3" "github.com/coreos/go-systemd/v22/unit" @@ -114,6 +122,31 @@ var _ = Describe("Ensurer", func() { }, }, ) + eContextK8s127WithSTACKITMCMANDCUSTOMCA = gcontext.NewInternalGardenContext( + &extensionscontroller.Cluster{ + CloudProfile: &gardencorev1beta1.CloudProfile{ + Spec: gardencorev1beta1.CloudProfileSpec{ + ProviderConfig: &runtime.RawExtension{ + Raw: encode(&stackitv1alpha1.CloudProfileConfig{ + CABundle: new(generateRootCA()), + }), + }, + }, + }, + Shoot: &gardencorev1beta1.Shoot{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + feature.ShootUseSTACKITMachineControllerManager: "true", + }, + }, + Spec: gardencorev1beta1.ShootSpec{ + Kubernetes: gardencorev1beta1.Kubernetes{ + Version: "1.27.1", + }, + }, + }, + }, + ) eContextK8s127WithSTACKITMCMAndAPIEndpoints = gcontext.NewInternalGardenContext( &extensionscontroller.Cluster{ CloudProfile: &gardencorev1beta1.CloudProfile{ @@ -576,6 +609,31 @@ WantedBy=multi-user.target expectedContainer.Env = []corev1.EnvVar{} Expect(deployment.Spec.Template.Spec.Containers).To(ConsistOf(expectedContainer)) }) + + It("should add stackit-ca-bundle to Deployment and sidecar when CloudProfile CA Bundle is set", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(BeEmpty()) + Expect(ensurer.EnsureMachineControllerManagerDeployment(context.TODO(), eContextK8s127WithSTACKITMCMANDCUSTOMCA, deployment, nil)).To(Succeed()) + expectedContainer := machinecontrollermanager.ProviderSidecarContainer(shoot, deployment.Namespace, "provider-stackit", "foo:bar") + expectedContainer.Env = []corev1.EnvVar{} + expectedContainer.Args = extensionswebhook.EnsureStringWithPrefix( + expectedContainer.Args, "--resource-exhausted-retry=", "30m", + ) + expectedContainer.VolumeMounts = append(expectedContainer.VolumeMounts, corev1.VolumeMount{ + Name: "stackit-ca", + MountPath: "/etc/ssl/certs/stackit-ca.crt", + SubPath: "stackit-ca.crt", + ReadOnly: true, + }) + Expect(deployment.Spec.Template.Spec.Containers).To(ConsistOf(expectedContainer)) + Expect(deployment.Spec.Template.Spec.Volumes).To(ConsistOf(corev1.Volume{ + Name: "stackit-ca", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: CASecretName, + }, + }, + })) + }) }) Describe("#EnsureMachineControllerManagerDeployment with stackit and custom env vars", func() { @@ -688,3 +746,34 @@ func encode(obj runtime.Object) []byte { data, _ := json.Marshal(obj) return data } + +func generateRootCA() string { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "" + } + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Minimalist Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + + certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + if err != nil { + return "" + } + + pemBlock := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + return base64.StdEncoding.EncodeToString(pemBlock) +} From 059d60375d6e2a2ecef94fa662da066a5dd23b0c Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 10:52:44 +0200 Subject: [PATCH 16/29] add unit test for DecodeCloudProfileCABundle Signed-off-by: Niclas Schad --- pkg/utils/utils_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 0e283c8d..a196326b 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -12,6 +12,14 @@ import ( ) var _ = Describe("Utils", func() { + Describe("DecodeCloudProfileCABundle", func() { + It("should successfully decode base64", func() { + val := "Zm9vYmFy" + decoded, err := utils.DecodeCloudProfileCABundle(val) + Expect(err).ToNot(HaveOccurred()) + Expect(decoded).To(Equal([]byte("foobar"))) + }) + }) DescribeTable("#SimpleMatch", func(pattern, text string, expected bool, expectedSocre int) { match, score := utils.SimpleMatch(pattern, text) Expect(match).To(Equal(expected)) From 232f7875f4ece8731c2071865f0799c3cfaa9bb6 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 10:58:57 +0200 Subject: [PATCH 17/29] use const variable instead of string for volume name Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer.go | 5 +++-- pkg/webhook/controlplane/ensurer_test.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index a0a489de..6adfb687 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -37,6 +37,7 @@ import ( const ( //nolint:gosec // These are not a credentials. Just the name of the kubernetes Secret CASecretName = "stackit-ca-bundle" + CAVolumeName = "stackit-ca" ) // NewEnsurer creates a new controlplane ensurer. @@ -90,7 +91,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, if cloudProfileConfig.CABundle != nil { newObj.Spec.Template.Spec.Volumes = append(newObj.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "stackit-ca", + Name: CAVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: CASecretName, @@ -98,7 +99,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, }, }) sidecarContainer.VolumeMounts = append(sidecarContainer.VolumeMounts, corev1.VolumeMount{ - Name: "stackit-ca", + Name: CAVolumeName, MountPath: "/etc/ssl/certs/stackit-ca.crt", SubPath: "stackit-ca.crt", ReadOnly: true, diff --git a/pkg/webhook/controlplane/ensurer_test.go b/pkg/webhook/controlplane/ensurer_test.go index 2c7c5691..c0cb3fb0 100644 --- a/pkg/webhook/controlplane/ensurer_test.go +++ b/pkg/webhook/controlplane/ensurer_test.go @@ -619,14 +619,14 @@ WantedBy=multi-user.target expectedContainer.Args, "--resource-exhausted-retry=", "30m", ) expectedContainer.VolumeMounts = append(expectedContainer.VolumeMounts, corev1.VolumeMount{ - Name: "stackit-ca", + Name: CAVolumeName, MountPath: "/etc/ssl/certs/stackit-ca.crt", SubPath: "stackit-ca.crt", ReadOnly: true, }) Expect(deployment.Spec.Template.Spec.Containers).To(ConsistOf(expectedContainer)) Expect(deployment.Spec.Template.Spec.Volumes).To(ConsistOf(corev1.Volume{ - Name: "stackit-ca", + Name: CAVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: CASecretName, From be9ae1a6c89bd1cae18e0f017845e97c25de6bf2 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 11:01:15 +0200 Subject: [PATCH 18/29] rename cloudProfileCABundleB64 => CABundleBase64 Signed-off-by: Niclas Schad --- .../cloud-provider-config/templates/stackit-ca-bundle.yaml | 6 +++--- charts/internal/cloud-provider-config/values.yaml | 2 +- pkg/controller/controlplane/valuesprovider.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index f5c3ee1f..ac1765b1 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -1,4 +1,4 @@ -{{- if .Values.cloudProfileCABundleB64 }} +{{- if .Values.CABundleBase64 }} apiVersion: v1 kind: Secret metadata: @@ -6,7 +6,7 @@ metadata: namespace: {{ .Release.Namespace }} type: Opaque data: - {{- if .Values.cloudProfileCABundleB64 }} - stackit-ca.crt: {{ .Values.cloudProfileCABundleB64 }} + {{- if .Values.CABundleBase64 }} + stackit-ca.crt: {{ .Values.CABundleBase64 }} {{- end }} {{- end }} diff --git a/charts/internal/cloud-provider-config/values.yaml b/charts/internal/cloud-provider-config/values.yaml index 655e83be..59f357ce 100644 --- a/charts/internal/cloud-provider-config/values.yaml +++ b/charts/internal/cloud-provider-config/values.yaml @@ -1,5 +1,5 @@ stackitOnly: false -cloudProfileCABundleB64: "" +CABundleBase64: "" # [Global] authUrl: fooURL domainName: fooDomain diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 238e62be..860c9c00 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -693,7 +693,7 @@ func getConfigChartValues( return nil, fmt.Errorf("CA bundle is not base64 encoded." + " Please ensure that caBundle in the CloudProfile is base64 encoded") } - values["cloudProfileCABundleB64"] = caBundle + values["CABundleBase64"] = caBundle } } From d8a0ca027d318403cfe35cd7be8bfee306297421 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 15:41:17 +0200 Subject: [PATCH 19/29] remove stackitv1alpha1.Cloudprofile's CABundle; use gardener's CABundle instead Signed-off-by: Niclas Schad --- .../templates/stackit-ca-bundle.yaml | 6 ++-- .../cloud-provider-config/values.yaml | 2 +- .../mutator/namespacedcloudprofile.go | 5 ---- .../mutator/namespacedcloudprofile_test.go | 19 ------------- .../validator/namespacedcloudprofile.go | 3 +- .../validator/namespacedcloudprofile_test.go | 17 ++--------- pkg/controller/controlplane/valuesprovider.go | 11 ++------ pkg/stackit/client/factory.go | 15 ++++------ pkg/utils/utils.go | 28 ------------------- pkg/utils/utils_test.go | 8 ------ pkg/webhook/controlplane/ensurer.go | 2 +- pkg/webhook/controlplane/ensurer_test.go | 9 ++---- 12 files changed, 18 insertions(+), 107 deletions(-) diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index ac1765b1..4cb90a1b 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -1,4 +1,4 @@ -{{- if .Values.CABundleBase64 }} +{{- if .Values.CABundle }} apiVersion: v1 kind: Secret metadata: @@ -6,7 +6,7 @@ metadata: namespace: {{ .Release.Namespace }} type: Opaque data: - {{- if .Values.CABundleBase64 }} - stackit-ca.crt: {{ .Values.CABundleBase64 }} + {{- if .Values.CABundle }} + stackit-ca.crt: {{ .Values.CABundle }} {{- end }} {{- end }} diff --git a/charts/internal/cloud-provider-config/values.yaml b/charts/internal/cloud-provider-config/values.yaml index 59f357ce..fd0fbbd7 100644 --- a/charts/internal/cloud-provider-config/values.yaml +++ b/charts/internal/cloud-provider-config/values.yaml @@ -1,5 +1,5 @@ stackitOnly: false -CABundleBase64: "" +CABundle: "" # [Global] authUrl: fooURL domainName: fooDomain diff --git a/pkg/admission/mutator/namespacedcloudprofile.go b/pkg/admission/mutator/namespacedcloudprofile.go index d66b6fa6..4c944a2e 100644 --- a/pkg/admission/mutator/namespacedcloudprofile.go +++ b/pkg/admission/mutator/namespacedcloudprofile.go @@ -64,11 +64,6 @@ func (p *namespacedCloudProfile) Mutate(_ context.Context, newObj, _ client.Obje statusConfig.APIEndpoints = specConfig.APIEndpoints } - // Overwrite CA bundle from spec - if specConfig.CABundle != nil { - statusConfig.CABundle = specConfig.CABundle - } - modifiedStatusConfig, err := json.Marshal(statusConfig) if err != nil { return err diff --git a/pkg/admission/mutator/namespacedcloudprofile_test.go b/pkg/admission/mutator/namespacedcloudprofile_test.go index 069efc06..04133e56 100644 --- a/pkg/admission/mutator/namespacedcloudprofile_test.go +++ b/pkg/admission/mutator/namespacedcloudprofile_test.go @@ -96,25 +96,6 @@ var _ = Describe("NamespacedCloudProfile Mutator", func() { Expect(mergedConfig.APIEndpoints.LoadBalancer).To(PointTo(Equal("https://custom-lb.example.com"))) }) - It("should correctly merge caBundle from spec", func() { - namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{ -"apiVersion":"stackit.provider.extensions.gardener.cloud/v1alpha1", -"kind":"CloudProfileConfig", -"machineImages":[ - {"name":"image-1","versions":[{"version":"1.0","image":"image-name-1"}]} -]}`)} - namespacedCloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{ -"apiVersion":"stackit.provider.extensions.gardener.cloud/v1alpha1", -"kind":"CloudProfileConfig", -"caBundle":"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"}`)} - - Expect(namespacedCloudProfileMutator.Mutate(ctx, namespacedCloudProfile, nil)).To(Succeed()) - - mergedConfig, err := helper.CloudProfileConfigFromRawExtension(namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig) - Expect(err).ToNot(HaveOccurred()) - Expect(mergedConfig.CABundle).To(PointTo(Equal("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"))) - }) - It("should correctly merge extended machineImages", func() { namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{ "apiVersion":"stackit.provider.extensions.gardener.cloud/v1alpha1", diff --git a/pkg/admission/validator/namespacedcloudprofile.go b/pkg/admission/validator/namespacedcloudprofile.go index e09b13c5..da3e2641 100644 --- a/pkg/admission/validator/namespacedcloudprofile.go +++ b/pkg/admission/validator/namespacedcloudprofile.go @@ -76,12 +76,11 @@ func (p *namespacedCloudProfile) validateNamespacedCloudProfileProviderConfig(pr }, MachineImages: providerConfig.MachineImages, APIEndpoints: providerConfig.APIEndpoints, - CABundle: providerConfig.CABundle, } if !equality.Semantic.DeepEqual(validationProviderConfig, providerConfig) { allErrs = append(allErrs, field.Forbidden( field.NewPath("spec.providerConfig"), - "must only set machineImages, stackitAPIEndpoints, and stackitCABundle", + "must only set machineImages and stackitAPIEndpoints", )) } diff --git a/pkg/admission/validator/namespacedcloudprofile_test.go b/pkg/admission/validator/namespacedcloudprofile_test.go index 2c63f349..b717fdcb 100644 --- a/pkg/admission/validator/namespacedcloudprofile_test.go +++ b/pkg/admission/validator/namespacedcloudprofile_test.go @@ -281,7 +281,7 @@ var _ = Describe("NamespacedCloudProfile Validator", func() { Expect(err).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeForbidden), "Field": Equal("spec.providerConfig"), - "Detail": Equal("must only set machineImages, stackitAPIEndpoints, and stackitCABundle"), + "Detail": Equal("must only set machineImages and stackitAPIEndpoints"), })))) }) @@ -297,24 +297,11 @@ var _ = Describe("NamespacedCloudProfile Validator", func() { Expect(namespacedCloudProfileValidator.Validate(ctx, namespacedCloudProfile, nil)).To(Succeed()) }) - It("should succeed for NamespacedCloudProfile specifying only caBundle", func() { - namespacedCloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{ -"apiVersion":"stackit.provider.extensions.gardener.cloud/v1alpha1", -"kind":"CloudProfileConfig", -"caBundle":"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----" -}`)} - - Expect(fakeClient.Create(ctx, cloudProfile)).To(Succeed()) - - Expect(namespacedCloudProfileValidator.Validate(ctx, namespacedCloudProfile, nil)).To(Succeed()) - }) - - It("should succeed for NamespacedCloudProfile specifying apiEndpoints, caBundle, and machineImages together", func() { + It("should succeed for NamespacedCloudProfile specifying apiEndpoints, and machineImages together", func() { namespacedCloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{ "apiVersion":"stackit.provider.extensions.gardener.cloud/v1alpha1", "kind":"CloudProfileConfig", "apiEndpoints":{"iaas":"https://custom-iaas.example.com"}, -"caBundle":"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----", "machineImages":[ {"name":"image-1","versions":[{"version":"1.0","image":"image-name-1","regions":[{"name":"image-region-1","id":"id-img-reg-1"}]}]} ] diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 860c9c00..fc9c7cc4 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -54,7 +54,6 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/feature" "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" - "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" ) const ( @@ -686,14 +685,10 @@ func getConfigChartValues( if feature.UseStackitMachineControllerManager(cluster) || getCSIDriver(controlPlaneConfig) == stackitv1alpha1.STACKIT || getCCMController(controlPlaneConfig) == stackitv1alpha1.STACKIT { - if cloudProfileConfig.CABundle != nil { + if cluster.CloudProfile != nil && cluster.CloudProfile.Spec.CABundle != nil { // caBundle is already in base64 format - caBundle := ptr.Deref(cloudProfileConfig.CABundle, "") - if !utils.IsBase64(caBundle) { - return nil, fmt.Errorf("CA bundle is not base64 encoded." + - " Please ensure that caBundle in the CloudProfile is base64 encoded") - } - values["CABundleBase64"] = caBundle + caBundle := ptr.Deref(cluster.CloudProfile.Spec.CABundle, "") + values["CABundle"] = caBundle } } diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 156f4187..3216bb2e 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -16,7 +16,6 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/helper" stackitv1alpha1 "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/v1alpha1" "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" - "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/utils" ) const ( @@ -47,9 +46,10 @@ func New(region string, cluster *extensionscontroller.Cluster) Factory { if cloudProfileConfig, err := helper.CloudProfileConfigFromCluster(cluster); err == nil { apiEndpoints = ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) - if cloudProfileConfig.CABundle != nil { - caBundle = ptr.Deref(cloudProfileConfig.CABundle, "") - } + } + + if cluster.CloudProfile != nil && cluster.CloudProfile.Spec.CABundle != nil { + caBundle = ptr.Deref(cluster.CloudProfile.Spec.CABundle, "") } return &factory{ @@ -115,12 +115,7 @@ func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit. } if caBundle != "" { - var ca []byte - ca, err := utils.DecodeCloudProfileCABundle(caBundle) - if err != nil { - return nil, err - } - customHttpClient, err := newHTTPClientWithCustomCA(ca) + customHttpClient, err := newHTTPClientWithCustomCA([]byte(caBundle)) if err != nil { return nil, err } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2ced4455..f1308182 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,11 +5,9 @@ package utils import ( - "encoding/base64" "fmt" "strings" - gardenerutils "github.com/gardener/gardener/pkg/utils" utilsnet "k8s.io/utils/net" ) @@ -73,32 +71,6 @@ func ComputeEgressCIDRs(ips []string) []string { return result } -func IsBase64(s string) bool { - // Support padded encoding - if _, err := base64.StdEncoding.DecodeString(s); err == nil { - return true - } - // Support unpadded encodings - if _, err := base64.RawStdEncoding.DecodeString(s); err == nil { - return true - } - return false -} - -// DecodeCloudProfileCABundle returns the decoded data from gardener CloudProfile.caBundle -func DecodeCloudProfileCABundle(data string) ([]byte, error) { - if !IsBase64(data) { - return nil, fmt.Errorf("caBundle must be base64 encoded") - } - - cert, err := gardenerutils.DecodeBase64(data) - if err != nil { - return nil, err - } - - return cert, nil -} - // BuildLabelKey constructs a label key from a custom domain and suffix. // If customDomain is empty, it defaults to "kubernetes.io". // Example: BuildLabelKey("ske.stackit.cloud", "cluster") returns "ske.stackit.cloud/cluster" diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index a196326b..0e283c8d 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -12,14 +12,6 @@ import ( ) var _ = Describe("Utils", func() { - Describe("DecodeCloudProfileCABundle", func() { - It("should successfully decode base64", func() { - val := "Zm9vYmFy" - decoded, err := utils.DecodeCloudProfileCABundle(val) - Expect(err).ToNot(HaveOccurred()) - Expect(decoded).To(Equal([]byte("foobar"))) - }) - }) DescribeTable("#SimpleMatch", func(pattern, text string, expected bool, expectedSocre int) { match, score := utils.SimpleMatch(pattern, text) Expect(match).To(Equal(expected)) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 6adfb687..0c465ee6 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -89,7 +89,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, apiEndpoints := ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) - if cloudProfileConfig.CABundle != nil { + if cluster.CloudProfile != nil && cluster.CloudProfile.Spec.CABundle != nil { newObj.Spec.Template.Spec.Volumes = append(newObj.Spec.Template.Spec.Volumes, corev1.Volume{ Name: CAVolumeName, VolumeSource: corev1.VolumeSource{ diff --git a/pkg/webhook/controlplane/ensurer_test.go b/pkg/webhook/controlplane/ensurer_test.go index c0cb3fb0..b8c054df 100644 --- a/pkg/webhook/controlplane/ensurer_test.go +++ b/pkg/webhook/controlplane/ensurer_test.go @@ -10,7 +10,6 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" - "encoding/base64" "encoding/json" "encoding/pem" "math/big" @@ -126,11 +125,7 @@ var _ = Describe("Ensurer", func() { &extensionscontroller.Cluster{ CloudProfile: &gardencorev1beta1.CloudProfile{ Spec: gardencorev1beta1.CloudProfileSpec{ - ProviderConfig: &runtime.RawExtension{ - Raw: encode(&stackitv1alpha1.CloudProfileConfig{ - CABundle: new(generateRootCA()), - }), - }, + CABundle: new(generateRootCA()), }, }, Shoot: &gardencorev1beta1.Shoot{ @@ -775,5 +770,5 @@ func generateRootCA() string { Bytes: certBytes, }) - return base64.StdEncoding.EncodeToString(pemBlock) + return string(pemBlock) } From aa8823383267a293b748774f5e163530b1296b8a Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Tue, 9 Jun 2026 15:48:09 +0200 Subject: [PATCH 20/29] Gardener's Cloudprofile CABundle is not encoded. Encode it for the helm-chart Signed-off-by: Niclas Schad --- .../cloud-provider-config/templates/stackit-ca-bundle.yaml | 2 +- pkg/controller/controlplane/valuesprovider.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index 4cb90a1b..506471c8 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -7,6 +7,6 @@ metadata: type: Opaque data: {{- if .Values.CABundle }} - stackit-ca.crt: {{ .Values.CABundle }} + stackit-ca.crt: {{ .Values.CABundle | b64enc }} {{- end }} {{- end }} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index fc9c7cc4..4cbb27e3 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -686,7 +686,6 @@ func getConfigChartValues( getCSIDriver(controlPlaneConfig) == stackitv1alpha1.STACKIT || getCCMController(controlPlaneConfig) == stackitv1alpha1.STACKIT { if cluster.CloudProfile != nil && cluster.CloudProfile.Spec.CABundle != nil { - // caBundle is already in base64 format caBundle := ptr.Deref(cluster.CloudProfile.Spec.CABundle, "") values["CABundle"] = caBundle } From de1feeabcce2b37abc8a454cf9061b88dda85704 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 17:03:45 +0200 Subject: [PATCH 21/29] use EnsureVolumeWithName and EnsureVolumeMountWithName instead of appending everytime Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 0c465ee6..074c7642 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -90,7 +90,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, apiEndpoints := ptr.Deref(cloudProfileConfig.APIEndpoints, stackitv1alpha1.APIEndpoints{}) if cluster.CloudProfile != nil && cluster.CloudProfile.Spec.CABundle != nil { - newObj.Spec.Template.Spec.Volumes = append(newObj.Spec.Template.Spec.Volumes, corev1.Volume{ + newObj.Spec.Template.Spec.Volumes = extensionswebhook.EnsureVolumeWithName(newObj.Spec.Template.Spec.Volumes, corev1.Volume{ Name: CAVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ @@ -98,7 +98,7 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, }, }, }) - sidecarContainer.VolumeMounts = append(sidecarContainer.VolumeMounts, corev1.VolumeMount{ + sidecarContainer.VolumeMounts = extensionswebhook.EnsureVolumeMountWithName(sidecarContainer.VolumeMounts, corev1.VolumeMount{ Name: CAVolumeName, MountPath: "/etc/ssl/certs/stackit-ca.crt", SubPath: "stackit-ca.crt", From e85a96e362adc392a053732c78c5f36b5162c011 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 17:04:16 +0200 Subject: [PATCH 22/29] add stackit-ca-bundle from config chart to internal chart Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 4cbb27e3..b0c0155d 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -135,6 +135,7 @@ var ( Objects: []*chart.Object{ {Type: &corev1.Secret{}, Name: openstack.CloudProviderConfigName}, {Type: &corev1.Secret{}, Name: openstack.CloudProviderDiskConfigName}, + {Type: &corev1.Secret{}, Name: "stackit-ca-bundle"}, }, } From 588e65e43ba81a7ae4c9a68a24d64b81bca2985e Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 17:07:16 +0200 Subject: [PATCH 23/29] use EnsureEnvVarWithName for the ENV vars Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 074c7642..232c6ae3 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -108,13 +108,13 @@ func (e *ensurer) EnsureMachineControllerManagerDeployment(ctx context.Context, sidecarContainer.Env = []corev1.EnvVar{} if apiEndpoints.IaaS != nil { - sidecarContainer.Env = append(sidecarContainer.Env, corev1.EnvVar{ + sidecarContainer.Env = extensionswebhook.EnsureEnvVarWithName(sidecarContainer.Env, corev1.EnvVar{ Name: "STACKIT_IAAS_ENDPOINT", Value: *apiEndpoints.IaaS, }) } if apiEndpoints.TokenEndpoint != nil { - sidecarContainer.Env = append(sidecarContainer.Env, corev1.EnvVar{ + sidecarContainer.Env = extensionswebhook.EnsureEnvVarWithName(sidecarContainer.Env, corev1.EnvVar{ Name: "STACKIT_TOKEN_BASEURL", Value: *apiEndpoints.TokenEndpoint, }) From a9668785e9830febdc0a41b6a0fca5234255f246 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Wed, 24 Jun 2026 09:27:18 +0200 Subject: [PATCH 24/29] keep caBundle in the Cloudprofile to not breaking strict decoding Signed-off-by: Niclas Schad --- hack/api-reference/api.md | 2 +- pkg/apis/stackit/v1alpha1/types_cloudprofile.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hack/api-reference/api.md b/hack/api-reference/api.md index 77bdac1f..8981aef5 100644 --- a/hack/api-reference/api.md +++ b/hack/api-reference/api.md @@ -430,7 +430,7 @@ string (Optional) -

CABundle is the CA certificate bundle for API endpoints.
This field is currently ignored and reserved for future use.

+

CABundle is the CA certificate bundle for API endpoints.
This field is currently ignored and reserved for future use.
Deprecated: Will be removed soon. This field is not used anymore

diff --git a/pkg/apis/stackit/v1alpha1/types_cloudprofile.go b/pkg/apis/stackit/v1alpha1/types_cloudprofile.go index 38a3ad36..bc565426 100644 --- a/pkg/apis/stackit/v1alpha1/types_cloudprofile.go +++ b/pkg/apis/stackit/v1alpha1/types_cloudprofile.go @@ -33,6 +33,8 @@ type CloudProfileConfig struct { APIEndpoints *APIEndpoints `json:"apiEndpoints,omitempty"` // CABundle is the CA certificate bundle for API endpoints. // This field is currently ignored and reserved for future use. + // + // Deprecated: Will be removed soon. This field is not used anymore // +optional CABundle *string `json:"caBundle,omitempty"` // Bastion are the bastion options for configuring the bastion server From bcdf4ae1c2bfe4bd37c82444051f0993aa69137a Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 3 Jul 2026 09:24:18 +0200 Subject: [PATCH 25/29] improve error message for newHTTPClientWithCustomCA() Signed-off-by: Niclas Schad --- pkg/stackit/client/factory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 3216bb2e..cbece73e 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -91,7 +91,7 @@ func newHTTPClientWithCustomCA(caBundle []byte) (*http.Client, error) { caCertPool, err := x509.SystemCertPool() if err != nil { // we could also fall back here and use an empty pool via x509.NewCertPool() - return nil, err + return nil, fmt.Errorf("failed to load system cert pool: %w", err) } if ok := caCertPool.AppendCertsFromPEM(caBundle); !ok { return nil, fmt.Errorf("failed to append CA bundle to cert pool") From 85951a05835291e2331c4701d26ce2d66fc42e59 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 3 Jul 2026 09:26:02 +0200 Subject: [PATCH 26/29] rename stackit-ca-bundle to cloudprofile-ca-bundle Signed-off-by: Niclas Schad --- .../cloud-provider-config/templates/stackit-ca-bundle.yaml | 2 +- .../templates/deployment-csi-driver-controller.yaml | 2 +- .../templates/stackit-cloud-controller-manager.yaml | 2 +- pkg/controller/controlplane/valuesprovider.go | 4 ++-- pkg/webhook/controlplane/ensurer.go | 2 +- pkg/webhook/controlplane/ensurer_test.go | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml index 506471c8..14185525 100644 --- a/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml +++ b/charts/internal/cloud-provider-config/templates/stackit-ca-bundle.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: - name: stackit-ca-bundle + name: cloudprofile-ca-bundle namespace: {{ .Release.Namespace }} type: Opaque data: diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml index 7703d6f8..ebab6de2 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml @@ -347,4 +347,4 @@ spec: - name: stackit-ca secret: optional: true - secretName: stackit-ca-bundle + secretName: cloudprofile-ca-bundle diff --git a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml index d805851b..44168188 100644 --- a/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-cloud-controller-manager/templates/stackit-cloud-controller-manager.yaml @@ -113,7 +113,7 @@ spec: path: "serviceaccount.json" - name: stackit-ca secret: - secretName: stackit-ca-bundle + secretName: cloudprofile-ca-bundle optional: true - name: kubeconfig projected: diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index b0c0155d..802fa10a 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -135,7 +135,7 @@ var ( Objects: []*chart.Object{ {Type: &corev1.Secret{}, Name: openstack.CloudProviderConfigName}, {Type: &corev1.Secret{}, Name: openstack.CloudProviderDiskConfigName}, - {Type: &corev1.Secret{}, Name: "stackit-ca-bundle"}, + {Type: &corev1.Secret{}, Name: "cloudprofile-ca-bundle"}, }, } @@ -682,7 +682,7 @@ func getConfigChartValues( } } - // Deploy the stackit-ca-bundle when at least one of stackit-mcm, stackit-csi or stackit-ccm is deployed + // Deploy the cloudprofile-ca-bundle when at least one of stackit-mcm, stackit-csi or stackit-ccm is deployed if feature.UseStackitMachineControllerManager(cluster) || getCSIDriver(controlPlaneConfig) == stackitv1alpha1.STACKIT || getCCMController(controlPlaneConfig) == stackitv1alpha1.STACKIT { diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 232c6ae3..03f07a39 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -36,7 +36,7 @@ import ( const ( //nolint:gosec // These are not a credentials. Just the name of the kubernetes Secret - CASecretName = "stackit-ca-bundle" + CASecretName = "cloudprofile-ca-bundle" CAVolumeName = "stackit-ca" ) diff --git a/pkg/webhook/controlplane/ensurer_test.go b/pkg/webhook/controlplane/ensurer_test.go index b8c054df..7b5c1d49 100644 --- a/pkg/webhook/controlplane/ensurer_test.go +++ b/pkg/webhook/controlplane/ensurer_test.go @@ -605,7 +605,7 @@ WantedBy=multi-user.target Expect(deployment.Spec.Template.Spec.Containers).To(ConsistOf(expectedContainer)) }) - It("should add stackit-ca-bundle to Deployment and sidecar when CloudProfile CA Bundle is set", func() { + It("should add cloudprofile-ca-bundle to Deployment and sidecar when CloudProfile CA Bundle is set", func() { Expect(deployment.Spec.Template.Spec.Containers).To(BeEmpty()) Expect(ensurer.EnsureMachineControllerManagerDeployment(context.TODO(), eContextK8s127WithSTACKITMCMANDCUSTOMCA, deployment, nil)).To(Succeed()) expectedContainer := machinecontrollermanager.ProviderSidecarContainer(shoot, deployment.Namespace, "provider-stackit", "foo:bar") From d1bc75885cb91f76f43da1007384b35335c30a81 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 3 Jul 2026 09:27:36 +0200 Subject: [PATCH 27/29] use constant for cloudprofile-ca-bundle Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 2 +- pkg/openstack/types.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 802fa10a..d6a9565b 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -135,7 +135,7 @@ var ( Objects: []*chart.Object{ {Type: &corev1.Secret{}, Name: openstack.CloudProviderConfigName}, {Type: &corev1.Secret{}, Name: openstack.CloudProviderDiskConfigName}, - {Type: &corev1.Secret{}, Name: "cloudprofile-ca-bundle"}, + {Type: &corev1.Secret{}, Name: openstack.CloudProfileCASecretName}, }, } diff --git a/pkg/openstack/types.go b/pkg/openstack/types.go index fc3a090c..6b38156b 100644 --- a/pkg/openstack/types.go +++ b/pkg/openstack/types.go @@ -57,6 +57,8 @@ const ( // DNSCABundle is a constant for the key in a DNS secret that holds the Openstack CA Bundle for the KeyStone server. DNSCABundle = "OS_CACERT" + // CloudProfileCASecretName is the name of the secret containing the CloudPorifle caBundle + CloudProfileCASecretName = "cloudprofile-ca-bundle" // CloudProviderConfigName is the name of the secret containing the cloud provider config. CloudProviderConfigName = "cloud-provider-config" // CloudProviderDiskConfigName is the name of the secret containing the cloud provider config for disk/volume handling. It is used by kube-controller-manager. From 40db20cb763c89e86fa622f11a5202824612f2e6 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 3 Jul 2026 09:38:07 +0200 Subject: [PATCH 28/29] remove the mcm flag --resource-exhausted-retry as this version was reverted Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/webhook/controlplane/ensurer_test.go b/pkg/webhook/controlplane/ensurer_test.go index 7b5c1d49..f1975176 100644 --- a/pkg/webhook/controlplane/ensurer_test.go +++ b/pkg/webhook/controlplane/ensurer_test.go @@ -610,9 +610,6 @@ WantedBy=multi-user.target Expect(ensurer.EnsureMachineControllerManagerDeployment(context.TODO(), eContextK8s127WithSTACKITMCMANDCUSTOMCA, deployment, nil)).To(Succeed()) expectedContainer := machinecontrollermanager.ProviderSidecarContainer(shoot, deployment.Namespace, "provider-stackit", "foo:bar") expectedContainer.Env = []corev1.EnvVar{} - expectedContainer.Args = extensionswebhook.EnsureStringWithPrefix( - expectedContainer.Args, "--resource-exhausted-retry=", "30m", - ) expectedContainer.VolumeMounts = append(expectedContainer.VolumeMounts, corev1.VolumeMount{ Name: CAVolumeName, MountPath: "/etc/ssl/certs/stackit-ca.crt", From 04db70958474ab39ed6a4414a74d0a521913b793 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 3 Jul 2026 09:44:12 +0200 Subject: [PATCH 29/29] remove gosec nolint directive Signed-off-by: Niclas Schad --- pkg/webhook/controlplane/ensurer.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/webhook/controlplane/ensurer.go b/pkg/webhook/controlplane/ensurer.go index 03f07a39..4e329a70 100644 --- a/pkg/webhook/controlplane/ensurer.go +++ b/pkg/webhook/controlplane/ensurer.go @@ -35,7 +35,6 @@ import ( ) const ( - //nolint:gosec // These are not a credentials. Just the name of the kubernetes Secret CASecretName = "cloudprofile-ca-bundle" CAVolumeName = "stackit-ca" )