CVE-2023-30840 (Go, published 2023-05-08T18:15Z)
Description: value: Fluid is an open source Kubernetes-native distributed dataset orchestrator and accelerator for data-intensive applications. Starting in version 0.7.0 and prior to version 0.8.6, if a malicious user gains control of a Kubernetes node running fluid csi pod (controlled by the `csi-nodeplugin-fluid` node-daemonset), they can leverage the fluid-csi service account to modify specs of all the nodes in the cluster. However, since this service account lacks `list node` permissions, the attacker may need to use other techniques to identify vulnerable nodes.

Once the attacker identifies and modifies the node specs, they can manipulate system-level-privileged components to access all secrets in the cluster or execute pods on other nodes. This allows them to elevate privileges beyond the compromised node and potentially gain full privileged access to the whole cluster.

To exploit this vulnerability, the attacker can make all other nodes unschedulable (for example, patch node with taints) and wait for system-critical components with high privilege to appear on the compromised node. However, this attack requires two prerequisites: a compromised node and identifying all vulnerable nodes through other means.

Version 0.8.6 contains a patch for this issue. As a workaround, delete the `csi-nodeplugin-fluid` daemonset in `fluid-system` namespace and avoid using CSI mode to mount FUSE file systems. Alternatively, using sidecar mode to mount FUSE file systems is recommended.
Severity: nan (CVSS v3 base score: 7.8)
Weakness: CWE-863 \u2014 Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
Repository: https://github.com/fluid-cloudnative/fluid
Files changed:
charts/fluid/fluid/CHANGELOG.md
charts/fluid/fluid/templates/csi/daemonset.yaml
charts/fluid/fluid/templates/role/csi/rbac.yaml
charts/fluid/fluid/templates/role/webhook/rabc.yaml
charts/fluid/fluid/templates/webhook/webhook.yaml
charts/fluid/fluid/values.yaml
cmd/csi/app/csi.go
pkg/csi/config/config.go
pkg/csi/plugins/driver.go
pkg/csi/plugins/nodeserver.go
pkg/csi/plugins/register.go
pkg/utils/kubelet/node_auth_client.go
Commit message: Merge pull request from GHSA-93xx-cvmc-9w3v

* Fix rbacs and limit CSI Plugin's node related access

Signed-off-by: trafalgarzzz <trafalgarz@outlook.com>

* Update change log

Signed-off-by: trafalgarzzz <trafalgarz@outlook.com>

---------

Signed-off-by: trafalgarzzz <trafalgarz@outlook.com>

Vulnerable code:
* Support pass image pull secrets from fluid charts to alluxioruntime controller
          - name: host-etc-dir
            mountPath: /host-etc
            path: /etc
          name: host-etc-dir
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "patch"]
  - apiGroups: [""]
    resources: ["nodes/proxy"]
    verbs: ["*"]
      - validatingwebhookconfigurations
      - '*'
      - secrets
      - events
  - apiGroups:
      - coordination.k8s.io
    resources:
      - leases
    verbs:
      - '*'
	endpoint    string
	nodeID      string
	metricsAddr string
	pprofAddr   string
	pruneFs     []string
	prunePath   string
		NodeId:    nodeID,
		Endpoint:  endpoint,
		PruneFs:   pruneFs,
		PrunePath: prunePath,
	NodeId    string
	Endpoint  string
	PruneFs   []string
	PrunePath string
	client           client.Client
	apiReader        client.Reader
	csiDriver        *csicommon.CSIDriver
	nodeId, endpoint string
