From e3178323e45dc9fada6944bd4a972de5fdb1d8bb Mon Sep 17 00:00:00 2001 From: Jeff Cantrill Date: Thu, 24 Sep 2026 11:29:25 -0400 Subject: [PATCH 1/3] feat(lfme): harden deployment to run as non-root with more restricted SELinux policy ref: LOG-10199 --- .../logfilemetricexporter/daemonset_test.go | 32 ++++++++++++++++ .../metrics/logfilemetricexporter/factory.go | 38 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/internal/metrics/logfilemetricexporter/daemonset_test.go b/internal/metrics/logfilemetricexporter/daemonset_test.go index e0e734011..a3aad182b 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,37 @@ 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)) + + sc := dsInstance.Spec.Template.Spec.Containers[0].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..c509638e2 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 ( @@ -124,6 +135,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, + }, + } +} From c9cf0be37511b8e4787e269d204394269d874102 Mon Sep 17 00:00:00 2001 From: Jeff Cantrill Date: Thu, 24 Sep 2026 12:03:54 -0400 Subject: [PATCH 2/3] feat(lfme): run exporter directly without a bash shell wrapper (LOG-10201) The LogFileMetricExporter container was started via /bin/bash -c " ", which requires bash in the image and inserts a shell as PID 1. Set the container command to the exporter binary directly with each flag as a discrete arg (no shell interpolation was relied upon). This removes the runtime dependency on bash, enabling a smaller/hardened base image (e.g. ubi-micro), and runs the exporter as PID 1 for correct signal handling. Co-Authored-By: Claude Opus 4.8 --- .../metrics/logfilemetricexporter/factory.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/metrics/logfilemetricexporter/factory.go b/internal/metrics/logfilemetricexporter/factory.go index c509638e2..bde44a674 100644 --- a/internal/metrics/logfilemetricexporter/factory.go +++ b/internal/metrics/logfilemetricexporter/factory.go @@ -123,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}, From 42dfae5a6e348ac40a9a1e5dc20c54d432d55cc0 Mon Sep 17 00:00:00 2001 From: Jeff Cantrill Date: Fri, 25 Sep 2026 14:30:50 -0400 Subject: [PATCH 3/3] test(lfme): assert exporter container command and args Verify the LogFileMetricExporter daemonset container invokes the exporter binary directly via Command with each flag as a separate Arg, and reuse the container reference for the SecurityContext checks. Co-Authored-By: Claude Opus 4.8 --- .../logfilemetricexporter/daemonset_test.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/metrics/logfilemetricexporter/daemonset_test.go b/internal/metrics/logfilemetricexporter/daemonset_test.go index a3aad182b..c8b082adf 100644 --- a/internal/metrics/logfilemetricexporter/daemonset_test.go +++ b/internal/metrics/logfilemetricexporter/daemonset_test.go @@ -75,7 +75,24 @@ var _ = Describe("Reconcile LogFileMetricExporter Daemonset", func() { Expect(reqClient.Get(context.TODO(), dsKey, dsInstance)).Should(Succeed()) Expect(dsInstance.Spec.Template.Spec.Containers).To(HaveLen(1)) - sc := dsInstance.Spec.Template.Spec.Containers[0].SecurityContext + 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"))