How to swap an image in docker registry - with rigged image
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.
IMAGE | TAG | DIGIT |
---|---|---|
Proper | LATEST | c9249fd |
Fake | LATEST | 39eda93 |
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
- https://docs.docker.com/registry/
- https://github.com/theupdateframework/notary
- https://docs.docker.com/engine/security/seccomp/
- https://docs.docker.com/engine/security/apparmor/
- https://github.com/REMnux/docker
- https://docs.remnux.org/run-tools-in-containers/remnux-containers
- https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
Tags: Docker Security docker registry
Maybe you want to share? :)