|
| 1 | +# Per-container Isolation Posture |
| 2 | + |
| 3 | +**Introduced:** v8.94.0 |
| 4 | +**Always on. No configuration.** |
| 5 | + |
| 6 | +Docker Dash has detected sandboxed OCI runtimes (Kata Containers, gVisor/runsc, |
| 7 | +Firecracker) at the **host** level since v8.8.1, badging them on the System page. |
| 8 | +That answers "is a stronger runtime available here?" but not the question that |
| 9 | +follows: **which containers aren't using it?** |
| 10 | + |
| 11 | +This feature is the per-container half. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 1. Two surfaces, deliberately different audiences |
| 16 | + |
| 17 | +| Surface | Who sees it | When | |
| 18 | +|---------|-------------|------| |
| 19 | +| **Container detail → Isolation card** | Everyone | Always | |
| 20 | +| **Security Posture finding** | Operators with a sandboxed runtime installed | Only when actionable | |
| 21 | + |
| 22 | +The card is unconditional: it shows which runtime backs the container, whether |
| 23 | +that runtime is sandboxed, and — if the container can reach past it — exactly how. |
| 24 | +That is useful whether or not you run gVisor. |
| 25 | + |
| 26 | +The posture finding is conditional, on purpose. See §3. |
| 27 | + |
| 28 | +## 2. What counts as "reach" |
| 29 | + |
| 30 | +Ways a container can reach past its runtime toward the host: |
| 31 | + |
| 32 | +| Signal | Severity | |
| 33 | +|--------|:--------:| |
| 34 | +| Privileged | critical | |
| 35 | +| Docker socket mounted | critical | |
| 36 | +| `CapAdd=ALL`, `SYS_MODULE` | critical | |
| 37 | +| `SYS_ADMIN`, `SYS_RAWIO` | high | |
| 38 | +| Host PID namespace | high | |
| 39 | +| `seccomp=unconfined`, `apparmor=unconfined` | high | |
| 40 | +| `SYS_PTRACE`, `SYS_BOOT`, `NET_ADMIN`, `DAC_READ_SEARCH` | medium | |
| 41 | +| Host network / IPC namespace | medium | |
| 42 | +| SELinux `label=disable` | medium | |
| 43 | + |
| 44 | +Capability spelling is normalised (`SYS_ADMIN`, `cap_sys_admin`, `CAP_SYS_ADMIN` |
| 45 | +are one signal) and de-duplicated. When a container is already privileged its |
| 46 | +capabilities are **not** re-listed — privileged implies all of them, and |
| 47 | +enumerating them again is noise. |
| 48 | + |
| 49 | +Disabled seccomp/AppArmor/SELinux confinement and Docker-socket mounts are |
| 50 | +reported **per container** here for the first time; the CIS benchmark checks |
| 51 | +seccomp and AppArmor on the daemon, not per workload. |
| 52 | + |
| 53 | +## 3. Why the posture check is usually silent |
| 54 | + |
| 55 | +A finding is raised only when all three hold: |
| 56 | + |
| 57 | +1. the container has host-level reach, **and** |
| 58 | +2. it is not already on a sandboxed runtime, **and** |
| 59 | +3. the host has a sandboxed runtime registered. |
| 60 | + |
| 61 | +Condition 3 means most estates see nothing. That is the intended behaviour. |
| 62 | +Without it, the remediation would read "go install gVisor and reconfigure your |
| 63 | +daemon" — a large ask, and one the CIS benchmark already opens by flagging the |
| 64 | +privileged container on its own terms. **Docker Dash does not nag you about |
| 65 | +software you have not installed.** |
| 66 | + |
| 67 | +The same condition is evaluated from one cached `docker info` *before* any |
| 68 | +container work, so on hosts without a sandboxed runtime the per-container inspect |
| 69 | +loop never runs. The product decision and the performance guard are the same line. |
| 70 | + |
| 71 | +Per-host scan cap: 200 running containers. When it truncates, the finding says so |
| 72 | +rather than silently under-reporting. |
| 73 | + |
| 74 | +## 4. Relationship to the CIS benchmark |
| 75 | + |
| 76 | +They are complementary, not overlapping: |
| 77 | + |
| 78 | +- **CIS** reports privileged / `CapAdd` / `PidMode=host` as failures in their own |
| 79 | + right — *the door is open*. |
| 80 | +- **Isolation posture** treats those same switches as **inputs** and asks a |
| 81 | + different question — *you own a lock and haven't used it*. |
| 82 | + |
| 83 | +One finding per container, never one per switch. Severity is capped at **high** |
| 84 | +even for critical reach, because the verdict is "this could be contained better", |
| 85 | +not "this is breached" — inflating it would make the posture score double-count |
| 86 | +what CIS already counted. |
| 87 | + |
| 88 | +## 5. Remediation is guidance, not a button |
| 89 | + |
| 90 | +The finding suggests recreating the container with `--runtime=<sandboxed>` |
| 91 | +(Compose: `runtime:` on the service), and warns that Kata and gVisor do not |
| 92 | +support every syscall or device passthrough. There is no one-click fix: moving a |
| 93 | +workload onto a sandboxed runtime can break it, and Docker Dash does not make |
| 94 | +that call for you. Same principle as the exposed-port check, which refuses to |
| 95 | +close ports on your behalf. |
| 96 | + |
| 97 | +If the workload must stay on the shared kernel, the alternative remediation is to |
| 98 | +reduce the reach — drop capabilities, remove the privileged flag or the socket |
| 99 | +mount. |
| 100 | + |
| 101 | +## 6. API |
| 102 | + |
| 103 | +``` |
| 104 | +GET /api/containers/:id/isolation |
| 105 | +→ { runtime, sandboxed, sandboxAvailable, sandboxOptions, signals, severity, actionable } |
| 106 | +``` |
| 107 | + |
| 108 | +`requireAuth`, read-only. This endpoint exists so the runtime taxonomy (which |
| 109 | +names count as sandboxed) stays in one place — `_categorizeRuntimes` in |
| 110 | +`src/services/docker.js` — instead of being duplicated in the frontend. |
| 111 | + |
| 112 | +## 7. Limitations |
| 113 | + |
| 114 | +- **Docker and Podman only.** Kubernetes, Nomad and Incus have their own |
| 115 | + isolation models (RuntimeClass, task drivers); mapping them onto Docker's |
| 116 | + `HostConfig.Runtime` would be a guess. |
| 117 | +- **Running containers only.** A stopped container has no reach. |
| 118 | +- **User-namespace remapping is shown but not scored.** `userns-remap` is off by |
| 119 | + default nearly everywhere, and a signal that fires on almost every container is |
| 120 | + not a signal. |
| 121 | +- **`youki` is classified as sandboxed** by the pre-existing host-level runtime |
| 122 | + patterns, which is arguable — it is a standard OCI runtime written in Rust. Left |
| 123 | + unchanged because that classification also drives the System page badge. |
0 commit comments