# Firma & Verificación de Images con Kyverno & Cosign

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](https://kyverno.io/) y Cosign de [Sigstore](https://www.sigstore.dev/).

A grandes rasgos, ¿Que vamos a hacer?

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709161710554/93c53275-18f6-4755-9754-69b14a2ab08c.png align="center")

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.

```bash
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709044688397/9f1f2951-bc0f-4b0d-8de7-42527f371c9f.png align="center")

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](https://github.com/safernandez666/webserver) 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á](https://docs.sigstore.dev/system_config/installation/).

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.

```abap
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.*

```abap
export COSIGN_PASSWORD=TUPASS
cosign generate-key-pair
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709141260950/f9d41104-ab63-48c3-a0ea-a8e395c6fde8.png align="center")

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709141390514/26d3f912-a84a-42ff-8b2b-cf14589be71b.png align="center")

Aqui les dejo el repositorio con los archivos necesarios, pero vamos a explicar el primer pipeline.

```javascript
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709142198983/4fc7f136-bbda-41da-89ad-c30d5d74005b.png align="center")

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**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709142277777/02011208-f283-469b-96d1-3c3859271b83.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709142287079/9875fac1-09b5-47a8-987b-c0a2f613028e.png align="center")

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.

```javascript
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'
      }
    }
  }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709142486244/b8050db2-c315-40c5-804c-de17761dbaba.png align="center")

## 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.

```yaml
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709157928219/6a7f287e-c885-4981-8729-ded80ca6c95b.png align="center")

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.

```bash
kubectl run pruebas-unsigned --image nginx -n pruebas
```

Uala! No puede reconocer que este firmada por nosotros.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709158183854/204450c9-b75e-4234-bf8d-6ef643c6c13d.png align="center")

Ahora vamos a probar la que corresponde, que tenemos subida y firmada en **Dockerhub**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709158311778/8928c2a4-9d92-4c51-94c3-106740f1f55d.png align="center")

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](https://github.com/kyverno/policy-reporter#readme) de esta manera.

```bash
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
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709158848523/1543cb95-8215-4d04-aa6b-f21099a3a719.png align="center")

Espero que les sirva.

## Referencias

[https://medium.com/@calvineotieno010/secure-your-ci-cd-pipelines-with-sigstore-and-kyverno-signing-and-verifying-image-c6adfa868910](https://medium.com/@calvineotieno010/secure-your-ci-cd-pipelines-with-sigstore-and-kyverno-signing-and-verifying-image-c6adfa868910)

  
[https://medium.com/@seifeddinerajhi/sign-and-verify-container-images-with-cosign-and-kyverno-a-complete-guide-b32b1f6e6264](https://medium.com/@seifeddinerajhi/sign-and-verify-container-images-with-cosign-and-kyverno-a-complete-guide-b32b1f6e6264)

  
[https://blog.kubesimplify.com/kyverno-and-cosign](https://blog.kubesimplify.com/kyverno-and-cosign)

  
[https://www.youtube.com/watch?v=Hj4CAGwcN74](https://www.youtube.com/watch?v=Hj4CAGwcN74)
