Firma de módulos Docker con Docker Content Trust

Introducción

La firma de artefactos docker, busca tener la garantía de que una imagen Docker no ha sido alterada una vez generada. Implica dos procesos:

  • Proceso de firma: Integrado en la generación de la imagen y previo a publicarla en un registro Docker.

  • Verificación de la firma: Dependiente del entorno donde se va a ejecutar la imagen (Docker, Kubernetes…). Implica verificar la firma justo después de descargar la imagen y antes de ejecutarla.

Tanto en la firma como en la verificación intervienen una serie de elementos de infraestructura y procesos que se detallan en el presente documento.

Proceso de firma y verificación

Docker contempla la firma de artefactos mediante un procedimiento denominado Docker Content Trust (DCT).

En este proceso entra en juego Notary (https://github.com/theupdateframework/notary ). Se trata de una herramienta que siguiendo la especificación de DCT, permite firmar y publicar y gestionar contenido confiable mediante firma.

Notary se integra con Docker durante la generación de las imágenes, para incluirles una firma electrónica, los datos de la firma se almacenan en el servidor de Notary para poder ser verificados posteriormente cuando se descarguen.

Una vez firmada la imagen, esta se despliega en un registro Docker desde donde se podrá descargar.

La verificación de la imagen al descargarla, de forma previa a su ejecución se puede realizar de forma directa por Docker si se activa DCT. El problema viene cuando la imagen se despliega en Kubernetes, como se hace con Rancher 2 y Openshift. En este caso, es necesaria otra pieza, un Admision Controller que haga la verficación. En esta guía se ha elegido Connaisseur, que se puede instalar directamente sobre Kubernetes.

Existen otros Admision Controller para la verificación de la firma en Kubernetes. En concreto se han estudiado dos:

Se ha elegido Connaisseur por su facilidad de instalación y configuración

Instalación de Notary

NOTA: En esta guía se hace referencia a la variable de entorno $DEVOP_TOOLS_BASE como directorio base donde se han instalado las devop-tools de https://github.com/mmorancassy/devops-tools/. Dicha variable no existe y se habrá de sustituir por la ruta correspondiente

Notary se debe integrar en las herramientas de devops de la máquina donde estas están instaladas, de manera que se despliega dockerizado e integrado en el mismo docker-compose que arranca toda la suite (gitlab, Jenkins, nexus…)

El despliegue de Notary implica incluir tres nuevos servicios docker a las herramientas. En concreto:

  • Notary-server

  • Notary-signer

  • Notary-db

Descarga de Notary

Notary se dockeriza desde el propio código fuente de su repositorio en github (https://github.com/theupdateframework/notary.git).

Para ello se clona dicho repositorio en el directorio $DEVOP_TOOLS_BASE/devops-tools

1 2 cd $DEVOP_TOOLS_BASE/devops-tools git clone https://github.com/theupdateframework/notary.git

En un paso posterior se integra este directorio en el fichero docker-compose de las devops-tools para incluir la generación de las imágenes docker de Notary a partir de este código fuente.

Certificados

Notary-server y Notary-signer necesitan sendos certificados para exponer via https sus endpoints. Se deben crear mediante el script $DEVOP_TOOLS_BASE/devops-tools/centos7-notary.sh

1 2 3 .$DEVOP_TOOLS_BASE/devops-tools/centos7-notary.sh mkdir $DEVOP_TOOLS_BASE/devops-tools/notary/certs mv /tmp/tls-notary $DEVOP_TOOLS_BASE/devops-tools/notary/certs

 

Modificar la configuración del docker-compose

Para incluir los servicios de Notary en el docker-compose desde el que se arrancan todos los servicios dockerizados, se debe modificar el fichero $DEVOP_TOOLS_BASE/devops-tools/cicd-tools/docker-compose.fullcicd.yml y añadir las variables de entorno utilizadas en dichos servicios, en el fichero $DEVOP_TOOLS_BASE/devops-tools/cicd-tools/.env

Para ello:

1 nano $DEVOP_TOOLS_BASE/devops-tools/cicd-tools/.env

Y añadir la siguiente configuración:

1 2 3 4 5 6 7 8 9 #Notary config NOTARY_CERTS=../notary/certs NOTARY_SERVER_CONFIG=../notary/fixtures/server-config.json NOTARY_SIGNER_CONFIG=../notary/fixtures/signer-config.json NOTARY_DB_MIGRATIONS=../notary/migrations/migrate.sh NOTARY_DB_INITDB=../notary/mysql-initdb.d NOTARY_CLI=../notary/cli/notary NOTARY_CLI_HOME=../notary/notary-cli-config NOTARY_KEYS=../notary/keys

 

1 nano $DEVOP_TOOLS_BASE/devops-tools/cicd-tools/docker-compose.fullcicd.yml

Y añadir la siguiente configuración:

Que incluye: Los 3 servicios de Notary

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 notary-server: build: context: ../docker-images/notary dockerfile: server.Dockerfile networks: - mdb - sig volumes: - ${NOTARY_CERTS}:/opt/certs/ - ${NOTARY_SERVER_CONFIG}:/server-config.json - ${NOTARY_DB_MIGRATIONS}:/migrate.sh ports: - "8080" - "4443:4443" entrypoint: /usr/bin/env sh command: -c "/migrate.sh && notary-server -config=/server-config.json" container_name: notary-server depends_on: - notary-db - notary-signer notary-signer: build: context: ../docker-images/notary dockerfile: signer.Dockerfile networks: mdb: sig: aliases: - notarysigner volumes: - ${NOTARY_CERTS}:/opt/certs/ - ${NOTARY_SIGNER_CONFIG}:/signer-config.json - ${NOTARY_DB_MIGRATIONS}:/migrate.sh entrypoint: /usr/bin/env sh command: -c "/migrate.sh && notary-signer -config=/signer-config.json" container_name: notary-signer depends_on: - notary-db notary-db: networks: - mdb volumes: - ${NOTARY_DB_INITDB}:/docker-entrypoint-initdb.d - ${DATA_PATH}/notary/notary_data:/var/lib/mysql image: mariadb:10.4 environment: - TERM=dumb - MYSQL_ALLOW_EMPTY_PASSWORD="true" command: mysqld --innodb_file_per_table container_name: notary-db

Y mapea varios volúmenes en la imagen de Jenkins ya existente:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jenkinscicd: image: onesaitplatform/jenkins:${JENKINS_VERSION} container_name: jenkinscicd environment: - RESTORE_DEFAULTS=false - RESTORE_MAVEN_SETTINGS=false volumes: - ${DATA_PATH}${JENKINS_DATA}:/var/jenkins_home:rw - ${DATA_PATH}${PLATFORM_CONFIG}:/tmp/op:rw - ${DOCKER_DATA}:/var/run/docker.sock - ${NOTARY_CLI}:/bin/notary - ${NOTARY_CLI_HOME}:/root/.notary - ${NOTARY_CLI_HOME}/cacert.crt:/etc/ssl/certs/cacert-notary.crt - ${NOTARY_KEYS}:/root/.docker/trust restart: on-failure

 

Configuración de Notary

A través del docker-compose, pasándola por parámetros, se debe externalizar la configuración tanto del notary-signer, como el notary-server, así como de la base de datos.

1 2 3 mkdir –p $DEVOP_TOOLS_BASE/devops-tools/notary/fixtures cd $DEVOP_TOOLS_BASE/devops-tools/notary/fixtures nano server-config.json

Añadir el siguiente contenido a server-config.json

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "server": { "http_addr": ":4443", "tls_key_file": "/opt/certs/notary-server.key", "tls_cert_file": "/opt/certs/notary-server.crt" }, "trust_service": { "type": "remote", "hostname": "notary-signer", "port": "7899", "tls_ca_file": "/opt/certs/cacert.crt", "key_algorithm": "ecdsa", "tls_client_cert": "/opt/certs/notary-server.crt", "tls_client_key": "/opt/certs/notary-server.key" }, "logging": { "level": "debug" }, "storage": { "backend": "mysql", "db_url": "server@tcp(notary-db:3306)/notaryserver?parseTime=True" } }

 

1 nano signer-config.json

Y añadir el siguiente contenido a signer-config.json

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "server": { "grpc_addr": ":7899", "tls_cert_file": "/opt/certs/notary-signer.crt", "tls_key_file": "/opt/certs/notary-signer.key", "client_ca_file": "/opt/certs/cacert.crt" }, "logging": { "level": "debug" }, "storage": { "backend": "mysql", "db_url": "signer@tcp(notary-db:3306)/notarysigner?parseTime=True" } }

