| Property | Value |
|---|---|
| Severity | Warning |
| Category | Security |
| Default | Enabled |
| Auto-fix | No |
Description
This rule detects Dockerfiles where the final stage runs as root (either explicitly viaUSER root/USER 0 or implicitly by having no USER
instruction) and the stage positively signals mutable or persistent
state through VOLUME instructions or data/state directory patterns.
Running as root in a container that manages persistent state is a
higher-risk combination than running root without state. Root access over
writable volumes, mounted sockets, log directories, and database storage
increases the blast radius of a compromise: an attacker can corrupt
persistent data, tamper with host-mounted files, or escalate via
root-owned resources that outlive the container.
This rule is more targeted than hadolint/DL3002 (“last USER should not be
root”). DL3002 warns whenever USER root appears, regardless of what the
container does. This rule only fires when root intersects with mutable
state.
Stateful signals detected
VOLUMEinstructions (highest confidence)WORKDIRpaths matching data/state directoriesCOPY/ADDdestinations to data/state directoriesRUN mkdircreating data/state directories
/data, /srv, /var/lib/*, /var/log/*,
/var/cache/*, /var/run/*, /var/spool/*.
Suppression
The rule is automatically suppressed when:- A privilege-drop tool is referenced in ENTRYPOINT or CMD (
gosu,su-exec,suexec,setpriv). These are unambiguous privilege-drop executables. Generic script names likedocker-entrypoint.shorentrypoint.share not treated as suppression signals because they could do anything without inspecting their content. - The base image is known to default to non-root: Distroless
:nonroottags, Chainguard/cgr.dev images. - The effective final
USERis non-root. - The final stage inherits from a local stage whose last
USERis non-root.
Relationship to hadolint/DL3002
hadolint/DL3002 | tally/stateful-root-runtime | |
|---|---|---|
| Fires when | Last USER is explicitly root | Effective user is root (explicit or implicit) and stateful signal exists |
| Scope | Any root USER in final stage | Root + state combination only |
| Privilege-drop aware | No | Yes (suppresses for gosu/su-exec patterns) |
| Non-root base aware | N/A (only checks explicit USER) | Yes (suppresses for distroless:nonroot, chainguard) |
USER root + VOLUME /data triggers both). This is intentional:
- DL3002 gives a broad “consider non-root” nudge.
- This rule highlights the specific elevated-risk combination of root + state.
EnabledRules coordination because they
serve different purposes and neither has fixes that could overlap. If you want
only the targeted warning, disable DL3002 and keep this rule.
References
- Dockerfile reference — USER
- Dockerfile reference — VOLUME
- Docker Blog — Understanding the Docker USER Instruction
- Chainguard Best Practices