From 1def93a0f9597a9f4cfcd5e36dfd92ebe1d9088b Mon Sep 17 00:00:00 2001 From: Gabe Alford Date: Tue, 24 Oct 2023 14:41:37 -0600 Subject: [PATCH] wip: enable gke autopilot support --- api/falcon/v1alpha1/falconnodesensor_types.go | 53 +++- api/falcon/v1alpha1/zz_generated.deepcopy.go | 80 ++++++ ...con.crowdstrike.com_falconnodesensors.yaml | 51 ++++ config/default/kustomization.yaml | 1 + config/manager/kustomization.yaml | 2 +- config/manager/manager.yaml | 1 + config/rbac/role.yaml | 10 + .../falconnodesensor_controller.go | 55 ++++ deploy/falcon-operator.yaml | 63 ++++- internal/controller/assets/daemonset.go | 256 +++++++++++++----- internal/controller/assets/daemonset_test.go | 82 +++++- internal/controller/assets/priorityclass.go | 29 ++ .../controller/assets/priorityclass_test.go | 40 +++ pkg/common/common_test.go | 18 +- pkg/common/constants.go | 3 +- pkg/common/funcs.go | 18 +- 16 files changed, 679 insertions(+), 83 deletions(-) create mode 100644 internal/controller/assets/priorityclass.go create mode 100644 internal/controller/assets/priorityclass_test.go diff --git a/api/falcon/v1alpha1/falconnodesensor_types.go b/api/falcon/v1alpha1/falconnodesensor_types.go index 06494c6e..c998e1c2 100644 --- a/api/falcon/v1alpha1/falconnodesensor_types.go +++ b/api/falcon/v1alpha1/falconnodesensor_types.go @@ -65,16 +65,67 @@ type FalconNodeSensorConfig struct { // +kubebuilder:default=false // +operator-sdk:csv:customresourcedefinitions:type=spec,order=8 NodeCleanup *bool `json:"disableCleanup,omitempty"` + + // Configure resource requests and limits for the DaemonSet Sensor. Only applies when using the eBPF backend. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon eBPF Sensor Resources",order=9 + SensorResources Resources `json:"resources,omitempty"` + // Sets the backend to be used by the DaemonSet Sensor. // +kubebuilder:default=kernel // +kubebuilder:validation:Enum=kernel;bpf - // +operator-sdk-csv:customresourcedefinitions:type=spec,order=9 + // +operator-sdk-csv:customresourcedefinitions:type=spec,order=10 Backend string `json:"backend,omitempty"` + // Enables the use of GKE Autopilot. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="GKE Autopilot Settings",order=11 + GKE AutoPilot `json:"gke,omitempty"` + + // Enable priority class for the DaemonSet. This is useful for GKE Autopilot clusters, but can be set for any cluster. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Priority Class",order=12 + PriorityClass PriorityClassConfig `json:"priorityClass,omitempty"` + // Version of the sensor to be installed. The latest version will be selected when this version specifier is missing. Version *string `json:"version,omitempty"` } +type PriorityClassConfig struct { + // Enables the operator to deploy a PriorityClass instead of rolling your own. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Deploy Priority Class to cluster",order=2 + Deploy *bool `json:"deploy,omitempty"` + + // Name of the priority class to use for the DaemonSet. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Name of the Priority Class to use",order=2 + Name string `json:"name,omitempty"` + + // Value of the priority class to use for the DaemonSet. Requires the Deploy field to be set to true. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Priority Class Value",order=3 + Value *int32 `json:"value,omitempty"` +} + +type Resources struct { + // Sets the resource limits for the DaemonSet Sensor. Only applies when using the eBPF backend. + // +operator-sdk:csv:customresourcedefinitions:type=spec + Limits ResourceList `json:"limits,omitempty"` + // Sets the resource requests for the DaemonSet Sensor. Only applies when using the eBPF backend. + // +operator-sdk:csv:customresourcedefinitions:type=spec + Requests ResourceList `json:"requests,omitempty"` +} + +type ResourceList struct { + // +operator-sdk:csv:customresourcedefinitions:type=spec + // +kubebuilder:validation:Pattern="^(([0-9]{4,}|[2-9][5-9][0-9])m$)|[0-9]+$" + CPU string `json:"cpu,omitempty"` + // +operator-sdk:csv:customresourcedefinitions:type=spec + // +kubebuilder:validation:Pattern="^(([5-9][0-9]{2}[Mi]+)|([0-9.]+[iEGTP]+))|(([5-9][0-9]{8})|([0-9]{10,}))$" + Memory string `json:"memory,omitempty"` +} + +type AutoPilot struct { + // Enables the use of GKE Autopilot. + // +operator-sdk:csv:customresourcedefinitions:type=spec + Enabled *bool `json:"autopilot,omitempty"` +} + type FalconNodeUpdateStrategy struct { // +kubebuilder:default=RollingUpdate // +kubebuilder:validation:Enum=RollingUpdate;OnDelete diff --git a/api/falcon/v1alpha1/zz_generated.deepcopy.go b/api/falcon/v1alpha1/zz_generated.deepcopy.go index 87b21835..53d869ed 100644 --- a/api/falcon/v1alpha1/zz_generated.deepcopy.go +++ b/api/falcon/v1alpha1/zz_generated.deepcopy.go @@ -216,6 +216,26 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoPilot) DeepCopyInto(out *AutoPilot) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoPilot. +func (in *AutoPilot) DeepCopy() *AutoPilot { + if in == nil { + return nil + } + out := new(AutoPilot) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FalconAPI) DeepCopyInto(out *FalconAPI) { *out = *in @@ -784,6 +804,9 @@ func (in *FalconNodeSensorConfig) DeepCopyInto(out *FalconNodeSensorConfig) { *out = new(bool) **out = **in } + out.SensorResources = in.SensorResources + in.GKE.DeepCopyInto(&out.GKE) + in.PriorityClass.DeepCopyInto(&out.PriorityClass) if in.Version != nil { in, out := &in.Version, &out.Version *out = new(string) @@ -955,6 +978,31 @@ func (in *FalconSensor) DeepCopy() *FalconSensor { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PriorityClassConfig) DeepCopyInto(out *PriorityClassConfig) { + *out = *in + if in.Deploy != nil { + in, out := &in.Deploy, &out.Deploy + *out = new(bool) + **out = **in + } + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriorityClassConfig. +func (in *PriorityClassConfig) DeepCopy() *PriorityClassConfig { + if in == nil { + return nil + } + out := new(PriorityClassConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegistrySpec) DeepCopyInto(out *RegistrySpec) { *out = *in @@ -990,3 +1038,35 @@ func (in *RegistryTLSSpec) DeepCopy() *RegistryTLSSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceList) DeepCopyInto(out *ResourceList) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceList. +func (in *ResourceList) DeepCopy() *ResourceList { + if in == nil { + return nil + } + out := new(ResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resources) DeepCopyInto(out *Resources) { + *out = *in + out.Limits = in.Limits + out.Requests = in.Requests +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources. +func (in *Resources) DeepCopy() *Resources { + if in == nil { + return nil + } + out := new(Resources) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml b/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml index 9c011104..ce568528 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml @@ -143,6 +143,13 @@ spec: on the nodes. Disabling might have unintended consequences for certain operations such as sensor downgrading. type: boolean + gke: + description: Enables the use of GKE Autopilot. + properties: + autopilot: + description: Enables the use of GKE Autopilot. + type: boolean + type: object image: description: Location of the Falcon Sensor image. Use only in cases when you mirror the original image to your repository/name:tag @@ -379,6 +386,50 @@ spec: type: object x-kubernetes-map-type: atomic type: object + priorityClass: + description: Enable priority class for the DaemonSet. This is + useful for GKE Autopilot clusters, but can be set for any cluster. + properties: + deploy: + description: Enables the operator to deploy a PriorityClass + instead of rolling your own. + type: boolean + name: + description: Name of the priority class to use for the DaemonSet. + type: string + value: + description: Value of the priority class to use for the DaemonSet. + Requires the Deploy field to be set to true. + format: int32 + type: integer + type: object + resources: + description: Configure resource requests and limits for the DaemonSet + Sensor. Only applies when using the eBPF backend. + properties: + limits: + description: Sets the resource limits for the DaemonSet Sensor. + Only applies when using the eBPF backend. + properties: + cpu: + pattern: ^(([0-9]{4,}|[2-9][5-9][0-9])m$)|[0-9]+$ + type: string + memory: + pattern: ^(([5-9][0-9]{2}[Mi]+)|([0-9.]+[iEGTP]+))|(([5-9][0-9]{8})|([0-9]{10,}))$ + type: string + type: object + requests: + description: Sets the resource requests for the DaemonSet + Sensor. Only applies when using the eBPF backend. + properties: + cpu: + pattern: ^(([0-9]{4,}|[2-9][5-9][0-9])m$)|[0-9]+$ + type: string + memory: + pattern: ^(([5-9][0-9]{2}[Mi]+)|([0-9.]+[iEGTP]+))|(([5-9][0-9]{8})|([0-9]{10,}))$ + type: string + type: object + type: object serviceAccount: description: Add metadata to the DaemonSet Service Account for IAM roles. diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index c6643cb8..44d4073c 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -16,6 +16,7 @@ bases: - ../crd - ../rbac - ../manager + #- ../resources # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index ba6efc9d..2db715e8 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -13,4 +13,4 @@ kind: Kustomization images: - name: controller newName: quay.io/crowdstrike/falcon-operator - newTag: latest + newTag: test diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 3cc081d2..8f9cab48 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -65,6 +65,7 @@ spec: args: - --leader-elect image: controller:latest + imagePullPolicy: Always name: manager env: - name: WATCH_NAMESPACE diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 18b5d097..c010c8e8 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -282,6 +282,16 @@ rules: - list - update - watch +- apiGroups: + - scheduling.k8s.io + resources: + - priorityclass + verbs: + - create + - delete + - get + - update + - watch - apiGroups: - security.openshift.io resourceNames: diff --git a/controllers/falcon_node/falconnodesensor_controller.go b/controllers/falcon_node/falconnodesensor_controller.go index 5479188b..04904674 100644 --- a/controllers/falcon_node/falconnodesensor_controller.go +++ b/controllers/falcon_node/falconnodesensor_controller.go @@ -16,6 +16,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + schedulingv1 "k8s.io/api/scheduling/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -58,6 +59,7 @@ func (r *FalconNodeSensorReconciler) SetupWithManager(mgr ctrl.Manager) error { //+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch //+kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=clusterrolebindings,verbs=get;list;watch;create //+kubebuilder:rbac:groups="security.openshift.io",resources=securitycontextconstraints,resourceNames=privileged,verbs=use +//+kubebuilder:rbac:groups="scheduling.k8s.io",resources=priorityclass,verbs=get;watch;create;delete;update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -115,6 +117,13 @@ func (r *FalconNodeSensorReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{Requeue: true}, nil } + if nodesensor.Spec.Node.PriorityClass.Deploy != nil { + err := r.handlePriorityClass(ctx, nodesensor, logger) + if err != nil { + return ctrl.Result{}, err + } + } + serviceAccount := common.NodeServiceAccountName created, err = r.handlePermissions(ctx, nodesensor, logger) @@ -245,6 +254,11 @@ func (r *FalconNodeSensorReconciler) Reconcile(ctx context.Context, req ctrl.Req volumeUpdates := updateDaemonSetVolumes(dsUpdate, dsTarget, logger) updated = updateDaemonSetContainerProxy(dsUpdate, nodesensor, logger) + if dsUpdate.Spec.Template.Spec.PriorityClassName != dsTarget.Spec.Template.Spec.PriorityClassName { + dsUpdate.Spec.Template.Spec.PriorityClassName = dsTarget.Spec.Template.Spec.PriorityClassName + updated = true + } + // Update the daemonset and re-spin pods with changes if imgUpdate || tolsUpdate || affUpdate || containerVolUpdate || volumeUpdates || updated { err = r.Update(ctx, dsUpdate) @@ -369,6 +383,47 @@ func (r *FalconNodeSensorReconciler) handleNamespace(ctx context.Context, nodese return true, nil } +// handlePriorityClass creates and updates the priority class +func (r *FalconNodeSensorReconciler) handlePriorityClass(ctx context.Context, nodesensor *falconv1alpha1.FalconNodeSensor, logger logr.Logger) error { + existingPC := &schedulingv1.PriorityClass{} + pcName := nodesensor.Name + "-priorityclass" + update := false + + if nodesensor.Spec.Node.PriorityClass.Name == "" { + nodesensor.Spec.Node.PriorityClass.Name = pcName + } + pc := assets.PriorityClass(nodesensor.Spec.Node.PriorityClass.Name, nodesensor.Spec.Node.PriorityClass.Value) + + err := r.Get(ctx, types.NamespacedName{Name: nodesensor.Name, Namespace: nodesensor.TargetNs()}, pc) + if err != nil && errors.IsNotFound(err) { + err = r.Create(ctx, pc) + if err != nil { + logger.Error(err, "Failed to create PriorityClass", "PriorityClass.Name", nodesensor.Spec.Node.PriorityClass.Name) + return err + } + } else if err != nil { + logger.Error(err, "Failed to get FalconNodeSensor ResourceQuota") + return err + } + + if nodesensor.Spec.Node.PriorityClass.Value != nil && existingPC.Value != *nodesensor.Spec.Node.PriorityClass.Value { + update = true + } + + if nodesensor.Spec.Node.PriorityClass.Name != "" && existingPC.Name != nodesensor.Spec.Node.PriorityClass.Name { + update = true + } + + if update { + err = r.Update(ctx, existingPC) + if err != nil { + return err + } + } + + return nil +} + // handleConfigMaps creates and updates the node sensor configmap func (r *FalconNodeSensorReconciler) handleConfigMaps(ctx context.Context, config *node.ConfigCache, nodesensor *falconv1alpha1.FalconNodeSensor, logger logr.Logger) (*corev1.ConfigMap, bool, error) { var updated bool diff --git a/deploy/falcon-operator.yaml b/deploy/falcon-operator.yaml index 4dd29d49..ff14d346 100644 --- a/deploy/falcon-operator.yaml +++ b/deploy/falcon-operator.yaml @@ -2700,6 +2700,13 @@ spec: on the nodes. Disabling might have unintended consequences for certain operations such as sensor downgrading. type: boolean + gke: + description: Enables the use of GKE Autopilot. + properties: + autopilot: + description: Enables the use of GKE Autopilot. + type: boolean + type: object image: description: Location of the Falcon Sensor image. Use only in cases when you mirror the original image to your repository/name:tag @@ -2936,6 +2943,50 @@ spec: type: object x-kubernetes-map-type: atomic type: object + priorityClass: + description: Enable priority class for the DaemonSet. This is + useful for GKE Autopilot clusters, but can be set for any cluster. + properties: + deploy: + description: Enables the operator to deploy a PriorityClass + instead of rolling your own. + type: boolean + name: + description: Name of the priority class to use for the DaemonSet. + type: string + value: + description: Value of the priority class to use for the DaemonSet. + Requires the Deploy field to be set to true. + format: int32 + type: integer + type: object + resources: + description: Configure resource requests and limits for the DaemonSet + Sensor. Only applies when using the eBPF backend. + properties: + limits: + description: Sets the resource limits for the DaemonSet Sensor. + Only applies when using the eBPF backend. + properties: + cpu: + pattern: ^(([0-9]{4,}|[2-9][5-9][0-9])m$)|[0-9]+$ + type: string + memory: + pattern: ^(([5-9][0-9]{2}[Mi]+)|([0-9.]+[iEGTP]+))|(([5-9][0-9]{8})|([0-9]{10,}))$ + type: string + type: object + requests: + description: Sets the resource requests for the DaemonSet + Sensor. Only applies when using the eBPF backend. + properties: + cpu: + pattern: ^(([0-9]{4,}|[2-9][5-9][0-9])m$)|[0-9]+$ + type: string + memory: + pattern: ^(([5-9][0-9]{2}[Mi]+)|([0-9.]+[iEGTP]+))|(([5-9][0-9]{8})|([0-9]{10,}))$ + type: string + type: object + type: object serviceAccount: description: Add metadata to the DaemonSet Service Account for IAM roles. @@ -3555,6 +3606,16 @@ rules: - list - update - watch +- apiGroups: + - scheduling.k8s.io + resources: + - priorityclass + verbs: + - create + - delete + - get + - update + - watch - apiGroups: - security.openshift.io resourceNames: @@ -3702,7 +3763,7 @@ spec: value: null - name: OPERATOR_NAME value: falcon-operator - image: quay.io/crowdstrike/falcon-operator:latest + image: quay.io/crowdstrike/falcon-operator:test imagePullPolicy: Always livenessProbe: httpGet: diff --git a/internal/controller/assets/daemonset.go b/internal/controller/assets/daemonset.go index 9e40755f..2b8207ee 100644 --- a/internal/controller/assets/daemonset.go +++ b/internal/controller/assets/daemonset.go @@ -7,6 +7,7 @@ import ( "github.com/crowdstrike/falcon-operator/pkg/common" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -60,16 +61,181 @@ func dsUpdateStrategy(node *falconv1alpha1.FalconNodeSensor) appsv1.DaemonSetUpd return appsv1.DaemonSetUpdateStrategy{Type: appsv1.OnDeleteDaemonSetStrategyType} } +func sensorCapabilities(node *falconv1alpha1.FalconNodeSensor, initContainer bool) *corev1.Capabilities { + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + if initContainer { + return &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_ADMIN", + "SYS_PTRACE", + "SYS_CHROOT", + "DAC_READ_SEARCH", + }, + } + } + return &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_ADMIN", + "SETGID", + "SETUID", + "SYS_PTRACE", + "SYS_CHROOT", + "DAC_OVERRIDE", + "SETPCAP", + "DAC_READ_SEARCH", + "BPF", + "PERFMON", + "SYS_RESOURCE", + "NET_RAW", + "CHOWN", + }, + } + } + return nil +} + +func dsResources(node *falconv1alpha1.FalconNodeSensor) corev1.ResourceRequirements { + if node.Spec.Node.Backend == "bpf" { + var limitResources, requestsResources corev1.ResourceList + + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + limitResources = corev1.ResourceList{ + "cpu": resource.MustParse("750m"), + "memory": resource.MustParse("1.5Gi"), + } + requestsResources = corev1.ResourceList{ + "cpu": resource.MustParse("750m"), + "memory": resource.MustParse("1.5Gi"), + } + } + + if node.Spec.Node.SensorResources.Limits.CPU != "" { + limitResources["cpu"] = resource.MustParse(node.Spec.Node.SensorResources.Limits.CPU) + } + if node.Spec.Node.SensorResources.Limits.Memory != "" { + limitResources["memory"] = resource.MustParse(node.Spec.Node.SensorResources.Limits.Memory) + } + if node.Spec.Node.SensorResources.Requests.CPU != "" { + requestsResources["cpu"] = resource.MustParse(node.Spec.Node.SensorResources.Requests.CPU) + } + if node.Spec.Node.SensorResources.Requests.Memory != "" { + requestsResources["memory"] = resource.MustParse(node.Spec.Node.SensorResources.Requests.Memory) + } + + return corev1.ResourceRequirements{ + Limits: limitResources, + Requests: requestsResources, + } + } + + node.Spec.Node.SensorResources = falconv1alpha1.Resources{} + return corev1.ResourceRequirements{} +} + +func priorityClass(node *falconv1alpha1.FalconNodeSensor) string { + if node.Spec.Node.PriorityClass.Name == "" && node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + node.Spec.Node.PriorityClass.Name = "falcon-operator-node-security-critical" + } + + return node.Spec.Node.PriorityClass.Name +} + +// initArgs - remove this function when 6.53 is no longer supported. 6.53+ will use InitContainerArgs() +func initArgs(node *falconv1alpha1.FalconNodeSensor) []string { + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + return common.InitContainerArgs() + } + return common.LegacyInitContainerArgs() +} + +// cleanupArgs - remove this function when 6.53 is no longer supported. 6.53+ will use InitCleanupArgs() +func cleanupArgs(node *falconv1alpha1.FalconNodeSensor) []string { + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + return common.InitCleanupArgs() + } + return common.LegacyInitCleanupArgs() +} + +// volumes - remove this function when 6.53 is no longer supported. 6.53+ will only use falconstore +func volumes(node *falconv1alpha1.FalconNodeSensor) []corev1.Volume { + pathTypeUnset := corev1.HostPathUnset + pathDirCreate := corev1.HostPathDirectoryOrCreate + + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + return []corev1.Volume{ + { + Name: "falconstore", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: common.FalconStoreFile, + Type: &pathTypeUnset, + }, + }, + }, + } + } + + return []corev1.Volume{ + { + Name: "falconstore", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: common.FalconStoreFile, + Type: &pathTypeUnset, + }, + }, + }, + { + Name: "falconstore-hostdir", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: common.FalconHostInstallDir, + Type: &pathDirCreate, + }, + }, + }, + } +} + +func volumeMounts(node *falconv1alpha1.FalconNodeSensor, name string) []corev1.VolumeMount { + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + return []corev1.VolumeMount{} + } + + return []corev1.VolumeMount{ + { + Name: name, + MountPath: common.FalconInitHostInstallDir, + }, + } +} + +func volumesCleanup(node *falconv1alpha1.FalconNodeSensor) []corev1.Volume { + if node.Spec.Node.GKE.Enabled != nil && *node.Spec.Node.GKE.Enabled { + return []corev1.Volume{} + } + return []corev1.Volume{ + { + Name: "opt-crowdstrike", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: common.FalconHostInstallDir, + }, + }, + }, + } + +} + func Daemonset(dsName, image, serviceAccount string, node *falconv1alpha1.FalconNodeSensor) *appsv1.DaemonSet { privileged := true escalation := true - readOnlyFs := false + readOnlyFSDisabled := false + readOnlyFSEnabled := true hostpid := true hostnetwork := true hostipc := true - runAs := int64(0) - pathTypeUnset := corev1.HostPathUnset - pathDirCreate := corev1.HostPathDirectoryOrCreate + runAsRoot := int64(0) return &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ @@ -101,21 +267,17 @@ func Daemonset(dsName, image, serviceAccount string, node *falconv1alpha1.Falcon ImagePullSecrets: pullSecrets(node), InitContainers: []corev1.Container{ { - Name: "init-falconstore", - Image: image, - Command: common.FalconShellCommand, - Args: common.InitContainerArgs(), - VolumeMounts: []corev1.VolumeMount{ - { - Name: "falconstore-hostdir", - MountPath: common.FalconInitHostInstallDir, - }, - }, + Name: "init-falconstore", + Image: image, + Command: common.FalconShellCommand, + Args: initArgs(node), + VolumeMounts: volumeMounts(node, "falconstore-hostdir"), SecurityContext: &corev1.SecurityContext{ Privileged: &privileged, - RunAsUser: &runAs, - ReadOnlyRootFilesystem: &readOnlyFs, + RunAsUser: &runAsRoot, + ReadOnlyRootFilesystem: &readOnlyFSEnabled, AllowPrivilegeEscalation: &escalation, + Capabilities: sensorCapabilities(node, true), }, }, }, @@ -124,9 +286,10 @@ func Daemonset(dsName, image, serviceAccount string, node *falconv1alpha1.Falcon { SecurityContext: &corev1.SecurityContext{ Privileged: &privileged, - RunAsUser: &runAs, - ReadOnlyRootFilesystem: &readOnlyFs, + RunAsUser: &runAsRoot, + ReadOnlyRootFilesystem: &readOnlyFSDisabled, AllowPrivilegeEscalation: &escalation, + Capabilities: sensorCapabilities(node, false), }, Name: "falcon-node-sensor", Image: image, @@ -146,28 +309,11 @@ func Daemonset(dsName, image, serviceAccount string, node *falconv1alpha1.Falcon MountPath: common.FalconStoreFile, }, }, + Resources: dsResources(node), }, }, - Volumes: []corev1.Volume{ - { - Name: "falconstore", - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: common.FalconStoreFile, - Type: &pathTypeUnset, - }, - }, - }, - { - Name: "falconstore-hostdir", - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: common.FalconHostInstallDir, - Type: &pathDirCreate, - }, - }, - }, - }, + Volumes: volumes(node), + PriorityClassName: priorityClass(node), }, }, }, @@ -176,10 +322,12 @@ func Daemonset(dsName, image, serviceAccount string, node *falconv1alpha1.Falcon func RemoveNodeDirDaemonset(dsName, image, serviceAccount string, node *falconv1alpha1.FalconNodeSensor) *appsv1.DaemonSet { privileged := true + nonPrivileged := false escalation := true - readOnlyFs := false + allowEscalation := false + readOnlyFs := true hostpid := true - runAs := int64(0) + runAsRoot := int64(0) return &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ @@ -211,29 +359,24 @@ func RemoveNodeDirDaemonset(dsName, image, serviceAccount string, node *falconv1 Name: "cleanup-opt-crowdstrike", Image: image, Command: common.FalconShellCommand, - Args: common.InitCleanupArgs(), + Args: cleanupArgs(node), SecurityContext: &corev1.SecurityContext{ Privileged: &privileged, - RunAsUser: &runAs, + RunAsUser: &runAsRoot, ReadOnlyRootFilesystem: &readOnlyFs, AllowPrivilegeEscalation: &escalation, + Capabilities: sensorCapabilities(node, true), }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "opt-crowdstrike", - MountPath: common.FalconInitHostInstallDir, - }, - }, + VolumeMounts: volumeMounts(node, "opt-crowdstrike"), }, }, ServiceAccountName: serviceAccount, Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - RunAsUser: &runAs, + Privileged: &nonPrivileged, ReadOnlyRootFilesystem: &readOnlyFs, - AllowPrivilegeEscalation: &escalation, + AllowPrivilegeEscalation: &allowEscalation, }, Name: "cleanup-sleep", Image: image, @@ -241,16 +384,7 @@ func RemoveNodeDirDaemonset(dsName, image, serviceAccount string, node *falconv1 Args: common.CleanupSleep(), }, }, - Volumes: []corev1.Volume{ - { - Name: "opt-crowdstrike", - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: common.FalconHostInstallDir, - }, - }, - }, - }, + Volumes: volumesCleanup(node), }, }, }, diff --git a/internal/controller/assets/daemonset_test.go b/internal/controller/assets/daemonset_test.go index 1a32cc68..da4260e6 100644 --- a/internal/controller/assets/daemonset_test.go +++ b/internal/controller/assets/daemonset_test.go @@ -129,6 +129,58 @@ func TestDsUpdateStrategy(t *testing.T) { } } +func TestSensorCapabilities(t *testing.T) { + falconNode := falconv1alpha1.FalconNodeSensor{} + enabled := true + + // Check default return value when GKE is not enabled + got := sensorCapabilities(&falconNode, false) + wantNil := (*corev1.Capabilities)(nil) + if diff := cmp.Diff(wantNil, got); diff != "" { + t.Errorf("sensorCapabilities() mismatch (-want +got): %s", diff) + } + + // Check return value when GKE is enabled + falconNode.Spec.Node.GKE.Enabled = &enabled + got = sensorCapabilities(&falconNode, false) + want := &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_ADMIN", + "SETGID", + "SETUID", + "SYS_PTRACE", + "SYS_CHROOT", + "DAC_OVERRIDE", + "SETPCAP", + "DAC_READ_SEARCH", + "BPF", + "PERFMON", + "SYS_RESOURCE", + "NET_RAW", + "CHOWN", + }, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("sensorCapabilities() mismatch (-want +got): %s", diff) + } + + // check if initContainer is enabled + got = sensorCapabilities(&falconNode, true) + want = &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_ADMIN", + "SYS_PTRACE", + "SYS_CHROOT", + "DAC_READ_SEARCH", + }, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("sensorCapabilities() mismatch (-want +got): %s", diff) + } +} + func TestDaemonset(t *testing.T) { falconNode := falconv1alpha1.FalconNodeSensor{} falconNode.Namespace = "falcon-system" @@ -138,11 +190,12 @@ func TestDaemonset(t *testing.T) { privileged := true escalation := true - readOnlyFs := false + readOnlyFSDisabled := false + readOnlyFSEnabled := true hostpid := true hostnetwork := true hostipc := true - runAs := int64(0) + runAsRoot := int64(0) pathTypeUnset := corev1.HostPathUnset pathDirCreate := corev1.HostPathDirectoryOrCreate @@ -177,11 +230,11 @@ func TestDaemonset(t *testing.T) { Name: "init-falconstore", Image: image, Command: common.FalconShellCommand, - Args: common.InitContainerArgs(), + Args: initArgs(&falconNode), SecurityContext: &corev1.SecurityContext{ Privileged: &privileged, - RunAsUser: &runAs, - ReadOnlyRootFilesystem: &readOnlyFs, + RunAsUser: &runAsRoot, + ReadOnlyRootFilesystem: &readOnlyFSEnabled, AllowPrivilegeEscalation: &escalation, }, VolumeMounts: []corev1.VolumeMount{ @@ -197,8 +250,8 @@ func TestDaemonset(t *testing.T) { { SecurityContext: &corev1.SecurityContext{ Privileged: &privileged, - RunAsUser: &runAs, - ReadOnlyRootFilesystem: &readOnlyFs, + RunAsUser: &runAsRoot, + ReadOnlyRootFilesystem: &readOnlyFSDisabled, AllowPrivilegeEscalation: &escalation, }, Name: "falcon-node-sensor", @@ -260,10 +313,12 @@ func TestRemoveNodeDirDaemonset(t *testing.T) { dsName := "test-DaemonSet" privileged := true + nonPrivileged := false escalation := true - readOnlyFs := false + allowEscalation := false + readOnlyFs := true hostpid := true - runAs := int64(0) + runAsRoot := int64(0) want := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ @@ -293,10 +348,10 @@ func TestRemoveNodeDirDaemonset(t *testing.T) { Name: "cleanup-opt-crowdstrike", Image: image, Command: common.FalconShellCommand, - Args: common.InitCleanupArgs(), + Args: cleanupArgs(&falconNode), SecurityContext: &corev1.SecurityContext{ Privileged: &privileged, - RunAsUser: &runAs, + RunAsUser: &runAsRoot, ReadOnlyRootFilesystem: &readOnlyFs, AllowPrivilegeEscalation: &escalation, }, @@ -312,10 +367,9 @@ func TestRemoveNodeDirDaemonset(t *testing.T) { Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - RunAsUser: &runAs, + Privileged: &nonPrivileged, ReadOnlyRootFilesystem: &readOnlyFs, - AllowPrivilegeEscalation: &escalation, + AllowPrivilegeEscalation: &allowEscalation, }, Name: "cleanup-sleep", Image: image, diff --git a/internal/controller/assets/priorityclass.go b/internal/controller/assets/priorityclass.go new file mode 100644 index 00000000..290b40f2 --- /dev/null +++ b/internal/controller/assets/priorityclass.go @@ -0,0 +1,29 @@ +package assets + +import ( + "github.com/crowdstrike/falcon-operator/pkg/common" + schedulingv1 "k8s.io/api/scheduling/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func PriorityClass(name string, value *int32) *schedulingv1.PriorityClass { + defaultValue := int32(1000000000) + labels := common.CRLabels("priorityclass", name, common.FalconKernelSensor) + + if value == nil { + value = &defaultValue + } + + return &schedulingv1.PriorityClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: schedulingv1.SchemeGroupVersion.String(), + Kind: "PriorityClass", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + }, + Description: "This priority class would be used to deploy CrowdStrike Falcon node sensor", + Value: *value, + } +} diff --git a/internal/controller/assets/priorityclass_test.go b/internal/controller/assets/priorityclass_test.go new file mode 100644 index 00000000..e012fa43 --- /dev/null +++ b/internal/controller/assets/priorityclass_test.go @@ -0,0 +1,40 @@ +package assets + +import ( + "testing" + + "github.com/crowdstrike/falcon-operator/pkg/common" + "github.com/google/go-cmp/cmp" + schedulingv1 "k8s.io/api/scheduling/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestPriorityClass tests the PriorityClass function +func TestPriorityClass(t *testing.T) { + name := "test" + value := int32(1000000000) + want := &schedulingv1.PriorityClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: schedulingv1.SchemeGroupVersion.String(), + Kind: "PriorityClass", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: common.CRLabels("priorityclass", name, common.FalconKernelSensor), + }, + Description: "This priority class would be used to deploy CrowdStrike Falcon node sensor", + Value: value, + } + + // Test with nil value + got := PriorityClass(name, nil) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("PriorityClass() mismatch (-want +got): %s", diff) + } + + // Test with defined value + got = PriorityClass(name, &value) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("PriorityClass() mismatch (-want +got): %s", diff) + } +} diff --git a/pkg/common/common_test.go b/pkg/common/common_test.go index f4849745..7bee957a 100644 --- a/pkg/common/common_test.go +++ b/pkg/common/common_test.go @@ -18,19 +18,33 @@ type FakeDiscovery struct { } func TestInitContainerArgs(t *testing.T) { - want := []string{"-c", `if [ -x "/opt/CrowdStrike/falcon-daemonset-init" ]; then echo "Executing falcon-daemonset-init -i"; falcon-daemonset-init -i ; else if [ -d "/host_opt/CrowdStrike/falconstore" ]; then echo "Re-creating /opt/CrowdStrike/falconstore as it is a directory instead of a file"; rm -rf /host_opt/CrowdStrike/falconstore; fi; mkdir -p /host_opt/CrowdStrike/ && touch /host_opt/CrowdStrike/falconstore; fi`} + want := []string{"-c", `echo "Running /opt/CrowdStrike/falcon-daemonset-init -i"; /opt/CrowdStrike/falcon-daemonset-init -i`} if got := InitContainerArgs(); !reflect.DeepEqual(got, want) { t.Errorf("InitContainerArgs() = %v, want %v", got, want) } } func TestInitCleanupArgs(t *testing.T) { - want := []string{"-c", `if [ -x "/opt/CrowdStrike/falcon-daemonset-init" ]; then echo "Running falcon-daemonset-init -u"; falcon-daemonset-init -u; else echo "Manually removing /host_opt/CrowdStrike/"; rm -rf /host_opt/CrowdStrike/; fi`} + want := []string{"-c", `echo "Running /opt/CrowdStrike/falcon-daemonset-init -u"; /opt/CrowdStrike/falcon-daemonset-init -u`} if got := InitCleanupArgs(); !reflect.DeepEqual(got, want) { t.Errorf("InitCleanupArgs() = %v, want %v", got, want) } } +func TestLegacyInitContainerArgs(t *testing.T) { + want := []string{"-c", `if [ -x "/opt/CrowdStrike/falcon-daemonset-init -i" ]; then echo "Executing falcon-daemonset-init -i"; falcon-daemonset-init -i ; else if [ -d "/host_opt/CrowdStrike/falconstore" ]; then echo "Re-creating /opt/CrowdStrike/falconstore as it is a directory instead of a file"; rm -rf /host_opt/CrowdStrike/falconstore; fi; mkdir -p /host_opt/CrowdStrike/ && touch /host_opt/CrowdStrike/falconstore; fi`} + if got := LegacyInitContainerArgs(); !reflect.DeepEqual(got, want) { + t.Errorf("LegacyInitContainerArgs() = %v, want %v", got, want) + } +} + +func TestLegacyInitCleanupArgs(t *testing.T) { + want := []string{"-c", `if [ -x "/opt/CrowdStrike/falcon-daemonset-init -u" ]; then echo "Running falcon-daemonset-init -u"; falcon-daemonset-init -u; else echo "Manually removing /host_opt/CrowdStrike/"; rm -rf /host_opt/CrowdStrike/; fi`} + if got := LegacyInitCleanupArgs(); !reflect.DeepEqual(got, want) { + t.Errorf("LegacyInitCleanupArgs() = %v, want %v", got, want) + } +} + func TestCleanupSleep(t *testing.T) { want := []string{"-c", "sleep 10"} if got := CleanupSleep(); !reflect.DeepEqual(got, want) { diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 31ff6b5f..7e6820e8 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -17,9 +17,10 @@ const ( FalconInitDataDir = "/host_opt/CrowdStrike/" FalconStoreFile = "/opt/CrowdStrike/falconstore" FalconInitStoreFile = "/host_opt/CrowdStrike/falconstore" - FalconDaemonsetInitBinary = "/opt/CrowdStrike/falcon-daemonset-init" + FalconDaemonsetInitBinary = "/opt/CrowdStrike/falcon-daemonset-init -i" FalconDaemonsetInitBinaryInvocation = "falcon-daemonset-init -i" FalconDaemonsetCleanupBinaryInvocation = "falcon-daemonset-init -u" + FalconDaemonsetCleanupBinary = "/opt/CrowdStrike/falcon-daemonset-init -u" FalconContainerProbePath = "/live" FalconAdmissionClientStartupProbePath = "/startz" FalconAdmissionClientLivenessProbePath = "/livez" diff --git a/pkg/common/funcs.go b/pkg/common/funcs.go index 0f8245dc..639db104 100644 --- a/pkg/common/funcs.go +++ b/pkg/common/funcs.go @@ -16,6 +16,20 @@ import ( ) func InitContainerArgs() []string { + return []string{ + "-c", + fmt.Sprintf("echo \"Running %[1]s\"; %[1]s", FalconDaemonsetInitBinary), + } +} + +func InitCleanupArgs() []string { + return []string{ + "-c", + fmt.Sprintf("echo \"Running %[1]s\"; %[1]s", FalconDaemonsetCleanupBinary), + } +} + +func LegacyInitContainerArgs() []string { return []string{ "-c", // Versions of falcon-sensor 6.53+ will contain an init binary that we invoke @@ -29,11 +43,11 @@ func InitContainerArgs() []string { } } -func InitCleanupArgs() []string { +func LegacyInitCleanupArgs() []string { return []string{ "-c", // Versions of falcon-sensor 6.53+ will contain an init binary that we invoke with a cleanup argument - `if [ -x "` + FalconDaemonsetInitBinary + `" ]; then ` + + `if [ -x "` + FalconDaemonsetCleanupBinary + `" ]; then ` + `echo "Running ` + FalconDaemonsetCleanupBinaryInvocation + `"; ` + FalconDaemonsetCleanupBinaryInvocation + `; else ` + `echo "Manually removing ` + FalconInitDataDir + `"; ` + `rm -rf ` + FalconInitDataDir +