Asímismo hay que configurar la Base de datos con un Script de migración (por si se hacen actualizaciones) y los datos iniciales:

1 2 mkdir –p $DEVOP_TOOLS_BASE/devops-tools/notary/migrations nano migrate.sh

Y añadirle el siguiente contenido:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #!/usr/bin/env sh # When run in the docker containers, the working directory # is the root of the repo. iter=0 case $SERVICE_NAME in notary_server) MIGRATIONS_PATH=${MIGRATIONS_PATH:-migrations/server/mysql} DB_URL=${DB_URL:-mysql://server@tcp(notary-db:3306)/notaryserver} # have to poll for DB to come up until migrate -path=$MIGRATIONS_PATH -database="${DB_URL}" up do iter=$(( iter+1 )) if [[ $iter -gt 30 ]]; then echo "notaryserver database failed to come up within 30 seconds" exit 1; fi echo "waiting for $DB_URL to come up." sleep 1 done echo "notaryserver database migrated to latest version" ;; notary_signer) MIGRATIONS_PATH=${MIGRATIONS_PATH:-migrations/signer/mysql} DB_URL=${DB_URL:-mysql://signer@tcp(notary-db:3306)/notarysigner} # have to poll for DB to come up until migrate -path=$MIGRATIONS_PATH -database="${DB_URL}" up do iter=$(( iter+1 )) if [[ $iter -gt 30 ]]; then echo "notarysigner database failed to come up within 30 seconds" exit 1; fi echo "waiting for $DB_URL to come up." sleep 1 done echo "notarysigner database migrated to latest version" ;; esac

