Un linter Ansible 100% autonome via Gitlab CI/CD & Docker

Un linter Ansible 100% autonome via Gitlab CI/CD & Docker

Dans tous les projets, l'automatisation est un enjeu afin de pouvoir se dégager du temps sur d'autre taches mais aussi, et surtout, éviter les erreurs lié aux manipulations humaines.

mais où commence la paranoïa ? et ou commence le bien fondé ?
beaucoup de blabla dans ce post, mais imagé tout de même et le lien publique du résultat fonctionnel.
Dans ce pipeline, l'image docker sera re-builder à chaque fois que nécessaire (schedule), et non pas mise à jour.
Ce n'est pas pareil et on garde une image optimisée sinon on risque de finir avec une image de plus d'1 Go par exemple en empilant les layers.

si vous ne savez monter un runner (container Proxmox, VM ou autre) un article y sera consacré plus tard, que vous hébergiez ou non votre instance Gitlab, cela n'a aucune importance, ça fonctionne.

Le besoin

Un linter simple, rapide, efficace, sécurisé, dont on connais la source (connu et reconnu) et intégré dans une CI/CD Gitlab afin de pouvoir contrôler la qualité du code ansible.
Petit bonus: Ce pipeline est 100% autonome une fois initialisé.

rien que ça !

La sécurité au cœur du projet.
L’importance de la sécurité doit être dans toutes les étapes.
La source et le contenu de ce qui sera présenté, est pensé dans un soucis de traçabilité (source) et une simplification du code (Gitlab, Dockerfile)
Light and rootless.
Permet de se passer des commentaires dans le code.
Son autonomie et son caractère non bloquant pour les jobs (choix de chacun) en fait un pipeline souple et sécurisé.
De nos jours, la sécurité doit être au centre de tout développement et pensée dès le départ.

Origine du problème

