Overview

Docker registry mechanism allows to quick download images from the image repository. This makes the publish/pull processes much easier. Unfortunately, this great feature can open new possibilities to an attacker who can corrupt the image in the registry.

In this scenario, we assume that the attacker already got write access to the docker image repository (Like public one docker hub or steal developer’s/Jenkins’s credentials to the private docker registry)

Preconditions

  • The attacker has to have write access to the docker repository
  • No app signature checks applied

How to change docker images

Docker registry structure of locating images look like that:

repository/image:tag

Let assume we have a private registry with image busybox with tag latest

docker push localhost:5000/busybox
The push refers to repository [localhost:5000/busybox]
d2421964bad1: Pushed
latest: digest: sha256:c9249fdf56138f0d929e2080ae98ee9cb2946f71498fc1484288e6a935b5e5bc size: 527

As you see tag name is latest and Digest of the current image starts with c9249fd

Next step to be done is to take the rigged image and send it as busybox with tag latest

Let’s remove the current busybox image from local repo and rename alpine image to busybox and push into our registry

docker rmi busybox
docker image tag alpine localhost:5000/busybox
docker push localhost:5000/busybox
The push refers to repository [localhost:5000/busybox]
3e207b409db3: Pushed 
latest: digest: sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01 size: 528

As you see tag name is latest and Digest of the current image starts with 39eda93

Let’s remove our image of fake busybox from our local repository

docker rmi localhost:5000/busybox:latest
Untagged: localhost:5000/busybox:latest
Untagged: localhost:5000/busybox@sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01

and pull it from our private registry

docker pull localhost:5000/busybox:latest
latest: Pulling from busybox
Digest: sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01
Status: Downloaded newer image for localhost:5000/busybox:latest
localhost:5000/busybox:latest

Next step to be done is being sure that only digest has not been changed:

docker run -it localhost:5000/busybox:latest sh
/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.11.6
PRETTY_NAME="Alpine Linux v3.11"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

As we can see It is an easy way to change docker image in behind of tag. So pullers of specific docker repository wouldn’t be able to determine if the image changed.

Only noticeable thing changed was digests which can be used for image validity check.

IMAGETAGDIGIT
ProperLATESTc9249fd
FakeLATEST39eda93

Way to secure

  • Never download docker images from untrusted repository maintainers
  • Limit registry write access to a minimum (In best case only build server)
  • Implement the signing of docker images (see: https://github.com/theupdateframework/notary)
  • Be prepared for this possibility:
    • Use docker custom security profiles like AppArmor, Seccomp
    • Use dynamic analysis tools like Remux
    • Don’t execute images with excessive privileges (namespaces, privileged flag, added capabilities)

How to create your own docker registry

To run own docker registry you need to have docker engine version >=1.6.0

docker run -d -p 5000:5000 --name registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
cbdbe7a5bc2a: Already exists 
47112e65547d: Pull complete 
46bcb632e506: Pull complete 
c1cc712bcecd: Pull complete 
3db6272dcbfa: Pull complete 
Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d
Status: Downloaded newer image for registry:2
437d56f489f27e990b6b6e7b43358c670087b5731474a086b52713067056a728

Docker registry is simply docker image we run as a container. If everything came fine we should see running docker registry container:

docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
437d56f489f2        registry:2          "/entrypoint.sh /etc…"   30 seconds ago      Up 28 seconds       0.0.0.0:5000->5000/tcp   registry

Ok now let be sure that we don’t have any repository in our docker registry:

curl -X GET localhost:5000/v2/_catalog
{"repositories":[]}

Then we can build or retag an image and push into the local registry

docker build .
docker push localhost:5000/myfirstimage

Now let see if our local registry has been updated:

docker push localhost:5000/myfirstimage
The push refers to repository [localhost:5000/myfirstimage]
1079c30efc82: Pushed
latest: digest: sha256:a7766145a775d39e53a713c75b6fd6d318740e70327aaa3ed5d09e0ef33fc3df size: 527

Let’s check if the image is placed on the repository:

curl -X GET localhost:5000/v2/_catalog
{"repositories":["myfirstimage"]}

and what tags for the image myfirstimage are place in docker repository:

curl -X GET localhost:5000/v2/myfirstimage/tags/list
{"name":"myfirstimage","tags":["latest"]}

As everything is in place the client can pull the image with the command:

docker pull localhost:5000/myfirstimage

To stop the process and remove all the data we need to run:

docker container stop registry && docker container rm -v registry

Sources