Copy and Paste
Copy and Paste

Reputation: 526

Docker: Pulling an image by digest, that internally uses a tag

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

Answers (3)

AlexanderYastrebov
AlexanderYastrebov

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

DazWilkin
DazWilkin

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

Related Questions