# CL-0018: Explicit Root User

**Severity:** MEDIUM

**References:**
- [OWASP Docker Security Rule #2](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-2-set-a-user)
- [Docker security: rootless mode and userns-remap](https://docs.docker.com/engine/security/userns-remap/)

## What it detects

Any service whose `user:` value has a root user portion — `root` or `0` — regardless of the group. This covers `user: root`, `user: "0"`, `user: "root:root"`, and `user: "0:0"`, but also `user: "root:1000"`, `user: "0:staff"`, and any other `root:<group>` / `0:<gid>` form (only the part before the `:` is checked). Services without a `user:` directive are not flagged — the absence of `user:` does not imply root, as the image's `USER` instruction determines the UID.

## Why it matters

Most popular Docker Hub images — including official `nginx`, `redis`, `postgres`, `mysql`, and most `*:alpine` / `*:slim` variants — start as root and switch to an unprivileged user via the entrypoint (often using `gosu` or `su-exec`). Explicitly setting `user: root` keeps the container as UID 0 for the workload itself, undoing whichever step in that chain otherwise drops privileges. A container running as UID 0:
- Has full write access to all bind-mounted host paths (subject to the userns caveat below)
- Can exploit kernel vulnerabilities more easily (many require `CAP_SYS_ADMIN` or UID 0)
- Amplifies the impact of every other misconfiguration (e.g., `privileged`, `cap_add`, host mounts)

This rule catches the explicit override case. It does not flag the absence of `user:` — that would be unactionable without knowing the image's intended UID. Tools like Trivy and hadolint take the opposite tack and flag absence; compose-lint deliberately stays narrow to keep findings actionable.

### Userns / rootless interaction

If the daemon is configured with `userns-remap` or you're running rootless Docker, container UID 0 maps to a high-numbered host UID (e.g. `100000`), and bind-mount writes obey host permissions for *that* UID — not host root. Explicit `user: root` is still a finding in that case because:

- Most installations do *not* configure userns-remap (it's opt-in and breaks many workloads).
- Even with remap, UID 0 inside the container still owns most of the in-container filesystem and amplifies kernel-bug exploitability.
- A future operator who toggles remap off (e.g. on a different host) silently inherits a more dangerous configuration.

The rule fires regardless of daemon config; suppress only when you have verified the threat model on a specific host.

## Fix

Remove the `user:` directive to respect the image's built-in `USER` instruction, or set an explicit non-root user:

```yaml
# Before
services:
  app:
    image: myapp:1.0
    user: root

# After — let the image decide
services:
  app:
    image: myapp:1.0

# Or — set a specific non-root user
services:
  app:
    image: myapp:1.0
    user: "1000:1000"
```

## See also

- [CL-0010](CL-0010.md) — `userns_mode: host` (disables daemon-level userns-remap, related to the caveat above)
- [CL-0003](CL-0003.md) — `no-new-privileges:true` (limits privilege gain via `execve` regardless of starting UID)
