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..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 diff --git a/pkg/stackit/client/dns.go b/pkg/stackit/client/dns.go index 5c0af9c6..0a415825 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..aca99dfd 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,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 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("..", "..", "..")