Hace poco, tuvimos el requerimiento de un cliente para realizar el firmado de sus imágenes Docker almacenadas en Azure Container Registry (ACR). Dado que era una tarea relativamente nueva para nosotros, tuvimos que documentarnos en el tema y realizar varias pruebas de concepto.
Luego de algunos intentos y de resolver varios problemas que se nos presentaron en el camino, logramos definir un proceso claro y repetible para el firmado de las imágenes Docker, a la vez que lo implementamos exitosamente en el cliente.
Debido a la dificultad que presentó realizar esta tarea y a la falta de consistencia en la documentación encontrada decidí escribir este artículo, en el cual trato de integrar todo el conocimiento y herramientas necesarias para realizar el correcto firmado de imágenes Docker siguiendo el protocolo Docker Content Trust (DCT).
Resumen
🔰 Requerimientos Iniciales
- El ordenador desde el cual se llevará a cabo el presente tutorial, debe tener instaladas y configuradas las siguientes herramientas:
- Docker Engine (servidor): Guía de instalación
- Notary CLI (cliente): Guía de instalación
- Azure CLI (cliente): Guía de instalación
🌉 La Arquitectura de Docker Content Trust (DCT)
Los componentes necesarios para realizar el proceso de firmado de imágenes Docker bajo el esquema DCT son los siguientes:
🔑 Las Keys para el firmado
Para el firmado de imágenes Docker necesitaremos principalmente 3 tipos de Keys (claves). Las describimos a continuación:
- Root Key: Es la clave raíz para todo un registro de contenedores (container registry) y es la clave base sobre la cual se crean las Repository keys. En caso de perder la Root Key, es imposible recuperarla.
- Repository Key: Es una clave correspondiente a un repositorio de imágenes en particular y servirá para firmar sólo los tags pertenecientes a dicho repositorio. Es creada a partir de la Root Key por lo que de perderse ésta ya no podrán crearse nuevas Repository Keys.
- Delegation Key: Es la key correspondiente a un usuario con permisos para firmar las imágenes de un repositorio particular. Estos permisos se asignan mediante un proceso llamado delegación.

🛡️ El Notary Server
Cuando se generan las firmas de las imágenes Docker, éstas no se guardan con la imagen Docker en sí sino que se almacenan en un servidor Llamado Notary Server. Hay que tener en cuentan que no todos los registros de contenedores de imágenes soportan DCT y por ende cuentan con un Notary Server habilitado.
En nuestro caso usaremos Azure Container Registry (ACR) para almacenar nuestras imágenes el cual nos permite habilitar el protocolo DCT en sus registros de contenedores. En este caso, el Notary Server es accesible por la misma URL del registro de contenedor (usando https) como en la imagen de ejemplo.

Cuando activamos el Notary Server, éste se habilita para el registro de contenedores, es decir para todos los repositorios almacenados en dicho registro. Adicionalmente, un repositorio de imágenes puede tener tags firmados y no firmados conviviendo sin problemas.
🚀 Desplegando un registro de contenedores en Azure (ACR)
Antes de todo debemos tener desplegado un registro de contenedores Docker, en nuestro caso haremos uso de Azure Container Registry (ACR) puesto que incluye el soporte de Docker Content Trust para el firmado de imágenes.
🐳 Creando el registro
Al momento de crear el registro de contenedores, le damos un nombre y seleccionamos como SKU a Premium puesto que este Tier es el que nos permitirá habilitar el servicio de Docker Content Trust:

En la siguiente página habilitamos el acceso público al registro, dejamos las demás opciones por defecto y creamos el registro:

Una vez creado el registro debemos habilitar DCT, para ello en la lista de opciones del registro (panel izquierdo) vamos a Policies ➜ Content trust, lo habilitamos seleccionando Status ➜ Enabled y guardamos (Save):

👥 Asignando permisos para el firmado de imágenes
Adicionalmente debemos asignar permisos (rol) a nuestro usuario para que éste tenga la capacidad de firmar las imágenes almacenadas en nuestro registro.
En el panel izquierdo vamos a Access control (IAM) y en la parte inferior del panel principal, en la sección Grant access to this resource seleccionamos la opción Add role assignment:

En la lista de roles que se mostrarán buscamos el rol AcrImageSigner, lo seleccionamos y hacemos click en Next:

En la página siguiente seleccionamos +Select members y se nos mostrará a la derecha un panel en el cual buscaremos a nuestro usuario ingresando el email. Seleccionamos a nuestro usuario y ya aparecerá en la sección Members del panel principal. Seleccionamos Review + assign guardando los cambios:

Verificamos los permisos asignados regresando a la página inicial de Access control (IAM), seleccionamos la opción View my access y se nos mostrará un panel derecho con la lista de roles asignados a nuestro usuario. Revisamos que el rol AcrImageSigner se encuentre en la lista:

