He usado un antiguo proyecto hecho unos zorros, sin actualizar desde hace mucho tiempo para que el pipeline automatizado me vaya sugiriendo con el tiempo al menos actualizaciones de dependencias y de contenedores, así como a nivel de código.

La idea es que una vez que el código llega a github mediante un push, GH lo detecte, notifique a Snyk los cambios y tanto Github como Snyk empiecen a ejecutar una serie de acciones que comprendan las siguientes tareas:

  1. Bajar las dependencias, compilar el proyecto, crear el paquete, ejecutar los tests, buscar CVEs críticos en el código, notificar al canal Discord sobre el resultado.
  2. Con el paquete creado anteriormente, crear un contenedor Docker, subirlo a un hub público o privado, y notificar al canal Discord.
  3. Con el contenedor creado anteriormente, permitir que Snyk o parecido, haga un scan sobre el contenido del contenedor, es decir, monitorizarlo y notificar al canal Discord.
  4. Crear releases en GH y notificarlo en Discord.
  5. K8s coge la release del contenedor Docker y crea los pods para ponerlos en producción. En esta publicación no mostraré esto, lo dejo para otra publicación.

Una vez que tienes un proyecto con sus tests y quieres que pase por un proceso de CI/CD con la seguridad en mente, lo primero que piensas es en que tienes que subirlo a github, guardar el código, para ello usaríamos algo como :

git add .
git commit -m "first commit."
git tag -a -m "first commit."
git push --follow-tags

Una vez que el código está en GH, nos gustaría que fuese GH quien hiciese compilaciones y ejecuciones de los tests del proyecto cada vez que alguien sube algo, ejecutase scaneos en el código y en las dependencias buscando CVEs conocidos, errores importantes en el código o simplemente sugerencias, todo ello para tratar de mantener un proyecto lo más limpio y seguro posible, no?

Para ello, GH nos proporciona acciones, básicamente son ficheros .yml que se colocan en un directorio determinado de tu proyecto para que una vez que están subidos, los detecte y empiece una ejecución asíncrona.

Para crear una acción, en la pestaña Actions de tu proyecto, veras New Workflow, todo empieza así. Cuando haces click, verás un montón de actions en la que vas querrás darle a configure y muy poco más.

Si le das a Java-with-Maven, GH te creará un fichero que por defecto se llamará maven.yml en un directorio nuevo que creará en tu repositorio llamado .github/workflows.

El contenido se verá tal que así:

# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: Java CI with Maven

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: maven
    - name: Build with Maven
      run: mvn -B package --file pom.xml

Qué hace este fichero? manda hacer un checkout del proyecto, y ejecuta las fases correspondientes de package, a saber:

  1. validate.
  2. compile.
  3. test.
  4. package.

Si en ese fichero, añadimos a nivel del -name anterior lo siguiente:

    - name: Discord notification
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has been build.'

notificará a un canal de Discord el resultado de la compilación.

Para ello primero tenemos que crear un canal en Discord y un webhook para comunicar Github y Discord, una vez que tenemos ese webhook, crearemos un secreto en nuestro repositorio para que sea github quien los gestione. Esos webhooks pueden ser revocables en el tiempo, por lo que si los configurais así, tendréis que cambiarlos en GH.

Una vez que tienes creado tu canal discord y estés dentro, tú como administrador puedes ir a Settings/Integraciones y verás Webhooks, le das, haces click en Crear Webhook, copias la url en algún lugar temporal, y nos vamos a Github.

En tu repositorio, te vas a Settings, Security, Secrets, Actions. Haces click a New repository secrets, en NAME pones DISCORD_WEBHOOK, en Secret, la url que copiaste antes. click en Add Secret. A partir de ahora, todos los secretos que haré mención en este escrito se harán igual.

Una vez que tu primer action está listo, haces commit y push en la web de Github. Haces un git pull en tu ordenador para traerte los cambios. Prácticamente enseguida, GH ejecutará la acción, podrás ver el resultado en la pestaña actions. Obviamente, el camino inverso también puede ser realizado, creas los directorios .github/workflows en la raiz de tu proyecto y colocas ahí los ficheros .yml. Personalmente encuentro que es mejor usar el configurador de la web por los errores de tabulación que puedas introducir.

