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:
IBM Portieris: https://github.com/IBM/portieris
Connaisseur: https://github.com/sse-secure-systems/connaisseur
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
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
.$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:
nano $DEVOP_TOOLS_BASE/devops-tools/cicd-tools/.env
Y añadir la siguiente configuración:
#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
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
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:
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.
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
{
"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"
}
}
nano signer-config.json
Y añadir el siguiente contenido a signer-config.json
{
"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:
mkdir –p $DEVOP_TOOLS_BASE/devops-tools/notary/migrations
nano migrate.sh
Y añadirle el siguiente contenido:
#!/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:
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:
CREATE DATABASE IF NOT EXISTS `notaryserver`;
CREATE USER "server"@"%" IDENTIFIED BY "";
GRANT ALL PRIVILEGES ON `notaryserver`.* TO "server"@"%";
nano initial-notarysigner.sql
Y añadir el siguiente contenido:
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.
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:
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:
{
"remote_server": {
"url": "https://10.1.0.17:4443",
"root_ca": "cacert.crt"
}
}
Crear el directorio de claves de Notary
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:
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:
docker exec –it jenkinscicd /bin/bash
update-ca-certificates
Nota: en imágenes RHEL el directorio y comando son:
/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:
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
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:
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:
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:
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:
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:
sh "docker login -u <user> -p <password> <registro>:<puerto>"
Añadir en image-generation.sh los export necesarios para DCT
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:
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:
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:
- 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:
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:
make install
En caso de querer modificar algún valor, se puede actualizar con:
make upgrade