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 ctfpalA 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
- A deleted file.
RUN rm /root/.ssh/id_rsawrites a.wh.id_rsawhiteout into the top layer. Nothing touches the layer that contains the key. A running container does not have it; the tarball does. - The build history. The image records every instruction as written, including the whole
RUNcommand line. A password passed tocurl -u, a--build-argholding a token, a private key echoed into a file - all permanent, and all visible without extracting a single layer. - 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 see | What it means |
|---|---|
.wh.<name> | the layer deletes that file; look in the layer below |
.wh..wh..opq | the layer replaces the whole directory; everything under it in earlier layers is still there |
An empty_layer history step | metadata only - ENV, CMD, LABEL - no filesystem change |
| A layer that is a gzip member | a 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.