Existe una manera para comprobar en local la validez de esos ficheros yml, usando act. https://github.com/nektos/act
Es lo que usarías si tienes que depurar estos ficheros yml, pero en esta publicación, no haré más mención a act, lo dejo para otra publicación.

Luego pensaríamos en crear un fichero Dockerfile para empezar a tener un despligue continuo. Existen muchas maneras de escribir un fichero Dockerfile, tantas que a día de hoy se recomienda sobre todo que el fichero Dockerfile se construya en modo multistage, es decir, que en las primeras líneas se pongan las capas que menos vayan a cambiar y al final del mismo se pongan las que potencialmente más puedan cambiar en el tiempo. La explicación es el tiempo de cacheo necesario para crear la imagen, nos interesa que tarde lo menos posible.

También querremos que la imagen sea lo más liviana posible e incluso al ponerla en producción, que ofrezca a los posibles atacantes los mínimos puntos de ataque y que sea un usuario con privilegios mínimos el que ejecute la aplicación empaquetada.

Un ejemplo, no es el mejor posible pero sirve para hacerse una idea:

La documentación oficial está en: https://docs.docker.com/engine/security/

Este proyecto está alojado en:
http://github.com/alonsoir/demo-jdbc

Los ficheros que vamos a analizar son:

1) Dockerfile

    # build stage build the jar with all our resources
    FROM openjdk:8-jdk as build
    ARG PROFILE
    VOLUME /tmp
    WORKDIR /
    ADD . .

    RUN ./mvnw clean install
    RUN mv /$JAR_PATH/target/demo-jdbc-0.0.1-SNAPSHOT.jar /app.jar

    # package stage
    FROM openjdk:8-jdk-alpine
    WORKDIR /
    # copy only the built jar and nothing else
    COPY --from=build /app.jar /

    COPY entrypoint.sh /entrypoint.sh

    EXPOSE 8080

    ENTRYPOINT ["/entrypoint.sh"]

Vamos a analizarlos un poco, el fichero Dockerfile multistage empieza con FROM openjdk:8-jdk as build, la cual indica que va a usar una versión (deprecada a día de hoy) para compilar el ejecutable, con maven.

En este punto deberíamos usar una versión mas moderna del JDK, por ejemplo:
FROM openjdk:11-jdk-slim-bullseye AS build-env

Una buena razón para elegir un JDK moderno es que suelen tener los problemas de seguridad muy acotados y se tratan de arreglar enseguida.

Mirad lo que dice Snyk sobre esa imagen jdk11:

https://snyk.io/test/docker/openjdk%3Ajdk-slim-bullseye

Y ahora, mirad lo que dice Snyk sobre esa imagen jdk8:

https://security.snyk.io/package/linux/debian:9/openjdk-8

Openjdk8 tiene vulnerabilidades críticas, altas, medianas y bajas, mientras que la 11 tiene vulnerabilidades bajas.

CORRECCIÓN, durante este fin de semana, han aparecido vulnerabilidades críticas.

https://snyk.io/test/docker/openjdk%3A11-jdk-slim

No existe el software 100% seguro, es normal que aparezcan dichas vulnerabilidades, pero estaremos de acuerdo en que es mejor tener vulnerabilidades bajas que críticas.

El resto de operaciones que aparecen en la fase de construcción son las típicas que aparecen en un Dockerfile, indicar en que volumen de la máquina anfitrión quieres trabajar (/tmp), en cual directorio quieres alojar el resultado de la operación de compilación (/ del contenedor) y mandamos ejecutar mvnw que está guardado en el mismo repositorio del proyecto.

Esto puede ser controversial porque habrá gente que opine lo siguiente, ¿por qué no descargas maven mediante wget o mediante la herramienta del sistema operativo de la fase build, Alonso?. Ofrecer wget en el contenedor de producción es algo que jamás deberíamos hacer, porque si permites esa herramienta, un atacante podría usarla y la usará para descargarse su propio toolbox de herramientas maliciosas.
Se trata de ponérselo lo más difícil posible a los criminales.

Ahora, la fase de copiar el jar para que una máquina virtual lo ejecute. Por favor, voy a usar el término copiar el jar como simplificación de instalar la aplicación en el contenedor.

Lo recomendable aquí en mi opinión es usar un sistema operativo con una JDK ligera, actualizada, sin CVEs críticos abiertos y lo más importante, sin bash o zsh. Una imagen distroless. Siguiendo esta política, lo único que debemos hacer luego es copiar el archivo *-RELEASE.jar en el contenedor.

