From f3333d273bb2ba8a8d3e2b26b140dbd7edd85495 Mon Sep 17 00:00:00 2001 From: Jan Steffen Date: Mon, 23 Mar 2026 13:52:47 +0100 Subject: [PATCH 1/5] feat(imagevector): add stackit-pod-identity-webhook image --- imagevector/images.go | 2 ++ imagevector/images.yaml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/imagevector/images.go b/imagevector/images.go index 1b8da504..c7e393d5 100644 --- a/imagevector/images.go +++ b/imagevector/images.go @@ -35,4 +35,6 @@ const ( ImageNameStackitAlbControllerManager = "stackit-alb-controller-manager" // ImageNameStackitCloudControllerManager is a constant for an image in the image vector with name 'stackit-cloud-controller-manager'. ImageNameStackitCloudControllerManager = "stackit-cloud-controller-manager" + // ImageNameStackitPodIdentityWebhook is a constant for an image in the image vector with name 'stackit-pod-identity-webhook'. + ImageNameStackitPodIdentityWebhook = "stackit-pod-identity-webhook" ) diff --git a/imagevector/images.yaml b/imagevector/images.yaml index ac1212a6..ca00a8a5 100644 --- a/imagevector/images.yaml +++ b/imagevector/images.yaml @@ -135,3 +135,6 @@ images: - name: stackit-alb-controller-manager repository: reg3.infra.ske.eu01.stackit.cloud/temp/alb-controller-manager tag: "1245" +- name: stackit-pod-identity-webhook + repository: reg3.infra.ske.eu01.stackit.cloud/stackitcloud/stackit-pod-identity-webhook + tag: "726f2f0@sha256:fca1f67cd7e6a515e795a34ae45d0c239379d051e494dc202033f6987b41b154" From 1c3d5d725a95e940d4ac1414974029ef28b1baf0 Mon Sep 17 00:00:00 2001 From: Jan Steffen Date: Mon, 23 Mar 2026 13:55:37 +0100 Subject: [PATCH 2/5] feat(charts): add stackit-pod-identity-webhook charts --- .../stackit-pod-identity-webhook/Chart.yaml | 3 + .../templates/deployment.yaml | 80 +++++++++++++++++++ .../templates/helpers.tpl | 49 ++++++++++++ .../templates/poddisruptionbudget.yaml | 13 +++ .../templates/rbac.yaml | 32 ++++++++ .../templates/service.yaml | 18 +++++ .../stackit-pod-identity-webhook/values.yaml | 55 +++++++++++++ .../stackit-pod-identity-webhook/Chart.yaml | 3 + .../templates/helpers.tpl | 49 ++++++++++++ .../mutatingwebhookconfiguration.yaml | 36 +++++++++ .../stackit-pod-identity-webhook/values.yaml | 7 ++ 11 files changed, 345 insertions(+) create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/Chart.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml create mode 100644 charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/Chart.yaml create mode 100644 charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl create mode 100644 charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml create mode 100644 charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/Chart.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/Chart.yaml new file mode 100644 index 00000000..8775571a --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v1 +name: stackit-pod-identity-webhook +version: 0.1.0 diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml new file mode 100644 index 00000000..a9117bd1 --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "stackit-pod-identity-webhook.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "stackit-pod-identity-webhook.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "stackit-pod-identity-webhook.selectorLabels" . | nindent 8 }} + workload-identity.stackit.cloud/skip-pod-identity-webhook: "true" + gardener.cloud/role: controlplane + high-availability-config.resources.gardener.cloud/type: controller + networking.gardener.cloud/to-dns: allowed + networking.gardener.cloud/to-public-networks: allowed + networking.gardener.cloud/to-private-networks: allowed + networking.resources.gardener.cloud/to-kube-apiserver-tcp-443: allowed + spec: + serviceAccountName: {{ .Values.serviceAccount.name | default (include "stackit-pod-identity-webhook.fullname" .) }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + priorityClassName: {{ .Values.priorityClassName }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: {{ index .Values.images "stackit-pod-identity-webhook" }} + args: + - --cert-dir=/etc/webhook/certs + - --port={{ .Values.webhook.port }} + ports: + - name: https + containerPort: {{ .Values.webhook.port }} + protocol: TCP + - name: metrics + containerPort: 8080 + protocol: TCP + - name: health + containerPort: 8081 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: health + readinessProbe: + httpGet: + path: /readyz + port: health + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: certs + mountPath: /etc/webhook/certs + readOnly: true + volumes: + - name: certs + secret: + secretName: {{ .Values.webhook.tlsSecretName }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl new file mode 100644 index 00000000..1ae4cccd --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl @@ -0,0 +1,49 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "stackit-pod-identity-webhook.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "stackit-pod-identity-webhook.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "stackit-pod-identity-webhook.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "stackit-pod-identity-webhook.labels" -}} +helm.sh/chart: {{ include "stackit-pod-identity-webhook.chart" . }} +{{ include "stackit-pod-identity-webhook.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "stackit-pod-identity-webhook.selectorLabels" -}} +app.kubernetes.io/name: {{ include "stackit-pod-identity-webhook.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml new file mode 100644 index 00000000..47e33d83 --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml @@ -0,0 +1,13 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "stackit-pod-identity-webhook.labels" . | nindent 4 }} +spec: + maxUnavailable: 1 + selector: + matchLabels: + {{- include "stackit-pod-identity-webhook.selectorLabels" . | nindent 6 }} + unhealthyPodEvictionPolicy: AlwaysAllow diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml new file mode 100644 index 00000000..afe278b7 --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccount.name | default (include "stackit-pod-identity-webhook.fullname" .) }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + app.kubernetes.io/instance: {{ .Release.Name }} +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} +rules: +- apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "stackit-pod-identity-webhook.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ .Values.serviceAccount.name | default (include "stackit-pod-identity-webhook.fullname" .) }} + namespace: {{ .Release.Namespace }} diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml new file mode 100644 index 00000000..c4f06dc5 --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: https + selector: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml new file mode 100644 index 00000000..559b160d --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml @@ -0,0 +1,55 @@ +replicaCount: 2 + +# String to override the name for the chart +nameOverride: "" +# String to fully override the fullname of the chart +fullnameOverride: "" + +webhook: + port: 9443 + # The secret name containing tls.crt and tls.key for the webhook server + # If certmanager.enabled is true, this secret will be created by cert-manager + tlsSecretName: "stackit-pod-identity-webhook-certs" + +service: + type: ClusterIP + port: 443 + targetPort: 9443 + +resources: + limits: + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + +serviceAccount: + create: true + annotations: {} + name: "stackit-pod-identity-webhook" + +# PodSecurityContext holds pod-level security attributes and common container settings. +podSecurityContext: + runAsNonRoot: true + runAsUser: 1239 + runAsGroup: 1239 + fsGroup: 1239 + +# SecurityContext holds security configuration that will be applied to a container. +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + +# NodeSelector is a selector which must be true for the pod to fit on a node. +nodeSelector: {} + +# Tolerations are applied to pods, and allow (but do not require) the pods to schedule onto nodes with matching taints. +tolerations: [] + +# Affinity is a group of affinity scheduling rules. +affinity: {} + +priorityClassName: gardener-system-300 diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/Chart.yaml b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/Chart.yaml new file mode 100644 index 00000000..8775571a --- /dev/null +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v1 +name: stackit-pod-identity-webhook +version: 0.1.0 diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl new file mode 100644 index 00000000..1ae4cccd --- /dev/null +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl @@ -0,0 +1,49 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "stackit-pod-identity-webhook.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "stackit-pod-identity-webhook.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "stackit-pod-identity-webhook.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "stackit-pod-identity-webhook.labels" -}} +helm.sh/chart: {{ include "stackit-pod-identity-webhook.chart" . }} +{{ include "stackit-pod-identity-webhook.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "stackit-pod-identity-webhook.selectorLabels" -}} +app.kubernetes.io/name: {{ include "stackit-pod-identity-webhook.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml new file mode 100644 index 00000000..71f9480a --- /dev/null +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml @@ -0,0 +1,36 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +webhooks: + - name: stackit-pod-identity-webhook.stackit.cloud + clientConfig: + service: + name: {{ include "stackit-pod-identity-webhook.fullname" . }} + namespace: {{ .Release.Namespace }} + path: "/mutate--v1-pod" + rules: + - operations: ["CREATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + admissionReviewVersions: ["v1"] + sideEffects: None + failurePolicy: {{ .Values.webhook.failurePolicy | default "Ignore" }} + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: ["kube-system", "garden"] + - key: gardener.cloud/role + operator: DoesNotExist + - key: workload-identity.stackit.cloud/skip-pod-identity-webhook + operator: DoesNotExist + objectSelector: + matchExpressions: + - key: workload-identity.stackit.cloud/skip-pod-identity-webhook + operator: DoesNotExist diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml new file mode 100644 index 00000000..51febcf6 --- /dev/null +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml @@ -0,0 +1,7 @@ +webhook: + caBundle: "" # will be set by valuesprovider + # failurePolicy for the webhook (Ignore or Fail). + # Defaults to Fail to guarantee that pods are not started without the required workload identity configuration. + # Note: If the webhook is down, pod creation in monitored namespaces will be blocked. + # Specific pods or namespaces can be excluded using the skip label. + failurePolicy: Fail From 0f653106fad8c064bd61714ec79814ca2f972203 Mon Sep 17 00:00:00 2001 From: Jan Steffen Date: Mon, 23 Mar 2026 14:02:06 +0100 Subject: [PATCH 3/5] feat(controlplane): implement STACKIT pod identity webhook deployment --- pkg/controller/controlplane/valuesprovider.go | 103 +++++++++++++----- pkg/stackit/types.go | 3 + 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index a2e22f7f..62f89a65 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -10,7 +10,6 @@ import ( "fmt" "maps" "path/filepath" - "sort" "strings" "github.com/Masterminds/semver/v3" @@ -28,6 +27,7 @@ import ( kutil "github.com/gardener/gardener/pkg/utils/kubernetes" secretutils "github.com/gardener/gardener/pkg/utils/secrets" secretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -37,13 +37,10 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" vpaautoscalingv1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" "k8s.io/utils/ptr" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -59,8 +56,9 @@ import ( ) const ( - caNameControlPlane = "ca-" + openstack.Name + "-controlplane" - cloudControllerManagerServerName = openstack.CloudControllerManagerName + "-server" + caNameControlPlane = "ca-" + openstack.Name + "-controlplane" + cloudControllerManagerServerName = openstack.CloudControllerManagerName + "-server" + stackitPodIdentityWebhookServerName = stackit.STACKITPodIdentityWebhookName + "-server" CSIStackitPrefix = "stackit-blockstorage" @@ -103,6 +101,16 @@ func secretConfigsFunc(namespace string) []extensionssecretmanager.SecretConfigW }, Options: []secretsmanager.GenerateOption{secretsmanager.SignedByCA(caNameControlPlane)}, }, + { + Config: &secretutils.CertificateSecretConfig{ + Name: stackitPodIdentityWebhookServerName, + CommonName: stackit.STACKITPodIdentityWebhookName, + DNSNames: kutil.DNSNamesForService(stackit.STACKITPodIdentityWebhookName, namespace), + CertType: secretutils.ServerCert, + SkipPublishingCACertificate: true, + }, + Options: []secretsmanager.GenerateOption{secretsmanager.SignedByCA(caNameControlPlane)}, + }, } } @@ -117,12 +125,6 @@ func shootAccessSecretsFunc(namespace string) []*gutil.AccessSecret { } } -func makeUnstructured(gvk schema.GroupVersionKind) *unstructured.Unstructured { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(gvk) - return obj -} - var ( configChart = &chart.Chart{ Name: openstack.CloudProviderConfigName, @@ -207,6 +209,14 @@ var ( {Type: &vpaautoscalingv1.VerticalPodAutoscaler{}, Name: openstack.STACKITALBControllerManagerName}, }, }, + { + Name: stackit.STACKITPodIdentityWebhookName, + Images: []string{imagevector.ImageNameStackitPodIdentityWebhook}, + Objects: []*chart.Object{ + {Type: &appsv1.Deployment{}, Name: stackit.STACKITPodIdentityWebhookName}, + {Type: &corev1.Service{}, Name: stackit.STACKITPodIdentityWebhookName}, + }, + }, }, } @@ -306,6 +316,12 @@ var ( {Type: &rbacv1.RoleBinding{}, Name: openstack.UsernamePrefix + openstack.CSIResizerName}, }, }, + { + Name: stackit.STACKITPodIdentityWebhookName, + Objects: []*chart.Object{ + {Type: &admissionregistrationv1.MutatingWebhookConfiguration{}, Name: stackit.STACKITPodIdentityWebhookName}, + }, + }, }, } @@ -478,7 +494,7 @@ func (vp *valuesProvider) GetControlPlaneShootChartValues( ctx context.Context, cp *extensionsv1alpha1.ControlPlane, cluster *extensionscontroller.Cluster, - _ secretsmanager.Reader, + secretsReader secretsmanager.Reader, _ map[string]string, ) (map[string]any, error) { // Decode providerConfig @@ -493,7 +509,7 @@ func (vp *valuesProvider) GetControlPlaneShootChartValues( if err != nil { return nil, err } - return vp.getControlPlaneShootChartValues(ctx, cpConfig, cp, cloudProfileConfig, cluster) + return vp.getControlPlaneShootChartValues(ctx, cpConfig, cp, cloudProfileConfig, cluster, secretsReader) } // GetStorageClassesChartValues returns the values for the shoot storageclasses chart applied by the generic actuator. @@ -708,6 +724,11 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf } } + podIdentityWebhook, err := getSTACKITPodIdentityWebhookChartValues(cluster, secretsReader, scaledDown) + if err != nil { + return nil, err + } + storageCSIDriver := getCSIDriver(cpConfig) switch storageCSIDriver { case stackitv1alpha1.OPENSTACK: @@ -732,6 +753,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf }, openstack.CloudControllerManagerName: ccm, openstack.STACKITCloudControllerManagerName: stackitccm, + stackit.STACKITPodIdentityWebhookName: podIdentityWebhook, }) if vp.deployALBIngressController { @@ -1028,7 +1050,7 @@ func DeploySTACKITALB(cpConfig *stackitv1alpha1.ControlPlaneConfig) bool { } // getControlPlaneShootChartValues collects and returns the control plane shoot chart values. -func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, cpConfig *stackitv1alpha1.ControlPlaneConfig, cp *extensionsv1alpha1.ControlPlane, cloudProfileConfig *stackitv1alpha1.CloudProfileConfig, cluster *extensionscontroller.Cluster) (map[string]any, error) { +func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, cpConfig *stackitv1alpha1.ControlPlaneConfig, cp *extensionsv1alpha1.ControlPlane, cloudProfileConfig *stackitv1alpha1.CloudProfileConfig, cluster *extensionscontroller.Cluster, secretsReader secretsmanager.Reader) (map[string]any, error) { var csiNodeDriverValues map[string]any values := make(map[string]any) @@ -1056,8 +1078,14 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c return nil, err } + podIdentityWebhook, err := vp.getSTACKITPodIdentityWebhookShootChartValues(secretsReader) + if err != nil { + return nil, err + } + maps.Copy(values, map[string]any{ - openstack.CloudControllerManagerName: map[string]any{"enabled": true}, + openstack.CloudControllerManagerName: map[string]any{"enabled": true}, + stackit.STACKITPodIdentityWebhookName: podIdentityWebhook, }) return values, nil @@ -1225,16 +1253,6 @@ func (vp *valuesProvider) getControlPlaneShootChartCSISTACKITValues(ctx context. return values } -func (vp *valuesProvider) getAllWorkerPoolsZones(cluster *extensionscontroller.Cluster) []string { - zones := sets.NewString() - for _, worker := range cluster.Shoot.Spec.Provider.Workers { - zones.Insert(worker.Zones...) - } - list := zones.UnsortedList() - sort.Strings(list) - return list -} - func cleanupSeedLegacyCSISnapshotValidation(ctx context.Context, client k8sclient.Client, namespace string) error { stackitSnapShotName := fmt.Sprintf("%s-%s", CSIStackitPrefix, openstack.CSISnapshotValidationName) @@ -1276,3 +1294,36 @@ func cleanupCloudProviderConfigSecret(ctx context.Context, client k8sclient.Clie return nil } + +func getSTACKITPodIdentityWebhookChartValues( + cluster *extensionscontroller.Cluster, + secretsReader secretsmanager.Reader, + scaledDown bool, +) (map[string]any, error) { + tlsSecret, found := secretsReader.Get(stackitPodIdentityWebhookServerName) + if !found { + return nil, fmt.Errorf("secret %q not found", stackitPodIdentityWebhookServerName) + } + + return map[string]any{ + "replicaCount": extensionscontroller.GetControlPlaneReplicas(cluster, scaledDown, 2), + "webhook": map[string]any{ + "tlsSecretName": tlsSecret.Name, + }, + }, nil +} + +func (vp *valuesProvider) getSTACKITPodIdentityWebhookShootChartValues( + secretsReader secretsmanager.Reader, +) (map[string]any, error) { + caSecret, found := secretsReader.Get(caNameControlPlane) + if !found { + return nil, fmt.Errorf("secret %q not found", caNameControlPlane) + } + + return map[string]any{ + "webhook": map[string]any{ + "caBundle": gardenerutils.EncodeBase64(caSecret.Data[secretutils.DataKeyCertificateBundle]), + }, + }, nil +} diff --git a/pkg/stackit/types.go b/pkg/stackit/types.go index a8e8f540..d12e76f4 100644 --- a/pkg/stackit/types.go +++ b/pkg/stackit/types.go @@ -16,6 +16,9 @@ const ( EtherTypeIPv6 = "IPv6" DirectionEgress = "egress" DirectionIngress = "ingress" + + // STACKITPodIdentityWebhookName is a constant for the name of the Pod Identity Webhook. (stackit) + STACKITPodIdentityWebhookName = "stackit-pod-identity-webhook" ) var ( From 5d86145e7b3c9da5a4e609834cf3820f1cb19cf3 Mon Sep 17 00:00:00 2001 From: Jan Steffen Date: Mon, 23 Mar 2026 14:02:42 +0100 Subject: [PATCH 4/5] test(controlplane): add tests for STACKIT pod identity webhook --- .../controlplane/valuesprovider_test.go | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index 6b2c9c29..bbe25634 100644 --- a/pkg/controller/controlplane/valuesprovider_test.go +++ b/pkg/controller/controlplane/valuesprovider_test.go @@ -495,6 +495,13 @@ var _ = Describe("ValuesProvider", func() { }, }) + stackitPodIdentityWebhookChartSeedValues := map[string]any{ + "replicaCount": 2, + "webhook": map[string]any{ + "tlsSecretName": "stackit-pod-identity-webhook-server", + }, + } + BeforeEach(func() { c.EXPECT().Get(ctx, cpConfigKey, &corev1.Secret{}).DoAndReturn(clientGet(cpConfig)) c.EXPECT().Delete(context.TODO(), &networkingv1.NetworkPolicy{ObjectMeta: metav1.ObjectMeta{Name: "allow-kube-apiserver-to-csi-snapshot-validation", Namespace: cp.Namespace}}) @@ -515,6 +522,7 @@ var _ = Describe("ValuesProvider", func() { By("creating secrets managed outside of this package for whose secretsmanager.Get() will be called") Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca-provider-openstack-controlplane", Namespace: namespace}})).To(Succeed()) Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "cloud-controller-manager-server", Namespace: namespace}})).To(Succeed()) + Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "stackit-pod-identity-webhook-server", Namespace: namespace}})).To(Succeed()) // This call is made for emergency Loadbalancer API access. // It will return a NotFound error by default to not interfere with existing tests. @@ -557,6 +565,7 @@ var _ = Describe("ValuesProvider", func() { "replicas": 1, }, }), + stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartSeedValues, openstack.STACKITALBControllerManagerName: empty(), })) }) @@ -600,6 +609,7 @@ var _ = Describe("ValuesProvider", func() { "replicas": 1, }, }), + stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartSeedValues, openstack.STACKITALBControllerManagerName: empty(), })) }) @@ -881,6 +891,12 @@ var _ = Describe("ValuesProvider", func() { }) Describe("#GetControlPlaneShootChartValues", func() { + stackitPodIdentityWebhookChartShootValues := map[string]any{ + "webhook": map[string]any{ + "caBundle": "", + }, + } + BeforeEach(func() { By("creating secrets managed outside of this package for whose secretsmanager.Get() will be called") Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca-provider-openstack-controlplane", Namespace: namespace}})).To(Succeed()) @@ -903,7 +919,8 @@ var _ = Describe("ValuesProvider", func() { "rescanBlockStorageOnResize": rescanBlockStorageOnResize, "userAgentHeaders": []string{domainName, tenantName, technicalID}, }), - openstack.CSINodeName: enabledFalse, + openstack.CSINodeName: enabledFalse, + stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartShootValues, })) }) @@ -921,7 +938,8 @@ var _ = Describe("ValuesProvider", func() { "rescanBlockStorageOnResize": rescanBlockStorageOnResize, "userAgentHeaders": []string{domainName, tenantName, technicalID}, }), - openstack.CSINodeName: enabledFalse, + openstack.CSINodeName: enabledFalse, + stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartShootValues, })) }) }) From c9b922011719baa0a6bb2244a7ff6022251ff74a Mon Sep 17 00:00:00 2001 From: Jan Steffen Date: Wed, 25 Mar 2026 08:53:42 +0100 Subject: [PATCH 5/5] feat(review): apply post review suggestions --- .../templates/deployment.yaml | 92 +++++++++++++------ .../templates/helpers.tpl | 49 ---------- .../templates/poddisruptionbudget.yaml | 6 +- .../templates/rbac.yaml | 32 ------- .../templates/service.yaml | 19 ++-- .../templates/vpa.yaml | 21 +++++ .../stackit-pod-identity-webhook/values.yaml | 54 ++--------- .../seed-controlplane/requirements.yaml | 3 + .../templates/helpers.tpl | 49 ---------- .../mutatingwebhookconfiguration.yaml | 16 +--- .../templates/rbac.yaml | 21 +++++ .../stackit-pod-identity-webhook/values.yaml | 6 +- .../shoot-system-components/requirements.yaml | 3 + pkg/controller/controlplane/valuesprovider.go | 44 +++++---- .../controlplane/valuesprovider_test.go | 26 +++--- pkg/stackit/types.go | 4 +- 16 files changed, 181 insertions(+), 264 deletions(-) delete mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl delete mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml create mode 100644 charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/vpa.yaml delete mode 100644 charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl create mode 100644 charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/rbac.yaml diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml index a9117bd1..d6de71a6 100644 --- a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/deployment.yaml @@ -1,43 +1,66 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} + name: stackit-pod-identity-webhook namespace: {{ .Release.Namespace }} labels: - {{- include "stackit-pod-identity-webhook.labels" . | nindent 4 }} + app.kubernetes.io/name: stackit-pod-identity-webhook + high-availability-config.resources.gardener.cloud/type: server spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: - {{- include "stackit-pod-identity-webhook.selectorLabels" . | nindent 6 }} + app.kubernetes.io/name: stackit-pod-identity-webhook template: metadata: labels: - {{- include "stackit-pod-identity-webhook.selectorLabels" . | nindent 8 }} + app.kubernetes.io/name: stackit-pod-identity-webhook workload-identity.stackit.cloud/skip-pod-identity-webhook: "true" gardener.cloud/role: controlplane - high-availability-config.resources.gardener.cloud/type: controller networking.gardener.cloud/to-dns: allowed - networking.gardener.cloud/to-public-networks: allowed - networking.gardener.cloud/to-private-networks: allowed networking.resources.gardener.cloud/to-kube-apiserver-tcp-443: allowed spec: - serviceAccountName: {{ .Values.serviceAccount.name | default (include "stackit-pod-identity-webhook.fullname" .) }} - {{- with .Values.podSecurityContext }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "topology.kubernetes.io/zone" + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: stackit-pod-identity-webhook + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: stackit-pod-identity-webhook + automountServiceAccountToken: false + podSecurityContext: + runAsNonRoot: true + runAsUser: 1239 + runAsGroup: 1239 + fsGroup: 1239 securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - priorityClassName: {{ .Values.priorityClassName }} + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + priorityClassName: gardener-system-200 containers: - - name: {{ .Chart.Name }} - {{- with .Values.containerSecurityContext }} + - name: stackit-pod-identity-webhook securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true image: {{ index .Values.images "stackit-pod-identity-webhook" }} args: - --cert-dir=/etc/webhook/certs - --port={{ .Values.webhook.port }} + env: + - name: KUBECONFIG + value: /var/run/secrets/gardener.cloud/shoot/generic-kubeconfig/kubeconfig ports: - name: https containerPort: {{ .Values.webhook.port }} @@ -57,24 +80,35 @@ spec: path: /readyz port: health resources: - {{- toYaml .Values.resources | nindent 12 }} + limits: + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi volumeMounts: - name: certs mountPath: /etc/webhook/certs readOnly: true + - mountPath: /var/run/secrets/gardener.cloud/shoot/generic-kubeconfig + name: kubeconfig + readOnly: true volumes: - name: certs secret: secretName: {{ .Values.webhook.tlsSecretName }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} + - name: kubeconfig + projected: + defaultMode: 420 + sources: + - secret: + items: + - key: kubeconfig + path: kubeconfig + name: {{ .Values.global.genericTokenKubeconfigSecretName }} + optional: false + - secret: + items: + - key: token + path: token + name: shoot-access-pod-identity-webhook + optional: false \ No newline at end of file diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl deleted file mode 100644 index 1ae4cccd..00000000 --- a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/helpers.tpl +++ /dev/null @@ -1,49 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "stackit-pod-identity-webhook.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "stackit-pod-identity-webhook.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "stackit-pod-identity-webhook.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "stackit-pod-identity-webhook.labels" -}} -helm.sh/chart: {{ include "stackit-pod-identity-webhook.chart" . }} -{{ include "stackit-pod-identity-webhook.selectorLabels" . }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "stackit-pod-identity-webhook.selectorLabels" -}} -app.kubernetes.io/name: {{ include "stackit-pod-identity-webhook.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml index 47e33d83..17cd1a17 100644 --- a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/poddisruptionbudget.yaml @@ -1,13 +1,13 @@ apiVersion: policy/v1 kind: PodDisruptionBudget metadata: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} + name: stackit-pod-identity-webhook namespace: {{ .Release.Namespace }} labels: - {{- include "stackit-pod-identity-webhook.labels" . | nindent 4 }} + app.kubernetes.io/name: stackit-pod-identity-webhook spec: maxUnavailable: 1 selector: matchLabels: - {{- include "stackit-pod-identity-webhook.selectorLabels" . | nindent 6 }} + app.kubernetes.io/name: stackit-pod-identity-webhook unhealthyPodEvictionPolicy: AlwaysAllow diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml deleted file mode 100644 index afe278b7..00000000 --- a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/rbac.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ .Values.serviceAccount.name | default (include "stackit-pod-identity-webhook.fullname" .) }} - namespace: {{ .Release.Namespace }} - labels: - app.kubernetes.io/name: {{ .Chart.Name }} - helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app.kubernetes.io/instance: {{ .Release.Name }} -automountServiceAccountToken: false ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} -rules: -- apiGroups: [""] - resources: ["serviceaccounts"] - verbs: ["get", "list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "stackit-pod-identity-webhook.fullname" . }} -subjects: -- kind: ServiceAccount - name: {{ .Values.serviceAccount.name | default (include "stackit-pod-identity-webhook.fullname" .) }} - namespace: {{ .Release.Namespace }} diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml index c4f06dc5..cc0c6584 100644 --- a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/service.yaml @@ -1,18 +1,21 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} + name: stackit-pod-identity-webhook namespace: {{ .Release.Namespace }} labels: - app.kubernetes.io/name: {{ .Chart.Name }} - app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: stackit-pod-identity-webhook + endpoint-slice-hints.resources.gardener.cloud/consider: "true" + annotations: + networking.resources.gardener.cloud/from-all-webhook-targets-allowed-ports: '[{"protocol":"TCP","port":{{ .Values.webhook.port }}}]' + service.kubernetes.io/topology-mode: auto spec: - type: {{ .Values.service.type }} + type: ClusterIP ports: - - port: {{ .Values.service.port }} - targetPort: {{ .Values.service.targetPort }} + - port: 443 + targetPort: {{ .Values.webhook.port }} protocol: TCP name: https selector: - app.kubernetes.io/name: {{ .Chart.Name }} - app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: stackit-pod-identity-webhook + trafficDistribution: PreferClose diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/vpa.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/vpa.yaml new file mode 100644 index 00000000..3f53258c --- /dev/null +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/templates/vpa.yaml @@ -0,0 +1,21 @@ +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: stackit-pod-identity-webhook + namespace: {{ .Release.Namespace }} +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: stackit-pod-identity-webhook + updatePolicy: + updateMode: Auto + resourcePolicy: + containerPolicies: + - containerName: stackit-pod-identity-webhook + minAllowed: + memory: 80M + maxAllowed: + cpu: {{ .Values.vpa.resourcePolicy.maxAllowed.cpu }} + memory: {{ .Values.vpa.resourcePolicy.maxAllowed.memory }} + controlledValues: RequestsOnly diff --git a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml index 559b160d..bac2e5eb 100644 --- a/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-pod-identity-webhook/values.yaml @@ -1,55 +1,15 @@ replicaCount: 2 -# String to override the name for the chart -nameOverride: "" -# String to fully override the fullname of the chart -fullnameOverride: "" +images: + stackit-pod-identity-webhook: image-repository:image-tag webhook: port: 9443 # The secret name containing tls.crt and tls.key for the webhook server - # If certmanager.enabled is true, this secret will be created by cert-manager tlsSecretName: "stackit-pod-identity-webhook-certs" -service: - type: ClusterIP - port: 443 - targetPort: 9443 - -resources: - limits: - memory: 128Mi - requests: - cpu: 50m - memory: 64Mi - -serviceAccount: - create: true - annotations: {} - name: "stackit-pod-identity-webhook" - -# PodSecurityContext holds pod-level security attributes and common container settings. -podSecurityContext: - runAsNonRoot: true - runAsUser: 1239 - runAsGroup: 1239 - fsGroup: 1239 - -# SecurityContext holds security configuration that will be applied to a container. -containerSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - -# NodeSelector is a selector which must be true for the pod to fit on a node. -nodeSelector: {} - -# Tolerations are applied to pods, and allow (but do not require) the pods to schedule onto nodes with matching taints. -tolerations: [] - -# Affinity is a group of affinity scheduling rules. -affinity: {} - -priorityClassName: gardener-system-300 +vpa: + resourcePolicy: + maxAllowed: + cpu: 1 + memory: 512Mi diff --git a/charts/internal/seed-controlplane/requirements.yaml b/charts/internal/seed-controlplane/requirements.yaml index efa98d6c..768bfa37 100644 --- a/charts/internal/seed-controlplane/requirements.yaml +++ b/charts/internal/seed-controlplane/requirements.yaml @@ -19,3 +19,6 @@ dependencies: repository: http://localhost:10191 version: 0.1.0 condition: stackit-alb-controller-manager.enabled +- name: stackit-pod-identity-webhook + repository: http://localhost:10191 + version: 0.1.0 diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl deleted file mode 100644 index 1ae4cccd..00000000 --- a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/helpers.tpl +++ /dev/null @@ -1,49 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "stackit-pod-identity-webhook.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "stackit-pod-identity-webhook.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "stackit-pod-identity-webhook.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "stackit-pod-identity-webhook.labels" -}} -helm.sh/chart: {{ include "stackit-pod-identity-webhook.chart" . }} -{{ include "stackit-pod-identity-webhook.selectorLabels" . }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "stackit-pod-identity-webhook.selectorLabels" -}} -app.kubernetes.io/name: {{ include "stackit-pod-identity-webhook.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml index 71f9480a..7dd4eec6 100644 --- a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/mutatingwebhookconfiguration.yaml @@ -1,18 +1,14 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} - namespace: {{ .Release.Namespace }} + name: stackit-pod-identity-webhook labels: - app.kubernetes.io/name: {{ .Chart.Name }} - app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: stackit-pod-identity-webhook webhooks: - name: stackit-pod-identity-webhook.stackit.cloud clientConfig: - service: - name: {{ include "stackit-pod-identity-webhook.fullname" . }} - namespace: {{ .Release.Namespace }} - path: "/mutate--v1-pod" + url: {{ .Values.webhook.url | quote }} + caBundle: {{ .Values.webhook.caBundle | quote }} rules: - operations: ["CREATE"] apiGroups: [""] @@ -20,14 +16,12 @@ webhooks: resources: ["pods"] admissionReviewVersions: ["v1"] sideEffects: None - failurePolicy: {{ .Values.webhook.failurePolicy | default "Ignore" }} + failurePolicy: Fail namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: NotIn values: ["kube-system", "garden"] - - key: gardener.cloud/role - operator: DoesNotExist - key: workload-identity.stackit.cloud/skip-pod-identity-webhook operator: DoesNotExist objectSelector: diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/rbac.yaml b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/rbac.yaml new file mode 100644 index 00000000..6b269d93 --- /dev/null +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/templates/rbac.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: extensions.gardener.cloud:provider-stackit:pod-identity-webhook +rules: +- apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: extensions.gardener.cloud:provider-stackit:pod-identity-webhook +subjects: +- kind: ServiceAccount # from shoot access secret + name: pod-identity-webhook + namespace: kube-system +roleRef: + kind: ClusterRole + name: extensions.gardener.cloud:provider-stackit:pod-identity-webhook + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml index 51febcf6..fdc089c0 100644 --- a/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml +++ b/charts/internal/shoot-system-components/charts/stackit-pod-identity-webhook/values.yaml @@ -1,7 +1,3 @@ webhook: caBundle: "" # will be set by valuesprovider - # failurePolicy for the webhook (Ignore or Fail). - # Defaults to Fail to guarantee that pods are not started without the required workload identity configuration. - # Note: If the webhook is down, pod creation in monitored namespaces will be blocked. - # Specific pods or namespaces can be excluded using the skip label. - failurePolicy: Fail + controlPlaneNamespace: "" # will be set by valuesprovider diff --git a/charts/internal/shoot-system-components/requirements.yaml b/charts/internal/shoot-system-components/requirements.yaml index aed217a2..c7748e58 100644 --- a/charts/internal/shoot-system-components/requirements.yaml +++ b/charts/internal/shoot-system-components/requirements.yaml @@ -15,3 +15,6 @@ dependencies: repository: http://localhost:10191 version: 0.1.0 condition: stackit-blockstorage-csi-driver.enabled +- name: stackit-pod-identity-webhook + repository: http://localhost:10191 + version: 0.1.0 diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 62f89a65..0204a3e2 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -58,7 +58,7 @@ import ( const ( caNameControlPlane = "ca-" + openstack.Name + "-controlplane" cloudControllerManagerServerName = openstack.CloudControllerManagerName + "-server" - stackitPodIdentityWebhookServerName = stackit.STACKITPodIdentityWebhookName + "-server" + stackitPodIdentityWebhookServerName = stackit.PodIdentityWebhookName + "-server" CSIStackitPrefix = "stackit-blockstorage" @@ -104,8 +104,8 @@ func secretConfigsFunc(namespace string) []extensionssecretmanager.SecretConfigW { Config: &secretutils.CertificateSecretConfig{ Name: stackitPodIdentityWebhookServerName, - CommonName: stackit.STACKITPodIdentityWebhookName, - DNSNames: kutil.DNSNamesForService(stackit.STACKITPodIdentityWebhookName, namespace), + CommonName: stackit.PodIdentityWebhookName, + DNSNames: kutil.DNSNamesForService(stackit.PodIdentityWebhookName, namespace), CertType: secretutils.ServerCert, SkipPublishingCACertificate: true, }, @@ -122,6 +122,7 @@ func shootAccessSecretsFunc(namespace string) []*gutil.AccessSecret { gutil.NewShootAccessSecret(openstack.CSISnapshotterName, namespace), gutil.NewShootAccessSecret(openstack.CSIResizerName, namespace), gutil.NewShootAccessSecret(openstack.CSISnapshotControllerName, namespace), + gutil.NewShootAccessSecret(stackit.PodIdentityWebhookName, namespace), } } @@ -210,11 +211,13 @@ var ( }, }, { - Name: stackit.STACKITPodIdentityWebhookName, + Name: stackit.PodIdentityWebhookName, Images: []string{imagevector.ImageNameStackitPodIdentityWebhook}, Objects: []*chart.Object{ - {Type: &appsv1.Deployment{}, Name: stackit.STACKITPodIdentityWebhookName}, - {Type: &corev1.Service{}, Name: stackit.STACKITPodIdentityWebhookName}, + {Type: &appsv1.Deployment{}, Name: stackit.PodIdentityWebhookName}, + {Type: &policyv1.PodDisruptionBudget{}, Name: stackit.PodIdentityWebhookName}, + {Type: &corev1.Service{}, Name: stackit.PodIdentityWebhookName}, + {Type: &vpaautoscalingv1.VerticalPodAutoscaler{}, Name: stackit.PodIdentityWebhookName}, }, }, }, @@ -317,9 +320,9 @@ var ( }, }, { - Name: stackit.STACKITPodIdentityWebhookName, + Name: stackit.PodIdentityWebhookName, Objects: []*chart.Object{ - {Type: &admissionregistrationv1.MutatingWebhookConfiguration{}, Name: stackit.STACKITPodIdentityWebhookName}, + {Type: &admissionregistrationv1.MutatingWebhookConfiguration{}, Name: stackit.PodIdentityWebhookName}, }, }, }, @@ -724,7 +727,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf } } - podIdentityWebhook, err := getSTACKITPodIdentityWebhookChartValues(cluster, secretsReader, scaledDown) + podIdentityWebhook, err := getPodIdentityWebhookChartValues(cluster, secretsReader, scaledDown) if err != nil { return nil, err } @@ -753,7 +756,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf }, openstack.CloudControllerManagerName: ccm, openstack.STACKITCloudControllerManagerName: stackitccm, - stackit.STACKITPodIdentityWebhookName: podIdentityWebhook, + stackit.PodIdentityWebhookName: podIdentityWebhook, }) if vp.deployALBIngressController { @@ -1078,14 +1081,14 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c return nil, err } - podIdentityWebhook, err := vp.getSTACKITPodIdentityWebhookShootChartValues(secretsReader) + podIdentityWebhook, err := vp.getPodIdentityWebhookShootChartValues(cp.Namespace, secretsReader) if err != nil { return nil, err } maps.Copy(values, map[string]any{ - openstack.CloudControllerManagerName: map[string]any{"enabled": true}, - stackit.STACKITPodIdentityWebhookName: podIdentityWebhook, + openstack.CloudControllerManagerName: map[string]any{"enabled": true}, + stackit.PodIdentityWebhookName: podIdentityWebhook, }) return values, nil @@ -1295,7 +1298,7 @@ func cleanupCloudProviderConfigSecret(ctx context.Context, client k8sclient.Clie return nil } -func getSTACKITPodIdentityWebhookChartValues( +func getPodIdentityWebhookChartValues( cluster *extensionscontroller.Cluster, secretsReader secretsmanager.Reader, scaledDown bool, @@ -1306,14 +1309,15 @@ func getSTACKITPodIdentityWebhookChartValues( } return map[string]any{ - "replicaCount": extensionscontroller.GetControlPlaneReplicas(cluster, scaledDown, 2), + "replicaCount": extensionscontroller.GetControlPlaneReplicas(cluster, scaledDown, 1), "webhook": map[string]any{ "tlsSecretName": tlsSecret.Name, }, }, nil } -func (vp *valuesProvider) getSTACKITPodIdentityWebhookShootChartValues( +func (vp *valuesProvider) getPodIdentityWebhookShootChartValues( + controlPlaneNamespace string, secretsReader secretsmanager.Reader, ) (map[string]any, error) { caSecret, found := secretsReader.Get(caNameControlPlane) @@ -1321,9 +1325,15 @@ func (vp *valuesProvider) getSTACKITPodIdentityWebhookShootChartValues( return nil, fmt.Errorf("secret %q not found", caNameControlPlane) } + caBundle, ok := caSecret.Data[secretutils.DataKeyCertificateBundle] + if !ok || len(caBundle) == 0 { + return nil, fmt.Errorf("secret %q is missing non-empty %q data", caNameControlPlane, secretutils.DataKeyCertificateBundle) + } + return map[string]any{ "webhook": map[string]any{ - "caBundle": gardenerutils.EncodeBase64(caSecret.Data[secretutils.DataKeyCertificateBundle]), + "caBundle": caBundle, + "url": fmt.Sprintf("https://%s.%s:443/mutate--v1-pod", stackit.PodIdentityWebhookName, controlPlaneNamespace), }, }, nil } diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index bbe25634..c8273888 100644 --- a/pkg/controller/controlplane/valuesprovider_test.go +++ b/pkg/controller/controlplane/valuesprovider_test.go @@ -16,6 +16,7 @@ import ( v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" "github.com/gardener/gardener/pkg/utils" + secretutils "github.com/gardener/gardener/pkg/utils/secrets" secretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager" fakesecretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager/fake" testutils "github.com/gardener/gardener/pkg/utils/test" @@ -496,9 +497,9 @@ var _ = Describe("ValuesProvider", func() { }) stackitPodIdentityWebhookChartSeedValues := map[string]any{ - "replicaCount": 2, + "replicaCount": 1, "webhook": map[string]any{ - "tlsSecretName": "stackit-pod-identity-webhook-server", + "tlsSecretName": stackitPodIdentityWebhookServerName, }, } @@ -520,9 +521,9 @@ var _ = Describe("ValuesProvider", func() { c.EXPECT().Delete(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-%s", CSIStackitPrefix, openstack.CloudProviderConfigName), Namespace: namespace}}) By("creating secrets managed outside of this package for whose secretsmanager.Get() will be called") - Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca-provider-openstack-controlplane", Namespace: namespace}})).To(Succeed()) + Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca-provider-openstack-controlplane", Namespace: namespace}, Data: map[string][]byte{secretutils.DataKeyCertificateBundle: []byte("fake-ca-cert")}})).To(Succeed()) Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "cloud-controller-manager-server", Namespace: namespace}})).To(Succeed()) - Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "stackit-pod-identity-webhook-server", Namespace: namespace}})).To(Succeed()) + Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: stackitPodIdentityWebhookServerName, Namespace: namespace}})).To(Succeed()) // This call is made for emergency Loadbalancer API access. // It will return a NotFound error by default to not interfere with existing tests. @@ -565,7 +566,7 @@ var _ = Describe("ValuesProvider", func() { "replicas": 1, }, }), - stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartSeedValues, + stackit.PodIdentityWebhookName: stackitPodIdentityWebhookChartSeedValues, openstack.STACKITALBControllerManagerName: empty(), })) }) @@ -609,7 +610,7 @@ var _ = Describe("ValuesProvider", func() { "replicas": 1, }, }), - stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartSeedValues, + stackit.PodIdentityWebhookName: stackitPodIdentityWebhookChartSeedValues, openstack.STACKITALBControllerManagerName: empty(), })) }) @@ -893,13 +894,14 @@ var _ = Describe("ValuesProvider", func() { Describe("#GetControlPlaneShootChartValues", func() { stackitPodIdentityWebhookChartShootValues := map[string]any{ "webhook": map[string]any{ - "caBundle": "", + "caBundle": []byte("fake-ca-cert"), + "url": fmt.Sprintf("https://stackit-pod-identity-webhook.%s:443/mutate--v1-pod", namespace), }, } BeforeEach(func() { By("creating secrets managed outside of this package for whose secretsmanager.Get() will be called") - Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca-provider-openstack-controlplane", Namespace: namespace}})).To(Succeed()) + Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca-provider-openstack-controlplane", Namespace: namespace}, Data: map[string][]byte{secretutils.DataKeyCertificateBundle: []byte("fake-ca-cert")}})).To(Succeed()) Expect(fakeClient.Create(context.TODO(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "cloud-controller-manager-server", Namespace: namespace}})).To(Succeed()) }) @@ -919,8 +921,8 @@ var _ = Describe("ValuesProvider", func() { "rescanBlockStorageOnResize": rescanBlockStorageOnResize, "userAgentHeaders": []string{domainName, tenantName, technicalID}, }), - openstack.CSINodeName: enabledFalse, - stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartShootValues, + openstack.CSINodeName: enabledFalse, + stackit.PodIdentityWebhookName: stackitPodIdentityWebhookChartShootValues, })) }) @@ -938,8 +940,8 @@ var _ = Describe("ValuesProvider", func() { "rescanBlockStorageOnResize": rescanBlockStorageOnResize, "userAgentHeaders": []string{domainName, tenantName, technicalID}, }), - openstack.CSINodeName: enabledFalse, - stackit.STACKITPodIdentityWebhookName: stackitPodIdentityWebhookChartShootValues, + openstack.CSINodeName: enabledFalse, + stackit.PodIdentityWebhookName: stackitPodIdentityWebhookChartShootValues, })) }) }) diff --git a/pkg/stackit/types.go b/pkg/stackit/types.go index d12e76f4..e5e2fa82 100644 --- a/pkg/stackit/types.go +++ b/pkg/stackit/types.go @@ -17,8 +17,8 @@ const ( DirectionEgress = "egress" DirectionIngress = "ingress" - // STACKITPodIdentityWebhookName is a constant for the name of the Pod Identity Webhook. (stackit) - STACKITPodIdentityWebhookName = "stackit-pod-identity-webhook" + // PodIdentityWebhookName is a constant for the name of the Pod Identity Webhook. (stackit) + PodIdentityWebhookName = "stackit-pod-identity-webhook" ) var (