Ejecutar:

1 2 3 4 chmod 777 migrations.sh mkdir –p $DEVOP_TOOLS_BASE/devops-tools/notary/mysql-initdb.d cd $DEVOP_TOOLS_BASE/devops-tools/notary/mysql-initdb.d nano initial-notaryserver.sql

Y añadirle el siguiente contenido:

1 2 3 4 5 CREATE DATABASE IF NOT EXISTS `notaryserver`; CREATE USER "server"@"%" IDENTIFIED BY ""; GRANT ALL PRIVILEGES ON `notaryserver`.* TO "server"@"%";

 

1 nano initial-notarysigner.sql

Y añadir el siguiente contenido:

1 2 3 4 5 CREATE DATABASE IF NOT EXISTS `notarysigner`; CREATE USER "signer"@"%" IDENTIFIED BY ""; GRANT ALL PRIVILEGES ON `notarysigner`.* TO "signer"@"%";

 

Instalar y configurar Notary CLI

Notary CLI es una herramienta de Notary utilizada desde la parte cliente (En nuestro caso Jenkins) para firmar los artefactos y comunicarse con el Notary-server. Por lo que es necesario descargarlo y posteriormente mapearlo al contenedor de Jenkins.

1 2 3 4 5 mkdir –p $DEVOP_TOOLS_BASE/devops-tools/notary/cli cd devops-tools/notary/cli wget https://github.com/theupdateframework/notary/releases/download/v0.6.1/notary-Linux-amd64 mv $DEVOP_TOOLS_BASE/notary-Linux-amd64 notary chmod 755 notary

Notary CLI utiliza una configuración para conectarse al servidor de notary. Para crear dicha configuración:

1 2 3 4 mkdir –p $DEVOP_TOOLS_BASE/devops-tools/notary/notary-cli-config cd $DEVOP_TOOLS_BASE/devops-tools/notary/notary-cli-config cp ../certs/cacert.crt . nano config.json

Y añadir el siguiente contenido:

1 2 3 4 5 6 { "remote_server": { "url": "https://10.1.0.17:4443", "root_ca": "cacert.crt" } }

 

Crear el directorio de claves de Notary

1 mkdir –p devops-tools/notary/keys

Establecer la confianza entre el Jenkins y Notary

Al ser el certificado de Notary un certificado autofirmado, es necesario establecer la confianza en Jenkins. Para ello se ha mapeado el cacert.crt al directorio de certificados de confianza (/etc/ssl/certs/) del contenedor de Jenkins. Este se ha hecho en un paso previo en el docker-compose, aquí simplemente se ilustra:

