Signing Docker images using Docker Content Trust

In this blog I want to introduce you to the concept of signing Docker images. Signing your docker images will add some layer of trust to your images. This can guarantee a consumer of your image that this image is for sure published by you and hasn’t been tampered with by others.

You might already used PGP to sign your Git commits. In this blogpost I shown a nice way of setting PGP signing keys using Krypton that adds 2FA. In practice Docker image signing is the same concept.

If this all sounds a bit fuzzy to you, please continue reading, hopefully I am able to make things more clear. ;-)

Docker Content Trust (DCT) provides the ability to use digital signatures for data sent to and received from remote Docker registries. These signatures allow client-side or runtime verification of the integrity and publisher of specific image tags.

Through DCT, image publishers can sign their images and image consumers can ensure that the images they use are signed. Publishers could be individuals or organizations manually signing their content or automated software supply chains signing content as part of their release process.

In practice for a consumer nothing changes until DCT is enabled. From then you will only be able to work with Signed images. So for us developers it is important that we sign our images, so they can also be used by more restricted environments with DCT enabled.

DCT builds on top of Notary which is a more general purpose solution to sign artifacts. DCT implements this functionality for Docker images.

To be able to sign your images we will have to create x509 certificates which are used to sign repositories but also to define who is allowed to sign the repositories. You can also import existing certificates. E.g. managed by a PKI solution or created via openssl commandline.

Setting up your certificates

Before we start I want to mention it is very important to backup your certificates and store the credentials to use them in a password manager. Especially the root certificate we are about to generate.

WARNING: Loss of the root key is very difficult to recover from. Correcting this loss requires intervention from Docker Support to reset the repository state. This loss also requires manual intervention from every consumer that used a signed tag from this repository prior to the loss.

The keys will be stored at following location ~/.docker/trust/private (MacOSX/Linux) or %HOMEPATH%\.docker\trust\private (Windows). Please consult Docker Content Trust Documentation to properly manage the backups of your signing keys.

There are different types of keys, which have some hierachical structure as depicted in below diagram.

Key Description
root key Root of content trust for an image tag. When content trust is enabled, you create the root key once. Also known as the offline key, because it should be kept offline.
targets This key allows you to sign image tags, to manage delegations including delegated keys or permitted delegation paths. Also known as the repository key, since this key determines what tags can be signed into an image repository.
snapshot This key signs the current collection of image tags, preventing mix and match attacks.
timestamp This key allows Docker image repositories to have freshness security guarantees without requiring periodic content refreshes on the client’s side.
delegation Delegation keys are optional tagging keys and allow you to delegate signing image tags to other publishers without having to share your targets key.

Lets have a look on the docker trust cli.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker trust --help
Usage: docker trust COMMAND

Manage trust on Docker images

Management Commands:
key Manage keys for signing Docker images
signer Manage entities who can sign Docker images

Commands:
inspect Return low-level information about keys and signatures
revoke Remove trust for an image
sign Sign an image

Run 'docker trust COMMAND --help' for more information on a command.

As you can see the docker trust command allows us to manage keys, manage the entities allowed to sign our images, inspect the signatures, revoke and sign our images.

Lets first create our signing keys. The --dir command below defines where you would like to store the public key.

1
2
3
4
5
6
7
$ docker trust key generate marco --dir ~/.docker/trust
docker trust key generate marco
Generating key for marco...
Enter passphrase for new marco key with ID 8ede853:
Repeat passphrase for new marco key with ID 8ede853:
Successfully generated and loaded private key. Corresponding public key
available: /Users/marco/.docker/trust/marco.pub

Once we have our key created we can use notary to get an overview of our keys. Oh, don’t forget to store the credentials and ensure you will arrange some backups.

1
2
3
4
$ notary -d ~/.docker/trust key list
ROLE GUN KEY ID LOCATION
---- --- ------ --------
marco 8ede853f5c5f6b119202e86478d0dff4fc7e37f803fefcf2da7138d8dcce1401 /Users/marco/.docker/trust/private

Manage entities for a repository

Now we have our personal signing key we can authorize our key to sign docker images for a given repository. This will also create new target keys in case the repository doesn’t exist yet. The root key will be required to create new repository keys. The repository key will be required to add or remove new signing entities on the repository.

1
2
3
4
5
6
7
8
$ docker trust signer add --key ~/.docker/trust/marco.pub marco marcofranssen/whalesay
Adding signer "marco" to marcofranssen/whalesay...
Initializing signed repository for marcofranssen/whalesay...
Enter passphrase for root key with ID 2c799e5:
Enter passphrase for new repository key with ID b635efe:
Repeat passphrase for new repository key with ID b635efe:
Successfully initialized "marcofranssen/whalesay"
Successfully added signer: marco to marcofranssen/whalesay

