What your container image is still carrying.
Dunnage is the loose packing that goes into a ship's hold around the cargo. Nobody ordered it, it is not what anybody is paying for, and it travels anyway.
[SHIPPED] 2 secrets are in app/.env, which was deleted later and ships anyway
layer 2, line 1 a password or token set in a
configuration file
layer 2, line 2 a Stripe live key
This file is not in the finished image. It is in layer 2 and
it comes down with every pull. Deleting a file in a later step
never removes it from the layer that added it: it writes a
marker that hides the name.
[SHIPPED] 1 build argument is written into the history
NPM_TOKEN passed to layer 3
[SHIPPED] 3 things are in the image that should never be published
app/.git a git repository
not the files, the history: every commit, every branch, every
secret anybody committed and removed again, and the email
address of everybody who worked on it
root/.ssh/id_rsa a private ssh key
whoever pulls the image can log in wherever that key is trusted
[TELLS ] 1 path names the machine this was built on
hidden:09992812 in app/dist/bundle.js.map
A layer is immutable. RUN rm secret.txt in a later step does not go back
and take the file out of the earlier one: it writes a marker called a
whiteout that hides the name from the merged view.
docker run shows nothing. docker export shows nothing. docker save,
and everybody who pulls the image, gets the layer with the bytes still in
it.
The configuration is worse, because it needs no unpacking at all. A build
argument keeps the value it was given in the history line for its layer, so
docker history prints it. An ENV is in the config blob the registry
serves to anybody who can pull, and every process in the container inherits
it.
dive is a good tool and it is an explorer. It puts the layers in one pane and the file tree in the other and leaves the looking to you, which is fine when you already suspect something and no use at all before you push.
trivy and grype answer a different question: which of the packages in here have known vulnerabilities. That is worth knowing and it is not this. Trivy has open discussions about not seeing things removed in a later layer, because that is not what it is for.
Neither gives a verdict. This one does: this file was deleted in layer 5, it ships from layer 3, and there is a Stripe key on line 2 of it.
What is in a layer and not in the finished image. Whiteouts and opaque directory markers, resolved in layer order, so it can say where a file arrived and where it was hidden.
What is in the image that should never be published. Around fifty named
things rather than a search: .env, .npmrc, .git-credentials,
terraform.tfstate (which keeps every secret the plan touched in plain
text), private keys, kubeconfigs, and a whole .git directory, which is not
the files but the history, including every secret anybody committed and
removed again.
What the configuration hands over. Secrets in ENV, build arguments
written into the history with their values, and secrets in the commands
themselves. Whether it runs as root. Whether the base image is pinned to a
tag, which moves, rather than to a digest, which does not.
What it says about the machine that built it. The home directory in a build path, which is an account name and usually a person. The uid that owns the copied files. And the absolute paths inside a source map, which is the one nobody thinks of: a bundler writes the full path of every file it compiled, so shipping the map ships the layout of somebody's laptop.
Not without --reveal, not with it, and there is no flag for it.
The line is covered at the moment it comes off the disk, not at the moment it is printed, so there is no later point in the program where the original could go out by mistake. The marker is a fixed width, because one character per character hands over the length.
That covering had to be extended once during development, and it is the example worth keeping: the layer listing at the bottom of the report prints what each build step did, and a build step is the Dockerfile line with the arguments already substituted. The report was quietly publishing the token that the finding four inches above it said not to publish.
An image is mostly other people's software, and other people's software ships example configuration and test keys. A scanner that reports forty findings out of the base image is one where nobody ever reaches the line that is theirs.
- The paths the distribution owns are left alone:
/etc/ssl, the language runtimes, the package manager caches, anything under atestdataorfixturesdirectory. - A file whose name says it is a template (
.env.example,.dist,.sample) is a template. PASSWORD=changeme,API_KEY=your-key-here,${DB_PASSWORD},<your token>and********are not secrets.- A
.pemhas to actually contain a private key header. Certificates are public by design and every base image is full of them. Reporting those was the first thing this got wrong. - There is deliberately no "this string looks random" rule. Every rule names the thing it finds.
It reads a file, never a daemon and never a registry, so save the image first:
docker save myimage:latest -o image.tardunnage image.tardunnage --reveal image.tarIt also reads an unpacked OCI layout, which is what skopeo copy oci:...
leaves behind:
dunnage ./oci-layout--reveal names the build machine and prints the lines. It does not reveal
secrets, because nothing does.
Exit code is 1 when something is in there that should not be, 3 when something could not be read, 0 when nothing turned up.
Windows marks anything that came from the internet, so the first run may bring up a blue "Windows protected your PC" box. Right click the zip, Properties, tick Unblock at the bottom, and then extract it.
It does not guess which layers are yours. Telling your COPY apart from
the base image is guesswork, and a wrong guess either hides your problem or
blames you for Debian's. Every finding says which layer it is in, and the
distribution paths are filtered by path rather than by layer.
It only reads inside text files under a megabyte. A key inside a binary, inside a jar, or inside a nested archive is somewhere it never looked. The limit is here in writing rather than applied quietly, and when the overall budget runs out the report says so instead of ending with a clean bill of health.
It does not verify digests, so it will read a tampered archive without complaining. It is a tool for looking at your own build output before you push it, not for deciding whether somebody else's image is what it claims.
It does not fix anything. Nothing here rewrites an image, and that is not laziness: squashing layers to hide a leaked key leaves the key leaked and the registry with the old manifest. The advice is to rebuild, because that is the only thing that actually works.
Finding nothing is not a clean bill of health and the report says so, in those words, every time.
Nothing is written, nothing is unpacked to a temporary directory, no socket
is opened and no other program is run. Those are not promises in a readme:
tests read the source and fail the build if a write call, a temporary file,
an import of net or an import of os/exec ever turns up.
Needs Go and nothing else. No dependencies at all: an image is tar, gzip and json, which is three packages from the standard library.
go build -o dunnage .go test ./...MIT. Do what you like with it.