Rule catalog
All 50 deterministic checks the auditor runs, grouped by category. Rule set 1.0.0+50r.
Security
Container runs as root (no non-root USER)
Why it matters: A root process that escapes the container, or a compromised app, has far more power on the host. Running as an unprivileged user is a baseline hardening control.
Fix: Create and switch to a non-root user in the final stage: `RUN adduser -D app` then `USER app`.
Reference ↗Final stage explicitly runs as root
Why it matters: Explicitly selecting root in the runtime stage removes the isolation benefit of an unprivileged user and is rarely necessary for a running service.
Fix: Drop back to a non-root user after any root-only setup: end the stage with `USER app`.
Secret passed through a build `ARG`
Why it matters: ARG values are visible in image history and build logs. Build secrets leak this way constantly.
Fix: Use BuildKit secrets (`RUN --mount=type=secret …`) instead of ARG for credentials.
Secret baked into an `ENV`
Why it matters: ENV values are baked into the image and visible via `docker inspect` and image history to anyone who can pull the image.
Fix: Inject secrets at runtime (Docker/Compose secrets, orchestrator secret store), never in the image.
Sensitive file copied into the image
Why it matters: Copying secrets into an image bakes them into a distributable artifact — anyone who pulls the image gets the credential.
Fix: Remove the file from the build context (add it to `.dockerignore`) and inject the secret at runtime.
`sudo` installed or used in build
Why it matters: sudo inside a container is an anti-pattern: it enables privilege escalation and is unnecessary when you control the USER directly.
Fix: Perform root-only steps before switching USER; remove sudo from the image.
`ADD` fetches a remote URL
Why it matters: ADD-from-URL fetches unverified remote content at build time with no checksum, and does not use the build cache well.
Fix: Use `RUN curl -fsSL <url> -o file && echo "<sha> file" | sha256sum -c` or fetch via a package manager.
World-writable permissions (`chmod 777`)
Why it matters: World-writable files let any process (or attacker) in the container modify code and data, undermining integrity.
Fix: Grant the least permission needed (e.g. `chmod 750`) and set ownership with `COPY --chown`.
`.git` directory copied into the image
Why it matters: A shipped `.git` leaks full source history, branch names, and sometimes committed secrets, and bloats the image.
Fix: Add `.git` to `.dockerignore`; never COPY it explicitly.
Service runs in privileged mode
Why it matters: Privileged mode disables almost all container isolation — the container can access every device and effectively act as root on the host. A compromise becomes a host compromise.
Fix: Remove `privileged: true`. Grant only the specific capabilities you need via `cap_add`.
Docker socket mounted into a container
Why it matters: Access to the Docker socket is root-equivalent on the host: a process with the socket can start a privileged container and take over the machine.
Fix: Avoid mounting the socket. If unavoidable, use a read-only, filtered socket proxy that exposes only the endpoints needed.
Sensitive host path bind-mounted
Why it matters: Mounting host system paths lets the container read or modify host configuration and secrets, breaking isolation.
Fix: Mount only the specific sub-path you need, read-only (`:ro`) where possible; prefer named volumes.
Service uses host network mode
Why it matters: Host networking removes network namespace isolation: the container shares the host’s interfaces and can bind any port, bypassing Compose’s network controls.
Fix: Use a user-defined bridge network and publish only the ports you need.
Service shares host PID or IPC namespace
Why it matters: Sharing the host PID/IPC namespace lets the container see and signal host processes and access host shared memory — a serious isolation break.
Fix: Remove `pid: host` / `ipc: host` unless a specific, reviewed need exists.
Dangerous Linux capability added
Why it matters: Powerful capabilities can be used to escape the container or tamper with the host. `SYS_ADMIN` in particular is close to full root.
Fix: Grant the narrowest capability that works, or redesign to avoid it. Never use `cap_add: [ALL]`.
Missing `no-new-privileges`
Why it matters: Without this flag, a setuid binary inside the container can still gain privileges. Setting it is a cheap, broadly-safe hardening default.
Fix: Add `security_opt: ["no-new-privileges:true"]` to the service.
Hard-coded secret in environment
Why it matters: Secrets committed in Compose files leak through version control, CI logs, and `docker inspect`. Anyone with the repo has the credential.
Fix: Reference secrets via `${VAR}` from an untracked `.env`, or use Docker/Compose `secrets:` backed by an external store.
Database/backend port published to all interfaces
Why it matters: Publishing a database port to 0.0.0.0 exposes it to the network (and often the internet). Backends should be reachable only over the internal Compose network.
Fix: Remove the host port mapping and let other services reach it over the Compose network, or bind to `127.0.0.1` only.
Root filesystem is writable
Why it matters: A read-only root filesystem blocks a large class of tampering and persistence techniques; combine it with tmpfs for the few writable paths a service needs.
Fix: Set `read_only: true` and add `tmpfs:` (or named volumes) for directories that must be writable.
`.dockerignore` missing sensitive exclusions
Why it matters: An incomplete ignore file still lets secrets (`.env`) and history (`.git`) slip into the image via `COPY . .`.
Fix: Add the missing entries to `.dockerignore` (`.git`, `.env`, `.env.*`, `node_modules`).
Reliability
No HEALTHCHECK defined
Why it matters: Without a health check, orchestrators cannot tell a hung-but-running container from a healthy one, so traffic keeps flowing to a broken instance.
Fix: Add a HEALTHCHECK that probes a real readiness endpoint, e.g. `HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1`.
Reference ↗HEALTHCHECK explicitly disabled
Why it matters: Disabling the health check removes the orchestrator’s only signal that the container is actually serving.
Fix: Replace `HEALTHCHECK NONE` with a real probe unless a sidecar owns health checking.
Shell-form ENTRYPOINT/CMD breaks signal handling
Why it matters: Shell form runs your process as a child of `/bin/sh -c`, which does not forward SIGTERM — so graceful shutdown and zero-downtime deploys break.
Fix: Use exec (JSON-array) form: `ENTRYPOINT ["node", "server.js"]`.
No default command (CMD/ENTRYPOINT)
Why it matters: An image with no default command relies on callers always supplying one, which is fragile and undocumented.
Fix: Declare the process the image should run with CMD or ENTRYPOINT (exec form).
No restart policy
Why it matters: Without a restart policy a crashed container stays down until someone notices — the opposite of production self-healing.
Fix: Add `restart: unless-stopped` (or `always`) for long-running services.
No healthcheck
Why it matters: Without a health check, `depends_on: condition: service_healthy` cannot work and orchestrators route traffic to not-yet-ready or hung containers.
Fix: Add a `healthcheck:` that probes a readiness endpoint or a CLI check for the service.
`depends_on` without health conditions
Why it matters: Short `depends_on` waits for the container to START, not to be READY, so apps routinely crash on boot racing a not-yet-ready database.
Fix: Use the long form with `condition: service_healthy` and give the dependency a healthcheck.
Stateful service without a named volume
Why it matters: Without persistent named storage, a container recreate wipes the data — catastrophic for a database.
Fix: Mount a named volume at the data path (e.g. `pgdata:/var/lib/postgresql/data`) and declare it under top-level `volumes:`.
Performance
`apt-get update` not chained with `install`
Why it matters: Separate update/install layers cause stale package caches (the classic Docker caching bug) and can pull outdated or missing packages.
Fix: Chain them: `RUN apt-get update && apt-get install -y --no-install-recommends … && rm -rf /var/lib/apt/lists/*`.
Package cache not cleaned
Why it matters: Leftover package lists and caches bloat every downstream layer permanently — they cannot be removed by a later RUN.
Fix: Append cache cleanup to the install layer (`rm -rf /var/lib/apt/lists/*`, `apk --no-cache`, `yum clean all`).
Missing `--no-install-recommends`
Why it matters: Recommended packages pull in extra, often unneeded software that enlarges the image and widens the attack surface.
Fix: Add `--no-install-recommends` to `apt-get install`.
Single-stage build ships build tooling
Why it matters: Compilers, dev headers, and package caches in the final image bloat it and expand the attack surface. A multi-stage build ships only the artifact.
Fix: Split into a `build` stage and a slim runtime stage that `COPY --from=build` only the artifacts.
No memory limit
Why it matters: An unbounded container can consume all host memory and OOM-kill its neighbours — one leaky service takes down the box.
Fix: Set a memory limit sized to the workload (e.g. `mem_limit: 512m`).
No CPU limit
Why it matters: Without a CPU limit a busy service can starve everything else on the host of CPU time.
Fix: Set a CPU limit (e.g. `cpus: "1.5"`).
Observability
Missing OCI image labels
Why it matters: Standard labels (source, revision, version, description) make images traceable back to code and are used by registries and scanners.
Fix: Add OCI labels, e.g. `LABEL org.opencontainers.image.source="https://github.com/org/repo"`.
Reference ↗Unbounded container logging
Why it matters: The default json-file driver grows without bound and can fill the host disk, taking every container down with it.
Fix: Configure log rotation: `logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }`.
Missing service labels
Why it matters: Labels make containers discoverable and groupable by monitoring, log pipelines, and cost tooling.
Fix: Add labels such as `com.example.project`, `environment`, and `owner`.
Maintainability
Deprecated `MAINTAINER` instruction
Why it matters: MAINTAINER is deprecated in favour of a LABEL, which is structured and queryable.
Fix: Replace with `LABEL org.opencontainers.image.authors="name <email>"`.
No WORKDIR set
Why it matters: Relying on `/` or an implicit directory makes relative paths ambiguous and encourages accidental writes to the root filesystem.
Fix: Set an explicit `WORKDIR /app` before COPY/RUN steps.
Broad `COPY . .` without a `.dockerignore`
Why it matters: Copying everything drags in `.git`, `node_modules`, local env files, and build junk — bloating the image, busting the cache on every change, and risking secret leakage.
Fix: Add a `.dockerignore` excluding `.git`, `node_modules`, `.env*`, build output, and other non-runtime files.
Deployment Readiness
Development/debug mode in the image
Why it matters: Development servers are single-threaded, insecure, and slow, and debug modes can leak internals. They should never run in production.
Fix: Use the production runtime (e.g. `gunicorn`, `node server.js`, a compiled server) and set `NODE_ENV=production`.
Privileged port exposed while running non-root
Why it matters: Binding ports < 1024 requires extra privileges (CAP_NET_BIND_SERVICE). A non-root process may fail to bind, causing a confusing startup failure.
Fix: Listen on a high port (e.g. 8080) and map it at publish time, or grant the capability explicitly.
Source-code bind mount (development pattern)
Why it matters: Source bind mounts are a development convenience that, in production, overwrite the image’s code with whatever is on the host — undermining immutable, reproducible deploys.
Fix: Bake code into the image; keep bind mounts in a separate `compose.override.yml` used only for local development.
Fixed `container_name` prevents scaling
Why it matters: A fixed container name means the service cannot be scaled to more than one replica and collides across environments on the same host.
Fix: Remove `container_name` and let Compose name containers from the project + service + replica index.
Supply Chain
Base image uses a mutable `latest` tag
Why it matters: Mutable tags make builds non-reproducible: the same Dockerfile can produce different images over time, defeating rollbacks and making incidents hard to reproduce.
Fix: Pin an explicit version tag (e.g. `node:20.11-alpine`) and, ideally, a digest.
Reference ↗Base image has no explicit tag
Why it matters: An untagged image is an implicit `latest` — the same reproducibility problem, only less visible.
Fix: Add an explicit version tag to every FROM that pulls a remote image.
Base image is not pinned by digest
Why it matters: Even version tags can be re-pushed. A `@sha256:` digest guarantees the exact image bytes, which matters for supply-chain integrity and reproducible builds.
Fix: Append a digest: `FROM node:20.11-alpine@sha256:…`. Automate updates with a tool like Renovate.
Remote script piped straight into a shell
Why it matters: Curl-pipe-shell runs unreviewed, unpinned remote code at build time — a classic supply-chain and tampering risk. There is no integrity check on what gets executed.
Fix: Download to a file, verify a checksum or signature, then execute. Prefer official packages.
`ADD` used where `COPY` is safer
Why it matters: ADD has surprising behaviour (URL fetches, tar auto-extraction). COPY is explicit and predictable; reserve ADD for cases that need its features.
Fix: Replace `ADD <local> <dest>` with `COPY <local> <dest>` unless you rely on ADD’s extraction.
Service image uses `latest`
Why it matters: Mutable image tags make deployments non-reproducible and break rollbacks — you can’t redeploy the exact image that was running.
Fix: Pin an explicit version tag (and ideally a digest) for every service image.