Con esto ya contamos con un registro de contenedores con DCT habilitado y con los permisos adecuados para realizar el firmado de imágenes.
Para el caso de los registros de contenedores que no soporten DCT, la solución viene por desplegar un Notary Server de manera independiente. El procedimiento se sale del alcance del presente artículo, pero usted puede guiarse del proyecto oficial de Notary en GitHub.
✒️ El Proceso de Firmado
Antes de todo debemos iniciar sesión en Azure y el registro de contenedores sobre el cual trabajaremos:
az login
az acr login --name johnrc.azurecr.io
Verificamos que estamos correctamente conectados ejecutando:
# az acr list -o table
NAME RESOURCE LOCATION SKU LOGIN CREATION ADMIN
GROUP SERVER DATE ENABLED
------ --------- -------- ------- ----------------- -------------------- -------
johnrc rg_johnrc eastus Premium johnrc.azurecr.io 2023-05-01T01:27:43Z False
🗝️ Generando las Delegation Keys
Lo primero que tenemos que realizar es generar una Delegation Key para la firma de las imágenes. Para ello, ejecutamos:
docker trust key generate john
Se nos pedirá un password para encriptar la nueva Delegation Key generada. El comando creará un par de archivos, una clave pública y una clave privada. El archivo de clave privada se almacena siempre en la carpeta ~/.docker/trust/private, y el archivo de clave pública en la carpeta desde la cual ejecutamos el comando:
~/files # docker trust key generate john
Generating key for john...
Enter passphrase for new john key with ID 6d6ade9:
Repeat passphrase for new john key with ID 6d6ade9:
Successfully generated and loaded private key. Corresponding public key available: /root/files/john.pub

Podemos verificar la creación de la delegation key con el cliente de Notary, ejecutamos:
notary key list
# notary key list
ROLE GUN KEY ID LOCATION
---- --- ------------- --------
john 6d6ade9e896df /root/.docker/trust/private
En caso de que ya contemos con un par de archivos de clave pública y privada que queramos reutilizar (creadas anteriormente con OpenSSL por ejemplo), podemos cargar la clave privada con el siguiente comando:
docker trust key load private.key --name john
~/files # docker trust key load private.key --name john
Loading key from "private.key"...
Enter passphrase for new john key with ID 778705c:
Repeat passphrase for new john key with ID 778705c:
Successfully imported key from private.key

🔐 Creando las reglas de Delegación
Una regla de delegación permite indicar quien puede firmar imágenes en un repositorio específico. Para ello necesitamos el nombre del usuario, el archivo de clave pública generado en el paso anterior y el repositorio sobre el cual se asignarán los permisos.
El archivo de clave pública puede estar en distintos formatos (.pub/.crt/.pem), esto depende de si generamos la delegation key con docker trust, o de si la creamos manualmente con una utilidad como OpenSSL.
docker trust signer add --key john.pub john johnrc.azurecr.io/java11-utils
En este ejemplo el nombre del usuario es john, el archivo de clave pública es john.pub y el repositorio johnrc.azurecr.io/java11-utils.
# docker trust signer add --key john.pub john johnrc.azurecr.io/java11-utils
Adding signer "john" to johnrc.azurecr.io/java11-utils...
Initializing signed repository for johnrc.azurecr.io/java11-utils...
You are about to create a new root signing key passphrase. This passphrase
will be used to protect the most sensitive key in your signing system. Please
choose a long, complex passphrase and be careful to keep the password and the
key file itself secure and backed up. It is highly recommended that you use a
password manager to generate the passphrase and keep it safe. There will be no
way to recover this key. You can find the key in your config directory.
Enter passphrase for new root key with ID 86b2ff1:
Repeat passphrase for new root key with ID 86b2ff1:
Enter passphrase for new repository key with ID a2dbb4a:
Repeat passphrase for new repository key with ID a2dbb4a:
Successfully initialized "johnrc.azurecr.io/java11-utils"
Successfully added signer: john to johnrc.azurecr.io/java11-utils
Dado que es la primera vez que creamos una delegación en el repositorio johnrc.azurecr.io/java11-utils, DCT procede a inicializarlo y para ello se crean la Root Key y Repository Key correspondientes. Se nos pedirán passwords para encriptar la Root Key y Repository Key. Se recomienda usar passwords complejos y diferentes así como guardarlos en un lugar seguro para su uso posterior.
Verificamos las claves creadas. Podemos ver las claves raíz (root), de usuario (john) y de repositorio (targets):
# notary key list
ROLE GUN KEY ID LOCATION
---- --- ------ --------
root 18d9dffb748b55280ee /root/.docker/trust/private
john 42e9f806d309c9c4828 /root/.docker/trust/private
targets ...zurecr.io/java11-utils 5a9647e8d848082f4d1 /root/.docker/trust/private
La próxima vez que creemos una delegación sobre un repositorio diferente, por ejemplo johnrc.azurecr.io/python9-utils, sólo se creará una nueva Repository Key para dicho repositorio y se reutilizará la Root Key ya creada anteriormente. Vale recordar que la Root Key se crea sólo una vez y las diferentes Repository Keys se generan basadas en ella.