Running the notary command now will show you a bunch more keys.

1
2
3
4
5
$ notary -d ~/.docker/trust key list
ROLE GUN KEY ID LOCATION
---- --- ------ --------
marco 8ede853f5c5f6b119202e86478d0dff4fc7e37f803fefcf2da7138d8dcce1401 /Users/marco/.docker/trust/private
targets .../marcofranssen/whalesay b635efe210fb79e2425e3b7fa102324e5bb759151bdb089bdd79e820b75b160b /Users/marco/.docker/trust/private

Signing

Now we have all the keys in place to be able to sign a docker image called marcofranssen/whalesay. To be able to do so I will first have to create this image or simply download an existing image and tag it as such.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker pull docker/whalesay
docker tag docker/whalesay marcofranssen/whalesay:latest
$ docker trust sign marcofranssen/whalesay:latest
Signing and pushing trust data for local image marcofranssen/whalesay:latest, may overwrite remote trust data
The push refers to repository [docker.io/marcofranssen/whalesay]
5f70bf18a086: Layer already exists
d061ee1340ec: Layer already exists
d511ed9e12e1: Layer already exists
091abc5148e4: Layer already exists
b26122d57afa: Layer already exists
37ee47034d9b: Layer already exists
528c8710fd95: Layer already exists
1154ba695078: Layer already exists
latest: digest: sha256:4a79736c5f63638261bc21228b48e9991340ca6d977b73de3598be20606e5d87 size: 2402
Signing and pushing trust metadata
Enter passphrase for marco key with ID eb9dd99:
Successfully signed docker.io/marcofranssen/whalesay:latest

Now we can inspect the signing details of our image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker trust inspect --pretty marcofranssen/whalesay
Signatures for marcofranssen/whalesay

SIGNED TAG DIGEST SIGNERS
latest 4a79736c5f63638261bc21228b48e9991340ca6d977b73de3598be20606e5d87 marco

List of signers and their keys for marcofranssen/whalesay

SIGNER KEYS
marco eb9dd99255f9

Administrative keys for marcofranssen/whalesay

Repository Key: b635efeddff59751e8b6b59abb45383555103d702e7d3f46fbaaa9a8ac144ab8
Root Key: 0428c356406a6ea3543012855c117d13d784774e49aa6db461cfbad5726d187b

If you would like to revoke a signed image tag you can do that using the following command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker trust revoke marcofranssen/whalesay:latest
Enter passphrase for marco key with ID eb9dd99:
Successfully deleted signature for marcofranssen/whalesay:latest
$ docker trust inspect --pretty marcofranssen/whalesay
No signatures for marcofranssen/whalesay

List of signers and their keys for marcofranssen/whalesay

SIGNER KEYS
marco eb9dd99255f9

Administrative keys for marcofranssen/whalesay

Repository Key: b635efeddff59751e8b6b59abb45383555103d702e7d3f46fbaaa9a8ac144ab8
Root Key: 0428c356406a6ea3543012855c117d13d784774e49aa6db461cfbad5726d187b

Last but not least you could add your collegues as a signing entity to your repositories using their public key. This will require them to generate a key first and provide their public key in order to be authorized.

For your own reference you might want to create a bookmark now to the TL;DR below so you can easily lookup the commands and how to use as a future reference.

TL;DR

Generate / Load Keys

Create a new signing key or load an existing signing key.

NOTE: the name you provide will also be the name used in the signer data. Consider using a descriptive name.

1
2
docker trust key generate marco --dir ~/.docker/trust
docker trust key load key.pem --name marco

Adding delegation key

Add another team member to allow them to sign images for a given repository.

1
docker trust signer add --key buddy.pub buddy <image>:<tag>

Removing a signer

Remove a team member from being able to sign images for a given repository.

1
docker trust signer remove buddy <image>:<tag>

Signing an image

Sign a specific tag of the image.

1
docker trust sign <image>:<tag>

Revoke trust

Remove the signature from a specific image tag.

1
docker trust revoke <image>:<tag>

Inspect image trust

Inspect the signing data of an image.

1
docker trust inspect --pretty <image>:<tag>

Enabling content trust

Content trust is disabled by default. To work only with signed images Docker Content Trust should be enabled.

NOTE: this will disallow you from using non signed images. Docker pull will fail if the image is not signed. You will not be able to build your own image using a non-signed image in the FROM definition

Linux/MacOSX
1
export DOCKER_CONTENT_TRUST=1
Windows
1
set DOCKER_CONTENT_TRUST=1

References

Thanks for reading my blog.

Share