func NewDriver(nodeID, endpoint string, client client.Client, apiReader client.Reader) *driver {
		nodeId:    nodeID,
		endpoint:  endpoint,
		csiDriver: csiDriver,
		client:    client,
		apiReader: apiReader,
		nodeId:            d.nodeId,
		DefaultNodeServer: csicommon.NewDefaultNodeServer(d.csiDriver),
		client:            d.client,
		apiReader:         d.apiReader,
	"github.com/fluid-cloudnative/fluid/pkg/utils/kubeclient"
	client    client.Client
	apiReader client.Reader
	mutex     sync.Mutex
	node      *v1.Node
	_, err = utils.ChangeNodeLabelWithPatchMode(ns.client, node, labelsToModify)
	_, err = utils.ChangeNodeLabelWithPatchMode(ns.client, node, labelsToModify)
	if node, err = kubeclient.GetNode(ns.apiReader, ns.nodeId); err != nil {
	_, err = utils.ChangeNodeLabelWithPatchMode(ns.client, node, labelsToModify)
	csiDriver := NewDriver(cfg.NodeId, cfg.Endpoint, mgr.GetClient(), mgr.GetAPIReader())

Fixed code:
* Support pass image pull secrets from fluid charts to alluxioruntime controller
* Fix components rbacs and set Fluid CSI Plugin with node-authorized kube-client
          - name: kubelet-kube-config
            mountPath: /etc/kubernetes/kubelet.conf
            readOnly: true
          - name: kubelet-cert-dir
            mountPath: {{ .Values.csi.kubelet.certDir | quote }}
            readOnly: true
          - name: updatedb-conf
            mountPath: /host-etc/updatedb.conf
          - name: updatedb-conf-bak
            mountPath: /host-etc/updatedb.conf.bak
            path: {{ .Values.csi.kubelet.kubeConfigFile | quote }}
            type: File
          name: kubelet-kube-config
        - hostPath:
            path: {{ .Values.csi.kubelet.certDir | quote }}
          name: kubelet-cert-dir
        - hostPath:
            path: /etc/updatedb.conf
            type: FileOrCreate
          name: updatedb-conf
        - hostPath:
            path: /etc/updatedb.conf.backup
            type: FileOrCreate
          name: updatedb-conf-bak
    verbs: ["create", "patch"]
kind: Role
metadata:
  name: fluid-webhook
  namespace: {{ include "fluid.namespace" . }}
rules:
  - apiGroups:
      - ""
    resources:
      - secrets
    verbs:
      - get
      - update
    resourceNames:
      - fluid-webhook-certs
  # resourceNames won't protect create verb, so individually specify it for readability
  - apiGroups:
      - ""
    resources:
      - secrets
    verbs:
      - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: fluid-webhook-rolebinding
  namespace: {{ include "fluid.namespace" . }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: fluid-webhook
subjects:
  - kind: ServiceAccount
    name: fluid-webhook
    namespace: {{ include "fluid.namespace" . }}
---
apiVersion: rbac.authorization.k8s.io/v1
  # Can only list and watch secret `mutatingwebhookconfiguration` with a metadata.name field selector
  # See https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources
    resourceNames:
      - fluid-pod-admission-webhook
      - get
      - patch
      - list
      - watch
      tolerations:
        - operator: Exists
    kubeConfigFile: /etc/kubernetes/kubelet.conf
    certDir: /var/lib/kubelet/pki
	endpoint              string
	nodeID                string
	metricsAddr           string
	pprofAddr             string
	pruneFs               []string
	prunePath             string
	kubeletKubeConfigPath string
	startCmd.Flags().StringVarP(&kubeletKubeConfigPath, "kubelet-kube-config", "", "/etc/kubernetes/kubelet.conf", "The file path to kubelet kube config")
		NodeId:            nodeID,
		Endpoint:          endpoint,
		PruneFs:           pruneFs,
		PrunePath:         prunePath,
		KubeletConfigPath: kubeletKubeConfigPath,
	NodeId            string
	Endpoint          string
	PruneFs           []string
	PrunePath         string
	KubeletConfigPath string
	"k8s.io/client-go/kubernetes"
	client               client.Client
	apiReader            client.Reader
	nodeAuthorizedClient *kubernetes.Clientset
	csiDriver            *csicommon.CSIDriver
	nodeId, endpoint     string
func NewDriver(nodeID, endpoint string, client client.Client, apiReader client.Reader, nodeAuthorizedClient *kubernetes.Clientset) *driver {
		nodeId:               nodeID,
		endpoint:             endpoint,
		csiDriver:            csiDriver,
		client:               client,
		nodeAuthorizedClient: nodeAuthorizedClient,
		apiReader:            apiReader,
		nodeId:               d.nodeId,
		DefaultNodeServer:    csicommon.NewDefaultNodeServer(d.csiDriver),
		client:               d.client,
		apiReader:            d.apiReader,
		nodeAuthorizedClient: d.nodeAuthorizedClient,
	"encoding/json"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"
	client               client.Client
	apiReader            client.Reader
	nodeAuthorizedClient *kubernetes.Clientset
	mutex                sync.Mutex
	node                 *v1.Node
	// _, err = utils.ChangeNodeLabelWithPatchMode(ns.client, node, labelsToModify)
	err = ns.patchNodeWithLabel(node, labelsToModify)
	// _, err = utils.ChangeNodeLabelWithPatchMode(ns.client, node, labelsToModify)
	err = ns.patchNodeWithLabel(node, labelsToModify)
	if node, err = ns.nodeAuthorizedClient.CoreV1().Nodes().Get(context.TODO(), ns.nodeId, metav1.GetOptions{}); err != nil {

	// if node, err = kubeclient.GetNode(ns.apiReader, ns.nodeId); err != nil {
	// return nil, err
	// }

func (ns *nodeServer) patchNodeWithLabel(node *v1.Node, labelsToModify common.LabelsToModify) error {
	labels := labelsToModify.GetLabels()
	labelValuePair := map[string]interface{}{}

	for _, labelToModify := range labels {
		operationType := labelToModify.GetOperationType()
		labelToModifyKey := labelToModify.GetLabelKey()
		labelToModifyValue := labelToModify.GetLabelValue()

		switch operationType {
		case common.AddLabel, common.UpdateLabel:
			labelValuePair[labelToModifyKey] = labelToModifyValue
		case common.DeleteLabel:
			labelValuePair[labelToModifyKey] = nil
		default:
			err := fmt.Errorf("fail to update the label due to the wrong operation: %s", operationType)
			return err
		}
	}

	metadata := map[string]interface{}{
		"metadata": map[string]interface{}{
			"labels": labelValuePair,
		},
	}

	patchByteData, err := json.Marshal(metadata)
	if err != nil {
		return err
	}

	_, err = ns.nodeAuthorizedClient.CoreV1().Nodes().Patch(context.TODO(), node.Name, types.StrategicMergePatchType, patchByteData, metav1.PatchOptions{})
	if err != nil {
		return err
	}

	return nil
}

	// _, err = utils.ChangeNodeLabelWithPatchMode(ns.client, node, labelsToModify)
	err = ns.patchNodeWithLabel(node, labelsToModify)
	"github.com/fluid-cloudnative/fluid/pkg/utils/kubelet"
	client, err := kubelet.InitNodeAuthorizedClient(cfg.KubeletConfigPath)
	if err != nil {
		return err
	}

	csiDriver := NewDriver(cfg.NodeId, cfg.Endpoint, mgr.GetClient(), mgr.GetAPIReader(), client)
package kubelet

import (
	"github.com/pkg/errors"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

// InitNodeAuthorizedClient initializes node authorized client with kubelet's kube config.
// This is now an available workaround to implement a node-scoped daemonset.
// See discussion https://github.com/kubernetes/enhancements/pull/944#issuecomment-490242290
func InitNodeAuthorizedClient(kubeletKubeConfigPath string) (*kubernetes.Clientset, error) {
	config, err := clientcmd.BuildConfigFromFlags("", kubeletKubeConfigPath)
	if err != nil {
		return nil, errors.Wrapf(err, "fail to build kubelet config")
	}

	client, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, errors.Wrap(err, "fail to build client-go client from kubelet kubeconfig")
	}

	return client, nil
}