Firma & Verificación de Images con Kyverno & Cosign
La manera de saber qué imágenes corren en nuestro Cluster con Policy as a Code.
La inmutabilidad de nuestro Cluster es importante, sin lugar a dudas. Una buena pregunta seria ¿Sabemos que imagenes usamos en nuestros ambientes? ¿Son las que fueron homologadas por Seguridad? ¿Podría un atacante hacer un deployment de imágenes adulteradas? Es aquí donde una estrategia proactiva, en Kubernetes, puede ayudarnos con algo de Policy as a Code. Para ello usaremos Kyverno y Cosign de Sigstore.
A grandes rasgos, ¿Que vamos a hacer?
Construiremos una imagen, para luego subirla a nuestra registry. Una vez en la registry la firmaremos con Cosign. Luego aplicaremos una política en Kyverno que solo permitirá desplegar imagenes, previamente firmadas, en nuestro Cluster Kubernetes.
¿Que es Kyverno?
Es un proyecto Open Source que nos habilita a definir, validar y reforzar políticas en nuestro Cluster. Algo interesante que presenta es lo sencillo y flexible que se definen las políticas, que al ser de manera declarativa podemos aplicar control de versiones como cualquier código. Hay una gran cantidad de políticas para aplicar, como Seguridad, Cumplimient o mejores practicas. Esto nos ayuda a conocer que pasa en nuestro Cluster y a su vez tenerlo bajo Cumplimiento.
Instalación
Vamos a instalar via Helm, con el siguiente comando.
helm upgrade --install --wait --timeout 15m --atomic \
--version 3.0.0-alpha.1 \
--namespace kyverno --create-namespace \
--repo https://kyverno.github.io/kyverno kyverno kyverno
Esperamos que helm termine, vamos a revisar si tenemos Kyverno listo.
Ya tenemos la implementación lista, ahora vamos a seguir con el otro componente.
¿Que es Cosign Sigstore?
Los ataques de Supply Chain han estado en boga el ecosistema moderno de desarrollo de software. Los pipelines de creación y despliegue no son herméticos, lo que da lugar a puntos que pueden ser explotados por atacantes para inyectar código o artefactos en nuestro Cluster para extraer datos o moverse lateralmente dentro de él. Así pues, tenemos la necesidad de tomar huellas digitales criptográficas de los artefactos para poder verificar su autenticidad.
Con Cosing vamos a firmar nuestras imágenes y las vamos a verificar en nuestro Cluster. Ahí es donde ingresa Kyverno.
Todo esto lo podemos hacer de manera local, sin lugar a dudas, pero qué mejor que dejarlo en un pipeline. Yo voy a estar trabajando con Jenkins, dejo el repositorio que tiene el Jenkinsfile, con la configuración.
Los pasos de instalación, ya sea local o en nuestro servidor Jenkins, podés verlos acá.
A grandes rasgos vamos a hacer el build de la imagen, hacer el push para luego poder firmar la imagen. Cosign no admite la firma de imágenes que no se hayan publicado en un registro. Además, es necesario tener permiso de escritura en ese registro. Yo voy a estar usando Dockerhub, como registry, para esta prueba.
Generación de Llaves
Lo primero que tenemos que hacer es un nuevo par de llaves: cosign.key y cosign.pub. Importante: Las clave privada deben almacenarse de forma segura. Podrias gestionarlas mediante HashiCorp Vault o Amazon KMS y la clave pública en un gestor de secretos.
Antes creo un directorio, para mi comodidad, y le doy derechos al usuario jenkins.
sudo groupadd keys_group
sudo usermod -aG keys_group santiago
sudo usermod -aG keys_group jenkins
sudo chown -R :keys_group /opt/keys
sudo chmod -R 770 /opt/keys
Nota: Como estoy usando Ubuntu lo mejor que podrias hacer es crear un grupo, asociar a el usuario de ubuntu, en mi caso santiago, y el usuario jenkins. Te dejo los pasos.
export COSIGN_PASSWORD=TUPASS
cosign generate-key-pair
Listo tenemos el par de llaves, para comenzar.
Configuración de Jenkins
Esto se podría hacer local, para la prueba de concepto, pero preferí hacer 2 pipelines en Jenkins para que se comprenda el proceso de firmado. El primer pipeline va a firmar la imagen y el segundo va a verificar la firma. Para estos procesos usaremos el password que generó los certificados y subiremos los certificados a Jenkins.
Debería quedarnos de esta manera.
Aqui les dejo el repositorio con los archivos necesarios, pero vamos a explicar el primer pipeline.
pipeline {
agent any
environment {
DOCKER_VERSION = "v1.0" // Puedes cambiar esto por la versión que desees
DOCKER_REGISTRY = "safernandez666"
COSIGN_PASSWORD=credentials('cosign-password')
COSIGN_PRIVATE_KEY=credentials('cosign-private-key')
}
stages {
stage('cleanup') {
steps {
sh 'docker system prune -a --volumes --force'
}
}
stage('docker build') {
steps {
script {
sh 'docker build -t ${DOCKER_REGISTRY}/webserver:${DOCKER_VERSION} -f Dockerfile .'
}
}
}
stage('docker push') {
steps {
script {
sh 'docker push ${DOCKER_REGISTRY}/webserver:${DOCKER_VERSION}'
//sh 'docker tag ${DOCKER_REGISTRY}/webserver:${DOCKER_VERSION} -t ${DOCKER_REGISTRY}/webserver:latest'
//sh 'docker push ${DOCKER_REGISTRY}/webserver:latest'
}
}
}
stage('sign the container image') {
steps { // Firmamos la Imagen
withCredentials([file(credentialsId: 'cosign-private-key', variable: 'COSIGN_PRIVATE_KEY_FILE')]) {
sh 'cosign version'
sh 'cosign sign --key ${COSIGN_PRIVATE_KEY_FILE} -y ${DOCKER_REGISTRY}/webserver:${DOCKER_VERSION}'
}
}
}
}
}
Creamos la imagen, luego la subimos a Dockerhub para poder firmar. Miremos el Output del Job.
La magia ya ocurrió y no solo vemos la imagen con nuestro tag si no que tambien se agrego el tag de la firma en Dockerhub.
El segundo pipeline, en cierto punto es lo que hará Kyverno en nuestro Cluster, verificar que sea la imagen que nosotros homologamos. Dejo el Job y les muestro el output.
pipeline {
agent any
environment {
DOCKER_VERSION = "v1.0" // Puedes cambiar esto por la versión que desees
DOCKER_REGISTRY = "safernandez666"
COSIGN_PUBLIC_KEY=credentials('cosign-public-key')
}
stages {
stage('verify the container image') {
steps {
sh 'cosign version'
sh 'cosign verify --key $COSIGN_PUBLIC_KEY $DOCKER_REGISTRY/webserver:$DOCKER_VERSION'
}
}
}
}
Política de Kyverno
Listo! Ahora vamos a configurar Kyverno. Vamos aplicar una política que verifique que la imagen sea firmada por nuestro pipeline.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image
annotations:
policies.kyverno.io/title: Verify Image
policies.kyverno.io/category: Sample
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod
policies.kyverno.io/minversion: 1.4.2
policies.kyverno.io/description: >-
Using the Cosign project, OCI images may be signed to ensure supply chain
security is maintained. Those signatures can be verified before pulling into
a cluster. This policy checks the signature of an image repo called
ghcr.io/kyverno/test-verify-image to ensure it has been signed by verifying
its signature against the provided public key. This policy serves as an illustration for
how to configure a similar rule and will require replacing with your image(s) and keys.
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-image
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- image: "*"
key: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHz1ilBMvBTJcQZwlO/73xtAJT9m2
H8iCKSyMhQ4BcxmVoEBc4EwUiFWdiTDit3elMew7O//TkW1ppiFoxZa4yw==
-----END PUBLIC KEY-----
A grandes rasgos la política verifica con la llave publica si la imagen a desplegar esta o no firmada. En este caso no debería poder desplegarse por qué tenemos esta sentencia validationFailureAction en Enforce y no en Audit.
Revisamos si está aplicada.
Tengo dos políticas, como pueden ver, en una no permito aplicar servicios con NodePort y en la otra verificó las imágenes como les mostré anteriormente.
Ahora si vamos a ver la magia. Voy a hacer un deployment de una imagen NO firmada, por nuestro pipeline y ver que pasa. Cree un namespace llamado pruebas.
kubectl run pruebas-unsigned --image nginx -n pruebas
Uala! No puede reconocer que este firmada por nosotros.
Ahora vamos a probar la que corresponde, que tenemos subida y firmada en Dockerhub.
Todo funcionó como queríamos. Ya tenemos Cosign & Kyverno trabajando de manera proactiva en nuestro Cluster.
Bonus Track
Para verlo de manera gráfica, podes instalar el reporter de esta manera.
helm install policy-reporter policy-reporter/policy-reporter --set kyvernoPlugin.enabled=true --set ui.enabled=true --set ui.plugins.kyverno=true -n policy-reporter --create-namespace
kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter
Espero que les sirva.