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