Skip to content

[main] custom host path support #792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
Expand Down
5 changes: 5 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down
56 changes: 55 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/rancher/wrangler/v3/pkg/kubeconfig"
Expand Down Expand Up @@ -52,6 +54,7 @@ var (
sonobuoyImageTag string
clusterName string
securityScanJobTolerationsVal string
customScanHostPathsVal string
)

func main() {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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())
}
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only checks whether the path is rooted, and therefore would return true for /abc/.../bin. We must call filepath.Clean(path) to ensure that becomes /bin before we do all the checks we are doing here.

Suggested change
if !filepath.IsAbs(path) {
path = filepath.Clean(path)
if !filepath.IsAbs(path) {

return fmt.Errorf("path must be absolute: %s", path)
}

if path == "/" {
return fmt.Errorf("root path '/' is not allowed")
Comment on lines +263 to +264
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure we support running it on Windows nodes, either way this looks a bit more future proof:

Suggested change
if path == "/" {
return fmt.Errorf("root path '/' is not allowed")
if path == "/" || path == "\" {
return fmt.Errorf("root path is not allowed")

}

for _, protected := range protectedDirs {
if path == protected || strings.HasPrefix(path, protected+"/") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably not work very well if executed in Windows. Perhaps string(filepath.Separator) could be used instead. Either way, it would be good to have unit tests showing all the inputs and outputs we have considered.

Suggested change
if path == protected || strings.HasPrefix(path, protected+"/") {
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
}
4 changes: 3 additions & 1 deletion pkg/securityscan/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
57 changes: 56 additions & 1 deletion pkg/securityscan/core/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
_ "embed" // nolint
"encoding/json"
"fmt"
"strings"
"text/template"

corev1 "k8s.io/api/core/v1"
Expand All @@ -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

Expand All @@ -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{}{
Expand Down Expand Up @@ -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),
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
76 changes: 10 additions & 66 deletions pkg/securityscan/core/templates/pluginConfig.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/securityscan/scanHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down