Skip to content
NissanBossPublic

About

What your container image is still carrying: the file you deleted in a later layer and ship anyway, the build ARG sitting in the history, the .git folder nobody meant to send. It never prints a secret.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Dunnage

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

The problem

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.

What already exists

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 it looks at

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.

It never prints a secret

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.

It does not cry wolf

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 a testdata or fixtures directory.
  • 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 .pem has 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.

Running it

It reads a file, never a daemon and never a registry, so save the image first:

docker save myimage:latest -o image.tar
dunnage image.tar
dunnage --reveal image.tar

It 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.

What it does not do

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.

A gap is not a pass

Finding nothing is not a clean bill of health and the report says so, in those words, every time.

It only reads

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.

Building

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 ./...

Licence

MIT. Do what you like with it.

About

What your container image is still carrying: the file you deleted in a later layer and ship anyway, the build ARG sitting in the history, the .git folder nobody meant to send. It never prints a secret.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages