diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index c6b3ce6ca3..2a22cd3805 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -28,6 +28,7 @@ import ( "k8s.io/klog/v2" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -52,6 +53,7 @@ import ( podwh "github.com/liqotech/liqo/pkg/webhooks/pod" resourceslicewh "github.com/liqotech/liqo/pkg/webhooks/resourceslice" routecfgwh "github.com/liqotech/liqo/pkg/webhooks/routeconfiguration" + "github.com/liqotech/liqo/pkg/webhooks/secretcontroller" shadowpodswh "github.com/liqotech/liqo/pkg/webhooks/shadowpod" virtualnodewh "github.com/liqotech/liqo/pkg/webhooks/virtualnode" ) @@ -76,6 +78,7 @@ func main() { metricsAddr := pflag.String("metrics-address", ":8080", "The address the metric endpoint binds to") probeAddr := pflag.String("health-probe-address", ":8081", "The address the health probe endpoint binds to") leaderElection := pflag.Bool("enable-leader-election", false, "Enable leader election for the webhook pod") + secretName := pflag.String("secret-name", "", "The name of the secret containing the webhook certificates") // Global parameters clusterIDFlags := argsutils.NewClusterIDFlags(true, nil) @@ -103,6 +106,34 @@ func main() { config := restcfg.SetRateLimiter(ctrl.GetConfigOrDie()) + // create a client used for configuration + cl, err := client.New(config, client.Options{Scheme: scheme}) + if err != nil { + klog.Error(err) + os.Exit(1) + } + + // forge secret for the webhook + if *secretName != "" { + var secret corev1.Secret + if err := cl.Get(ctx, client.ObjectKey{Namespace: *liqoNamespace, Name: *secretName}, &secret); err != nil { + klog.Error(err) + os.Exit(1) + } + + if err := secretcontroller.HandleSecret(ctx, cl, &secret); err != nil { + klog.Error(err) + os.Exit(1) + } + + if err := cl.Update(ctx, &secret); err != nil { + klog.Error(err) + os.Exit(1) + } + + klog.Info("webhook secret correctly enforced") + } + // Create the main manager. mgr, err := ctrl.NewManager(config, ctrl.Options{ MapperProvider: mapper.LiqoMapperProvider(scheme), @@ -169,6 +200,14 @@ func main() { mgr.GetWebhookServer().Register("/mutate/firewallconfigurations", fwcfgwh.NewMutator()) mgr.GetWebhookServer().Register("/validate/routeconfigurations", routecfgwh.NewValidator(mgr.GetClient())) + // Register the secret controller + secretReconciler := secretcontroller.NewSecretReconciler(mgr.GetClient(), mgr.GetScheme(), + mgr.GetEventRecorderFor("secret-controller")) + if err := secretReconciler.SetupWithManager(mgr); err != nil { + klog.Errorf("Unable to set up the secret controller: %v", err) + os.Exit(1) + } + if leaderElection != nil && *leaderElection { leaderelection.LabelerOnElection(ctx, mgr, &leaderelection.PodInfo{ PodName: os.Getenv("POD_NAME"), diff --git a/deployments/liqo/files/liqo-webhook-ClusterRole.yaml b/deployments/liqo/files/liqo-webhook-ClusterRole.yaml index 2871e63bea..0921734d36 100644 --- a/deployments/liqo/files/liqo-webhook-ClusterRole.yaml +++ b/deployments/liqo/files/liqo-webhook-ClusterRole.yaml @@ -25,6 +25,28 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + - validatingwebhookconfigurations + verbs: + - get + - list + - update + - watch - apiGroups: - apps resources: diff --git a/deployments/liqo/templates/liqo-controller-manager-deployment.yaml b/deployments/liqo/templates/liqo-controller-manager-deployment.yaml index 02393b6c36..4b6083e34d 100644 --- a/deployments/liqo/templates/liqo-controller-manager-deployment.yaml +++ b/deployments/liqo/templates/liqo-controller-manager-deployment.yaml @@ -156,10 +156,6 @@ spec: {{- end }} {{- end }} resources: {{- toYaml .Values.controllerManager.pod.resources | nindent 10 }} - volumeMounts: - - name: webhook-certs - mountPath: /tmp/k8s-webhook-server/serving-certs/ - readOnly: true ports: - name: webhook containerPort: {{ .Values.webhook.port }} @@ -174,11 +170,6 @@ spec: httpGet: path: /readyz port: healthz - volumes: - - name: webhook-certs - secret: - secretName: {{ include "liqo.prefixedName" $webhookConfig }}-certs - defaultMode: 420 {{- if ((.Values.common).nodeSelector) }} nodeSelector: {{- toYaml .Values.common.nodeSelector | nindent 8 }} diff --git a/deployments/liqo/templates/liqo-webook-deployment.yaml b/deployments/liqo/templates/liqo-webhook-deployment.yaml similarity index 94% rename from deployments/liqo/templates/liqo-webook-deployment.yaml rename to deployments/liqo/templates/liqo-webhook-deployment.yaml index 15bb5539ad..5a2f2742d0 100644 --- a/deployments/liqo/templates/liqo-webook-deployment.yaml +++ b/deployments/liqo/templates/liqo-webhook-deployment.yaml @@ -52,6 +52,7 @@ spec: {{- end }} - --cluster-id=$(CLUSTER_ID) - --liqo-namespace=$(POD_NAMESPACE) + - --secret-name={{ include "liqo.prefixedName" $webhookConfig }}-certs - --podcidr={{ .Values.ipam.podCIDR }} - --vk-options-default-template={{ .Release.Namespace }}/{{ printf "%s-default" $kubeletConfig.name }} {{- if .Values.controllerManager.config.enableResourceEnforcement }} @@ -83,10 +84,6 @@ spec: - name: DEPLOYMENT_NAME value: {{ include "liqo.prefixedName" $webhookConfig }} resources: {{- toYaml .Values.webhook.pod.resources | nindent 10 }} - volumeMounts: - - name: webhook-certs - mountPath: /tmp/k8s-webhook-server/serving-certs/ - readOnly: true ports: - name: webhook containerPort: {{ .Values.webhook.port }} @@ -101,11 +98,12 @@ spec: httpGet: path: /readyz port: healthz + volumeMounts: + - name: webhook-certs + mountPath: /tmp/k8s-webhook-server volumes: - name: webhook-certs - secret: - secretName: {{ include "liqo.prefixedName" $webhookConfig }}-certs - defaultMode: 420 + emptyDir: {} {{- if ((.Values.common).nodeSelector) }} nodeSelector: {{- toYaml .Values.common.nodeSelector | nindent 8 }} diff --git a/deployments/liqo/templates/webhooks/job-patch/job-create-secret.yaml b/deployments/liqo/templates/webhooks/job-patch/job-create-secret.yaml deleted file mode 100644 index 2b73a49d2e..0000000000 --- a/deployments/liqo/templates/webhooks/job-patch/job-create-secret.yaml +++ /dev/null @@ -1,50 +0,0 @@ -{{- $cfg := (merge (dict "name" "webhook-certificate-patch-pre" "module" "webhook-certificate-patch") .) -}} -{{- $rbacConfig := (merge (dict "name" "webhook-certificate-patch") .) -}} -{{- $webhookConfig := (merge (dict "name" "webhook" "module" "webhook") .) -}} - -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": pre-install,pre-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} -spec: - ttlSecondsAfterFinished: 150 - template: - metadata: - name: {{ include "liqo.prefixedName" $cfg }} - labels: - {{- include "liqo.labels" $cfg | nindent 8 }} - spec: - containers: - - name: create - image: {{ .Values.webhook.patch.image }} - imagePullPolicy: {{ .Values.pullPolicy }} - args: - - create - - --host={{ include "liqo.prefixedName" $webhookConfig }},{{ include "liqo.prefixedName" $webhookConfig }}.{{ .Release.Namespace }},{{ include "liqo.prefixedName" $webhookConfig }}.{{ .Release.Namespace }}.svc,{{ include "liqo.prefixedName" $webhookConfig }}.{{ .Release.Namespace }}.svc.cluster.local - - --namespace={{ .Release.Namespace }} - - --secret-name={{ include "liqo.prefixedName" $webhookConfig }}-certs - - --cert-name=tls.crt - - --key-name=tls.key - securityContext: - {{- include "liqo.containerSecurityContext" . | nindent 10 }} - restartPolicy: OnFailure - serviceAccountName: {{ include "liqo.prefixedName" $rbacConfig }} - securityContext: - {{- include "liqo.podSecurityContext" . | nindent 8 }} - {{- if ((.Values.common).nodeSelector) }} - nodeSelector: - {{- toYaml .Values.common.nodeSelector | nindent 8 }} - {{- end }} - {{- if ((.Values.common).tolerations) }} - tolerations: - {{- toYaml .Values.common.tolerations | nindent 8 }} - {{- end }} - {{- if ((.Values.common).affinity) }} - affinity: - {{- toYaml .Values.common.affinity | nindent 8 }} - {{- end }} diff --git a/deployments/liqo/templates/webhooks/job-patch/job-patch-webhook.yaml b/deployments/liqo/templates/webhooks/job-patch/job-patch-webhook.yaml deleted file mode 100644 index 9db634053c..0000000000 --- a/deployments/liqo/templates/webhooks/job-patch/job-patch-webhook.yaml +++ /dev/null @@ -1,48 +0,0 @@ -{{- $cfg := (merge (dict "name" "webhook-certificate-patch-post" "module" "webhook-certificate-patch") .) -}} -{{- $rbacConfig := (merge (dict "name" "webhook-certificate-patch") .) -}} -{{- $webhookConfig := (merge (dict "name" "webhook" "module" "webhook") .) -}} - -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": post-install,post-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} -spec: - ttlSecondsAfterFinished: 150 - template: - metadata: - name: {{ include "liqo.prefixedName" $cfg }} - labels: - {{- include "liqo.labels" $cfg | nindent 8 }} - spec: - containers: - - name: create - image: {{ .Values.webhook.patch.image }} - imagePullPolicy: {{ .Values.pullPolicy }} - args: - - patch - - --webhook-name={{ include "liqo.prefixedName" $webhookConfig }} - - --namespace={{ .Release.Namespace }} - - --secret-name={{ include "liqo.prefixedName" $webhookConfig }}-certs - securityContext: - {{- include "liqo.containerSecurityContext" . | nindent 10 }} - restartPolicy: OnFailure - serviceAccountName: {{ include "liqo.prefixedName" $rbacConfig }} - securityContext: - {{- include "liqo.podSecurityContext" . | nindent 8 }} - {{- if ((.Values.common).nodeSelector) }} - nodeSelector: - {{- toYaml .Values.common.nodeSelector | nindent 8 }} - {{- end }} - {{- if ((.Values.common).tolerations) }} - tolerations: - {{- toYaml .Values.common.tolerations | nindent 8 }} - {{- end }} - {{- if ((.Values.common).affinity) }} - affinity: - {{- toYaml .Values.common.affinity | nindent 8 }} - {{- end }} diff --git a/deployments/liqo/templates/webhooks/job-patch/rbac.yaml b/deployments/liqo/templates/webhooks/job-patch/rbac.yaml deleted file mode 100644 index 5e4031d701..0000000000 --- a/deployments/liqo/templates/webhooks/job-patch/rbac.yaml +++ /dev/null @@ -1,85 +0,0 @@ -{{- $cfg := (merge (dict "name" "webhook-certificate-patch" "module" "webhook-certificate-patch") .) -}} - ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} -rules: - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - create ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ include "liqo.prefixedName" $cfg }} -subjects: - - kind: ServiceAccount - name: {{ include "liqo.prefixedName" $cfg }} - namespace: {{ .Release.Namespace | quote }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": post-install,post-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} -rules: - - apiGroups: - - admissionregistration.k8s.io - resources: - - validatingwebhookconfigurations - - mutatingwebhookconfigurations - verbs: - - get - - update ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "liqo.prefixedName" $cfg }} - annotations: - "helm.sh/hook": post-install,post-upgrade - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded - labels: - {{- include "liqo.labels" $cfg | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "liqo.prefixedName" $cfg }} -subjects: - - kind: ServiceAccount - name: {{ include "liqo.prefixedName" $cfg }} - namespace: {{ .Release.Namespace | quote }} diff --git a/deployments/liqo/templates/webhooks/liqo-mutating-webhook.yaml b/deployments/liqo/templates/webhooks/liqo-mutating-webhook.yaml index 2d8c02cecb..3da892b542 100644 --- a/deployments/liqo/templates/webhooks/liqo-mutating-webhook.yaml +++ b/deployments/liqo/templates/webhooks/liqo-mutating-webhook.yaml @@ -6,6 +6,7 @@ metadata: name: {{ include "liqo.prefixedName" $webhookConfig }} labels: {{- include "liqo.labels" $webhookConfig | nindent 4 }} + liqo.io/webhook: "true" webhooks: - name: pod.mutate.liqo.io admissionReviewVersions: diff --git a/deployments/liqo/templates/webhooks/liqo-validating-webhook.yaml b/deployments/liqo/templates/webhooks/liqo-validating-webhook.yaml index ef68f77dcc..e40f73d8a8 100644 --- a/deployments/liqo/templates/webhooks/liqo-validating-webhook.yaml +++ b/deployments/liqo/templates/webhooks/liqo-validating-webhook.yaml @@ -6,6 +6,7 @@ metadata: name: {{ include "liqo.prefixedName" $webhookConfig }} labels: {{- include "liqo.labels" $webhookConfig | nindent 4 }} + liqo.io/webhook: "true" webhooks: - name: nsoff.validate.liqo.io admissionReviewVersions: diff --git a/deployments/liqo/templates/webhooks/liqo-webhook-secret.yaml b/deployments/liqo/templates/webhooks/liqo-webhook-secret.yaml new file mode 100644 index 0000000000..7876796eba --- /dev/null +++ b/deployments/liqo/templates/webhooks/liqo-webhook-secret.yaml @@ -0,0 +1,12 @@ +{{- $webhookConfig := (merge (dict "name" "webhook" "module" "webhook") .) -}} + +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "liqo.prefixedName" $webhookConfig }}-certs + labels: + {{- include "liqo.labels" $webhookConfig | nindent 4 }} + liqo.io/webhook: "true" + annotations: + liqo.io/webhook-service-name: {{ include "liqo.prefixedName" $webhookConfig }} +type: opaque diff --git a/pkg/consts/labels.go b/pkg/consts/labels.go index 81dc545b90..32e7168f65 100644 --- a/pkg/consts/labels.go +++ b/pkg/consts/labels.go @@ -44,4 +44,14 @@ const ( // IpamStorageResourceLabelValue is the constant representing // the value of the label assigned to all IpamStorage resources. IpamStorageResourceLabelValue = "true" + + // WebhookResourceLabelKey is the constant representing + // the key of the label assigned to all Webhook resources. + WebhookResourceLabelKey = "liqo.io/webhook" + // WebhookResourceLabelValue is the constant representing + // the value of the label assigned to all Webhook resources. + WebhookResourceLabelValue = "true" + // WebhookServiceNameAnnotationKey is the constant representing + // the key of the annotation containing the Webhook service name. + WebhookServiceNameAnnotationKey = "liqo.io/webhook-service-name" ) diff --git a/pkg/utils/mapper/mapper.go b/pkg/utils/mapper/mapper.go index 8d67bceaa3..10db044829 100644 --- a/pkg/utils/mapper/mapper.go +++ b/pkg/utils/mapper/mapper.go @@ -19,6 +19,7 @@ import ( "net/http" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + adminssionregistrationv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" @@ -105,6 +106,9 @@ func addDefaults(dClient *discovery.DiscoveryClient, mapper *meta.DefaultRESTMap if err = addGroup(dClient, storagev1.SchemeGroupVersion, mapper, GroupRequired); err != nil { return err } + if err = addGroup(dClient, adminssionregistrationv1.SchemeGroupVersion, mapper, GroupRequired); err != nil { + return err + } // Prometheus operator group if err = addGroup(dClient, monitoringv1.SchemeGroupVersion, mapper, GroupOptional); err != nil { diff --git a/pkg/webhooks/secretcontroller/doc.go b/pkg/webhooks/secretcontroller/doc.go new file mode 100644 index 0000000000..9c2f2d857b --- /dev/null +++ b/pkg/webhooks/secretcontroller/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package secretcontroller contains the logic of the secret controller. +package secretcontroller diff --git a/pkg/webhooks/secretcontroller/secret_controller.go b/pkg/webhooks/secretcontroller/secret_controller.go new file mode 100644 index 0000000000..4d5e3184ae --- /dev/null +++ b/pkg/webhooks/secretcontroller/secret_controller.go @@ -0,0 +1,297 @@ +// Copyright 2019-2024 The Liqo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package secretcontroller + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "os" + "time" + + adminssionregistrationv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/liqotech/liqo/pkg/consts" +) + +// NewSecretReconciler returns a new SecretReconciler. +func NewSecretReconciler(cl client.Client, s *runtime.Scheme, recorder record.EventRecorder) *SecretReconciler { + return &SecretReconciler{ + Client: cl, + Scheme: s, + + eventRecorder: recorder, + } +} + +// SecretReconciler reconciles a Secret object. +type SecretReconciler struct { + client.Client + *runtime.Scheme + + eventRecorder record.EventRecorder +} + +// cluster-role +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations,verbs=get;list;watch;update +// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=get;list;watch;update + +// Reconcile Secret resources for webhooks. +func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { + var secret corev1.Secret + if err = r.Get(ctx, req.NamespacedName, &secret); err != nil { + if errors.IsNotFound(err) { + klog.V(4).Infof("Secret %s not found", req.NamespacedName) + return ctrl.Result{}, nil + } + klog.Error(err, "unable to get Secret") + return ctrl.Result{}, err + } + + defer func() { + if err = r.Update(ctx, &secret); err != nil { + klog.Error(err, "unable to update Secret") + } + }() + + if err = HandleSecret(ctx, r.Client, &secret); err != nil { + klog.Error(err, "unable to handle Secret") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *SecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + p, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: consts.WebhookResourceLabelKey, + Operator: metav1.LabelSelectorOpIn, + Values: []string{consts.WebhookResourceLabelValue}, + }, + }, + }) + if err != nil { + return fmt.Errorf("unable to create label selector predicate: %w", err) + } + + return ctrl.NewControllerManagedBy(mgr).Named(consts.CtrlIdentity). + For(&corev1.Secret{}, builder.WithPredicates(p)). + Complete(r) +} + +// HandleSecret handles the given Secret for webhooks. +func HandleSecret(ctx context.Context, cl client.Client, secret *corev1.Secret) error { + serviceName := secret.Annotations[consts.WebhookServiceNameAnnotationKey] + + ca, caOk := secret.Data["ca"] + tlsKey, tlsKeyOk := secret.Data["tls.key"] + tlsCrt, tlsCrtOk := secret.Data["tls.crt"] + + if !caOk || !tlsKeyOk || !tlsCrtOk || + len(ca) == 0 || len(tlsKey) == 0 || len(tlsCrt) == 0 { + caB, crtB, keyB, err := createCA(serviceName, secret.Namespace) + if err != nil { + return fmt.Errorf("unable to create CA: %w", err) + } + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + secret.Data["ca"] = caB + secret.Data["tls.crt"] = crtB + secret.Data["tls.key"] = keyB + } else { + err := os.MkdirAll("/tmp/k8s-webhook-server/serving-certs/", 0o700) + if err != nil { + return fmt.Errorf("unable to create directory: %w", err) + } + err = writeFile("/tmp/k8s-webhook-server/serving-certs/tls.crt", bytes.NewBuffer(tlsCrt)) + if err != nil { + return fmt.Errorf("unable to write file: %w", err) + } + err = writeFile("/tmp/k8s-webhook-server/serving-certs/tls.key", bytes.NewBuffer(tlsKey)) + if err != nil { + return fmt.Errorf("unable to write file: %w", err) + } + } + + // patch webhook configurations + + var mwhcList adminssionregistrationv1.MutatingWebhookConfigurationList + if err := cl.List(ctx, &mwhcList); err != nil { + return fmt.Errorf("unable to list MutatingWebhookConfigurations: %w", err) + } + + for i := range mwhcList.Items { + mwhc := &mwhcList.Items[i] + for j := range mwhc.Webhooks { + hook := &mwhc.Webhooks[j] + hook.ClientConfig.CABundle = ca + } + + if err := cl.Update(ctx, mwhc); err != nil { + return fmt.Errorf("unable to update MutatingWebhookConfiguration: %w", err) + } + } + + var vwhcList adminssionregistrationv1.ValidatingWebhookConfigurationList + if err := cl.List(ctx, &vwhcList); err != nil { + return fmt.Errorf("unable to list ValidatingWebhookConfigurations: %w", err) + } + + for i := range vwhcList.Items { + vwhc := &vwhcList.Items[i] + for j := range vwhc.Webhooks { + hook := &vwhc.Webhooks[j] + hook.ClientConfig.CABundle = ca + } + + if err := cl.Update(ctx, vwhc); err != nil { + return fmt.Errorf("unable to update ValidatingWebhookConfiguration: %w", err) + } + } + + return nil +} + +// createCA generates a new CA and returns it. +func createCA(serviceName, namespace string) (caB, crtB, keyB []byte, err error) { + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to generate private key: %w", err) + } + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Liqo"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create certificate: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + dnsNames := []string{ + serviceName, serviceName + "." + namespace, serviceName + "." + namespace + ".svc", + serviceName + "." + namespace + ".svc.cluster.local"} + commonName := serviceName + "." + namespace + ".svc.cluster.local" + + // server cert config + cert := &x509.Certificate{ + DNSNames: dnsNames, + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + CommonName: commonName, + Organization: []string{"Liqo"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + // server private key + serverPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to generate private key: %w", err) + } + + // sign the server cert + serverCertBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &serverPrivKey.PublicKey, priv) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create certificate: %w", err) + } + + // PEM encode the server cert and key + serverCertPEM := new(bytes.Buffer) + err = pem.Encode(serverCertPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: serverCertBytes, + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to encode server certificate: %w", err) + } + + serverPrivKeyPEM := new(bytes.Buffer) + err = pem.Encode(serverPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(serverPrivKey), + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to encode server private key: %w", err) + } + + // save the server cert and key to disk in the path expected by the webhook server. + err = os.MkdirAll("/tmp/k8s-webhook-server/serving-certs/", 0o700) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create directory: %w", err) + } + err = writeFile("/tmp/k8s-webhook-server/serving-certs/tls.crt", serverCertPEM) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to write file: %w", err) + } + err = writeFile("/tmp/k8s-webhook-server/serving-certs/tls.key", serverPrivKeyPEM) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to write file: %w", err) + } + + return certPEM, serverCertPEM.Bytes(), serverPrivKeyPEM.Bytes(), nil +} + +// writeFile writes data in the file at the given path. +func writeFile(filepath string, sCert *bytes.Buffer) error { + f, err := os.Create(filepath) //nolint:gosec // the file path is not user input + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(sCert.Bytes()) + if err != nil { + return err + } + return nil +}