diff --git a/internal/metrics/logfilemetricexporter/daemonset_test.go b/internal/metrics/logfilemetricexporter/daemonset_test.go index e0e734011..c8b082adf 100644 --- a/internal/metrics/logfilemetricexporter/daemonset_test.go +++ b/internal/metrics/logfilemetricexporter/daemonset_test.go @@ -6,6 +6,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" loggingv1alpha1 "github.com/openshift/cluster-logging-operator/api/logging/v1alpha1" + "github.com/openshift/cluster-logging-operator/internal/auth" "github.com/openshift/cluster-logging-operator/internal/constants" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -63,6 +64,54 @@ var _ = Describe("Reconcile LogFileMetricExporter Daemonset", func() { Expect(dsInstance.Spec.Template.Spec.Containers[0].Resources.Requests).To(BeNil()) }) + It("should run the exporter container with a minimal, non-root security context", func() { + + // Reconcile the exporter daemonset + Expect(ReconcileDaemonset(*lfmeInstance, + reqClient, + constants.OpenshiftNS, + constants.LogfilesmetricexporterName, dsOwner)).To(Succeed()) + + Expect(reqClient.Get(context.TODO(), dsKey, dsInstance)).Should(Succeed()) + Expect(dsInstance.Spec.Template.Spec.Containers).To(HaveLen(1)) + + container := dsInstance.Spec.Template.Spec.Containers[0] + + // The exporter binary is invoked directly (not via a shell) with each flag as a separate arg. + Expect(container.Command).To(Equal([]string{"/usr/local/bin/log-file-metric-exporter"})) + Expect(container.Args).To(HaveLen(9)) + Expect(container.Args).To(ContainElements( + "-verbosity=2", + "-dir=/var/log/pods", + "-http=:2112", + "-keyFile=/etc/logfilemetricexporter/metrics/tls.key", + "-crtFile=/etc/logfilemetricexporter/metrics/tls.crt", + "-secureMetrics", + )) + Expect(container.Args).To(ContainElement(HavePrefix("-tlsMinVersion="))) + Expect(container.Args).To(ContainElement(HavePrefix("-cipherSuites="))) + Expect(container.Args).To(ContainElement(HavePrefix("-groups="))) + + sc := container.SecurityContext + Expect(sc).ToNot(BeNil()) + Expect(sc.SELinuxOptions).ToNot(BeNil()) + Expect(sc.SELinuxOptions.Type).To(Equal("container_logwriter_t")) + Expect(sc.RunAsUser).ToNot(BeNil()) + Expect(*sc.RunAsUser).To(Equal(int64(1000))) + Expect(sc.RunAsNonRoot).ToNot(BeNil()) + Expect(*sc.RunAsNonRoot).To(BeTrue()) + Expect(sc.ReadOnlyRootFilesystem).ToNot(BeNil()) + Expect(*sc.ReadOnlyRootFilesystem).To(BeTrue()) + Expect(sc.AllowPrivilegeEscalation).ToNot(BeNil()) + Expect(*sc.AllowPrivilegeEscalation).To(BeFalse()) + Expect(sc.Capabilities).ToNot(BeNil()) + Expect(sc.Capabilities.Drop).To(Equal(auth.RequiredDropCapabilities)) + Expect(sc.SeccompProfile).ToNot(BeNil()) + Expect(sc.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) + // The exporter only stats world-traversable log dirs, so no elevated group access is needed. + Expect(dsInstance.Spec.Template.Spec.SecurityContext).To(BeNil()) + }) + It("should reconcile successfully a daemonset with specified resources.requests", func() { lfmeInstance.Spec = loggingv1alpha1.LogFileMetricExporterSpec{ Resources: &corev1.ResourceRequirements{ diff --git a/internal/metrics/logfilemetricexporter/factory.go b/internal/metrics/logfilemetricexporter/factory.go index 914652415..bde44a674 100644 --- a/internal/metrics/logfilemetricexporter/factory.go +++ b/internal/metrics/logfilemetricexporter/factory.go @@ -12,7 +12,7 @@ import ( configv1 "github.com/openshift/api/config/v1" loggingv1a1 "github.com/openshift/cluster-logging-operator/api/logging/v1alpha1" - "github.com/openshift/cluster-logging-operator/internal/collector" + "github.com/openshift/cluster-logging-operator/internal/auth" "github.com/openshift/cluster-logging-operator/internal/constants" coreFactory "github.com/openshift/cluster-logging-operator/internal/factory" "github.com/openshift/cluster-logging-operator/internal/utils" @@ -28,6 +28,17 @@ const ( logPods = "varlogpods" logPodsValue = "/var/log/pods" metricsVolumePath = "/etc/logfilemetricexporter/metrics" + + // lfmeRunAsUser is the fixed non-root UID the exporter runs as. The exporter reads the + // hostPath log directories via group 0 (the default GID granted by OpenShift), which + // satisfies the 0750 root:root permissions on /var/log/pods without joining extra groups. + lfmeRunAsUser int64 = 1000 + // selinuxTypeLogWriter (container_logwriter_t) is an MCS-constrained container domain that + // grants read plus the inotify "watch"/"watch_reads" permissions on container_log_t, which + // the exporter requires to watch /var/log/pods. It is far more restrictive than the + // super-privileged spc_t; the otherwise-preferable container_logreader_t domain is not + // usable because it denies the inotify "watch" permission. + selinuxTypeLogWriter = "container_logwriter_t" ) var ( @@ -112,11 +123,18 @@ func newLogMetricsExporterContainer(exporter loggingv1a1.LogFileMetricExporter, Protocol: v1.ProtocolTCP, }, } - exporterContainer.Command = []string{"/bin/bash"} - exporterContainer.Args = []string{"-c", - "/usr/local/bin/log-file-metric-exporter -verbosity=2 -dir=/var/log/pods -http=:2112 -keyFile=/etc/logfilemetricexporter/metrics/tls.key -crtFile=/etc/logfilemetricexporter/metrics/tls.crt -secureMetrics -tlsMinVersion=" + - tls.MinTLSVersion(tlsProfileSpec) + " -cipherSuites=" + strings.Join(tls.TLSCiphers(tlsProfileSpec), ",") + - " -groups=" + strings.Join(tls.TLSGroups(tlsProfileSpec), ",")} + exporterContainer.Command = []string{"/usr/local/bin/log-file-metric-exporter"} + exporterContainer.Args = []string{ + "-verbosity=2", + "-dir=/var/log/pods", + "-http=:2112", + "-keyFile=/etc/logfilemetricexporter/metrics/tls.key", + "-crtFile=/etc/logfilemetricexporter/metrics/tls.crt", + "-secureMetrics", + "-tlsMinVersion=" + tls.MinTLSVersion(tlsProfileSpec), + "-cipherSuites=" + strings.Join(tls.TLSCiphers(tlsProfileSpec), ","), + "-groups=" + strings.Join(tls.TLSGroups(tlsProfileSpec), ","), + } exporterContainer.VolumeMounts = []v1.VolumeMount{ {Name: logContainers, ReadOnly: true, MountPath: logContainersValue}, @@ -124,6 +142,29 @@ func newLogMetricsExporterContainer(exporter loggingv1a1.LogFileMetricExporter, {Name: exporterMetricsVolumeName, ReadOnly: true, MountPath: metricsVolumePath}, } - collector.AddSecurityContextTo(exporterContainer) + exporterContainer.SecurityContext = securityContext() return exporterContainer } + +// securityContext returns the minimal security context required by the log-file-metric-exporter. +// The exporter runs as a fixed non-root UID with all capabilities dropped, a read-only root +// filesystem, no privilege escalation and the default seccomp profile. It runs under the +// MCS-constrained container_logwriter_t SELinux domain, which grants read plus the inotify +// watch the exporter needs on the host log tree while remaining far more restrictive than spc_t. +func securityContext() *v1.SecurityContext { + return &v1.SecurityContext{ + Capabilities: &v1.Capabilities{ + Drop: auth.RequiredDropCapabilities, + }, + SELinuxOptions: &v1.SELinuxOptions{ + Type: selinuxTypeLogWriter, + }, + RunAsUser: utils.GetPtr(lfmeRunAsUser), + RunAsNonRoot: utils.GetPtr(true), + ReadOnlyRootFilesystem: utils.GetPtr(true), + AllowPrivilegeEscalation: utils.GetPtr(false), + SeccompProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeRuntimeDefault, + }, + } +}