1 2 3 4 5 6 jenkinscicd: ····· volumes: ····· - ${NOTARY_CLI_HOME}/cacert.crt: /etc/ssl/certs/cacert-notary.crt ····

En caso de que se quiera hacer este cambio en caliente. Ejecutar:

1 2 docker exec –it jenkinscicd /bin/bash update-ca-certificates

Nota: en imágenes RHEL el directorio y comando son:

1 2 /etc/pki/ca-trust/source/anchors/ update-ca-trust

 

Generación de Claves de Firma

Se debe generaar una clave de firma para la plataforma. Para ilustrar los siguientes pasos y comandos de esta guía la llamaremos: onesaitkey con password: onesaitkeypwd

La forma de generarla ha sido accediendo al contenedor de Jenkins y generando la clave directamente:

1 2 docker exec –it jenkinscicd /bin/bash docker trust key generate onesaitkey --dir /root/.docker/trust --> password: onesaitkeypwd

Al estar el directorio del contenedor /root/.docker/trust mapeado en la máquina 10.1.0.17 al directorio $DEVOP_TOOLS_BASE/devops-tools/notary/keys, dichas claves quedan disponibles sin tener que acceder al contenedor.

Añadir el firmante onesaitkey para el artefacto pasando la password: onesaitkeypwd

1 docker trust signer add --key /root/.docker/trust/ onesaitkey.pub onesaitkey <registry_url>/<path>/<module>

 

Para facilitar la generación de claves y añadir firmantes, existe la posibilidad de crear sendos pipelines de Jenkins, que ejecuten esta tarea.

Pipeline de generación de claves

Este pipeline ejecuta el comando:

1 docker trust key generate <nombre_clave> --dir /root/.docker/trust

Admite como parámetros KEY_NAME y KEY_PASSWORD