Fournir une solution ou la maîtrise de la chaîne de bout en bout est totale (ormis 2 sources, l'image docker elle même et Trivy, faut bien prendre des choses existante aussi)

Prendre en compte la sécurité (Gitlab, Docker, contenu des playbook, inventaire, etc...
Pas d'aide disponible, rien trouvé sur internet qui aille en ce sens, cela ne veux pas dire que cela n'existe pas déjà, mais pas trouvé.
Il faut que cela soit facile, simple, réutilisable à souhait, autonome, que chacun puisse se l'approprier, d'où son partage avec la licence GNU GPL

Exploration des solutions

différentes sources ont été suivi, je vais surtout détaillé la source du Docker hub publique.
en vrac, cytopia, haxorof, hadolint, Debian, Red Hat ubi 8/9, etc ...
aucune n'étaient satisfaisante en terme de sécurité de base, aucun contrôle, suivi, ni en amont ni en aval.

Réinventons la roue

Les étapes:
1 - Recherche d'une image (système) de base dont la source est DOCKER OFFICIAL IMAGE voir VERIFIED PUBLISHER
2 - Création du Dockerfile (minimum d'install, de taille, fiabilité, sécurité, etc...)
3 - Trivy => Aquasec vs Bitnami
4 - A quoi cela ressemble ?
5 - Mise en place du pipeline

  1. La base de l'image
    Pour cela, une analyse (sécu) rapide de 3 images docker, testé avant et après un update/upgrade
    - Red Hat 8 (ubi8)
    - Debian 12
    - alpine 3.17(.3)
  • CVE Debian 12 (latest)
    test fait avec bintnami/trivy

Puis avec le Dockerfile suivant

Le résultat n'était pas acceptable

  • CVE Red Hat 8 (Ubi8, latest)
    test fait avec bintnami/trivy

Puis avec le Dockerfile suivant

Le résultat n'était pas acceptable

  • CVE Alpine 3.17.3
    test fait avec bintnami/trivy

Puis avec le Dockerfile suivant

FROM alpine:3.17.3

RUN apk update && \
    apk upgrade && \
    adduser -D ansible && \
    apk add --no-cache --update ansible-lint

USER ansible

HEALTHCHECK --interval=5m --timeout=5m CMD curl -f http://localhost/ || exit 1

CMD ["ansible-lint"]

Le résultat attendu est parfait

Pourquoi alpine 3.17 et pas la 3.18 ?

Dans mes tests fonctionnels d'ansible-lint, je me suis aperçu que la sortie console faisait apparaître des problème de dépendance ou lié à Ansible Galaxy, même si le job était fait, la sortie console n'était pas clair, lisible, et l'implémentation des dépendances, packages manquant impossible car non disponible (à ce moment la).
pour corriger cela, l'implémentation via pip étant nécessaire, la taille de l'image s'en ressent, et le check security de l'image ne passe plus forcément quand bien même le job est fait correctement.
Voici la sortie console résultante sur alpine:3.18.3

La base Docker

L’image gagnante est Alpine, dans sa version 3.17.3.
Les images Debian, Red Hat (UBI) posent de sérieux soucis de sécurité au-delà de leur taille trop importante pour le besoin.
Analyse faite avec Trivy que nous intégrerons dans la CI/CD.

2. Le Dockerfile
sur le projet gitlab, le Dockerfile contient en plus le package black, sinon, nous avons un petit Warning
WARNING Ignore loading rule from /usr/lib/python3.10/site-packages/ansiblelint/rules/jinja.py due to No module named 'black'

FROM alpine:3.17.3

RUN apk update && \
    apk upgrade && \
    adduser -D ansible && \
    apk add --no-cache --update ansible-lint black && \
    apk update && apk upgrade && \
    rm -rf /var/cache/apk/*

USER ansible

HEALTHCHECK --interval=5m --timeout=5m CMD curl -f http://localhost/ || exit 1

CMD ["ansible-lint"]

3. Trivy
quelle source choisir ?
Aquasec ou Bitnami (VMware) ?
Aquasec n'etant ni OFFICIAL IMAGE voir VERIFIED PUBLISHER
ce sera bitnami => VERIFIED PUBLISHER
Le fonctionnement, les options, les rapports sont les mêmes et les liens fournis renvoient chez Aquasec.
Trivy va nous permettre de vérifier non seulement l’image builder et/ou à (re)builder mais également vérifier notre Dockerfile, si l’on inclus des bad practice par exemple.

4. A quoi cela va ressembler ?

Maintenant que nous avons notre image de base, notre Dockerfile,notre contrôle qualité/sécurité avec Trivy,
Avant de rentrer dans le vif du sujet, regardons a quoi cela peut bien ressembler à l’aide un petit schéma.

Les jobs

Ils sont au nombre de 5.
- Initialise_docker_image.yml (joué une seul fois)
- scan_container.yml
- scan_Dockerfile.yml
- docker_build_image.yml
- Verify_docker_container.yml

Chaque job est conditionné au succès du précédant excepté scan_Dockerfile.yml, qui est conditionné sur l’echec de scan_container.yml

Explications:
Initialisation du registry container l’aide d’un premier jobles variables nécessaire Token, identifiant Docker hub, etc sont dans le menu CI/CD Variables de Gitlab => code clair.

Une fois le pipeline initialisé une tâche planifiée est créée et le pipeline devient autonome, à 100%.
Dans le menu intégration, la configuration mail est faite afin de recevoir tout les résultats des job quelque soit le résultat, failed ou success.

Nous sommes bien sur un build à chaque fois et non pas une mise à jour de l’image existante dans notre registry, ceci afin d’avoir une image à long terme qui soit lourde avec un nombre important de couche.

5. Mise en place du pipeline

Dans Gitlab, j'ai fait le choix de dédier une branche dans le projet, à ce linter ou plutôt au build de l'image docker qui embarquera ansible-lint.

.gitlab-ci.yml
une fois le job d'initialisation joué, il est commenté.

Initialisation

FROM alpine:3.17.3

RUN apk update && \
    apk upgrade && \
    adduser -D ansible && \
    apk add --no-cache --update ansible-lint

USER ansible

HEALTHCHECK --interval=5m --timeout=5m CMD curl -f http://localhost/ || exit 1

CMD ["ansible-lint"]

Scan image

scan_image:
  stage: security
  image:
    name: $CI_REGISTRY_IMAGE/trivy:latest
    entrypoint: [""]
  variables:
    TRIVY_AUTH_URL: $CI_REGISTRY
    TRIVY_USERNAME: $DEPLOY_USER
    TRIVY_PASSWORD: $DEPLOY_TOKEN
  script:
    - trivy --cache-dir .cache image
      --exit-code 1
      --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL
      --format table --output report.md
      --vuln-type os
      $CI_REGISTRY_IMAGE/$NAME:$VERSION
  artifacts:
    name: "security scan report $CI_COMMIt_SHA"
    paths:
      - report.md
    expire_in: 2 days
    when: on_failure

Scan Dockerfile

scan_Dockerfile:
  stage: build
  needs:
    - scan_image
  image:
    name: $CI_REGISTRY_IMAGE/trivy:latest
    entrypoint: [""]
  variables:
    TRIVY_AUTH_URL: $CI_REGISTRY
    TRIVY_USERNAME: $DEPLOY_USER
    TRIVY_PASSWORD: $DEPLOY_TOKEN
  script: |
        trivy config --exit-code 1 --format table --output dockerfile.md Dockerfile
  artifacts:
    name: "security scan report dockerfile"
    paths:
      - dockerfile.md
    expire_in: 7 days
    when: on_failure
  when: on_failure

Docker build

docker_build_image:
  stage: build
  needs:
    - scan_Dockerfile
  image: docker:latest
  services:
    - name: docker:dind
  script: |
    echo $BUILD_TOKEN | docker login -u $BUILD_USER --password-stdin
    docker pull alpine:$VERSION
    docker pull bitnami/trivy:latest
    docker logout
    echo $DEPLOY_TOKEN | docker login -u $DEPLOY_USER --password-stdin $CI_REGISTRY
    docker build -t $CI_REGISTRY_IMAGE/$NAME:$VERSION .
    docker tag bitnami/trivy:latest $CI_REGISTRY_IMAGE/trivy:latest
    docker push $CI_REGISTRY_IMAGE/$NAME:$VERSION
    docker logout
  when: on_success

Verify image

verify_Docker_image:
  stage: check
  needs:
    - docker_build_image
  image:
    name: $CI_REGISTRY_IMAGE/trivy:latest
    entrypoint: [""]
  variables:
    TRIVY_AUTH_URL: $CI_REGISTRY
    TRIVY_USERNAME: $DEPLOY_USER
    TRIVY_PASSWORD: $DEPLOY_TOKEN
  script:
    - trivy --cache-dir .cache image
      --exit-code 1
      --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL
      --format table --output report.md
      --vuln-type os
      $CI_REGISTRY_IMAGE/$NAME:$VERSION
  artifacts:
    name: "security scan report $CI_COMMIt_SHA"
    paths:
      - report.md
    expire_in: 2 days
    when: on_failure
  when: on_success

Structure du projet

Lien du projet

docker build pipeline
L'intégralité du code s'y trouve.
le pipeline à tourner un petit peu, sur un runner privé, vous avez un petit historique, avec des job ansible en exemple success et failed, pour voir et comprendre le fonctionnement.
pour de plus ample explication, un mail est dispo sur la page About
Vous verrez également comment j'ai insérer deux job pour utiliser le linter sur du code ansible présent également dans le projet pour l'exemple

Rien vu d'identique encore, mais encore une fois cela ne veut pas dire que cela existe pas.
N'oubliez pas,
Il est plus facile de se glorifier des erreurs des autres que de briller par ses propres compétences.

Voilà, c'était aussi simple que cela.