diff --git a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go index 11ae2af7777..64c10a27819 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go @@ -124,6 +124,37 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { startingDeadlineSeconds := int64(5) Expect(cronJob.Spec.StartingDeadlineSeconds).To(Equal(&startingDeadlineSeconds)) + // verify CronJob uses a dedicated SA, separate from the feature-server SA + cronJobSAName := services.GetFeastServiceName(resource, services.CronJobFeastType) + deploymentSAName := objMeta.Name + podSpec := cronJob.Spec.JobTemplate.Spec.Template.Spec + Expect(podSpec.ServiceAccountName).To(Equal(cronJobSAName)) + Expect(podSpec.ServiceAccountName).NotTo(Equal(deploymentSAName)) + + // verify the dedicated CronJob SA exists + cronJobSA := &corev1.ServiceAccount{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: cronJobSAName, + Namespace: objMeta.Namespace, + }, cronJobSA) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(cronJobSA)).To(BeTrue()) + + // verify restricted pod security context + Expect(podSpec.SecurityContext).NotTo(BeNil()) + Expect(*podSpec.SecurityContext.RunAsNonRoot).To(BeTrue()) + Expect(*podSpec.SecurityContext.RunAsUser).To(Equal(int64(1001))) + Expect(podSpec.SecurityContext.SeccompProfile).NotTo(BeNil()) + Expect(podSpec.SecurityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) + + // verify restricted container security context + for _, c := range append(podSpec.InitContainers, podSpec.Containers...) { + Expect(c.SecurityContext).NotTo(BeNil()) + Expect(*c.SecurityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(c.SecurityContext.Capabilities).NotTo(BeNil()) + Expect(c.SecurityContext.Capabilities.Drop).To(ContainElement(corev1.Capability("ALL"))) + } + checkCronJob(resource.Status.Applied.CronJob, cronJob.Spec) }) @@ -244,6 +275,12 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { Expect(cronJob.Spec.StartingDeadlineSeconds).To(Equal(&startingDeadlineSeconds)) Expect(cronJob.Spec.JobTemplate.Spec.Parallelism).To(Equal(&int32Var)) + // verify CronJob uses a dedicated SA, separate from the feature-server SA + cronJobSAName := services.GetFeastServiceName(resource, services.CronJobFeastType) + podSpec := cronJob.Spec.JobTemplate.Spec.Template.Spec + Expect(podSpec.ServiceAccountName).To(Equal(cronJobSAName)) + Expect(podSpec.ServiceAccountName).NotTo(Equal(objMeta.Name)) + checkCronJob(resource.Status.Applied.CronJob, cronJob.Spec) }) }) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 712644f7a0c..7d06c5d2ea9 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -745,7 +745,7 @@ var _ = Describe("FeatureStore Controller", func() { saList := corev1.ServiceAccountList{} err = k8sClient.List(ctx, &saList, listOpts) Expect(err).NotTo(HaveOccurred()) - Expect(saList.Items).To(HaveLen(1)) + Expect(saList.Items).To(HaveLen(2)) svcList := corev1.ServiceList{} err = k8sClient.List(ctx, &svcList, listOpts) diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index f3b978928f7..aa3da49bf7d 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -16,6 +16,9 @@ import ( ) func (feast *FeastServices) deployCronJob() error { + if err := feast.createCronJobServiceAccount(); err != nil { + return feast.setFeastServiceCondition(err, CronJobFeastType) + } if err := feast.createCronJobRole(); err != nil { return feast.setFeastServiceCondition(err, CronJobFeastType) } @@ -146,8 +149,15 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { func (feast *FeastServices) getCronJobPodSpec() corev1.PodSpec { podSpec := corev1.PodSpec{ - ServiceAccountName: feast.initFeastSA().Name, + ServiceAccountName: feast.initCronJobSA().Name, RestartPolicy: corev1.RestartPolicyNever, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: boolPtr(true), + RunAsUser: int64Ptr(1001), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, } feast.setCronJobContainers(&podSpec) return podSpec @@ -167,7 +177,7 @@ func (feast *FeastServices) setCronJobContainers(podSpec *corev1.PodSpec) { } func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string) corev1.Container { - return *getContainer( + container := getContainer( containerName, "", []string{ @@ -178,6 +188,42 @@ func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string feast.Handler.FeatureStore.Status.Applied.CronJob.ContainerConfigs.ContainerConfigs, "", ) + container.SecurityContext = &corev1.SecurityContext{ + AllowPrivilegeEscalation: boolPtr(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + } + return *container +} + +func (feast *FeastServices) createCronJobServiceAccount() error { + logger := log.FromContext(feast.Handler.Context) + sa := feast.initCronJobSA() + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, sa, controllerutil.MutateFn(func() error { + return feast.setCronJobServiceAccount(sa) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ServiceAccount", sa.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) initCronJobSA() *corev1.ServiceAccount { + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: feast.getCronJobRoleName(), + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + sa.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) + return sa +} + +func (feast *FeastServices) setCronJobServiceAccount(sa *corev1.ServiceAccount) error { + sa.Labels = feast.getFeastTypeLabels(CronJobFeastType) + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, sa, feast.Handler.Scheme) } func (feast *FeastServices) createCronJobRole() error { @@ -254,7 +300,7 @@ func (feast *FeastServices) setCronJobRoleBinding(roleBinding *rbacv1.RoleBindin roleBinding.Labels = feast.getFeastTypeLabels(CronJobFeastType) roleBinding.Subjects = []rbacv1.Subject{{ Kind: rbacv1.ServiceAccountKind, - Name: feast.initFeastSA().Name, + Name: feast.initCronJobSA().Name, Namespace: feast.Handler.FeatureStore.Namespace, }} roleBinding.RoleRef = rbacv1.RoleRef{