Skip to content

CL-0016: Dangerous host devices exposed via devices:

Severity: CRITICAL

Derivation (see severity model):

  • Baseline: A — the attacker already has code execution in this container, as the workload uid
  • Precondition: Direct — a mapped block device is read and written with ordinary file operations, at default capabilities. No technique, no second defect
  • Impact: Host — raw access to the disk backing the host filesystem reads and rewrites host state regardless of file permissions
  • Qualifier/modifier: none
  • Derived: Direct × Host = CRITICAL
  • Shipped: CRITICAL
  • Evidence: _cl0016devices: exposes the host device, verified live. The grant is capability-independent: a raw read of the host's NVMe device through --device succeeded at default capabilities, where the same read through a /dev bind mount was refused by the device cgroup

References: - CIS Docker Benchmark 5.18 — Ensure that host devices are not directly exposed to containers

What it detects

Services that expose dangerous host devices via devices:. The host path is normalized before matching, so equivalent spellings of the same device node are all detected — /dev/sda, //dev/sda, /dev/./sda and /dev/../dev/sda name one device to the kernel and Compose passes each through verbatim.

Pattern Risk
/dev/sd* SCSI/SATA block devices — raw disk read/write
/dev/nvme* NVMe block devices — raw disk read/write
/dev/vd* virtio block devices — the host root disk on KVM and Proxmox guests
/dev/xvd* Xen block devices — the host root disk on EC2
/dev/mmcblk* SD/eMMC block devices — the host root disk on a Raspberry Pi
/dev/md* Linux software RAID arrays
/dev/disk/* Block device symlinks — same risk as raw devices
/dev/loop* Loop devices — mount arbitrary disk images
/dev/dm-*, /dev/mapper/* Device mapper — encrypted/LVM volumes, raw block access
/dev/zfs ZFS pool control — create/destroy/import any pool the host can see
/dev/rbd* Ceph RBD — remote block device access
/dev/kmsg Kernel log buffer — read or inject kernel messages

Safe devices like /dev/net/tun are not flagged.

Devices this rule deliberately does not claim

A device that is live only alongside a capability another rule already flags belongs to that rule (ADR-020): it could otherwise fire only beside a strictly higher finding, or alone on a configuration that grants nothing. All verified at default capabilities on Docker 29.1.3:

Device Why not here
/dev/mem, /dev/port Operation not permitted without CAP_SYS_RAWIO, which CL-0024 flags CRITICAL. /dev/mem is further bounded to the sub-1MB region even with the capability, because CONFIG_STRICT_DEVMEM refuses the rest — offsets at 1 MiB and 4 GiB both fail
/dev/fuse mount(2) needs CAP_SYS_ADMIN (CL-0024, CRITICAL), which is the gate that holds on every host. On an AppArmor host it also needs an unconfined profile (CL-0009) — measured, SYS_ADMIN alone mounts where no AppArmor policy is loaded, so that second gate is posture-specific (ADR-020) and the drop does not rest on it
/dev/kmem, /dev/raw Docker refuses to create the container at all, so a finding could never describe a running service. CONFIG_DEVKMEM is also off on modern kernels

/dev/kmsg stays despite needing CAP_SYSLOG on a host with kernel.dmesg_restrict=1: that is a host sysctl whose upstream default is 0, where the read needs no capability, and no rule flags SYSLOG.

/dev/loop* stays too, and for a stronger reason than it was first given. The loop family was assumed to need mount(2), and so CAP_SYS_ADMIN — which would have grouped it with /dev/fuse above. That is wrong for the step that matters. Captured observation (Docker 29.4.3, no premise check — see below): a container given only --device /dev/loop-control, at --cap-drop ALL, issued LOOP_CTL_GET_FREE and allocated a loop device on the host. Attaching a backing file is a further step, but the allocation itself needs no capability, which puts the loop family on the same footing as the raw block devices above. The pattern matches /dev/loop-control as well as /dev/loop0, which is what this observation exercised.

It is a capture rather than a check on purpose: automating it would create and remove a host loop device on every CI run, and the premise suite is non-destructive by design. (The removal path works — LOOP_CTL_REMOVE restored the host — it is simply not something to do on a schedule.)

Why it matters

Exposing raw memory or block devices bypasses all container isolation. Block device access enables reading and overwriting the host filesystem directly, regardless of mount permissions; loop and device-mapper access lets a container construct arbitrary block devices and mount them. ZFS and Ceph control devices grant pool/cluster-level operations that affect everything that runtime can see.

These are equivalent to giving the container full root access to the host's storage — and unlike most escape paths, this one is capability-independent. Verified on Docker 29.1.3: a container with Docker's default capabilities, holding no cap_add at all, read the first sector of the host's NVMe device through a devices: mapping. Nothing in the default capability set, the default seccomp profile, or docker-default AppArmor stands in the way; the device cgroup is the gate, and devices: is what opens it.

Fix

Remove the dangerous device. If you need specific hardware access, prefer narrower mechanisms:

  • device_cgroup_rules: — express exactly which devices the container may access
  • A single specific device entry (e.g. /dev/ttyUSB0) instead of a wildcard or whole subtree
  • Kernel-level offload (e.g. user-space NVMe via SPDK) where a container shouldn't need block-device access at all

Where the container only needs the data on a disk rather than the disk itself, mount the filesystem instead:

# Before
services:
  app:
    image: myapp:1.0
    devices:
      - /dev/sda:/dev/sda

# After — use volumes for disk data access
services:
  app:
    image: myapp:1.0
    volumes:
      - /data:/app/data:ro

If raw device access is genuinely required (e.g., for disk management tooling), the workload should not run in a container.

ATT&CK coverage

Remediating this finding contributes to mitigating the following MITRE ATT&CK techniques (pinned to ATT&CK v18). compose-lint is a static analyser, so this is mitigation coverage — it detects nothing at runtime.

Technique Tactic
T1611 Escape to Host Privilege Escalation
T1552.001 Unsecured Credentials: Credentials In Files Credential Access

See also

  • CL-0013 — bind-mounting /dev whole (parallel risk at the path-mount level)
  • CL-0002privileged: true (grants every device the host has)