El código del pipeline es:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 pipeline { // Execute the Pipeline, or stage, on any available agent agent { node { label 'obpsmaster' } } parameters { string(name: 'KEY_NAME', defaultValue: '', description: 'Nombre de la clave a generar') string(name: 'KEY_PASSWORD', defaultValue: '', description: 'Password de la clave a generar') } stages { stage('generate key') { steps { sh """ docker trust key generate ${params.KEY_NAME} --dir /root/.docker/trust << EOF ${params.KEY_PASSWORD} ${params.KEY_PASSWORD} EOF """ } } } post { always { echo "key generated" } } }

Pipeline de asociación de claves a imágenes

Este pipeline ejecuta el comando:

1 docker trust signer add --key /root/.docker/trust/<nombre_clave_firma>.pub <nombre_clave_firma> <nombre_imagen_docker>

Admite como parámetros SIGN_KEY_NAME, IMAGE_TO_SIGN, SIGNER_KEY_PASSWORD y ROOT_KEY_PASSWORD

El código del pipeline es:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 pipeline { // Execute the Pipeline, or stage, on any available agent agent { node { label 'obpsmaster' } } parameters { string(name: 'SIGN_KEY_NAME', defaultValue: '', description: 'Clave de firma a utilizar') string(name: 'IMAGE_TO_SIGN', defaultValue: '', description: 'Lista de imagenes a generar (una imagen por linea)') string(name: 'SIGNER_KEY_PASSWORD', defaultValue: '', description: 'Password de firma') string(name: 'ROOT_KEY_PASSWORD', defaultValue: '', description: 'Password de root') } stages { stage ('create signing script'){ steps { script { sh """ cat > dialogscript.sh << EOF #!/usr/bin/expect -f set timeout -1 set SIGN_KEY_NAME [lindex \\\$argv 0] set IMAGE_TO_SIGN [lindex \\\$argv 1] set SIGNER_KEY_PASSWORD [lindex \\\$argv 2] set ROOT_KEY_PASSWORD [lindex \\\$argv 3] spawn docker trust signer add --key /root/.docker/trust/\\\$SIGN_KEY_NAME.pub productkey \\\$IMAGE_TO_SIGN expect "Enter passphrase for root key with ID" send -- "\\\$ROOT_KEY_PASSWORD\\r" expect "Enter passphrase for new repository key with ID" send -- "\\\$SIGNER_KEY_PASSWORD\\r" expect "Repeat passphrase for new repository key with ID" send -- "\\\$SIGNER_KEY_PASSWORD\\r" expect eof """ sh "chmod 766 dialogscript.sh" } } } stage('add signer') { steps { sh """export DOCKER_CONTENT_TRUST_SERVER="https://10.1.0.17:4443" && export DOCKER_CONTENT_TRUST=1 && ./dialogscript.sh ${params.SIGN_KEY_NAME} ${params.IMAGE_TO_SIGN} ${params.SIGNER_KEY_PASSWORD} ${params.ROOT_KEY_PASSWORD}""" } } stage('remove script'){ steps{ sh "rm dialogscript.sh" } } } post { always { echo "key generated" } } }

 

Firma

La firma se realiza en la generación de los docker en Jenkins. Para ello, sobre la estructura habitual de los proyectos:

  • Añadir al Jenkinsfile el login en el registro de Nexus seguro:

1 sh "docker login -u <user> -p <password> <registro>:<puerto>"
  • Añadir en image-generation.sh los export necesarios para DCT

1 2 3 4 export DOCKER_CONTENT_TRUST_SERVER=https://<notary-server>:<notary-server-port> export DOCKER_CONTENT_TRUST=1 pushImage datagrid-manager $TAG export DOCKER_CONTENT_TRUST=0

Donde en pushImage se ha añadido la password de la clave de firma:

1 2 3 4 5 6 7 8 9 pushImage() {                echo "Hace el tag de la imagen"                docker tag $GIT_GROUP/$1:$2 10.1.0.17:14443/$GIT_GROUP/$1:$2                echo "Hace el push de la imagen"                docker push 10.1.0.17:14443/$GIT_GROUP/$1:$2 << EOF onesaitkeypwd EOF }

 

Verificación en Kubernetes - Connaisseur

Docker Content Trust como tal no es válido para para verificar la firma en Kubernetes. Para hacer la verificación de firma en Kubernetes, es necesario añadir un Admision Controller que realice tal tarea de forma automática.

Instalación de Connaisseur

Instalar kubectl y configurarlo para que apunte al cluster donde se quiere realizar la instalación. La configuración de kubectl se realiza en el fichero ~.kube/config, que tiene que tener la configuración indicada para kubectl en Rancher en la sección de clusters.

La instalación de connaisseur se hace siguiendo los pasos indicados en https://github.com/sse-secure-systems/connaisseur :

Instalar previamente: kubectl, helm, git, make, openssl e yq

Clonar el repositorio:

git clone https://github.com/sse-secure-systems/connaisseur.git

Acceder al directorio connaisseur y modificar el fichero Makefile para añadir la opción --debug a todos los comandos helm del Makefile. Pej:

1 helm install connaisseur helm --wait --debug

Configurar en el fichero connaisseur/helm/values.yaml, el servidor de notary, certificado de la CA, clave de firma de root para los artefactos Docker y política de firma en el fichero helm/values.yaml

En concreto los valores a configurar son:

  • notary.host: <notary_server>:<notary_server_port>

  • notary.selfsigned: true

  • notary.selfsignedCert: <certificado de la CA del notary-server ($DEVOP_TOOLS_BASE/devops-tools/notary/notary-cli-config/certs/cacert.crt)>

  • notary.rootPubKey: <clave pública de firma>. Se extrae del directorio de claves de notary generadas en Jenkins según el procedimiento que se indica a continuación.

  • policy: Añadir las políticas a aplicar. Por defecto está ignorar la firma de todo excepto de los artefactos de onesait-banking:

1 2 3 - pattern: "10.1.0.17:14443/onesait-banking/*:*" verify: true

El procedimiento para extraer el valor de la propiedad notary.rootPubKey es ejecutar en la máquina donde están las devops-tools:

1 2 3 cd $DEVOP_TOOLS_BASE/devops-tools/notary/keys/private sed '/^role:\sroot$/d' $(grep -iRl "role: root" .) > root-priv.key openssl ec -in root-priv.key -pubout -out root-pub.pem

La password solicitada es la de la clave de firma

El valor a proveer al fichero values.yaml está el fichero root-pub.pem

 

Una vez configurado el vichero values.yaml, ejecutar desde el directorio connaisseur:

1 make install

En caso de querer modificar algún valor, se puede actualizar con:

1 make upgrade