---
name: audit-dockerfile
description: Audit one or more Dockerfiles for security issues. Use when reviewing a Dockerfile for secrets baked into image layers, hardcoded credentials, missing USER directive, unpinned base images, multi-stage secret leakage, or missing .dockerignore. Outputs prioritized findings with remediation snippets. NEVER echoes secret values.
argument-hint: "[path/to/Dockerfile]"
---

# Audit Dockerfile Security

Audit one or more Dockerfiles against the 9-check security checklist. Output findings inline, organized by severity, with remediation snippets.

## CRITICAL SAFETY RULE

**NEVER echo, display, or repeat the actual value of any secret, password, API key, token, or credential found in a Dockerfile.** If a finding involves a secret value:
- Report only the variable name and line number
- Describe the type of secret (e.g., "AWS IAM key ID", "database password", "SMTP credential")
- Do NOT include the literal value anywhere in the output

## Input

The path to the Dockerfile is: `$ARGUMENTS`

If `$ARGUMENTS` is empty, auto-detect all Dockerfiles in the current directory and up to 2 levels deep. Search for files named:
- `Dockerfile`
- `Dockerfile.<suffix>` (e.g., `Dockerfile.prod`, `Dockerfile.dev`)

Search paths: `.`, `*/`, `*/*/`

List all Dockerfiles found at the start of the output. Audit each one. If none are found, report that no Dockerfiles were detected and ask the user to provide the path.

## Checklist

Read the full checklist from `${CLAUDE_SKILL_DIR}/references/checklist.md`. It contains 9 checks with pass/fail criteria and remediation examples.

## Analysis Steps

Read each Dockerfile. Parse all instructions, paying attention to stage boundaries (`FROM ... AS <stage>`), build argument declarations (`ARG`), environment variable assignments (`ENV`), file copies (`COPY`, `ADD`), and run commands (`RUN`).

For each check, determine:
- **PASS** — the Dockerfile meets the pass condition
- **FAIL** — the Dockerfile violates the check (record a finding)
- **SKIP** — the check is not applicable (explain why)

When multiple Dockerfiles are audited, report all findings together, labelled by file path.

### Dual-Stage Build Context (read before Check 1 and Check 6)

Gemma client Dockerfiles commonly use a dual-stage pattern:
```dockerfile
FROM apache/airflow:x.y.z AS dev_build
ENV AIRFLOW__CORE__FERNET_KEY=<dev-only-key>   # intentional dev default
# ...
FROM dev_build AS prod_build
ARG fernet_key
ENV AIRFLOW__CORE__FERNET_KEY=${fernet_key}    # overrides with real key at build time
```

**Design intent**: `dev_build` contains throw-away credentials for local use. `prod_build` overrides them with real secrets injected as build args.

**The real danger**: Building with `--target prod_build` bakes production secrets into image layers. `docker history --no-trunc` will reveal the production key even after the override. Both the dev key (from dev_build) and the prod key (from prod_build's ARG→ENV) appear in image history.

**Correct production pattern** (saxonia-style): Build with `--target dev_build` and inject ALL secrets at runtime via `env_file: .env` (rendered by Ansible/1Password at deploy time). The production image never contains secrets.

Apply these severity rules:
- Hardcoded credentials in a `dev_build`/`development`/`local` named stage → **Important** (intentional dev-only, note the stage name, recommend runtime injection approach)
- ARG→ENV promotion in a `prod_build`/`production`/`release`/final stage → **Critical** (production secrets baked into image layers)
- If it cannot be determined which stage is used for production → **Critical**

### Check 1: Secrets in ARG/ENV

Scan all `ARG` and `ENV` instructions.

Identify ARG names that suggest secrets:
- Names containing: `KEY`, `SECRET`, `PASSWORD`, `PASS`, `TOKEN`, `FERNET`, `CONN`, `DSN`, `CREDENTIAL`, `AUTH`, `SMTP`, `PRIVATE`

Then check if any such ARG is subsequently assigned to an `ENV` in the same stage or a later stage in the same file. An assignment looks like:
- `ENV SOME_VAR=${arg_name}` or `ENV SOME_VAR=$arg_name`

For each ARG→ENV pairing found, apply the dual-stage severity rules above:
- In a production/final stage → **Critical**
- In a dev-only stage → **Important**

Report: the ARG name, the ENV name, the stage name, and the line numbers of both instructions. Do NOT report any default value that may be present in the `ARG` declaration.

Note: once a secret is assigned to `ENV`, it is permanently baked into that image layer and visible in `docker history` and `docker inspect` — even after a downstream stage overrides it.

Also flag standalone `ENV` instructions that assign directly to sensitive-sounding variable names (not via ARG), as these may contain hardcoded values — see Check 2.

### Check 2: Hardcoded Credentials in Plaintext

Scan all `ENV`, `RUN`, and `LABEL` instructions for patterns that indicate hardcoded credentials.

Flag as **Critical** findings (report variable name, line number, and credential type — NOT the value):

- **AWS IAM key IDs**: values matching the pattern `AKIA[A-Z0-9]{16}` in any ENV assignment
- **Passwords in ENV**: variable names containing `PASSWORD`, `PASS`, `SECRET`, `TOKEN` that have a non-empty non-variable value (i.e., a literal string, not `${SOME_VAR}`)
- **Email addresses in ENV**: values matching `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` assigned in `ENV` instructions
- **Connection strings with credentials**: values matching patterns like `postgresql://user:pass@`, `mysql://user:pass@`, `amqp://user:pass@` in any ENV
- **URLs with embedded tokens**: URLs containing query parameters like `?token=`, `?key=`, `?api_key=`, `?secret=`
- **S3 paths encoding account/environment info**: `s3://` paths hardcoded in ENV (note these as informational — not credentials, but environment-specific config that should not be in image layers)

For each finding, report:
- The instruction type and line number
- The variable name
- The credential type detected
- A one-sentence explanation of the risk
- Do NOT report the actual value

### Check 3: Sensitive Files COPYed into Image

Scan all `COPY` and `ADD` instructions.

Flag as **Critical**:
- `COPY . .` or `ADD . .` (broad glob) — check if a `.dockerignore` exists in the same directory. If `.dockerignore` is absent, this is a definite fail. If `.dockerignore` exists, note its presence but still flag for manual verification that sensitive paths are excluded (see Check 7)
- `COPY` of specific files whose names suggest secrets: `*.key`, `*.pem`, `*.p12`, `.env`, `*.env`, `secrets.toml`, `credentials.json`, `*.tfvars`, `*.pfx`
- `COPY` followed by a `RUN rm` in a subsequent layer — the file persists in the earlier layer regardless of deletion

For broad-glob COPYs, note the specific sensitive file types most likely to be swept in if `.dockerignore` is absent or incomplete.

### Check 4: Base Image SHA Pinning

Scan all `FROM` instructions (including multi-stage build stages).

For each `FROM` instruction:
- FAIL if the image reference does not include `@sha256:`
- PASS if `@sha256:<digest>` is present
- **Variable-substituted references** (e.g. `FROM ${AIRFLOW_IMAGE_NAME:-apache/airflow:3.0.3}`): evaluate the default value in the `:-` fallback. If the default does not include `@sha256:`, record a FAIL. If no default is present (bare variable like `FROM $IMAGE`), record as a SKIP with note: "Image is fully runtime-injected — verify the calling context pins by SHA."

This includes:
- The primary build stage
- All intermediate build stages
- `COPY --from=<image>` references (these are also image pulls that should be pinned)

Record each unpinned `FROM` separately with its line number and image reference (tag only, not any default value).

### Check 5: USER Directive — Non-Root Runtime

Check whether the Dockerfile sets a non-root `USER` before the final `CMD`/`ENTRYPOINT`.

- FAIL if no `USER` instruction exists anywhere in the final stage (the last `FROM` block)
- FAIL if the last `USER` instruction in the final stage sets `USER root` or `USER 0`
- PASS if the final stage ends with a `USER` instruction for a non-root user

For multi-stage builds, only the final stage matters for runtime. However, note if an intermediate stage explicitly overrides to root without switching back — this is a warning if that stage could be accidentally used as a final image.

The official `apache/airflow` base image already sets `USER airflow`. If the Dockerfile inherits from it and does not override `USER`, note this as a PASS with a note to verify the base image's USER is not overridden downstream.

### Check 6: Multi-Stage Build Secret Leakage

This check applies only to Dockerfiles with two or more `FROM` instructions (multi-stage builds). SKIP if the Dockerfile has a single stage.

Identify all named stages (`FROM <image> AS <stage-name>`).

Apply the dual-stage severity rules from the context section above.

Flag with appropriate severity if any of the following are true:

1. **Direct base leakage** (e.g., `FROM dev_build AS prod_build`): If the base stage held `ENV` secrets, all those layers are inherited by the production stage. The final image contains both the dev credentials (from inherited layers) and production credentials (from the override). Severity: **Critical** if the derived stage is a production target; **Important** if both stages are dev-only.

2. **ARG→ENV in production stage**: The production stage receives a secret as a build arg and promotes it to `ENV`. Any image built this way has the production secret in its layer history. Recommend switching to runtime env injection (`env_file` via compose). Severity: **Critical**.

3. **ENV set in a stage that is used as FROM**: Stage A sets `ENV SOME_SECRET=...` and stage B declares `FROM stage_A`. Stage B inherits all layer history. Severity: depends on whether stage B is the production target.

For each finding, report:
- The stages involved (names and line numbers)
- The specific secret variable names (NOT values) that are leaking
- The mechanism of leakage
- The recommended fix: for Gemma-pattern repos, recommend `--target dev_build` for production builds + runtime secret injection via `env_file`

### Check 7: .dockerignore Existence and Coverage

Check for the presence of a `.dockerignore` file in the same directory as the Dockerfile.

- FAIL if no `.dockerignore` file exists
- FAIL (partial) if `.dockerignore` exists but does not exclude these paths: `.env`, `.env.*`, `secrets/`, `.dlt/`, `*.key`, `*.pem`, `.git/`
- PASS if `.dockerignore` exists and covers all critical sensitive paths listed above

When reading `.dockerignore`, look for each of the critical patterns. Report which ones are present and which are missing.

If a `COPY . .` instruction exists (from Check 3), elevate this to **Important** rather than Recommended — the coverage gap directly enables secret leakage.

### Check 8: pip vs Managed Package Installation

Scan all `RUN` instructions for `pip install` or `pip3 install` calls.

- FAIL (Recommended) if any `RUN pip install` or `RUN pip3 install` is found in the final production stage
- PASS if package installation uses `uv` (via `RUN uv sync`, `RUN uv install`, or a copied `uv` binary)
- SKIP if no Python package installation is performed

Note: using `pip install uv` as a bootstrap step to then use uv for everything else is acceptable — flag only if `pip install` is used for application dependencies.

### Check 9: Unnecessary Packages and Tools in Final Image

Scan all `RUN apt-get install`, `RUN apk add`, `RUN yum install` (and similar) instructions in the **final stage** of the Dockerfile.

Flag as Recommended if any of these tools are installed in the final stage:
- Debugging/attack tools: `curl`, `wget`, `netcat`, `nc`, `ncat`, `nmap`, `strace`, `gdb`, `ltrace`
- Build tools that should live in a builder stage: `build-essential`, `gcc`, `g++`, `python3-dev`, `libpq-dev`, `make`, `cmake`
- Text editors: `vim`, `nano`, `emacs`
- Package managers: `git` (unless needed at runtime)

Note: `curl` or `wget` used only for a `HEALTHCHECK` instruction is marginally acceptable, but prefer using a dedicated binary or a minimal alternative. Note this distinction in the finding.

Also flag missing `--no-install-recommends` on `apt-get install` calls in the final stage — this silently inflates the image with unreviewed packages.

## Output Format

Output findings directly in the conversation. Do NOT write to a file — findings are shown inline.

Start with a brief inventory of Dockerfiles audited (file paths and total line counts).

### Severity Emoji

- 🔴 **Critical** — secret exposure or significant attack surface; fix immediately
- 🟠 **Important** — meaningful hardening gap; fix in near term
- 🟡 **Recommended** — security best practice; fix when practical
- ⚪ **Note** — informational; no action required but worth knowing

### Findings Section

Output a `## Findings` section. Group findings by severity (Critical first, then Important, Recommended, Notes). Within each group, list findings in check order.

For each finding use this format:

```
### [SEVERITY EMOJI] [SEVERITY] — [Short Title]

**File**: `<path/to/Dockerfile>` line <N>
**Variable / Instruction**: `<INSTRUCTION_NAME>` / `<VARIABLE_NAME>` (omit value)
**Issue**: One sentence describing what was found (no secret values).
**Risk**: One sentence explaining the security consequence.
**Remediation**:
<before/after snippet adapted from the checklist — use placeholder values, never real ones>
```

If a check has no findings, do not include it in this section.

### Summary Section

After all findings, output a `## Summary` section:

```
## Summary

| Severity | Count |
|---|---|
| 🔴 Critical | N |
| 🟠 Important | N |
| 🟡 Recommended | N |
| ⚪ Note | N |

### Checks Passed
- [List each check that passed, one line each]

### Checks Skipped
- [List each check that was skipped, with the reason]
```

End with a one-sentence overall assessment: e.g., "This Dockerfile bakes secrets into image layers and should not be used in production until credentials are removed." or "Dockerfile is well-structured with only minor hardening improvements recommended."
