diff --git a/pkg/controller/kibana/driver_test.go b/pkg/controller/kibana/driver_test.go index 0f8af3c6a3c..ca3db18b8cd 100644 --- a/pkg/controller/kibana/driver_test.go +++ b/pkg/controller/kibana/driver_test.go @@ -223,7 +223,7 @@ func TestDriverDeploymentParams(t *testing.T) { kb: kibanaFixture, initialObjects: defaultInitialObjects, }, - want: expectedDeploymentParams(), + want: pre710(expectedDeploymentParams()), wantErr: false, }, { @@ -233,7 +233,7 @@ func TestDriverDeploymentParams(t *testing.T) { initialObjects: defaultInitialObjects, policyAnnotations: map[string]string{"policy.k8s.elastic.co/kibana-config-hash": "2123345"}, }, - want: expectedDeploymentWithPolicyAnnotations(map[string]string{"policy.k8s.elastic.co/kibana-config-hash": "2123345"}), + want: pre710(expectedDeploymentWithPolicyAnnotations(map[string]string{"policy.k8s.elastic.co/kibana-config-hash": "2123345"})), wantErr: false, }, { @@ -249,7 +249,7 @@ func TestDriverDeploymentParams(t *testing.T) { initialObjects: defaultInitialObjects, }, want: func() deployment.Params { - params := expectedDeploymentParams() + params := pre710(expectedDeploymentParams()) params.PodTemplateSpec.Spec.Volumes = params.PodTemplateSpec.Spec.Volumes[1:] params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts = params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts[1:] params.PodTemplateSpec.Spec.Containers[0].VolumeMounts = params.PodTemplateSpec.Spec.Containers[0].VolumeMounts[1:] @@ -266,7 +266,7 @@ func TestDriverDeploymentParams(t *testing.T) { initialObjects: defaultInitialObjects, }, want: func() deployment.Params { - p := expectedDeploymentParams() + p := pre710(expectedDeploymentParams()) p.PodTemplateSpec.Labels["mylabel"] = "value" for i, c := range p.PodTemplateSpec.Spec.Containers { if c.Name == kbv1.KibanaContainerName { @@ -323,7 +323,7 @@ func TestDriverDeploymentParams(t *testing.T) { }, }, want: func() deployment.Params { - p := expectedDeploymentParams() + p := pre710(expectedDeploymentParams()) p.PodTemplateSpec.Annotations["kibana.k8s.elastic.co/config-hash"] = "2368465874" return p }(), @@ -340,7 +340,7 @@ func TestDriverDeploymentParams(t *testing.T) { initialObjects: defaultInitialObjects, }, want: func() deployment.Params { - p := expectedDeploymentParams() + p := pre710(expectedDeploymentParams()) p.PodTemplateSpec.Labels["kibana.k8s.elastic.co/version"] = "6.8.0" return p }(), @@ -357,12 +357,29 @@ func TestDriverDeploymentParams(t *testing.T) { initialObjects: defaultInitialObjects, }, want: func() deployment.Params { - p := expectedDeploymentParams() + p := pre710(expectedDeploymentParams()) p.PodTemplateSpec.Labels["kibana.k8s.elastic.co/version"] = "6.8.0" return p }(), wantErr: false, }, + { + name: "7.10+ contains security contexts", + args: args{ + kb: func() *kbv1.Kibana { + kb := kibanaFixture() + kb.Spec.Version = "7.10.0" + return kb + }, + initialObjects: defaultInitialObjects, + }, + want: func() deployment.Params { + p := expectedDeploymentParams() + p.PodTemplateSpec.Labels["kibana.k8s.elastic.co/version"] = "7.10.0" + return p + }(), + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -490,15 +507,25 @@ func expectedDeploymentParams() deployment.Params { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "kibana-plugins", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "temp-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, }, InitContainers: []corev1.Container{{ Name: "elastic-internal-init-config", ImagePullPolicy: corev1.PullIfNotPresent, Image: "my-image", Command: []string{"/usr/bin/env", "bash", "-c", InitConfigScript}, - SecurityContext: &corev1.SecurityContext{ - Privileged: &falseVal, - }, + SecurityContext: &defaultSecurityContext, Env: []corev1.EnvVar{ {Name: settings.EnvPodIP, Value: "", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "status.podIP"}, @@ -535,6 +562,16 @@ func expectedDeploymentParams() deployment.Params { ReadOnly: falseVal, MountPath: DataVolumeMountPath, }, + { + Name: "kibana-plugins", + ReadOnly: falseVal, + MountPath: "/usr/share/kibana/plugins", + }, + { + Name: "temp-volume", + ReadOnly: falseVal, + MountPath: "/tmp", + }, }, Resources: corev1.ResourceRequirements{ Requests: map[corev1.ResourceName]resource.Quantity{ @@ -571,6 +608,16 @@ func expectedDeploymentParams() deployment.Params { ReadOnly: falseVal, MountPath: DataVolumeMountPath, }, + { + Name: "kibana-plugins", + ReadOnly: falseVal, + MountPath: "/usr/share/kibana/plugins", + }, + { + Name: "temp-volume", + ReadOnly: falseVal, + MountPath: "/tmp", + }, }, Image: "my-image", Name: kbv1.KibanaContainerName, @@ -591,9 +638,11 @@ func expectedDeploymentParams() deployment.Params { }, }, }, - Resources: DefaultResources, + Resources: DefaultResources, + SecurityContext: &defaultSecurityContext, }}, AutomountServiceAccountToken: &falseVal, + SecurityContext: &defaultPodSecurityContext, }, }, } @@ -608,6 +657,16 @@ func expectedDeploymentWithPolicyAnnotations(policyAnnotations map[string]string return deploymentParams } +func pre710(params deployment.Params) deployment.Params { + params.PodTemplateSpec.Spec.Containers[0].SecurityContext = nil + params.PodTemplateSpec.Spec.InitContainers[0].SecurityContext = nil + params.PodTemplateSpec.Spec.SecurityContext = nil + params.PodTemplateSpec.Spec.Volumes = params.PodTemplateSpec.Spec.Volumes[:5] + params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts = params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts[:5] + params.PodTemplateSpec.Spec.Containers[0].VolumeMounts = params.PodTemplateSpec.Spec.Containers[0].VolumeMounts[:5] + return params +} + func kibanaFixture() *kbv1.Kibana { kbFixture := &kbv1.Kibana{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/controller/kibana/init_configuration.go b/pkg/controller/kibana/init_configuration.go index a3804c692f8..ed2481753a7 100644 --- a/pkg/controller/kibana/init_configuration.go +++ b/pkg/controller/kibana/init_configuration.go @@ -38,16 +38,11 @@ echo "Kibana configuration successfully prepared." // The script creates symbolic links from the generated configuration files in /mnt/elastic-internal/kibana-config/ to // an empty directory later mounted in /use/share/kibana/config func initConfigContainer(kb kbv1.Kibana) corev1.Container { - privileged := false - return corev1.Container{ // Image will be inherited from pod template defaults ImagePullPolicy: corev1.PullIfNotPresent, Name: InitConfigContainerName, - SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - }, - Command: []string{"/usr/bin/env", "bash", "-c", InitConfigScript}, + Command: []string{"/usr/bin/env", "bash", "-c", InitConfigScript}, VolumeMounts: []corev1.VolumeMount{ ConfigSharedVolume.InitContainerVolumeMount(), ConfigVolume(kb).VolumeMount(), diff --git a/pkg/controller/kibana/pod.go b/pkg/controller/kibana/pod.go index 1b59554f039..386198c1a39 100644 --- a/pkg/controller/kibana/pod.go +++ b/pkg/controller/kibana/pod.go @@ -33,6 +33,10 @@ import ( const ( DataVolumeName = "kibana-data" DataVolumeMountPath = "/usr/share/kibana/data" + PluginsVolumeName = "kibana-plugins" + PluginsVolumeMountPath = "/usr/share/kibana/plugins" + TempVolumeName = "temp-volume" + TempVolumeMountPath = "/tmp" KibanaBasePathEnvName = "SERVER_BASEPATH" KibanaRewriteBasePathEnvName = "SERVER_REWRITEBASEPATH" ) @@ -43,6 +47,14 @@ var ( // Since Kibana is stateless and the keystore is created on pod start, an EmptyDir is fine here. DataVolume = volume.NewEmptyDirVolume(DataVolumeName, DataVolumeMountPath) + // PluginsVolume can be used to persist plugins after installation via an init container when + // the Kibana pod has readOnlyRootFilesystem set to true. + PluginsVolume = volume.NewEmptyDirVolume(PluginsVolumeName, PluginsVolumeMountPath) + + // TempVolume can be used for some reporting features when the Kibana pod has + // readOnlyRootFilesystem set to true. + TempVolume = volume.NewEmptyDirVolume(TempVolumeName, TempVolumeMountPath) + DefaultMemoryLimits = resource.MustParse("1Gi") DefaultResources = corev1.ResourceRequirements{ Requests: map[corev1.ResourceName]resource.Quantity{ @@ -116,6 +128,19 @@ func NewPodTemplateSpec(ctx context.Context, client k8sclient.Client, kb kbv1.Ki builder.WithVolumes(volume.Volume()).WithVolumeMounts(volume.VolumeMount()) } + // Kibana 7.5.0 and above support running with a read-only root filesystem, + // but require a temporary volume to be mounted at /tmp for some reporting features + // and a plugin volume mounted at /usr/share/kibana/plugins. + // Limiting to 7.10.0 here as there was a bug in previous versions causing rebuilding + // of browser bundles to happen on plugin install, which would attempt a write to the + // root filesystem on restart. + if v.GTE(version.From(7, 10, 0)) { + builder.WithPodSecurityContext(defaultPodSecurityContext). + WithContainersSecurityContext(defaultSecurityContext). + WithVolumes(TempVolume.Volume()).WithVolumeMounts(TempVolume.VolumeMount()). + WithVolumes(PluginsVolume.Volume()).WithVolumeMounts(PluginsVolume.VolumeMount()) + } + if keystore != nil { builder.WithVolumes(keystore.Volume). WithInitContainers(keystore.InitContainer) diff --git a/pkg/controller/kibana/pod_test.go b/pkg/controller/kibana/pod_test.go index 7d2f19e5d1f..8b829707698 100644 --- a/pkg/controller/kibana/pod_test.go +++ b/pkg/controller/kibana/pod_test.go @@ -140,7 +140,7 @@ func TestNewPodTemplateSpec(t *testing.T) { }, }, { - name: "with user-provided labels", + name: "with user-provided labels, and 7.4.x shouldn't have security contexts set", keystore: nil, kb: kbv1.Kibana{ ObjectMeta: metav1.ObjectMeta{ @@ -165,6 +165,8 @@ func TestNewPodTemplateSpec(t *testing.T) { labels["label2"] = "value2" labels[kblabel.KibanaNameLabelName] = "overridden-kibana-name" assert.Equal(t, labels, pod.Labels) + assert.Nil(t, pod.Spec.SecurityContext) + assert.Nil(t, GetKibanaContainer(pod.Spec).SecurityContext) }, }, { @@ -192,7 +194,7 @@ func TestNewPodTemplateSpec(t *testing.T) { }, }, { - name: "with user-provided volumes and volume mounts", + name: "with user-provided volumes and 8.x should have volume mounts including /tmp and plugins volumes and security contexts", kb: kbv1.Kibana{Spec: kbv1.KibanaSpec{ PodTemplate: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -217,9 +219,11 @@ func TestNewPodTemplateSpec(t *testing.T) { }}, assertions: func(pod corev1.PodTemplateSpec) { assert.Len(t, pod.Spec.InitContainers, 1) - assert.Len(t, pod.Spec.InitContainers[0].VolumeMounts, 3) - assert.Len(t, pod.Spec.Volumes, 1) - assert.Len(t, GetKibanaContainer(pod.Spec).VolumeMounts, 1) + assert.Len(t, pod.Spec.InitContainers[0].VolumeMounts, 5) + assert.Len(t, pod.Spec.Volumes, 3) + assert.Len(t, GetKibanaContainer(pod.Spec).VolumeMounts, 3) + assert.Equal(t, pod.Spec.SecurityContext, &defaultPodSecurityContext) + assert.Equal(t, GetKibanaContainer(pod.Spec).SecurityContext, &defaultSecurityContext) }, }, { diff --git a/pkg/controller/kibana/securitycontext.go b/pkg/controller/kibana/securitycontext.go new file mode 100644 index 00000000000..9dd5bc33811 --- /dev/null +++ b/pkg/controller/kibana/securitycontext.go @@ -0,0 +1,28 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package kibana + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +var ( + defaultSecurityContext = corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(bool(false)), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + corev1.Capability("ALL"), + }, + }, + Privileged: ptr.To(bool(false)), + ReadOnlyRootFilesystem: ptr.To(bool(true)), + RunAsUser: ptr.To(int64(1000)), + RunAsGroup: ptr.To(int64(1000)), + } + defaultPodSecurityContext = corev1.PodSecurityContext{ + FSGroup: ptr.To(int64(1000)), + } +)