diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 8c9f72f5..66c95cc8 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -41,6 +41,8 @@ spec: value: '{{ .Values.global.cattle.clusterName }}' - name: CIS_OPERATOR_DEBUG value: '{{ .Values.image.cisoperator.debug }}' + - name: CUSTOM_SCAN_HOST_PATHS + value: '{{ .Values.customScanHostPaths | toJson }}' {{- if .Values.securityScanJob.overrideTolerations }} - name: SECURITY_SCAN_JOB_TOLERATIONS value: '{{ .Values.securityScanJob.tolerations | toJson }}' diff --git a/chart/values.yaml b/chart/values.yaml index dcc1b8a4..af668c81 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -39,6 +39,11 @@ securityScanJob: affinity: {} +## host paths which needs to be mounted in security-scan daemonset pods to run scan successfully +## paths must be absolute and must not override the protected dirs. +## protected dirs are: /bin, /boot, /dev, /etc, /lib, /lib64, /proc, /root, /run, /sbin, /selinux, /sys, /tmp, /usr, /var +customScanHostPaths: [] + global: cattle: systemDefaultRegistry: "" diff --git a/main.go b/main.go index 847fd539..c02afe2b 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,8 @@ import ( "errors" "fmt" "os" + "path/filepath" + "strings" "time" "github.com/rancher/wrangler/v3/pkg/kubeconfig" @@ -52,6 +54,7 @@ var ( sonobuoyImageTag string clusterName string securityScanJobTolerationsVal string + customScanHostPathsVal string ) func main() { @@ -134,6 +137,12 @@ func main() { Name: "alertEnabled", EnvVars: []string{"CIS_ALERTS_ENABLED"}, }, + &cli.StringFlag{ + Name: "custom-scan-host-paths", + EnvVars: []string{"CUSTOM_SCAN_HOST_PATHS"}, + Value: "", + Destination: &customScanHostPathsVal, + }, } app.Action = run @@ -176,6 +185,22 @@ func run(c *cli.Context) error { } } + customHostPaths := []string{} + customScanHostPathsVal = c.String("custom-scan-host-paths") + + if customScanHostPathsVal != "" { + + err := json.Unmarshal([]byte(customScanHostPathsVal), &customHostPaths) + if err != nil { + logrus.Fatalf("invalid value received for custom-scan-host-paths flag:%s", err.Error()) + } + + err = validateCustomScanHostPaths(customHostPaths) + if err != nil { + logrus.Fatalf("validation failed for custom-scan-host-paths:%s", err.Error()) + } + } + kubeConfig, err := kubeconfig.GetNonInteractiveClientConfig(kubeConfig).ClientConfig() if err != nil { logrus.Fatalf("failed to find kubeconfig: %v", err) @@ -195,7 +220,7 @@ func run(c *cli.Context) error { logrus.Fatalf("Error starting CIS-Operator: %v", err) } - ctl, err := cisoperator.NewController(ctx, kubeConfig, cisoperatorapiv1.ClusterScanNS, name, imgConfig, securityScanJobTolerations) + ctl, err := cisoperator.NewController(ctx, kubeConfig, cisoperatorapiv1.ClusterScanNS, name, imgConfig, securityScanJobTolerations, customHostPaths) if err != nil { logrus.Fatalf("Error building controller: %s", err.Error()) } @@ -224,3 +249,32 @@ func validateConfig(imgConfig *cisoperatorapiv1.ScanImageConfig) error { } return nil } + +func validateCustomScanHostPaths(hostPaths []string) error { + protectedDirs := []string{"/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/root", "/run", "/sbin", "/selinux", "/sys", "/tmp", "/usr", "/var"} + + hostPathSet := make(map[string]bool) + + for _, path := range hostPaths { + if !filepath.IsAbs(path) { + return fmt.Errorf("path must be absolute: %s", path) + } + + if path == "/" { + return fmt.Errorf("root path '/' is not allowed") + } + + for _, protected := range protectedDirs { + if path == protected || strings.HasPrefix(path, protected+"/") { + return fmt.Errorf("path %s is not allowed as it affects protected directory %s", path, protected) + } + } + + if _, exists := hostPathSet[path]; exists { + return fmt.Errorf("duplicate path detected: %s", path) + } + hostPathSet[path] = true + } + + return nil +} diff --git a/pkg/securityscan/controller.go b/pkg/securityscan/controller.go index 972b0b9f..952e9238 100644 --- a/pkg/securityscan/controller.go +++ b/pkg/securityscan/controller.go @@ -70,10 +70,11 @@ type Controller struct { daemonsets appsctlv1.DaemonSetController daemonsetCache appsctlv1.DaemonSetCache securityScanJobTolerations []corev1.Toleration + customScanHostPaths []string } func NewController(ctx context.Context, cfg *rest.Config, namespace, name string, - imgConfig *cisoperatorapiv1.ScanImageConfig, securityScanJobTolerations []corev1.Toleration) (ctl *Controller, err error) { + imgConfig *cisoperatorapiv1.ScanImageConfig, securityScanJobTolerations []corev1.Toleration, customScanHostPaths []string) (ctl *Controller, err error) { if cfg == nil { cfg, err = rest.InClusterConfig() if err != nil { @@ -154,6 +155,7 @@ func NewController(ctx context.Context, cfg *rest.Config, namespace, name string ctl.daemonsets = ctl.appsFactory.Apps().V1().DaemonSet() ctl.daemonsetCache = ctl.appsFactory.Apps().V1().DaemonSet().Cache() ctl.securityScanJobTolerations = securityScanJobTolerations + ctl.customScanHostPaths = customScanHostPaths return ctl, nil } diff --git a/pkg/securityscan/core/configmap.go b/pkg/securityscan/core/configmap.go index ec71c157..dc7d3a38 100644 --- a/pkg/securityscan/core/configmap.go +++ b/pkg/securityscan/core/configmap.go @@ -4,6 +4,8 @@ import ( "bytes" _ "embed" // nolint "encoding/json" + "fmt" + "strings" "text/template" corev1 "k8s.io/api/core/v1" @@ -16,6 +18,17 @@ import ( cisoperatorapiv1 "github.com/rancher/cis-operator/pkg/apis/cis.cattle.io/v1" ) +var requiredPaths = map[string]string{ + "var-rancher": "/var/lib/rancher", + "etc-rancher": "/etc/rancher", + "etc-cni": "/etc/cni/net.d", + "var-cni": "/var/lib/cni", + "var-log": "/var/log", + "run-log": "/run/log", + "etc-kubelet": "/etc/kubernetes/kubelet", + "var-kubelet": "/var/lib/kubelet", +} + //go:embed templates/pluginConfig.template var pluginConfigTemplate string @@ -31,7 +44,8 @@ const ( ConfigFileName = "config.json" ) -func NewConfigMaps(clusterscan *cisoperatorapiv1.ClusterScan, clusterscanprofile *cisoperatorapiv1.ClusterScanProfile, clusterscanbenchmark *cisoperatorapiv1.ClusterScanBenchmark, _ string, imageConfig *cisoperatorapiv1.ScanImageConfig, configmapsClient wcorev1.ConfigMapController) (cmMap map[string]*corev1.ConfigMap, err error) { +func NewConfigMaps(clusterscan *cisoperatorapiv1.ClusterScan, clusterscanprofile *cisoperatorapiv1.ClusterScanProfile, clusterscanbenchmark *cisoperatorapiv1.ClusterScanBenchmark, _ string, imageConfig *cisoperatorapiv1.ScanImageConfig, + configmapsClient wcorev1.ConfigMapController, customScanHostPaths []string) (cmMap map[string]*corev1.ConfigMap, err error) { cmMap = make(map[string]*corev1.ConfigMap) configdata := map[string]interface{}{ @@ -62,6 +76,8 @@ func NewConfigMaps(clusterscan *cisoperatorapiv1.ClusterScan, clusterscanprofile customBenchmarkConfigMapName = customcm.Name } + hostPathVolumes, hostPathVolumeMounts := pluginConfigHostPathVolumesData(customScanHostPaths) + plugindata := map[string]interface{}{ "namespace": cisoperatorapiv1.ClusterScanNS, "name": name.SafeConcatName(cisoperatorapiv1.ClusterScanPluginsConfigMap, clusterscan.Name), @@ -74,6 +90,8 @@ func NewConfigMaps(clusterscan *cisoperatorapiv1.ClusterScan, clusterscanprofile "configDir": cisoperatorapiv1.CustomBenchmarkBaseDir, "customBenchmarkConfigMapName": customBenchmarkConfigMapName, "customBenchmarkConfigMapData": customBenchmarkConfigMapData, + "hostPathVolumes": hostPathVolumes, + "hostPathVolumeMounts": hostPathVolumeMounts, } plugincm, err := generateConfigMap(clusterscan, "pluginConfig.template", pluginConfigTemplate, plugindata) if err != nil { @@ -176,3 +194,40 @@ func getCustomBenchmarkConfigMap(benchmark *cisoperatorapiv1.ClusterScanBenchmar } return configmapsClient.Create(&configmapCopy) } + +func pluginConfigHostPathVolumesData(customScanHostPaths []string) ([]*corev1.Volume, []*corev1.VolumeMount) { + volumes := make([]*corev1.Volume, 0, len(requiredPaths)+len(customScanHostPaths)) + volumeMounts := make([]*corev1.VolumeMount, 0, len(requiredPaths)+len(customScanHostPaths)) + hostPaths := make(map[string]bool, len(requiredPaths)) + + // Add required volumes + for name, path := range requiredPaths { + path = strings.TrimSuffix(path, "/") + volumes = append(volumes, &corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: path}, + }, + }) + volumeMounts = append(volumeMounts, &corev1.VolumeMount{Name: name, MountPath: path, ReadOnly: true}) + hostPaths[path] = true + } + + // Add custom volumes if they are not already included + for idx, path := range customScanHostPaths { + if !hostPaths[path] { + path = strings.TrimSuffix(path, "/") + name := fmt.Sprintf("custom-volume-%d", idx) + volumes = append(volumes, &corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: path}, + }, + }) + volumeMounts = append(volumeMounts, &corev1.VolumeMount{Name: name, MountPath: path, ReadOnly: true}) + hostPaths[path] = true + } + } + + return volumes, volumeMounts +} diff --git a/pkg/securityscan/core/templates/pluginConfig.template b/pkg/securityscan/core/templates/pluginConfig.template index e75c8743..7ed12f0a 100644 --- a/pkg/securityscan/core/templates/pluginConfig.template +++ b/pkg/securityscan/core/templates/pluginConfig.template @@ -29,39 +29,11 @@ data: key: CriticalAddonsOnly operator: Exists volumes: + {{- range .hostPathVolumes }} - hostPath: - path: / - name: root - - hostPath: - path: /etc/passwd - name: etc-passwd - - hostPath: - path: /etc/group - name: etc-group - - hostPath: - path: /var/lib/rancher - name: var-rancher - - hostPath: - path: /etc/rancher - name: etc-rancher - - hostPath: - path: /etc/cni/net.d - name: etc-cni - - hostPath: - path: /var/lib/cni - name: var-cni - - hostPath: - path: /var/log - name: var-log - - hostPath: - path: /run/log - name: run-log - - hostPath: - path: /etc/kubernetes/kubelet - name: etc-kubelet - - hostPath: - path: /var/lib/kubelet - name: var-kubelet + path: {{ .VolumeSource.HostPath.Path }} + name: {{ .Name }} + {{- end }} {{- if .isCustomBenchmark }} - configMap: defaultMode: 420 @@ -106,44 +78,16 @@ data: {{- end }} imagePullPolicy: IfNotPresent securityContext: - privileged: true + privileged: false volumeMounts: - mountPath: /tmp/results name: results readOnly: false - - mountPath: /node - name: root - readOnly: true - - mountPath: /etc/passwd - name: etc-passwd - readOnly: true - - mountPath: /etc/group - name: etc-group - readOnly: true - - mountPath: /var/lib/rancher - name: var-rancher - readOnly: true - - mountPath: /etc/rancher - name: etc-rancher - readOnly: true - - mountPath: /etc/cni/net.d - name: etc-cni - readOnly: true - - mountPath: /var/lib/cni - name: var-cni - readOnly: true - - mountPath: /var/log/ - name: var-log - readOnly: true - - mountPath: /run/log/ - name: run-log - readOnly: true - - mountPath: /etc/kubernetes/kubelet - name: etc-kubelet - readOnly: true - - mountPath: /var/lib/kubelet - name: var-kubelet - readOnly: true + {{- range .hostPathVolumeMounts }} + - mountPath: {{ .MountPath }} + name: {{ .Name }} + readOnly: {{ .ReadOnly }} + {{- end }} {{- if .isCustomBenchmark }} - mountPath: /etc/kbs/custombenchmark/cfg name: custom-benchmark-volume diff --git a/pkg/securityscan/scanHandler.go b/pkg/securityscan/scanHandler.go index 9ce01e92..9926b1d6 100644 --- a/pkg/securityscan/scanHandler.go +++ b/pkg/securityscan/scanHandler.go @@ -96,7 +96,7 @@ func (c *Controller) handleClusterScans(ctx context.Context) error { v1.ClusterScanConditionReconciling.True(obj) return objects, obj.Status, fmt.Errorf("Error when getting Benchmark: %w", err) } - cmMap, err := ciscore.NewConfigMaps(obj, profile, benchmark, c.Name, c.ImageConfig, c.configmaps) + cmMap, err := ciscore.NewConfigMaps(obj, profile, benchmark, c.Name, c.ImageConfig, c.configmaps, c.customScanHostPaths) if err != nil { v1.ClusterScanConditionFailed.True(obj) message := fmt.Sprintf("Error when creating ConfigMaps: %v", err)