From 4d4ac2e444e917fa3bd84f9b75e3e35834abf487 Mon Sep 17 00:00:00 2001 From: Ricardo Maraschini Date: Wed, 10 Jun 2026 11:05:46 +0200 Subject: [PATCH 1/2] feat: propagate tls profile to aws-pod-identity-webhook aws-pod-identity-webhook needs to receive two flags: - --tls-min-version - needs to comply with the hcp apiserver min tls version. - --tls-cipher-suites - needs to comply with the hcp apiserver cipher suites config. this commit propagates both configuration into the flags. --- ...eComponents_kube_apiserver_deployment.yaml | 2 + ...eComponents_kube_apiserver_deployment.yaml | 2 + .../hostedcontrolplane/v2/kas/deployment.go | 33 +-- .../v2/kas/deployment_test.go | 204 ++++++++++++++++++ 4 files changed, 229 insertions(+), 12 deletions(-) diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index aaf09d808866..f7a2fd601aef 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -315,6 +315,8 @@ spec: - --tls-cert=/var/run/app/certs/tls.crt - --tls-key=/var/run/app/certs/tls.key - --token-audience=openshift + - --tls-min-version=VersionTLS12 + - --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 image: aws-pod-identity-webhook imagePullPolicy: IfNotPresent name: aws-pod-identity-webhook diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index 0b4e199b96eb..5494b8aba8b3 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -315,6 +315,8 @@ spec: - --tls-cert=/var/run/app/certs/tls.crt - --tls-key=/var/run/app/certs/tls.key - --token-audience=openshift + - --tls-min-version=VersionTLS12 + - --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 image: aws-pod-identity-webhook imagePullPolicy: IfNotPresent name: aws-pod-identity-webhook diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go index a8f0aa040007..98c9951fc2bd 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go @@ -299,22 +299,31 @@ func updateBootstrapInitContainer(deployment *appsv1.Deployment, hcp *hyperv1.Ho } func applyAWSPodIdentityWebhookContainer(podSpec *corev1.PodSpec, hcp *hyperv1.HostedControlPlane) { + command := []string{ + "/usr/bin/aws-pod-identity-webhook", + "--annotation-prefix=eks.amazonaws.com", + "--in-cluster=false", + "--kubeconfig=/var/run/app/kubeconfig/kubeconfig", + "--logtostderr", + "--port=4443", + fmt.Sprintf("--aws-default-region=%s", hcp.Spec.Platform.AWS.Region), + "--tls-cert=/var/run/app/certs/tls.crt", + "--tls-key=/var/run/app/certs/tls.key", + "--token-audience=openshift", + } + + if tlsMinVersion := config.MinTLSVersion(hcp.Spec.Configuration.GetTLSSecurityProfile()); tlsMinVersion != "" { + command = append(command, fmt.Sprintf("--tls-min-version=%s", tlsMinVersion)) + } + if cipherSuites := config.CipherSuites(hcp.Spec.Configuration.GetTLSSecurityProfile()); len(cipherSuites) != 0 { + command = append(command, fmt.Sprintf("--tls-cipher-suites=%s", strings.Join(cipherSuites, ","))) + } + podSpec.Containers = append(podSpec.Containers, corev1.Container{ Name: "aws-pod-identity-webhook", Image: "aws-pod-identity-webhook", ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{ - "/usr/bin/aws-pod-identity-webhook", - "--annotation-prefix=eks.amazonaws.com", - "--in-cluster=false", - "--kubeconfig=/var/run/app/kubeconfig/kubeconfig", - "--logtostderr", - "--port=4443", - fmt.Sprintf("--aws-default-region=%s", hcp.Spec.Platform.AWS.Region), - "--tls-cert=/var/run/app/certs/tls.crt", - "--tls-key=/var/run/app/certs/tls.key", - "--token-audience=openshift", - }, + Command: command, Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("10m"), diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment_test.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment_test.go index a00ee0b3dfd7..537feb63f0b8 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment_test.go @@ -1,14 +1,30 @@ package kas import ( + "slices" "strings" "testing" . "github.com/onsi/gomega" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + + configv1 "github.com/openshift/api/config/v1" + corev1 "k8s.io/api/core/v1" ) +// findContainerByNameInPod finds a container by name in a PodSpec and returns a pointer to it. +// Returns nil if the container is not found. +func findContainerByNameInPod(podSpec *corev1.PodSpec, name string) *corev1.Container { + for i := range podSpec.Containers { + if podSpec.Containers[i].Name == name { + return &podSpec.Containers[i] + } + } + return nil +} + func TestAddImagePrePullInitContainers(t *testing.T) { testCases := []struct { name string @@ -85,3 +101,191 @@ func TestAddImagePrePullInitContainers(t *testing.T) { }) } } + +func TestApplyAWSPodIdentityWebhookContainer(t *testing.T) { + testCases := []struct { + name string + hcp *hyperv1.HostedControlPlane + validatePod func(*GomegaWithT, *corev1.PodSpec) + }{ + { + name: "When TLS security profile is nil it should use default Intermediate profile", + hcp: &hyperv1.HostedControlPlane{ + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "aws-pod-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil()) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-min-version=VersionTLS12")) + g.Expect(slices.ContainsFunc(webhookContainer.Command, func(arg string) bool { + return strings.HasPrefix(arg, "--tls-cipher-suites=") + })).To(BeTrue()) + }, + }, + { + name: "When TLS security profile is Old it should add TLS configuration", + hcp: &hyperv1.HostedControlPlane{ + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileOldType, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "aws-pod-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil()) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-min-version=VersionTLS10")) + g.Expect(slices.ContainsFunc(webhookContainer.Command, func(arg string) bool { + return strings.HasPrefix(arg, "--tls-cipher-suites=") + })).To(BeTrue()) + }, + }, + { + name: "When TLS security profile is Intermediate it should add TLS configuration", + hcp: &hyperv1.HostedControlPlane{ + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileIntermediateType, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "aws-pod-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil()) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-min-version=VersionTLS12")) + g.Expect(slices.ContainsFunc(webhookContainer.Command, func(arg string) bool { + return strings.HasPrefix(arg, "--tls-cipher-suites=") + })).To(BeTrue()) + }, + }, + { + name: "When TLS security profile is Modern it should add TLS 1.3 configuration", + hcp: &hyperv1.HostedControlPlane{ + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileModernType, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "aws-pod-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil()) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-min-version=VersionTLS13")) + g.Expect(slices.ContainsFunc(webhookContainer.Command, func(arg string) bool { + return strings.HasPrefix(arg, "--tls-cipher-suites=") + })).To(BeFalse()) + }, + }, + { + name: "When TLS security profile is Custom with TLS 1.2 it should add tls-min-version flag", + hcp: &hyperv1.HostedControlPlane{ + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileCustomType, + Custom: &configv1.CustomTLSProfile{ + TLSProfileSpec: configv1.TLSProfileSpec{ + MinTLSVersion: configv1.VersionTLS12, + Ciphers: []string{ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + }, + }, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "aws-pod-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil()) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-min-version=VersionTLS12")) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")) + }, + }, + { + name: "When TLS security profile is Custom with TLS 1.3 it should add tls-min-version flag", + hcp: &hyperv1.HostedControlPlane{ + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AWSPlatform, + AWS: &hyperv1.AWSPlatformSpec{ + Region: "us-east-1", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileCustomType, + Custom: &configv1.CustomTLSProfile{ + TLSProfileSpec: configv1.TLSProfileSpec{ + MinTLSVersion: configv1.VersionTLS13, + }, + }, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "aws-pod-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil()) + g.Expect(webhookContainer.Command).To(ContainElement("--tls-min-version=VersionTLS13")) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + podSpec := &corev1.PodSpec{} + applyAWSPodIdentityWebhookContainer(podSpec, tc.hcp) + tc.validatePod(g, podSpec) + }) + } +} From d0083c95e1b251e06411cd031b2ce7b37c45e8cb Mon Sep 17 00:00:00 2001 From: Ricardo Maraschini Date: Tue, 16 Jun 2026 10:36:42 +0200 Subject: [PATCH 2/2] feat: propagate tls profile to azure-workload-identity-webhook sets both the min tls version and the cipher suites for the azure workload identity webhook container. --- ...eComponents_kube_apiserver_deployment.yaml | 2 + .../azure_workload_identity_webhook_test.go | 241 +++++++++++++++--- .../hostedcontrolplane/v2/kas/deployment.go | 39 ++- 3 files changed, 245 insertions(+), 37 deletions(-) diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml index 7008c8048b57..5d3e5376343e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/kube-apiserver/AROSwift/zz_fixture_TestControlPlaneComponents_kube_apiserver_deployment.yaml @@ -323,6 +323,8 @@ spec: --kubeconfig=/var/run/app/kubeconfig/kubeconfig \ --metrics-addr=:9441 \ --log-level=info \ + --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 \ + --tls-min-version=VersionTLS12 \ --disable-cert-rotation command: - /bin/sh diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/azure_workload_identity_webhook_test.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/azure_workload_identity_webhook_test.go index 3931a38df2e5..ce3578a4dfd2 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/azure_workload_identity_webhook_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/azure_workload_identity_webhook_test.go @@ -8,6 +8,8 @@ import ( hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests" + configv1 "github.com/openshift/api/config/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,13 +38,7 @@ func TestApplyAzureWorkloadIdentityWebhookContainer(t *testing.T) { }, }, validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { - var webhookContainer *corev1.Container - for i := range podSpec.Containers { - if podSpec.Containers[i].Name == "azure-workload-identity-webhook" { - webhookContainer = &podSpec.Containers[i] - break - } - } + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) @@ -72,13 +68,7 @@ func TestApplyAzureWorkloadIdentityWebhookContainer(t *testing.T) { }, }, validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { - var webhookContainer *corev1.Container - for i := range podSpec.Containers { - if podSpec.Containers[i].Name == "azure-workload-identity-webhook" { - webhookContainer = &podSpec.Containers[i] - break - } - } + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") g.Expect(webhookContainer).NotTo(BeNil()) envMap := make(map[string]string) @@ -107,13 +97,7 @@ func TestApplyAzureWorkloadIdentityWebhookContainer(t *testing.T) { }, }, validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { - var webhookContainer *corev1.Container - for i := range podSpec.Containers { - if podSpec.Containers[i].Name == "azure-workload-identity-webhook" { - webhookContainer = &podSpec.Containers[i] - break - } - } + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") g.Expect(webhookContainer).NotTo(BeNil()) g.Expect(webhookContainer.StartupProbe).NotTo(BeNil()) @@ -164,13 +148,7 @@ func TestApplyAzureWorkloadIdentityWebhookContainer(t *testing.T) { manifests.AzureWorkloadIdentityWebhookKubeconfig("").Name, )) - var webhookContainer *corev1.Container - for i := range podSpec.Containers { - if podSpec.Containers[i].Name == "azure-workload-identity-webhook" { - webhookContainer = &podSpec.Containers[i] - break - } - } + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") g.Expect(webhookContainer).NotTo(BeNil()) mountPaths := make(map[string]string) @@ -181,13 +159,218 @@ func TestApplyAzureWorkloadIdentityWebhookContainer(t *testing.T) { g.Expect(mountPaths).To(HaveKeyWithValue(azureWorkloadIdentityWebhookKubeconfigVolumeName, "/var/run/app/kubeconfig")) }, }, + { + name: "Azure workload identity container start script has the kubernetes api server port set", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-ns", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + TenantID: "test-tenant-id", + Cloud: "AzurePublicCloud", + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") + + g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) + g.Expect(webhookContainer.Command).To(Equal([]string{"/bin/sh", "-ec"})) + g.Expect(webhookContainer.Args).To(HaveLen(1)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`https://localhost:6443/version`)) + }, + }, + { + name: "Azure workload identity container start script has tls flags by default", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-ns", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + TenantID: "test-tenant-id", + Cloud: "AzurePublicCloud", + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") + + g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) + g.Expect(webhookContainer.Command).To(Equal([]string{"/bin/sh", "-ec"})) + g.Expect(webhookContainer.Args).To(HaveLen(1)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-min-version=VersionTLS12`)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-cipher-suites=`)) + }, + }, + { + name: "When using intermediate TLS profile it should have the correct tls flags", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-ns", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + TenantID: "test-tenant-id", + Cloud: "AzurePublicCloud", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileIntermediateType, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") + + g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) + g.Expect(webhookContainer.Command).To(Equal([]string{"/bin/sh", "-ec"})) + g.Expect(webhookContainer.Args).To(HaveLen(1)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-min-version=VersionTLS12`)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-cipher-suites=`)) + }, + }, + { + name: "When using old TLS profile it should have the correct tls flags", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-ns", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + TenantID: "test-tenant-id", + Cloud: "AzurePublicCloud", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileOldType, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") + + g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) + g.Expect(webhookContainer.Command).To(Equal([]string{"/bin/sh", "-ec"})) + g.Expect(webhookContainer.Args).To(HaveLen(1)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-min-version=VersionTLS10`)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-cipher-suites=`)) + }, + }, + { + name: "When using modern TLS profile it should have the correct tls flags", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-ns", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + TenantID: "test-tenant-id", + Cloud: "AzurePublicCloud", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileModernType, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") + + g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) + g.Expect(webhookContainer.Command).To(Equal([]string{"/bin/sh", "-ec"})) + g.Expect(webhookContainer.Args).To(HaveLen(1)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-min-version=VersionTLS13`)) + g.Expect(webhookContainer.Args[0]).NotTo(ContainSubstring(`--tls-cipher-suites=`)) + }, + }, + { + name: "When using custom TLS profile it should have the correct tls flags", + hcp: &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-hcp", + Namespace: "test-ns", + }, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzurePlatformSpec{ + TenantID: "test-tenant-id", + Cloud: "AzurePublicCloud", + }, + }, + Configuration: &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileCustomType, + Custom: &configv1.CustomTLSProfile{ + TLSProfileSpec: configv1.TLSProfileSpec{ + MinTLSVersion: configv1.VersionTLS11, + Ciphers: []string{ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + }, + }, + }, + }, + }, + }, + }, + }, + validatePod: func(g *GomegaWithT, podSpec *corev1.PodSpec) { + webhookContainer := findContainerByNameInPod(podSpec, "azure-workload-identity-webhook") + g.Expect(webhookContainer).NotTo(BeNil(), "webhook container should exist") + + g.Expect(webhookContainer.Image).To(Equal("azure-workload-identity-webhook")) + g.Expect(webhookContainer.Command).To(Equal([]string{"/bin/sh", "-ec"})) + g.Expect(webhookContainer.Args).To(HaveLen(1)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-min-version=VersionTLS11`)) + g.Expect(webhookContainer.Args[0]).To(ContainSubstring(`--tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`)) + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { g := NewWithT(t) podSpec := &corev1.PodSpec{} - applyAzureWorkloadIdentityWebhookContainer(podSpec, tc.hcp) + err := applyAzureWorkloadIdentityWebhookContainer(podSpec, tc.hcp) + g.Expect(err).To(BeNil()) tc.validatePod(g, podSpec) }) } diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go index 98c9951fc2bd..dc03b82ea26e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" "strings" + "text/template" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/common" @@ -39,9 +40,10 @@ const ( azureWorkloadIdentityWebhookServingCertVolumeName = "azure-wi-webhook-serving-certs" azureWorkloadIdentityWebhookKubeconfigVolumeName = "azure-wi-webhook-kubeconfig" +) - azureWorkloadIdentityWebhookWaitForKASVersionTemplate = `set -u -until curl -kfsS "https://localhost:%d/version" >/dev/null; do +var azureWorkloadIdentityWebhookWaitForKASVersionTemplate = template.Must(template.New("azure-workload-identity-webhook").Parse(`set -u +until curl -kfsS "https://localhost:{{ .KASPodPort }}/version" >/dev/null; do echo "waiting for kube-apiserver /version endpoint to become available" sleep 2 done @@ -52,9 +54,11 @@ exec /usr/bin/azure-workload-identity-webhook \ --kubeconfig=/var/run/app/kubeconfig/kubeconfig \ --metrics-addr=:9441 \ --log-level=info \ +{{- range $flag, $value := .ExtraCommandLineFlags }} + {{ $flag }}={{ $value }} \ +{{- end }} --disable-cert-rotation -` -) +`)) func adaptDeployment(cpContext component.WorkloadContext, deployment *appsv1.Deployment) error { hcp := cpContext.HCP @@ -116,7 +120,9 @@ func adaptDeployment(cpContext component.WorkloadContext, deployment *appsv1.Dep if hcp.Spec.Platform.Azure == nil { return fmt.Errorf("azure platform type requires spec.platform.azure") } - applyAzureWorkloadIdentityWebhookContainer(&deployment.Spec.Template.Spec, hcp) + if err := applyAzureWorkloadIdentityWebhookContainer(&deployment.Spec.Template.Spec, hcp); err != nil { + return fmt.Errorf("failed to create azure workload identity webhook container: %w", err) + } } if hcp.Spec.AuditWebhook != nil && len(hcp.Spec.AuditWebhook.Name) > 0 { @@ -352,15 +358,31 @@ func applyAWSPodIdentityWebhookContainer(podSpec *corev1.PodSpec, hcp *hyperv1.H ) } -func applyAzureWorkloadIdentityWebhookContainer(podSpec *corev1.PodSpec, hcp *hyperv1.HostedControlPlane) { - waitForKASScript := fmt.Sprintf(azureWorkloadIdentityWebhookWaitForKASVersionTemplate, netutil.KASPodPort(hcp)) +func applyAzureWorkloadIdentityWebhookContainer(podSpec *corev1.PodSpec, hcp *hyperv1.HostedControlPlane) error { + extraCommandLineFlags := map[string]string{} + if tlsMinVersion := config.MinTLSVersion(hcp.Spec.Configuration.GetTLSSecurityProfile()); tlsMinVersion != "" { + extraCommandLineFlags["--tls-min-version"] = tlsMinVersion + } + if cipherSuites := config.CipherSuites(hcp.Spec.Configuration.GetTLSSecurityProfile()); len(cipherSuites) != 0 { + extraCommandLineFlags["--tls-cipher-suites"] = strings.Join(cipherSuites, ",") + } + + templateData := map[string]any{ + "KASPodPort": netutil.KASPodPort(hcp), + "ExtraCommandLineFlags": extraCommandLineFlags, + } + + containerArgs := bytes.NewBuffer(nil) + if err := azureWorkloadIdentityWebhookWaitForKASVersionTemplate.Execute(containerArgs, templateData); err != nil { + return fmt.Errorf("failed to execute azure-workload-identity-webhook command template: %w", err) + } podSpec.Containers = append(podSpec.Containers, corev1.Container{ Name: "azure-workload-identity-webhook", Image: "azure-workload-identity-webhook", ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/bin/sh", "-ec"}, - Args: []string{waitForKASScript}, + Args: []string{containerArgs.String()}, Env: []corev1.EnvVar{ {Name: "AZURE_TENANT_ID", Value: hcp.Spec.Platform.Azure.TenantID}, {Name: "AZURE_ENVIRONMENT", Value: hcp.Spec.Platform.Azure.Cloud}, @@ -423,6 +445,7 @@ func applyAzureWorkloadIdentityWebhookContainer(podSpec *corev1.PodSpec, hcp *hy }, }, ) + return nil } func buildKASAuditWebhookConfigFileVolume(auditWebhookRef *corev1.LocalObjectReference) corev1.Volume {