Kubernetes Security: Restricting Volume Types

This page describes a highlighted problem produced by the Docker and Kubernetes Security scanner plugin.

You could find more details on the internal page: Cloud (IaC) Security plugin

If this project has been helpful to you, please consider giving it a ⭐ on GitHub to help others discover it.

Problem

Using disallowed volume types (e.g., hostPath, legacy in-tree volumes, or raw device mounts) can break container isolation and enable privilege escalation or node compromise.

Description

When a pod can mount arbitrary host paths or devices, an attacker who gains code execution in that pod may:

  • Read or modify sensitive host files (credentials, kubelet data, logs).
  • Interact with container/runtime sockets (e.g., /var/run/*sock) to escalate privileges.
  • Persist or laterally move by planting files on the node.
    Legacy in-tree storage plugins also widen the attack surface and bypass centralized governance.

The Restricted Pod Security level allows only API/CSI-mediated, isolated volume types:
configMap, secret, downwardAPI, projected, emptyDir, ephemeral (generic ephemeral volumes), persistentVolumeClaim, and csi.
These do not expose arbitrary host paths/devices and can be controlled by cluster policy (read-only mounts, quotas, StorageClasses, etc.).

Solution

  • Allow only the safe volume types listed above.
  • Prefer readOnly: true for configMap, secret, downwardAPI, and projected mounts.
  • Use emptyDir for scratch space; consider sizeLimit (and medium: Memory only when truly needed).
  • Use ephemeral or persistentVolumeClaim (backed by approved CSI drivers) for durable data.
  • Enforce at admission time with Pod Security Restricted (and, if needed, Kyverno/OPA policies to allowlist specific StorageClasses/CSI drivers).

Problematic code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: risky-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: risky
  template:
    metadata:
      labels:
        app: risky
    spec:
      containers:
        - name: app
          image: alpine
          command: ["sh", "-c", "sleep 3600"]
          volumeMounts:
            - name: docker-sock
              mountPath: /var/run/docker.sock
            - name: host-etc
              mountPath: /host/etc
              readOnly: true
      volumes:
        - name: docker-sock
          hostPath:
            path: /var/run/docker.sock   # Direct access to runtime socket
            type: Socket
        - name: host-etc
          hostPath:
            path: /etc                   # Arbitrary host filesystem access
            type: Directory

Verified code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: safe-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: safe
  template:
    metadata:
      labels:
        app: safe
    spec:
      containers:
        - name: app
          image: alpine
          command: ["sh", "-c", "sleep 3600"]
          volumeMounts:
            - name: app-config
              mountPath: /etc/app/config
              readOnly: true
            - name: db-creds
              mountPath: /var/run/secrets/db
              readOnly: true
            - name: cache
              mountPath: /var/cache/app
            - name: data
              mountPath: /var/lib/app
      volumes:
        - name: app-config
          configMap:
            name: app-config
        - name: db-creds
          secret:
            secretName: db-creds
        - name: cache
          emptyDir:
            sizeLimit: 256Mi
        - name: data
          persistentVolumeClaim:
            claimName: app-data # Backed by an approved StorageClass/CSI driver

Source of the rule: Pod Security Standards