Reputation: 526
Take for example the following repository hosted on dockerhub:
https://hub.docker.com/r/frolvlad/alpine-miniconda3
The following command is executed to pull an image via digest:
docker pull frolvlad/alpine-miniconda3:python3.7@sha256:9bc9c096713a6e47ca1b4a0d354ea3f2a1f67669c9a2456352d28481a6ce2fbe
Based off of the docker documentation pulling an image via digest has the following property:
Using this feature “pins” an image to a specific version in time
From what I understand, the docker image pulled with that digest at any point is immutable.
Though it doesn't comment on the mutable, or seemingly mutable references within.
Most importantly the first line of the docker file reads
FROM frolvlad/alpine-glibc:alpine-3.9
I would assume based off my readings if the author changes this first line in the dockerfile and pushes (even if it is the same tag), I would not be impacted as I am pointing to the image digest. However,
due to the fact that the author referenced a tag in the dockerfile, and not a digest, how can I confirm what dockerfile/base image was used to build their image? As it would seem just analyzing the frolvlad/alpine-glibc:alpine-3.9
dockerfile would not be sufficient as it could have been different at the time of the image creation.
Upvotes: 5
Views: 8169
Reputation: 603
When digest is specified tag is ignored, see dockerfile FROM should deny the usage of a digest that does not match the tag
/tmp/foo$ docker image ls alpine --digests | grep latest
alpine latest sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a 7e01a0d0a1dc 3 weeks ago 7.34MB
/tmp/foo$ echo 'FROM alpine:latest@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a' | docker build . -f -
Sending build context to Docker daemon 1.583kB
Step 1/1 : FROM alpine:latest@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a
---> 7e01a0d0a1dc
Successfully built 7e01a0d0a1dc
/tmp/foo$ echo 'FROM alpine:whatever@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a' | docker build . -f -
Sending build context to Docker daemon 1.583kB
Step 1/1 : FROM alpine:whatever@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a
---> 7e01a0d0a1dc
Successfully built 7e01a0d0a1dc
Upvotes: 0
Reputation: 39069
It looks like docker image history
could be something that could help you identify those kind of changes.
Here is the full example of it on the tag and digest you are using as an example:
$ docker image history --no-trunc 34982ce484b5
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:34982ce484b5d709bffb6bf8cca2163ff9231d1a900305f888a5baf59a3414cd 4 weeks ago /bin/sh -c CONDA_VERSION="4.5.12" && CONDA_MD5_CHECKSUM="866ae9dff53ad0874e1d1a60b1ad1ef8" && apk add --no-cache --virtual=.build-dependencies wget ca-certificates bash && mkdir -p "$CONDA_DIR" && wget "http://repo.continuum.io/miniconda/Miniconda3-${CONDA_VERSION}-Linux-x86_64.sh" -O miniconda.sh && echo "$CONDA_MD5_CHECKSUM miniconda.sh" | md5sum -c && bash miniconda.sh -f -b -p "$CONDA_DIR" && echo "export PATH=$CONDA_DIR/bin:\$PATH" > /etc/profile.d/conda.sh && rm miniconda.sh && conda update --all --yes && conda config --set auto_update_conda False && rm -r "$CONDA_DIR/pkgs/" && apk del --purge .build-dependencies && mkdir -p "$CONDA_DIR/locks" && chmod 777 "$CONDA_DIR/locks" 190MB
<missing> 4 weeks ago /bin/sh -c #(nop)ENV PATH=/opt/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 0B
<missing> 4 weeks ago /bin/sh -c #(nop)ENV CONDA_DIR=/opt/conda 0B
<missing> 6 weeks ago /bin/sh -c ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && ALPINE_GLIBC_PACKAGE_VERSION="2.29-r0" && ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && apk add --no-cache --virtual=.build-dependencies wget ca-certificates && echo "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m 1QIDAQAB -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && wget "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && apk add --no-cache "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && rm "/etc/apk/keys/sgerrand.rsa.pub" && /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && echo "export LANG=$LANG" >/etc/profile.d/locale.sh && apk del glibc-i18n && rm "/root/.wget-hsts" && apk del .build-dependencies && rm "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" 6.71MB
<missing> 6 weeks ago /bin/sh -c #(nop)ENV LANG=C.UTF-8 0B
<missing> 8 weeks ago /bin/sh -c #(nop)CMD ["/bin/sh"] 0B
<missing> 8 weeks ago /bin/sh -c #(nop) ADD file:2a1fc9351afe35698918545b2d466d9805c2e8afcec52f916785ee65bbafeced in / 5.53MB
On the fourth line of the output, I can see that 6 weeks ago the image seems to have changed by a shell command that start with ALPINE_GLIBC_BASE_URL=...
, which is exactly the first RUN
command of the parent image.
So indeed, here you can see that this image has changed, in time, there, due to this.
But and here is the important point for you: you can also see that docker image is composed of the RUN
of its parent image, and does not contains any reference to it whatsoever (you can also run a docker image inspect [image_digest]
, to double check this).
Meaning that docker squashes every parts of an image together and make the digests out of what commands created the image in the first place, not from the fact that the FROM
did not change but the underlaying image did.
So I would highly suspect, with that in mind and with the reference to this other answer about digest that if you FROM
image change, then the resulting digest won't be the same, meaning that you will still be protected in that case.
Upvotes: 0
Reputation: 40061
You're correct that an image pulled by digest is effectively (!) unchangeable.
The image digest is a SHA-256 hash computed from the layers that constitute the image. As a result it's highly improbable that a different image would share the same digest.
Once created an image's layers don't change. So even if the FROM
image were changed, your existing images would not be changed by it.
However, if you rebuilt your images using the new (same-tagged) FROM
image, your image's digest would change and this would be a signal to you that's something has changed.
It is possible (and a good practice) to use digests in FROM
statements too (for the reasons you cite) but few developers do this. You may wish to ensure your Dockerfiles
use digests in FROM
statements to ensure you're always using the same image sources.
However, it's turtles all the way down (or up) though and so you are recursively delegating trust to images from which yours are derived all the way up to SCRATCH
.
This is one reason why image vulnerability tools are recommended.
I explored this for my own education recently:
https://medium.com/google-cloud/adventures-w-docker-manifests-78f255d662ff
Upvotes: 3