Skip to main content
Final stage creates a user but never switches to it.
PropertyValue
SeverityWarning
CategorySecurity
DefaultEnabled
Auto-fixYes (unsafe)

Description

This rule detects Dockerfiles where the final stage (or its FROM ancestry chain) creates a dedicated user via useradd or adduser, but the effective runtime identity stays root and no privilege-drop entrypoint pattern is detected. This is a high-signal indicator of an incomplete hardening attempt or cargo-culted user setup. On Windows containers, the rule also detects net user /add and New-LocalUser commands. The USER instruction sets the default process identity for the container at runtime. Creating a user without switching to it means the container runs as root despite the preparation work.

Suppression

The rule is automatically suppressed when:
  • The effective USER in the final stage is non-root.
  • A privilege-drop tool is referenced in ENTRYPOINT or CMD (gosu, su-exec, suexec, setpriv).
  • The base image is known to default to non-root: Distroless :nonroot tags, Chainguard/cgr.dev images, or local stage refs whose parent stage sets a non-root USER.
  • The created user is referenced in an ownership or permissions context:
    • Linux: COPY --chown, ADD --chown, or RUN chown
    • Windows: icacls /grant <user>, icacls /setowner <user>, New-Object ...AccessRule("<user>", ...)
    This indicates deliberate permissions orchestration rather than a forgotten step.

Cross-stage inheritance

The rule walks the FROM <stage> ancestry chain. If a parent stage creates a user that flows into the final image (via FROM), the rule detects it. User creation in stages referenced only by COPY --from does not trigger the rule, since COPY does not inherit /etc/passwd.

Relationship to other rules

hadolint/DL3002tally/stateful-root-runtimetally/user-created-but-never-used
Fires whenLast USER is explicitly rootRoot + stateful signal (VOLUME, data dirs)User created but never switched to
ScopeExplicit root USER onlyRoot + state combinationUser creation without USER switch
Privilege-drop awareNoYesYes
Ownership suppressionN/AN/AYes (—chown, chown, icacls)
The rules are complementary and may fire on the same Dockerfile. Neither suppresses the other.

Auto-fix

The rule offers an unsafe auto-fix (requires --fix-unsafe) that inserts USER <created_user> before the first ENTRYPOINT or CMD in the final stage. The fix is marked unsafe because:
  • Subsequent instructions might require root.
  • A privilege-drop pattern (gosu) might be more appropriate.
  • The inserted USER might not be the correct resolution in all cases.

References

Examples

Bad

# User created but never switched to
FROM ubuntu:22.04
RUN useradd -r appuser
CMD ["app"]
# User created in parent stage, never activated
FROM ubuntu:22.04 AS base
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

FROM base
CMD ["app"]
# Windows: user created but never switched to
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN net user appuser P@ssw0rd /add
CMD ["cmd", "/C", "app.exe"]

Good

# User created and activated
FROM ubuntu:22.04
RUN useradd -r -u 1000 appuser
USER appuser
CMD ["app"]
# Privilege-drop entrypoint (official image pattern)
FROM ubuntu:22.04
RUN useradd -r appuser
ENTRYPOINT ["gosu", "appuser", "docker-entrypoint.sh"]
CMD ["postgres"]
# User created and used for file ownership (suppressed)
FROM ubuntu:22.04
RUN useradd -r appuser
COPY --chown=appuser:appuser app /app
CMD ["app"]
# Numeric non-root UID (no useradd needed)
FROM gcr.io/distroless/static:nonroot
COPY app /app
CMD ["/app"]
# Windows: user created and used for ACL permissions (suppressed)
FROM mcr.microsoft.com/windows/servercore:ltsc2022
RUN net user appuser P@ssw0rd /add
RUN icacls C:\app /grant appuser:(OI)(CI)F
CMD ["cmd", "/C", "app.exe"]

Configuration

[rules.tally.user-created-but-never-used]
severity = "warning"  # Options: "off", "error", "warning", "info", "style"