Sé que mucha gente usará alpine o ubuntu,porque es muy liviana, pero en mi opinión es un error ofrecer un vector de ataque como es proporcionar bash y el gestor de paquetes para que alguien pueda interactuar con ese contenedor. Se deben ver cómo cajas fuertes, una vez que están desplegados en el k8s de turno o lo que sea.

Si no queremos usar imágenes distroless porque queremos empaquetar todo mediante rpm, dpkg o apk, en una fase de creación de releases, tendremos dichas operaciones en las que aprovecharemos también para subirlos a repositorios privados y firmarlos.

Es cierto que los criminales profesionales, me niego a llamarles hackers, se suelen traer su propio toolbox con ellos e includo crean su propio bash en memoria, hacen su trabajo y se van.

Se debería tratar de mitigar dichas operaciones, por ejemplo, denegar en el firewall del grupo de seguridad que gestione dichos pods toda conexión entrante desde internet hacia estos contenedores, solo se puede entrar a través de las ips gestionadas por el firewall web y necesitamos también un mecanismo en el kernel que impida construir en la memoria algún tipo de shell.

Se trata de cerrar todos los vectores posibles de ataques adaptándonos a lo que están haciendo los criminales.

Ya puestos, idealmente tendríamos para toda la infraestructura de contenedores gestionados en los pods la misma versión del kernel, el mismo espacio de usuario, las mismas librerias glibc necesarias para crear los procesos de cifrado y los sockets de conexión, etc.

Si la entropia de un contenedor viene dado por el número de componentes que contiene, es decir, el número de permutaciones que existen cuando dichos componentes interactuan entre sí, el kernel, el espacio de usuario donde habitan las librerias del sistema kernel y las librerías de la misma aplicación, todo ello compone una entropía que debería tender a ser la más pequeña posible, que tenga el menor número posible de permutaciones.

Todo el concepto distroless trata de minimizar dicho número de permutaciones. Trata también de homogeneizar en una infraestructura dicho número de componentes.

En el fondo lo que queremos es que nuestros contenedores ofrezcan cero puntos de entrada para explotar vulnerabilidades críticas o altas, pues aunque creemos dichas imagenes «seguras», en algún momento aparecen dichas vulnerabilidades.

En esos momentos en los que detectamos con nuestro scanner del pipeline dichas vulnerabilidades, debemos usar cuanto antes un contenedor que ya tenga arreglado dicha vulnerabilidad o monitorizar con más cuidado los sistemas de producción con la tecnología tipo SNORT que tengamos.

https://github.com/GoogleContainerTools/distroless

Después de dar la tabarra sobre el contenedor de ejecución, vemos que el Dockerfile indica que el directorio de trabajo es la / del contenedor, copia únicamente el jar a la raiz, el fichero entrypoint.sh y expone el puerto 8080.

BONUS

Mientras escribía el texto, snyk me advertía que una solución para el contenedor que hace de anfitrión de mi aplicación demo-jdbc era pasar de openjdk:8-jdk-alpine con 87 vulnerabilides conocidas, 3 críticas a openjdk:19-jdk-alpine con cero vulnerabilidaes conocidas, aunque hay que tener en cuenta que puede ser inestable. Hay que probar exhaustivamente la app y para ello es recomendable siempre tener nuestros tests de integración, unitarios y de stress para ver que tal se comporta en situaciones parecidas a la realidad. Es mejor saber que suponer.

https://packages.debian.org/search?keywords=openjdk-19-jre-headless

2) entrypoint.sh

    #!/bin/sh -l

    time=$(date)
    echo "::set-output name=time::$time"
    java -jar app.jar $1 $2

Este fichero simplemente va a usar el comando java -jar del contenedor para ejecutar la aplicación.

3) .github/workflows/docker-image.yml

name: Docker Image CI

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:

  build:

    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USER }}
          password: ${{ secrets.DOCKER_HUB_PASS }}

      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: .
          file: ./Dockerfile
          pull: true
          push: true
          cache-from: type=registry,ref=aironman/euromillions-test-java8:latest
          cache-to: type=inline
          tags: aironman/euromillions-test-java8:latest
          build-args: PROFILE=nectar,ARG2=test

      - name: Discord notification
        env:
            DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
        uses: Ilshidur/action-discord@master
        with:
            args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has been deployed in {{ EVENT_PAYLOAD.repository.html_url }}. '

