Signing Docker Images with Notary
This section describes the use of Unbound CORE ("CORE") with Docker Notary to enable Docker Content Trust ("DCT") to align with CIS Docker Benchmark and Docker Enterprise security best practices. DCT provides the ability to use digital signatures for data sent to and received from remote Docker registries. These signatures allow client-side verification for the Docker image integrity and the Docker image publisher identity. DCT works with The Update Framework ("TUF") to provide protection against man-in-the-middle, replay, and other attacks.
CORE can greatly help enterprise administrators to assert content integrity throughout development and production. In these stages, the signing keys are expected to spread through delegation to developers, sysadmins, and third-party ISVs. With CORE’s multi-party computation technology, DCT is extended to better protect cryptographic keys against malware or other forms of unauthorized use.
This document is intended for customers that want to protect the Notary root key with CORE.
The system architecture is shown in the following figure.
See Docker Notary Getting Started and Notary Advanced Usage for more information on some of the topics discussed here.
The following sections describe how to use CORE to protect the root key of a docker.io registry and notary service. This method can be used with any other registry, but we demonstrate the capability with docker.io since it is globally available and reflects a common usage scenario.
Instructions are provided for running on a CentOS/RedHat 7 platform.
Prerequisites
The following prerequisites are required to use the Unbound signing with CORE.
- CORE (UKC) 2.0.1907 or newer
- Client device, , with an OS listed in the System Requirements, and the following software:
- CORE (UKC) client 2.0.1907 or newer
- Notary client v0.6.1 (installation described below)
- Docker CE and Docker CLI
Command Line Interface (installation described below)
- GOLANG 1.3 (installation described below)
Prepare the Client Environment
As part of the setup described here, it is required to install the Go language. Go is used to create a special notary client that supports PKCSPublic-Key Cryptography Standards - Industry-standard cryptography specifications.#11, which is used instead of the default Notary client.
- Install the required packages.
- Add the Docker CE repository.
- Install the Docker CLI
Command Line Interface.
-
Install Go. See https://golang.org/dl/ for more information.
-
Using Go, get the Notary client.
go get github.com/theupdateframework/notary
-
Using Go, create the Notary binary file with PKCS
Public-Key Cryptography Standards - Industry-standard cryptography specifications.#11.
go install -tags pkcs11 github.com/theupdateframework/notary/cmd/notary
This command creates a binary file called notary in the folder /path/to/go/bin/.
- Copy the notary binary file to a location in your path, such as /usr/bin/.
- Test that the installation was successful by running this command:
- Start Docker
sudo yum install openssl wget git gcc
sudo yum install epel-release -y
sudo yum install jq -y
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce
sudo cp notary /usr/bin/
notary version
Example response:
notary
Version:
Git commit:
Go version: go1.13.3
sudo service docker start
Configure CORE Client
- Follow the CORE Setup tasks.
- Connect the Notary client to the CORE client.
- Configure OpenSSL on the CORE Client
- Configure OpenSSL to work with CORE by running this command:
- Check that CORE is configured to work with OpenSSL.
sudo ln -s <path to libekmpkcs11.so>/libekmpkcs11.so /usr/lib64/libykcs11.so
For the platform-dependent location of the libekmpkcs11.so, see Path to PKCS#11 Library.
Notes
1. After this step, PKCSPublic-Key Cryptography Standards - Industry-standard cryptography specifications.#11 requests are redirected to the CORE server via libekmpkcs11.so.
2. When listing keys in Notary, the location will display as yubikey, which is how Notary communicates with CORE.
/etc/ekm/dy_openssl
openssl engine
Verify that Dyadic Security engine support is listed in the response. For example:
(rdrand) Intel RDRAND engine
(dynamic) Dynamic engine loading support
(dyadicsec) Dyadic Security engine support
Configure the OpenSSL Configuration File
Update the OpenSSL configuration file with the following changes in order to enable creation of repository certificates with OpenSSL.
- Download the openssl.cnf file from the Unbound GitHub repo.
- Move the file to this location:
/root/.docker/certs/
Warning
It is recommended to make a backup of the current openssl.cnf file before overwriting it.
The new configuration file contains changes to these sections: CA_default, policy_match, v3_ca, repo_cert, and polsect.
Configure Notary in CORE Client
- Create a folder called ~/.notary.
- Create a file called config.json in that folder and edit it. Add the following contents.
- Log into Docker Hub with your Docker username and password.
- A method is needed to examine the hash of the Docker image. You can use Docker manifest or skopeo.
- To use Docker manifest add the parameters experimental and debug to ~/.docker/config.json.
-
To use skopeo, install the package.
sudo yum install skopeo
- Export the following environment variables.
mkdir ~/.notary
Notes
1. The ca.pem file is created in the Create the Root Key section.
2. The trust_pinning section should have an entry for every repository that you are working with.
3. Update the contents with the relevant information, such as the username.
{
"trust_dir": "~/.docker/trust",
"remote_server": {
"url": "https://notary.docker.io"
},
"trust_pinning": {
"ca": {
"docker.io/{username}/nginx" : "/root/.docker/certs/ca.pem"
},
"disable_tofu": true
}
}
sudo docker login -u <Docker username> -p <Docker password> docker.io
{
…
"HttpHeaders": {
"User-Agent": "Docker-Client/19.03.5 (linux)"
},
"experimental": "enabled",
"debug": true
}
Note
You must be running Docker CE to use manifest.
export NOTARY_TARGETS_PASSPHRASE=<New password for target>
export NOTARY_SNAPSHOT_PASSPHRASE=<New password for snapshot>
export NOTARY_DELEGATION_PASSPHRASE='{"username": "docker", "password": "<CORE password for docker user>"}'
export NOTARY_AUTH=$(echo "<username>:<password>" | base64)
Example of Using Notary
In this section, we do the following tasks for the example:
- Create a Self-Signed Certificate Authority
- Create the Root Key
- Initialize Repository on Notary
- Delegation and Image Signing
- Validate an Image
Create a Self-Signed Certificate Authority
You can create a self-signed certificate authority using CORE. In a production system, you might use your production CA.
On the CORE client:
- Create CA key.
- Export the CA key.
- Self-sign the CA.
-
Import CA certificate
ucl generate -t ECC -n ca --user docker -w '<user-password>'
ucl export -u <CA UID> -o ~/.docker/certs/ca.key --obfuscate
openssl req -config ~/.docker/certs/openssl.cnf -key ~/.docker/certs/ca.key -new -x509 -days 7300 -sha256 -extensions v3_ca -out ~/.docker/certs/ca.pem -subj "/CN=Docker Notary CA"
ucl import --input ~/.docker/certs/ca.pem --password '<user-password>' --user docker --name ca
Create the Root Key
A key and certificate are required for this step. See here for more information. For example, to export the certificate and obfuscated CA key, you can use the following commands. We use the names ca.key and ca.pem since they are used to sign the root keys and the certificate is trusted by all clients. You can use the self-signed key and certificate created in the previous section.
The following procedure creates the root key in CORE. It is not exportable and therefore the key is never exposed. This key is used as the default root of trust for all your docker repositories.
On the CORE client:
- Create root key.
- Create root certificate request. You must use the subject CN=root to convey to the Notary that it is the root key.
- Export root key.
-
Self-sign the root key.
- Import the root certificate.
- List the keys.
- Check existing keys in Notary.
ucl generate -t ECC -n root --user docker -w '<user-password>'
ucl csr -o ~/.docker/certs/root.csr --subject "CN=root" -n root --user docker --password '<user-password>' --format PEM
ucl export -u <ROOT UID> -o ~/.docker/certs/root.key --obfuscate
openssl x509 -req -sha256 -days 365 -in ~/.docker/certs/root.csr -signkey ~/.docker/certs/root.key -out ~/.docker/certs/root.crt
ucl import --input ~/.docker/certs/root.crt --password '<user-password>' --user docker --name root
ucl list
Example response:
Partition 0 part1: 4 objects found
Certificate : UID=a7ef7a4669bc6dc0 Name="ca"
Private ECC key : UID=b2be1a48d15b5650 Name="ca"
Certificate : UID=4d41e5b72ea4a9af Name="root"
Private ECC key : UID=581085b99643923f Name="root"
notary key list
You should see the root key under YubiKey.
ROLE GUN KEY ID LOCATION
---- --- ------ --------
root e38202b6664ad57...5579707260b1b yubikey
ca c666f638241127b...c8baedd3602b7 yubikey
Note
The location is listed as yubikey, which is how Notary communicates with CORE.
Initialize Repository on Notary
In this example, we use NGINX as the image that will get signed and validated.
- Get the image.
- Tag the image.
- Push the image to the local registry. The value for {username} is your user name on Docker Hub.
- Check existing image with Docker Trust.
- Check existing image with Notary.
- Create repository certificate request.
- Create the following file:
- Create repository certificate.
- Initialize repository on Notary.
- Delegate snapshot key to Notary server.
docker pull nginx
docker tag nginx:latest docker.io/{username}/nginx:latest
docker push docker.io/{username}/nginx:latest
export DOCKER_CONTENT_TRUST=1
docker trust inspect docker.io/{username}/nginx
Example response:
No signatures or cannot access docker.io/{username}/nginx
notary list docker.io/{username}/nginx
Example response:
* fatal: notary.docker.io does not have trust data for docker.io/{username}/nginx
ucl csr -o ~/.docker/certs/nginx.csr --subject "CN=docker.io/{username}/nginx" -u <Root key UID> --user docker --password <user-password> --format PEM
touch ~/.docker/certs/index.txt
openssl ca -config ~/.docker/certs/openssl.cnf -create_serial -extensions repo_cert -days 375 -in ~/.docker/certs/nginx.csr -out ~/.docker/certs/nginx.pem
notary -D init docker.io/{username}/nginx --publish --rootcert ~/.docker/certs/nginx.pem
Note
If you defined NOTARY_DELEGATION_PASSPHRASE it uses that as the passphrase. If not, when asked for the password, it should be in the format: {"username": "docker", "password": "<docker password>"}
notary -D key rotate docker.io/{username}/nginx snapshot --server-managed
We now have root and target keys for our repository and are ready to sign our first image.
Delegation and Image Signing
The following sections refer to a delegate, which is a different device from the client.
Prepare the Delegation Environment
Delegation can be used on any device. Run the following commands to prepare the device for usage.
- Add the Docker CE repository.
- Install the Docker CLI
Command Line Interface.
- To install Notary, use the following commands.
- Test that the installation was successful by running this command:
- Start Docker
- A method is needed to examine the hash of the Docker image. You can use Docker manifest or skopeo.
- To use Docker manifest add the parameters experimental and debug to ~/.docker/config.json. Docker manifest provides capabilities to examine the hash of the Docker image.
- To use skopeo, install the package.
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce
Note
We do not need to use the modified Notary client for PKCSPublic-Key Cryptography Standards - Industry-standard cryptography specifications.#11. The default Notary client is sufficient.
curl -L https://github.com/theupdateframework/notary/releases/download/v0.6.1/notary-Linux-amd64 -o notary
chmod +x notary
mv notary /usr/bin/
notary version
Example response:
notary
Version: 0.6.1
Git commit: d6e1431f
sudo service docker start
{
…
"HttpHeaders": {
"User-Agent": "Docker-Client/19.03.5 (linux)"
},
"experimental": "enabled",
"debug": true
}
Note
You must be running Docker CE to use manifest.
sudo yum install skopeo
Create Delegation Key
Delegations in Docker Content Trust (DCT) allow you to control who can and cannot sign an image tag. A delegation has a pair of private and public delegation keys. See here for more information.
Run the following steps on the device used for image signing using a delegated key.
- Create a folder called ~/.notary.
- Create a file called config.json in that folder and edit it. Add the following contents.
- Log into Docker Hub with your Docker username and password.
- Copy ca.pem from the Admin device to the delegation key owner device and put it in the following directory (create the folder if it does not exist).
- Check existing delegations.
- The delegate (such as a developer) creates a delegation key and certificate on their own device. The public key is then given to the admin (target key owner).
- Transfer the public key <delegation-key-name> from the delegate's device to the admin (target key owner) device. The key should be copied to:
- The target key owner (i.e., the CORE client) adds delegation to the repository using the public key.
- Check existing delegations.
mkdir ~/.notary
Notes
1. The ca.pem file is created in the Create the Root Key section.
2. The trust_pinning section should have an entry for every repository that you are working with.
{
"trust_dir": "~/.docker/trust",
"remote_server": {
"url": "https://notary.docker.io"
},
"trust_pinning": {
"ca": {
"docker.io/{username}/nginx" : "/root/.docker/certs/ca.pem"
},
"disable_tofu": true
}
}
sudo docker login -u <Docker username> -p <Docker password> docker.io
~/.docker/certs/
notary delegation list docker.io/{username}/nginx
Example response:
No delegations present in this repository
docker trust key generate <delegation-key-name> --dir ~/.docker/certs/
~/.docker/certs/<delegation-key-name>
notary -D delegation add docker.io/{username}/nginx targets/me ~/.docker/certs/<delegation-key-name> --all-paths -p
notary delegation list docker.io/{username}/nginx
Example response:
ROLE PATHS KEY IDS THRESHOLD
---- ----- ------- ---------
targets/me "" <all paths> 97eddf88e1f25d...dae4170f02c19a542 1
Sign an Image
The delegation key owner device can now sign an image on the developer's device.
- Get the latest Docker image.
- Check existing signatures with Notary.
- Get image manifest hash and size, which can be found using Docker manifest or by using skopeo.
- In Docker manifest, the hash is the digest field, excluding any prefix, such as sha256. The size is given in the size field.
-
In skopeo:
- Get image manifest hash.
- Get image manifest size.
skopeo inspect --raw docker://docker.io/{username}/nginx:latest | sha256sum
skopeo inspect --raw docker://docker.io/{username}/nginx:latest | wc -c
- Sign image using one of these methods:
- Check existing signatures with Docker.
- Check existing signatures with Notary.
docker pull docker.io/{username}/nginx
notary list docker.io/{username}/nginx
Example response:
No targets present in this repository.
docker manifest inspect docker.io/{username}/nginx -v
Example response:
{
"Ref": "docker.io/{username}/nginx:latest",
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:36b77d8bb27ffca25c7f6f53cadd059aca2747d46fb6ef34064e31727325784e",
"size": 948,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
}
notary -D addhash -p docker.io/{username}/nginx latest "<MANIFEST_SIZE>" --sha256 "<MANIFEST_HASH>" -r targets/me
Or:
docker push docker.io/{username}/nginx:latest
docker trust inspect docker.io/{username}/nginx
Example response:
[
{
"Name": "docker.io/{username}/nginx",
"SignedTags": [],
"Signers": [
{
"Name": "me",
"Keys": [
{
"ID": "97eddf88e1f25d9b8cae81faccb79134b39d2484a1488dcdae4170f02c19a542"
}
]
}
],
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [
{
"ID": "38a49f18df670f9cfaad15c519da80cf2f1e5093550e02a11b167dfec8c47c3d"
}
]
},
{
"Name": "Repository",
"Keys": [
{
"ID": "e80d2bb843ab0a79fe033c6100c7b6e5bd4c09ebb2517b882a5b6f3031c57bca"
}
]
}
]
}
]
notary list docker.io/{username}/nginx
Example response:
NAME DIGEST SIZE (BYTES) ROLE
---- ------ ------------ ----
latest 36b77d8bb27ffc...4e31727325784e 948 targets/me
Validate an Image
Validate using Docker manifest.
- Retrieve the hash and hash size in Notary.
- Validate that the hash and size have the same values as those stored in Docker.
notary lookup docker.io/{username}/nginx latest
For example:
[root@client ~]# notary lookup docker.io/{username}/nginx latest
latest sha256:36b77d8bb27ffca25c7f6f53cadd059aca2747d46fb6ef34064e31727325784e 948
[root@client ~]# docker manifest inspect docker.io/{username}/nginx -v | jq .Descriptor.size
948
[root@client ~]# docker manifest inspect docker.io/{username}/nginx -v | jq .Descriptor.digest
"sha256:36b77d8bb27ffca25c7f6f53cadd059aca2747d46fb6ef34064e31727325784e"
Validate using skopeo:
- Retrieve the hash and hash size in Docker and what is stored in Notary.
- Validate that they have the same values.
notary lookup docker.io/{username}/nginx latest
skopeo inspect --raw docker://docker.io/{username}/nginx:latest | wc -c
skopeo inspect --raw docker://docker.io/{username}/nginx:latest | sha256sum
For example:
[root@client ~]# notary lookup docker.io/{username}/nginx latest
latest sha256:36b77d8bb27ffca25c7f6f53cadd059aca2747d46fb6ef34064e31727325784e 948
[root@client ~]# skopeo inspect --raw docker://docker.io/{username}/nginx:latest | wc -c
948
[root@client ~]# skopeo inspect --raw docker://docker.io/{username}/nginx:latest | sha256sum
36b77d8bb27ffca25c7f6f53cadd059aca2747d46fb6ef34064e31727325784e -