From 693493e82bebe8a2ca6ee8ffb4eed51c8289c7b3 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 13:39:51 +0200 Subject: [PATCH 1/8] WIP: Use CA in cloudprofile for STACKIT clients Signed-off-by: Niclas Schad --- .../templates/deployment.yaml | 4 -- .../templates/secret.yaml | 9 --- pkg/stackit/client/dns.go | 8 ++- pkg/stackit/client/factory.go | 56 +++++++++++++++++-- pkg/stackit/client/iaas.go | 8 ++- pkg/stackit/client/loadbalancing.go | 8 ++- 6 files changed, 70 insertions(+), 23 deletions(-) delete mode 100644 charts/gardener-extension-provider-stackit/templates/secret.yaml 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/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index 5c0af9c6..34833c36 100644 --- a/pkg/stackit/client/dns.go +++ b/pkg/stackit/client/dns.go @@ -13,8 +13,8 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" ) -func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) (DNSClient, error) { - options := clientOptions(endpoints, credentials) +func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (DNSClient, error) { + options := clientOptions(endpoints, credentials, caBundle) if endpoints.DNS != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.DNS)) @@ -24,6 +24,10 @@ func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, cre if err != nil { return nil, err } + 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..57066f22 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 { + 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,10 +82,48 @@ 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) } -func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) []sdkconfig.ConfigurationOption { +// 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 { + panic("failed to parse root certificate") + } + 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, caBundle string) []sdkconfig.ConfigurationOption { result := []sdkconfig.ConfigurationOption{ sdkconfig.WithUserAgent(UserAgent), sdkconfig.WithServiceAccountKey(credentials.SaKeyJSON), diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index f941ac5c..1a12b807 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -126,8 +126,8 @@ 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) { - options := clientOptions(endpoints, credentials) +func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (IaaSClient, error) { + options := clientOptions(endpoints, credentials, caBundle) if endpoints.IaaS != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.IaaS)) @@ -141,6 +141,10 @@ func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, creden if err != nil { return nil, err } + 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..2f8a3e96 100644 --- a/pkg/stackit/client/loadbalancing.go +++ b/pkg/stackit/client/loadbalancing.go @@ -26,8 +26,8 @@ type loadBalancingClient struct { region string } -func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) (LoadBalancingClient, error) { - options := clientOptions(endpoints, credentials) +func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (LoadBalancingClient, error) { + options := clientOptions(endpoints, credentials, caBundle) if endpoints.LoadBalancer != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.LoadBalancer)) @@ -37,6 +37,10 @@ func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv if err != nil { return nil, err } + err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) + if err != nil { + return nil, err + } return &loadBalancingClient{ Client: apiClient.DefaultAPI, projectID: credentials.ProjectID, From ea819d1d21a18c0a89395e5e7468d22e10f9717e Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 13:42:11 +0200 Subject: [PATCH 2/8] remove caBundle reference in values.yaml Signed-off-by: Niclas Schad --- charts/gardener-extension-provider-stackit/values.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/charts/gardener-extension-provider-stackit/values.yaml b/charts/gardener-extension-provider-stackit/values.yaml index f72ae03f..74f0e1ea 100644 --- a/charts/gardener-extension-provider-stackit/values.yaml +++ b/charts/gardener-extension-provider-stackit/values.yaml @@ -75,9 +75,7 @@ config: capacity: 25Gi provisioner: block-storage.csi.stackit.cloud volumeBindingMode: WaitForFirstConsumer - stackitAPIEndpoints: - # Must be base64 encoded - caBundle: "" + stackitAPIEndpoints: {} registryCaches: # - server: reg.example.com # cache: reg-cache.example.com From 9ccbf72faa4ef7fa72a67dc5da8f7334f9198ea6 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 13:48:20 +0200 Subject: [PATCH 3/8] fix infrastructure_test.go Signed-off-by: Niclas Schad --- test/integration/infrastructure/stackit/infrastructure_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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("..", "..", "..", "..") From 86f2b8e29c1c8545d54983739858d4d08d6bb23e Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 13:48:34 +0200 Subject: [PATCH 4/8] remove unused caBundle parameter in clientOptions() Signed-off-by: Niclas Schad --- pkg/stackit/client/dns.go | 2 +- pkg/stackit/client/factory.go | 2 +- pkg/stackit/client/iaas.go | 2 +- pkg/stackit/client/loadbalancing.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index 34833c36..cce914ea 100644 --- a/pkg/stackit/client/dns.go +++ b/pkg/stackit/client/dns.go @@ -14,7 +14,7 @@ import ( ) func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (DNSClient, error) { - options := clientOptions(endpoints, credentials, caBundle) + options := clientOptions(endpoints, credentials) if endpoints.DNS != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.DNS)) diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 57066f22..79e72c9e 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -123,7 +123,7 @@ func InjectCAIntoHTTPClient(client *http.Client, caBundle string) error { client.Transport = transport return nil } -func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) []sdkconfig.ConfigurationOption { +func clientOptions(endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials) []sdkconfig.ConfigurationOption { result := []sdkconfig.ConfigurationOption{ sdkconfig.WithUserAgent(UserAgent), sdkconfig.WithServiceAccountKey(credentials.SaKeyJSON), diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index 1a12b807..00652706 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -127,7 +127,7 @@ 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, caBundle) + options := clientOptions(endpoints, credentials) if endpoints.IaaS != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.IaaS)) diff --git a/pkg/stackit/client/loadbalancing.go b/pkg/stackit/client/loadbalancing.go index 2f8a3e96..9032c50b 100644 --- a/pkg/stackit/client/loadbalancing.go +++ b/pkg/stackit/client/loadbalancing.go @@ -27,7 +27,7 @@ type loadBalancingClient struct { } func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv1alpha1.APIEndpoints, credentials *stackit.Credentials, caBundle string) (LoadBalancingClient, error) { - options := clientOptions(endpoints, credentials, caBundle) + options := clientOptions(endpoints, credentials) if endpoints.LoadBalancer != nil { options = append(options, sdkconfig.WithEndpoint(*endpoints.LoadBalancer)) From 57da7f71c6561c54631fc919481e5e58fc47577c Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 13:55:44 +0200 Subject: [PATCH 5/8] fix: only inject CA when caBundle is not empty Signed-off-by: Niclas Schad --- pkg/stackit/client/dns.go | 8 +++++--- pkg/stackit/client/factory.go | 2 +- pkg/stackit/client/iaas.go | 8 +++++--- pkg/stackit/client/loadbalancing.go | 8 +++++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index cce914ea..0a415825 100644 --- a/pkg/stackit/client/dns.go +++ b/pkg/stackit/client/dns.go @@ -24,9 +24,11 @@ func NewDNSClient(_ context.Context, endpoints stackitv1alpha1.APIEndpoints, cre if err != nil { return nil, err } - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) - 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, diff --git a/pkg/stackit/client/factory.go b/pkg/stackit/client/factory.go index 79e72c9e..437a4238 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -93,7 +93,7 @@ func InjectCAIntoHTTPClient(client *http.Client, caBundle string) error { return err } if ok := caCertPool.AppendCertsFromPEM([]byte(caBundle)); !ok { - panic("failed to parse root certificate") + return err } var transport *http.Transport if client.Transport != nil { diff --git a/pkg/stackit/client/iaas.go b/pkg/stackit/client/iaas.go index 00652706..583114c7 100644 --- a/pkg/stackit/client/iaas.go +++ b/pkg/stackit/client/iaas.go @@ -141,9 +141,11 @@ func NewIaaSClient(region string, endpoints stackitv1alpha1.APIEndpoints, creden if err != nil { return nil, err } - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) - 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, diff --git a/pkg/stackit/client/loadbalancing.go b/pkg/stackit/client/loadbalancing.go index 9032c50b..338cd1df 100644 --- a/pkg/stackit/client/loadbalancing.go +++ b/pkg/stackit/client/loadbalancing.go @@ -37,9 +37,11 @@ func NewLoadBalancingClient(_ context.Context, region string, endpoints stackitv if err != nil { return nil, err } - err = InjectCAIntoHTTPClient(apiClient.GetConfig().HTTPClient, caBundle) - 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, From c5e32befdd781b17ffcb795a005f41cbb4620461 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 21 May 2026 15:50:24 +0200 Subject: [PATCH 6/8] better error message 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 437a4238..aca99dfd 100644 --- a/pkg/stackit/client/factory.go +++ b/pkg/stackit/client/factory.go @@ -93,7 +93,7 @@ func InjectCAIntoHTTPClient(client *http.Client, caBundle string) error { return err } if ok := caCertPool.AppendCertsFromPEM([]byte(caBundle)); !ok { - return err + return fmt.Errorf("failed to append CA bundle to cert pool") } var transport *http.Transport if client.Transport != nil { From 2d7c269f4bc542172b149c10b71f4c1217f949e9 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 22 May 2026 09:38:09 +0200 Subject: [PATCH 7/8] add test for InjectCAIntoHTTPClient Signed-off-by: Niclas Schad --- pkg/stackit/client/factory_test.go | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 pkg/stackit/client/factory_test.go 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)) + }) + }) +}) From ec54626ecae22186a35a032cd677850334c57a91 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 22 May 2026 09:41:08 +0200 Subject: [PATCH 8/8] fix: shootexposure test Signed-off-by: Niclas Schad --- .../selfhostedshootexposure/selfhostedshootexposure_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/selfhostedshootexposure/selfhostedshootexposure_test.go b/test/integration/selfhostedshootexposure/selfhostedshootexposure_test.go index 2aeff0e0..b6507ad8 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("..", "..", "..")