Este fichero prácticamente lo genera GH, yo solo he añadido la parte de los secretos y la del Discord, para notificar que la descarga, construcción y subida al registro Docker que tengas se ha ejecutado correctamente. Más arriba he descrito como usar eso de los secretos en GH.

4) .github/workflows/maven.yml

# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: Java CI with Maven

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: maven
    - name: Build with Maven
      run: mvn -B package --file pom.xml

    - name: Discord notification
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has been build.'

Descrito más arriba.

5) .github/workflows/release.yml

# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: Java release with Maven

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  opensource-security:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@master
     - name: Run Snyk to check for vulnerabilities
       uses: snyk/actions/maven@master
       env:
         SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
       with:
        command: monitor
          args: --severity-threshold=critical

  code-security:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@master
     - name: Run Snyk to check for vulnerabilities
       uses: snyk/actions/maven@master
       env:
         SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
       with:
         command: code test
         args: --severity-threshold=critical

  release:
    needs: [opensource-security, code-security]
    runs-on: ubuntu-latest
    steps:
        - uses: actions/checkout@v2
        - name: Set up JDK 11
          uses: actions/setup-java@v1
          with:
            java-version: 11
            distribution: 'temurin'
            cache: maven
        - name: Set Git user
          run: |
            git config user.email ${{ secrets.EMAIL }}
            git config user.name "GitHub Actions"
        - name: Publish JAR
          run: mvn -B release:prepare release:perform -DskipTests
          env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        - name: Discord notification
          env:
              DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
          uses: Ilshidur/action-discord@master
          with:
            args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has been released.'

Este fichero trata de crear una RELEASE en Github, para ello primero ejecutará fases de escaneo descritas aquí:

https://github.com/snyk/actions/tree/master/maven

Ten en cuenta que snyk proporciona más actions aparte de snyk/actions/maven, comprueba cual es la que mejor se adapta a tus necesidades.

Cuando dichas operaciones de escaneo se cumplen, se ejecuta el comando mvn release.

6) .github/workflows/snyk-container.yml

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# A sample workflow which checks out the code, builds a container
# image using Docker and scans that image for vulnerabilities using
# Snyk. The results are then uploaded to GitHub Security Code Scanning
#
# For more examples, including how to limit scans to only high-severity
# issues, monitor images for newly disclosed vulnerabilities in Snyk and
# fail PR checks for new vulnerabilities, see https://github.com/snyk/actions/

name: Snyk Container

on:
  push:
    branches: [ "master" ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ "master" ]
  schedule:
    - cron: '32 8 * * 1'

permissions:
  contents: read

jobs:
  snyk:
    permissions:
      contents: read # for actions/checkout to fetch code
      security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
      actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Run Snyk to check for vulnerabilities
      uses: snyk/actions/maven@master
      env:
         SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      with:
         command: code monitor
    - name: Build a Docker image
      run: docker build -t aironman/euromillions-test-java8 .
    - name: Run Snyk to check Docker image for vulnerabilities
      # Snyk can be used to break the build when it detects vulnerabilities.
      # In this case we want to upload the issues to GitHub Code Scanning
      # Si fueses super celoso de la seguridad, dejarías ésto a false para que no construya el contenedor
      # Éste es un proyecto para aprender, por lo que lo dejo a true
      continue-on-error: true
      uses: snyk/actions/docker@14818c4695ecc4045f33c9cee9e795a788711ca4
      env:
        # In order to use the Snyk Action you will need to have a Snyk API token.
        # More details in https://github.com/snyk/actions#getting-your-snyk-token
        # or you can signup for free at https://snyk.io/login
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      with:
        image: aironman/euromillions-test-java8
        args: --file=Dockerfile
    - name: Upload result to GitHub Code Scanning
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: snyk.sarif

    - name: Discord notification
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has passed Snyk. Please go to Security link.'

Esta operación se va a ejecutar mediante cron a una determinada hora. Si no recuerdas como usar cron, puedes usar esta web para saber cuando se ejecutará.
https://crontab.guru/#32_8___1

Básicamente está diciendo que a las 08:32 de cada lunes, se ejecutará la tarea.

Muchas gracias por leer hasta aquí.

Deja un comentario