docs(worker): note world-readable /proc (cmdline/args) exposure of docker jobs

The per-job-podman bind-mount escape can read world-readable host state — including
world-readable /proc (process cmdlines/args via `-v /proc`), other job dirs'
world-readable files, and the dep cache — but NOT 0400 files like /proc/<pid>/environ
(env secrets stay protected on a root worker). Document this in the security notes
(README + compose + code comment) with the corollary: don't pass secrets as
command-line args (windmill uses env). Verified: uid-1000 container via `-v /proc`
reading a root process's environ -> 'Permission denied'; its cmdline -> readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-06-05 01:48:57 +00:00
co-authored by Claude Opus 4.8
parent cd1930c06a
commit 6269b1e86f
3 changed files with 19 additions and 12 deletions
+7 -3
View File
@@ -234,9 +234,13 @@ Go to http://localhost - default credentials: `admin@windmill.dev` / `changeme`
> etc.) stay root-owned — unreadable by a `docker run --pid=host` container. What this
> does **not** do is confine the container's *filesystem* view: the daemon runs outside
> the job's nsjail sandbox, so a `# docker` script can still `docker run -v <worker path>`
> to reach other concurrent job dirs / caches. So treat docker-capable workers as a
> **trusted-tenant** capability (a big improvement over privileged dind, but not a full
> sandbox) and prefer dedicated/per-workspace docker workers on shared multi-tenant fleets.
> to reach **world-readable** host state: other concurrent job dirs' world-readable files,
> the dependency cache, and world-readable `/proc` (e.g. `-v /proc` → process `cmdline`s /
> args) — though **not** `0400` files like `/proc/<pid>/environ`, so the worker's env
> secrets stay protected. (Corollary: don't pass secrets as command-line args; windmill
> uses env, not args.) So treat docker-capable workers as a **trusted-tenant** capability
> (a big improvement over privileged dind, but not a full sandbox) and prefer
> dedicated/per-workspace docker workers on shared multi-tenant fleets.
> If you enable **nsjail** (it's off by default), per-job podman is **not** auto-provided
> for docker jobs — nsjail means "fully sandbox jobs" and the daemon runs outside the jail,
> so docker jobs then require an explicit `DOCKER_HOST` or a separate non-nsjail worker group.
+7 -7
View File
@@ -654,14 +654,14 @@ fn spawn_docker_storage_monitor(
// SECURITY: this is only ever started in NON-nsjail modes (unshare/none) — caller
// refuses to auto-provide it under nsjail, since the daemon runs outside any sandbox.
// A `# docker` script controls this daemon over the Docker API and can bind-mount any
// path the daemon user can read (e.g. `docker run -v /tmp/windmill/...`), reaching
// other concurrent job dirs / caches — so it is NOT a filesystem sandbox, and
// docker-capable workers stay a TRUSTED-TENANT capability (prefer dedicated/per-
// WORLD-READABLE host path (e.g. `docker run -v /tmp/windmill/...` for other job dirs /
// caches, or `-v /proc` for process cmdlines/args) — so it is NOT a filesystem sandbox,
// and docker-capable workers stay a TRUSTED-TENANT capability (prefer dedicated/per-
// workspace docker workers on shared fleets). What IS protected, when the worker runs
// as root (the default) and the daemon is dropped to a non-root uid here: the worker's
// secrets in `/proc/<worker>/environ` (DATABASE_URL etc.) stay root-owned and
// unreadable by the uid-1000 container even via `docker run --pid=host`, and an escape
// is unprivileged.
// as root (the default) and the daemon is dropped to a non-root uid here: `0400` files
// like the worker's `/proc/<worker>/environ` (DATABASE_URL etc.) stay root-owned and
// unreadable by the uid-1000 container even via `-v /proc` or `--pid=host` (so don't
// pass secrets as command-line args), and a container escape is unprivileged.
#[cfg(feature = "dind")]
async fn start_per_job_podman(
job_dir: &str,
+5 -2
View File
@@ -70,8 +70,11 @@ services:
# rootless AND the worker's own secrets in /proc (DATABASE_URL etc.) stay
# root-owned, unreadable by a `docker run --pid=host` container. (NB: this does
# not confine the container's filesystem view — a `# docker` script can still
# `docker run -v` worker-visible paths, so treat docker workers as trusted-tenant
# / dedicate them on shared fleets.) Cap per-job image storage via the
# `docker run -v` to read WORLD-READABLE host state: other job dirs' world-readable
# files, the dep cache, and world-readable /proc like process cmdlines/args — though
# NOT 0400 files like /proc/<pid>/environ (env secrets stay safe; don't pass secrets
# as args). So treat docker workers as trusted-tenant / dedicate them on shared
# fleets.) Cap per-job image storage via the
# `docker_image_storage_size_mb` instance setting (default 8GB).
# If you enable nsjail (DISABLE_NSJAIL=false or job isolation = nsjail), per-job
# podman is NOT auto-provided for docker jobs — nsjail signals "fully sandbox jobs"