diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index f4e2a86c..d21ef857 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -442,8 +442,13 @@ spec: memory: 64Mi securityContext: allowPrivilegeEscalation: false + capabilities: + drop: + - ALL securityContext: runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: cryostat-operator-service-account terminationGracePeriodSeconds: 10 permissions: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 30fc50e2..5a323ce8 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -20,6 +20,8 @@ spec: serviceAccountName: service-account securityContext: runAsNonRoot: true + seccompProfile: + type: RuntimeDefault containers: - command: - /manager @@ -29,6 +31,9 @@ spec: name: manager securityContext: allowPrivilegeEscalation: false + capabilities: + drop: + - ALL livenessProbe: httpGet: path: /healthz diff --git a/internal/controllers/common/resource_definitions/resource_definitions.go b/internal/controllers/common/resource_definitions/resource_definitions.go index df67c5ce..a3a187db 100644 --- a/internal/controllers/common/resource_definitions/resource_definitions.go +++ b/internal/controllers/common/resource_definitions/resource_definitions.go @@ -180,7 +180,8 @@ func NewDeploymentForCR(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, image } } -func NewDeploymentForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, tls *TLSConfig) *appsv1.Deployment { +func NewDeploymentForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, tls *TLSConfig, + openshift bool) *appsv1.Deployment { replicas := int32(0) if cr.Spec.ReportOptions != nil { replicas = cr.Spec.ReportOptions.Replicas @@ -217,7 +218,7 @@ func NewDeploymentForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, "component": "reports", }, }, - Spec: *NewPodForReports(cr, imageTags, tls), + Spec: *NewPodForReports(cr, imageTags, tls, openshift), }, Replicas: &replicas, }, @@ -374,9 +375,12 @@ func NewPodForCR(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTags *I volumes = append(volumes, authResourceVolume) } - // Ensure PV mounts are writable + nonRoot := true sc := &corev1.PodSecurityContext{ - FSGroup: &fsGroup, + // Ensure PV mounts are writable + FSGroup: &fsGroup, + RunAsNonRoot: &nonRoot, + SeccompProfile: seccompProfile(openshift), } // Use HostAlias for loopback address to allow health checks to @@ -398,7 +402,12 @@ func NewPodForCR(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTags *I } } -func NewPodForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, tls *TLSConfig) *corev1.PodSpec { +// ALL capability to drop for restricted pod security. See: +// https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted +const capabilityAll corev1.Capability = "ALL" + +func NewPodForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, tls *TLSConfig, + openshift bool) *corev1.PodSpec { resources := corev1.ResourceRequirements{} if cr.Spec.ReportOptions != nil { resources = cr.Spec.ReportOptions.Resources @@ -487,6 +496,8 @@ func NewPodForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, tls *T Path: "/health", }, } + privEscalation := false + nonRoot := true return &corev1.PodSpec{ ServiceAccountName: cr.Name, Containers: []corev1.Container{ @@ -508,9 +519,19 @@ func NewPodForReports(cr *operatorv1beta1.Cryostat, imageTags *ImageTags, tls *T StartupProbe: &corev1.Probe{ ProbeHandler: probeHandler, }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &privEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{capabilityAll}, + }, + }, }, }, Volumes: volumes, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &nonRoot, + SeccompProfile: seccompProfile(openshift), + }, } } @@ -789,6 +810,7 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa Scheme: livenessProbeScheme, }, } + privEscalation := false return corev1.Container{ Name: cr.Name, Image: imageTag, @@ -813,6 +835,12 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa ProbeHandler: probeHandler, FailureThreshold: 18, }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &privEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{capabilityAll}, + }, + }, } } @@ -878,6 +906,7 @@ func NewGrafanaContainer(cr *operatorv1beta1.Cryostat, imageTag string, tls *TLS // Use HTTPS for liveness probe livenessProbeScheme = corev1.URISchemeHTTPS } + privEscalation := false return corev1.Container{ Name: cr.Name + "-grafana", Image: imageTag, @@ -908,6 +937,12 @@ func NewGrafanaContainer(cr *operatorv1beta1.Cryostat, imageTag string, tls *TLS }, }, Resources: cr.Spec.Resources.GrafanaResources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &privEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{capabilityAll}, + }, + }, } } @@ -915,6 +950,7 @@ func NewGrafanaContainer(cr *operatorv1beta1.Cryostat, imageTag string, tls *TLS var datasourceURL = "http://" + loopbackAddress + ":" + strconv.Itoa(int(datasourceContainerPort)) func NewJfrDatasourceContainer(cr *operatorv1beta1.Cryostat, imageTag string) corev1.Container { + privEscalation := false return corev1.Container{ Name: cr.Name + "-jfr-datasource", Image: imageTag, @@ -939,6 +975,12 @@ func NewJfrDatasourceContainer(cr *operatorv1beta1.Cryostat, imageTag string) co }, }, Resources: cr.Spec.Resources.DataSourceResources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &privEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{capabilityAll}, + }, + }, } } @@ -1179,3 +1221,15 @@ func newVolumeForCR(cr *operatorv1beta1.Cryostat) []corev1.Volume { }, } } + +func seccompProfile(openshift bool) *corev1.SeccompProfile { + // For backward-compatibility with OpenShift < 4.11, + // leave the seccompProfile empty. In OpenShift >= 4.11, + // the restricted-v2 SCC will populate it for us. + if openshift { + return nil + } + return &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + } +} diff --git a/internal/controllers/cryostat_controller.go b/internal/controllers/cryostat_controller.go index dba149ad..c53d7bb5 100644 --- a/internal/controllers/cryostat_controller.go +++ b/internal/controllers/cryostat_controller.go @@ -385,7 +385,7 @@ func (r *CryostatReconciler) reconcileReports(ctx context.Context, reqLogger log if err != nil { return reconcile.Result{}, err } - deployment := resources.NewDeploymentForReports(instance, imageTags, tls) + deployment := resources.NewDeploymentForReports(instance, imageTags, tls, r.IsOpenShift) if desired == 0 { if err := r.Client.Delete(ctx, deployment); err != nil && !errors.IsNotFound(err) { return reconcile.Result{}, err diff --git a/internal/controllers/cryostat_controller_test.go b/internal/controllers/cryostat_controller_test.go index e68c92af..c7ea95b5 100644 --- a/internal/controllers/cryostat_controller_test.go +++ b/internal/controllers/cryostat_controller_test.go @@ -1442,6 +1442,26 @@ var _ = Describe("CryostatController", func() { t.checkDeploymentHasNoAuthProperties() }) }) + Context("with report generator service", func() { + BeforeEach(func() { + cr := test.NewCryostatWithIngress() + cr.Spec.ReportOptions = &operatorv1beta1.ReportConfiguration{ + Replicas: 1, + } + t.objs = append(t.objs, cr) + t.reportReplicas = 1 + }) + JustBeforeEach(func() { + t.reconcileCryostatFully() + }) + It("should configure deployment appropriately", func() { + t.checkMainDeployment() + t.checkReportsDeployment() + }) + It("should create the reports service", func() { + t.checkService("cryostat-reports", test.NewReportsService()) + }) + }) }) }) @@ -1985,7 +2005,7 @@ func (t *cryostatTestInput) checkMainPodTemplate(deployment *appsv1.Deployment, "component": "cryostat", })) Expect(template.Spec.Volumes).To(ConsistOf(test.NewVolumes(t.minimal, t.TLS))) - Expect(template.Spec.SecurityContext).To(Equal(test.NewPodSecurityContext())) + Expect(template.Spec.SecurityContext).To(Equal(test.NewPodSecurityContext(t.controller.IsOpenShift))) // Check that the networking environment variables are set correctly coreContainer := template.Spec.Containers[0] @@ -2053,6 +2073,7 @@ func (t *cryostatTestInput) checkReportsDeployment() { "component": "reports", })) Expect(template.Spec.Volumes).To(ConsistOf(test.NewReportsVolumes(t.TLS))) + Expect(template.Spec.SecurityContext).To(Equal(test.NewReportsPodSecurityContext(t.controller.IsOpenShift))) var resources corev1.ResourceRequirements if cr.Spec.ReportOptions != nil { @@ -2127,6 +2148,7 @@ func checkCoreContainer(container *corev1.Container, minimal bool, tls bool, ext Expect(container.LivenessProbe).To(Equal(test.NewCoreLivenessProbe(tls))) Expect(container.StartupProbe).To(Equal(test.NewCoreStartupProbe(tls))) Expect(container.Resources).To(Equal(resources)) + Expect(container.SecurityContext).To(Equal(test.NewSecurityContext())) } func checkGrafanaContainer(container *corev1.Container, tls bool, tag *string, resources corev1.ResourceRequirements) { @@ -2142,6 +2164,7 @@ func checkGrafanaContainer(container *corev1.Container, tls bool, tag *string, r Expect(container.VolumeMounts).To(ConsistOf(test.NewGrafanaVolumeMounts(tls))) Expect(container.LivenessProbe).To(Equal(test.NewGrafanaLivenessProbe(tls))) Expect(container.Resources).To(Equal(resources)) + Expect(container.SecurityContext).To(Equal(test.NewSecurityContext())) } func checkDatasourceContainer(container *corev1.Container, tag *string, resources corev1.ResourceRequirements) { @@ -2157,6 +2180,7 @@ func checkDatasourceContainer(container *corev1.Container, tag *string, resource Expect(container.VolumeMounts).To(BeEmpty()) Expect(container.LivenessProbe).To(Equal(test.NewDatasourceLivenessProbe())) Expect(container.Resources).To(Equal(resources)) + Expect(container.SecurityContext).To(Equal(test.NewSecurityContext())) } func checkReportsContainer(container *corev1.Container, tls bool, tag *string, resources corev1.ResourceRequirements) { @@ -2171,6 +2195,7 @@ func checkReportsContainer(container *corev1.Container, tls bool, tag *string, r Expect(container.VolumeMounts).To(ConsistOf(test.NewReportsVolumeMounts(tls))) Expect(container.LivenessProbe).To(Equal(test.NewReportsLivenessProbe(tls))) Expect(container.Resources).To(Equal(resources)) + Expect(container.SecurityContext).To(Equal(test.NewSecurityContext())) } func (t *cryostatTestInput) checkEnvironmentVariables(expectedEnvVars []corev1.EnvVar) { diff --git a/internal/test/resources.go b/internal/test/resources.go index 3475c523..a000c83f 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -1587,10 +1587,39 @@ func NewReportsVolumes(tls bool) []corev1.Volume { } } -func NewPodSecurityContext() *corev1.PodSecurityContext { +func NewPodSecurityContext(openshift bool) *corev1.PodSecurityContext { fsGroup := int64(18500) + return commonPodSecurityContext(openshift, &fsGroup) +} + +func NewReportsPodSecurityContext(openshift bool) *corev1.PodSecurityContext { + return commonPodSecurityContext(openshift, nil) +} + +func commonPodSecurityContext(openshift bool, fsGroup *int64) *corev1.PodSecurityContext { + nonRoot := true + var seccompProfile *corev1.SeccompProfile + if !openshift { + seccompProfile = &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + } + } return &corev1.PodSecurityContext{ - FSGroup: &fsGroup, + FSGroup: fsGroup, + RunAsNonRoot: &nonRoot, + SeccompProfile: seccompProfile, + } +} + +func NewSecurityContext() *corev1.SecurityContext { + privEscalation := false + return &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + AllowPrivilegeEscalation: &privEscalation, } }