Skip to content
All tools
ForensicsRuns locallyNo account

Docker image inspector: layers, history and deleted secrets

Open a `docker save` tarball and read what a later layer only pretended to delete - plus the build history, which records every RUN command verbatim.

Open in ctfpal

A container image is a stack of filesystem diffs, and it turns up in CTF for the same reason a leaked .git/ does: deleting a file in a later layer does not remove it from the image. It adds a marker; the file itself is still sitting, in full, in the layer below.

The three places a secret survives

  1. A deleted file. RUN rm /root/.ssh/id_rsa writes a .wh.id_rsa whiteout into the top layer. Nothing touches the layer that contains the key. A running container does not have it; the tarball does.
  2. The build history. The image records every instruction as written, including the whole RUN command line. A password passed to curl -u, a --build-arg holding a token, a private key echoed into a file - all permanent, and all visible without extracting a single layer.
  3. The environment. ENV API_KEY=... is baked into the config and readable by anything that runs in the image.

Why `docker history` is not enough

docker history shows the instructions but not the contents, and a squashed or re-tagged image can lose them entirely. Reading the tarball directly shows both, and works on an image you were handed rather than one you can run - which is usually the situation.

What you seeWhat it means
.wh.<name>the layer deletes that file; look in the layer below
.wh..wh..opqthe layer replaces the whole directory; everything under it in earlier layers is still there
An empty_layer history stepmetadata only - ENV, CMD, LABEL - no filesystem change
A layer that is a gzip membera registry pull rather than a docker save; both are read

Common questions

How do I get the tarball?
`docker save <image> -o image.tar` for an image you have locally. From a registry without Docker, `skopeo copy docker://<image> oci-archive:image.tar` produces one this reads too - its layers are gzipped, which is handled.
It says a layer is not in the tarball.
The manifest names layers that were not included, which usually means a partial extract or a `--platform`-filtered save. Every layer that is present is still read; the missing ones are named rather than skipped silently.