← Back to Docker Build Secrets Check

Docker Build Secrets Check vs. reading a Dockerfile by hand

Skimming a Dockerfile for an obvious ARG DB_PASSWORD=hunter2 genuinely works, and it is the method most people already use. This page names no product or company being compared against: "reading the Dockerfile by hand" is a generic manual habit, not a competing tool.

CaseReading the Dockerfile by handThis tool
A literal secret-looking default, ARG DB_PASSWORD=hunter2Correctly caught by a reviewer who knows to look for itCorrectly caught the same way, as arg-literal
An ENV value resolved from a build argument, ENV API_KEY=$API_KEYNo literal secret text on the line to react to -- easy to read as cleanFlags the ENV variable's name against a secret-pattern list regardless of where its value comes from
A bare ARG SIGNING_TOKEN with no default at allReads as the safe version of the pattern already being watched for -- no value is visible to object toFlags it as an advisory-only arg-declared finding, since it still invites an unsafe --build-arg
A COPY .env /app/.env among a dozen ordinary COPY linesEasy to miss without checking every source path against a credential-filename listChecks every COPY/ADD source's basename and path against that fixed list every time
Checking every Dockerfile in a repository on every dependency-bump PRManual, and the first thing skipped under time pressureCLI scans a whole checkout in one pass; --fail-on-risk blocks CI

The gap that matters most: a value with no literal text to see

ENV TOKEN=$TOKEN and a bare ARG SIGNING_TOKEN both carry no secret-shaped string anywhere in the Dockerfile for a reviewer to react to -- there is nothing that looks wrong on the line itself. But per Docker's own "Build secrets" documentation, an ENV instruction's resolved value is written into the image's config layer regardless of where that value came from, and a bare ARG still invites a caller to pass a secret with --build-arg, which BuildKit records for the build unless it is mounted as a build secret instead. This tool checks the instruction and the variable's name, not whether a literal value is visible in the file, so both cases are flagged the same as a line with a literal default would be.

The gap that matters second: a credential file among ordinary COPY lines

A typical Dockerfile has several ordinary COPY instructions in a row -- package.json, source directories, config files -- and a COPY .env /app/.env or COPY id_rsa /root/.ssh/id_rsa reads no differently from any of them on a skim unless a reviewer is deliberately checking each source's basename against a list of well-known credential filenames. This tool checks every COPY/ADD source against that fixed list every time, so the answer does not depend on a reviewer's memory of which filenames count.

When to still read the Dockerfile yourself

This is a name-pattern heuristic, not a value scanner -- it never sees what a build actually resolves ARG or ENV to, so a name like API_KEY is flagged whether or not the value behind it is sensitive, and a genuine secret under a name this tool's pattern list does not recognise is a false negative it cannot close. Deciding whether a specific flagged instruction is a real exposure, and catching a secret under an unrecognised name, is still a job for reading the Dockerfile yourself.

Common questions

What does reading a Dockerfile by hand actually catch?

A reviewer who knows to look can spot the obvious case immediately: an ARG or ENV line with a literal-looking secret value sitting right there in the text, such as ARG DB_PASSWORD=hunter2. That review genuinely works when the value is literal and the line is short -- the same case this tool's classify.mjs also flags as arg-literal or env-secret.

Why does an ENV value resolved from a build argument slip past a skim?

ENV NODE_ENV=production API_KEY=$API_KEY carries no literal secret text on that line for a reviewer to react to -- the value only exists once the build resolves the ARG reference. But per Docker's own documentation, whatever that value resolves to is written into the image's config layer exactly the same as a literal would be, and stays there. A reviewer scanning for a suspicious-looking string has nothing to see on this line; this tool flags it because it checks the ENV variable's name against a secret-pattern list, not the literal text of its value.

Why is a bare ARG with no default easy to wave through?

ARG SIGNING_TOKEN with no default commits nothing to the Dockerfile itself, so on a skim it looks like the safe version of the ARG PASSWORD=hunter2 pattern a reviewer already knows to flag. It is still worth a second look: it invites a caller to pass a value with --build-arg, which BuildKit still records for the build unless that value is mounted as a build secret instead. This tool flags it too, as an advisory-only arg-declared finding, so it does not get silently classed as fine because it has no visible value.

Why is a COPY of a credential file easy to miss among ordinary COPY lines?

A Dockerfile can carry a dozen ordinary COPY instructions -- package.json, source directories, config files -- and a COPY .env /app/.env or COPY id_rsa /root/.ssh/id_rsa reads the same at a glance as any of them unless a reviewer is specifically checking each source path against a list of well-known credential filenames (.env, id_rsa, .npmrc, .aws/credentials, and others). That list is easy to not have fully in mind while reading past several ordinary-looking COPY lines in a row. This tool checks every COPY and ADD source's basename and path against that fixed list every time, regardless of how many other COPY lines surround it.

Does this replace understanding whether a flagged name actually holds a secret?

No. This is a name-pattern heuristic over Dockerfile text, not a value scanner -- it never sees what a build actually resolves ARG or ENV to, so a name like API_KEY is flagged because it looks like a secret even if the value behind it is not sensitive. Deciding whether a specific flagged instruction is a real exposure, and catching a secret under a name this tool's pattern list does not recognise, is still a job for reading the Dockerfile yourself.