✍️ Firmando y subiendo una imagen Docker
Para realizar nuestra prueba de firmado, crearemos una imagen Docker de ejemplo en el repositorio johnrc.azurecr.io/java11-utils colocándole el tag signed:
# docker build -t johnrc.azurecr.io/java11-utils:signed .
[+] Building 25.1s (6/6) FINISHED
=> [internal] load build definition from Dockerfile_alpine_java11 0.0s
=> => transferring dockerfile: 269B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.17.2 1.2s
=> CACHED [1/2] FROM docker.io/library/alpine:3.17.2@sha256:ff6bdca1701f3a 0.0s
=> [2/2] RUN apk add git util-linux && apk add openjdk11 22.8s
=> exporting to image 1.1s
=> => exporting layers 1.1s
=> => writing image sha256:1baba7d5e04a91b85fb4e4347aeb03ba456e4b36ef6bae 0.0s
=> => naming to johnrc.azurecr.io/java11-utils:signed
#
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
johnrc.azurecr.io/java11-utils signed 1baba7d5e04a 3 hours ago 297MB
Ya estamos listos para firmar y subir nuestra nueva imagen Docker al registro de contenedor de Azure. En el momento de subir la imagen, la información de firmado se guardará en el Notary Server. Ejecutamos:
docker push --disable-content-trust=false johnrc.azurecr.io/java11-utils:signed
En este ejemplo habilitamos el firmado de la imagen con la opción –disable-content-trust=false e indicamos el repositorio y tag de la imagen a firmar como johnrc.azurecr.io/java11-utils:signed
# docker push --disable-content-trust=false johnrc.azurecr.io/java11-utils:signed
The push refers to repository [johnrc.azurecr.io/java11-utils]
6cb6697bc6eb: Layer already exists
7cd52847ad77: Layer already exists
signed: digest: sha256:6ed4ea21547483944b71568e53d6f7664a6ea4b3c94d8a0a8640d1d030879b0c size: 741
Signing and pushing trust metadata
Enter passphrase for john key with ID 778705c:
Successfully signed johnrc.azurecr.io/java11-utils:signed
En este ejemplo tenemos localmente 2 imágenes: la que acabamos de crear y subir firmada (tag signed) y otra ya existente (tag v1) que se subió sin la opción –disable-content-trust=false (sin firmar):
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
johnrc.azurecr.io/java11-utils signed 1baba7d5e04a 3 hours ago 297MB
johnrc.azurecr.io/java11-utils v1 8d25badcd502 3 hours ago 14.4MB
El hecho de que le hayamos colocado el tag signed a una imagen no quiere decir que la imagen ya esté firmada. Para verificar si una imagen está realmente firmada o no vamos a utilizar el siguiente comando:
docker trust inspect --pretty johnrc.azurecr.io/java11-utils:signed
# docker trust inspect --pretty johnrc.azurecr.io/java11-utils:signed
Signatures for johnrc.azurecr.io/java11-utils:signed
SIGNED TAG DIGEST SIGNERS
signed 6ed4ea21547483944b71568e53d6f7664a6ea4b3c94d8a0a8640d1d03087 john
List of signers and their keys for johnrc.azurecr.io/java11-utils:signed
SIGNER KEYS
john 778705c2e4f4
Administrative keys for johnrc.azurecr.io/java11-utils:signed
Repository Key: a62a0297649e16318b32853af85dd8bcbc7fb276da62fed6f5d875fbda8f
Root Key: 4f719a5c838d9d22eccec72ed59c49935783fb2bb92ba289d5334bad14d5
Podemos ver que se indica quién firmó la imagen (john), el id de la clave de delegación utilizada (778705c2e4f4), y el Repository/Root Keys utilizados.
En caso analizemos el tag v1 con el mismo comando, veremos que el resultado es diferente:
# docker trust inspect --pretty johnrc.azurecr.io/java11-utils:v1
No signatures for johnrc.azurecr.io/java11-utils:v1
List of signers and their keys for johnrc.azurecr.io/java11-utils:v1
SIGNER KEYS
john 42e9f806d309
Administrative keys for johnrc.azurecr.io/java11-utils:v1
Repository Key: 5a9647e8d84808d7003b4416f29f18810da9dbbd55c62a90cbf93c02f4d1
Root Key: 368988454a765d285cd9489aec92d0a676dc797f07d94f92096bc08d0b086
El resultado indica claramente que la imagen analizada no cuenta con firma alguna.
Otra manera de verificar los tags firmados y no firmados en un repositorio Docker pero que funciona sólo para Azure Container Registry es ejecutando el siguiente comando de Azure CLI:
az acr repository show-tags -n johnrc.azurecr.io --repository java11-utils --detail -o table
Podemos ver en la columna Name los tags almacenados en el registro y en la columna Signed si dicho tag está firmado o no:
# az acr repository show-tags -n johnrc.azurecr.io --repository java11-utils --detail -o table
CreatedTime Digest LastUpdateTime Name Signed
------------- ------------------------------ ------------------- ------ --------
2023-04-21T17 sha256:826dc034c5846fa3599bdec 2023-04-21T17:06:37 latest False
2023-04-21T19 sha256:6ed4ea21547483944b71568 2023-04-21T19:25:53 signed True