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:
_cl0016—devices:exposes the host device, verified live. The grant is capability-independent: a raw read of the host's NVMe device through--devicesucceeded at default capabilities, where the same read through a/devbind 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 |