Skip to content

Commit

Permalink
fix(security): support restricted pod security standard (#450)
Browse files Browse the repository at this point in the history
* fix(security): support restricted pod security standard

* Make deployment compatible with OpenShift < 4.11

* Update tests
  • Loading branch information
ebaron committed Sep 15, 2022
1 parent 78021e0 commit 9c79984
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 9 deletions.
5 changes: 5 additions & 0 deletions bundle/manifests/cryostat-operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ spec:
serviceAccountName: service-account
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- command:
- /manager
Expand All @@ -29,6 +31,9 @@ spec:
name: manager
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
livenessProbe:
httpGet:
path: /healthz
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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{
Expand All @@ -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),
},
}
}

Expand Down Expand Up @@ -789,6 +810,7 @@ func NewCoreContainer(cr *operatorv1beta1.Cryostat, specs *ServiceSpecs, imageTa
Scheme: livenessProbeScheme,
},
}
privEscalation := false
return corev1.Container{
Name: cr.Name,
Image: imageTag,
Expand All @@ -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},
},
},
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -908,13 +937,20 @@ 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},
},
},
}
}

// datasourceURL contains the fixed URL to jfr-datasource's web server
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,
Expand All @@ -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},
},
},
}
}

Expand Down Expand Up @@ -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,
}
}
2 changes: 1 addition & 1 deletion internal/controllers/cryostat_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion internal/controllers/cryostat_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})
})
})

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
33 changes: 31 additions & 2 deletions internal/test/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down

0 comments on commit 9c79984

Please sign in to comment.