Sécuriser les images et automatiser leurs mises à jour avec Renovate
Date de l'article:02-08-2026
CI/CD Gitlab automate refactoring
Renforçement de la sécurité de la chaîne d'approvisionnement logicielle en verrouillant les images Docker par leur digest SHA-256. Nous automatiserons leurs mises à jours avec Renovate.
Voici les 4 étapes principales de cet article.
Elles se suivent dans l'ordre logique de mise en place, mais vous pouvez y accéder directement depuis cette liste.
Pourquoi pinner les images Docker avec un digest?
Une image Docker identifiée uniquement par son tag, par exemple maven:3.9.11, n'est pas immuable. Deux pipelines exécutés à plusieurs semaines d'intervalle peuvent donc récupérer des contenus différents malgré l'utilisation du même tag.
C'est pourquoi nous pouvons associer le tag à un digest SHA-256 afin de référencer précisément le contenu de l'image utilisé par le pipeline.
Fonctionnement du téléchargement des images
Lorsqu'aucun registre n'est explicitement indiqué dans le nom de l'image, le Docker executor de GitLab Runner récupère les images depuis
Docker Hub
Comme on ne le précise pas dans les jobs, GitLab interprète automatiquement cela comme venant, par exemple de:
docker.io/library/maven:3.9.11-eclipse-temurin-17
Autrement dit: registre: Docker Hub, namespace: library,
image: maven, tag: 3.9.11-eclipse-temurin-17
Son fonctionnement est similaire à la commande:
docker pull docker.io/library/maven:3.9.11-eclipse-temurin-17
Puis il démarre le conteneur avec cette image.
On conseille de garder la syntaxe courte. C'est la convention la plus répandue, c'est une bonne pratique qui rend la configuration claire.
Conversion des images
Dans l'article précédent, nous avions commencé à travailler sur le fichier "variables/global.yml" pour rassembler les images et leurs versions dans des variables et les utiliser dans nos pipelines. À présent, nous allons modifier ce fichier de versions afin de les préparer à recevoir leurs mises à jour.
1. Modifications dans le fichier de variables
Nous allons travailler sur le fichier
.gitlab/variables/global.yml
Rappel du contenu du fichier où l'on avait rassemblé les images
variables:
JAVA_VERSION: "17"
MAVEN_USER_HOME: "$CI_PROJECT_DIR/.m2"
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_IMAGE: "maven:3.9.11-eclipse-temurin-17"
MAVEN_IMAGE_JAVA21: "maven:3.9.11-eclipse-temurin-21"
ALPINE_IMAGE: "alpine:3.24"
POSTGRES_IMAGE: "postgres:16.14-alpine3.23"
TRIVY_IMAGE: "aquasec/trivy:0.72.0"
DOCKER_IMAGE: "docker:29.6.1"
DOCKER_DIND_IMAGE: "docker:29.6.1-dind"
K6_IMAGE: "grafana/k6:2.1.0"
2. Récupération des index digest par image
Puisque le registre est public, on ne va pas consommer inutilement des ressources Docker, on va récupérer les images directement depuis le registre Docker Hub.
Docker Hub peut afficher deux digests différents
- Manifest digest: digest d'une architecture précise (amd64, arm64...)
- Index digest: digest du manifeste multi-architecture
On utilise l'Index Digest afin que le registre fournisse automatiquement la bonne image selon l'architecture du runner
Exemple avec Maven: 
Utilisation combinée (Tag + digest SHA-256)
Il est également possible de conserver le tag pour la lisibilité humaine tout en forçant la vérification du SHA (exemple): image: redis:7.2@sha256:XXXXX
3. Entrez les digest SHA-256 dans fichier global
Il ne reste plus qu'à mettre à jour les références des images en leur ajoutant leur digest. Derrière les versions des images, ajoutez simplement
@sha256: suivi de l'INDEX DIGEST, copié depuis le registre officiel
variables:
JAVA_VERSION: "17"
MAVEN_USER_HOME: "$CI_PROJECT_DIR/.m2"
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_IMAGE: "maven:3.9.11-eclipse-temurin-17@sha256:e4a7ace3dc0d645ed97f8d9ad0b0d3f0b14fa8d150138f27f116d7105a639b82"
MAVEN_IMAGE_JAVA21: "maven:3.9.11-eclipse-temurin-21@sha256:6fdc855a6ed81d288ca7ca37ac6ff5e9308b612485c0801d70b25a858c83d237"
ALPINE_IMAGE: "alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8"
POSTGRES_IMAGE: "postgres:16.14-alpine3.23@sha256:42b8b8b29c8a4e933d88943e5b03001a78794905cf786e6e7634e9f2abd5a0d3"
DOCKER_IMAGE: "docker:29.6.1@sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515"
DOCKER_DIND_IMAGE: "docker:29.6.1-dind@#sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515"
TRIVY_IMAGE: "aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f"
K6_IMAGE: "grafana/k6:2.1.0@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669"
Les digests, eux, restent immuables. Lorsqu'un tag Docker évolue vers un nouveau contenu, Renovate peut mettre à jour le digest tout en conservant le même tag. Lorsqu'une nouvelle version du tag est disponible, il peut également mettre à jour simultanément le tag et le digest.
Les versions et SHA cités peuvent différer de votre environnement en raison du temps écoulé entre les mises à jour et la rédaction des articles;
En cas d'écart, seul le
repository est la source de vérité
Mise en place de Renovate
Une fois les images figées avec leurs digests, le contenu des images utilisées par les pipelines devient reproductible. En contrepartie, les versions n'évoluent plus automatiquement.
Renovate permet de résoudre ce problème en surveillant les images Docker, les dépendances ou les fichiers de configuration. Lorsqu'une nouvelle version est disponible, il crée automatiquement une Merge Request contenant la mise à jour proposée.
Pour Renovate, on travaillera sur ces fichiers:
.gitlab/
├── jobs/
│ └── renovate.yml
├── .gitlab-ci.yml
└── renovate.json
Processus et étapes
- Création d'un Personal Access Token GitLab spécialement utilisé pour Renovate
- Création du fichier renovate à la racine du projet et test de la regex
- Création d'un pipeline GitLab qui exécute Renovate
- Création du pipeline schedule et intégration dans la CI
Création du token
Renovate doit pouvoir communiquer avec l'API GitLab pour analyser le projet, créer des branches et ouvrir des Merge Requests. Il lui faut donc un jeton d'accès lui permettant de s'authentifier auprès de GitLab.
GitLab propose plusieurs mécanismes pour authentifier une automatisation: notamment les Personal Access Tokens, les Project Access Tokens et les Group Access Tokens. Le choix dépend du périmètre d'accès souhaité et de l'offre GitLab utilisée.
Les
Project Access Tokens et les
Group Access Tokens requièrent un abonnement payant. J'utiliserai un "
Personal Access Token" combiné à
un compte de service (Bot) avec l'offre gratuite de GitLab.
Le "Personal Access Token" est associé à notre compte utilisateur. Il faut donc lui attribuer uniquement les droits nécessaires et le stocker comme une variable CI/CD protégée et masquée.
gitlab/#setting-up-project-access
Un token d'accès doit être considéré comme un mot de passe: ne jamais le commiter dans le dépôt, l'afficher dans les logs ou l'intégrer directement dans un fichier de configuration.
1. Créer le Personal Access Token:
Depuis le projet → Avatar → Edit profile → Access Tokens
Nom : renovate
Expiration : 1 an (ou sans expiration)
Scopes : api
Cliquer sur
Create personal access token et copier sa valeur
2. Ajouter le token dans le projet
Depuis le projet: Settings → CI/CD → Variables
Ajouter la variable:
Key: RENOVATE_TOKEN
Value: le TOKEN_PAT
Options:
Masked
Protected
Contraintes GitLab: une variable "Masked" doit tenir sur une seule ligne, faire au moins 8 caractères et respecter un jeu de caractères restreint.
Une variable "Protected" n'est exposée que sur les branches/tags protégés: Pensez donc à protéger la branche develop, sinon le schedule n'aura pas accès au token.
Analyse des besoins
Faisons une petite analyse de nos besoins avant l'écriture du fichier Renovate. Cela nous permettra de savoir ce qu'il nous faut configurer exactement
Pour respecter notre stratégie Git, Renovate ne devra scanner et envoyer ses MR que depuis la branche [develop]
Dans cette plateforme, l'application Flashcards est utilisée uniquement pour la CI/CD, GitHub reste la référence en charge de la mise à jour de ces fichiers.
Renovate ne devra donc pas scanner le dossier .github, le Dockerfile, et le pom.xml
Pour GitLab, on donne accès à sa CI (.gitlab/* ) et particulièrement au fichier variables/global.yml contenant toutes les images (et versions) utilisées dans la CI/CD
On évite d'avoir plusieurs MR ouvertes simultanément et Renovate ne devra en lancer que 1 par 1 en reprenant develop comme point de départ à chaque fois.
Si plusieurs mises à jour sont détectées, elles seront regroupées en une seule MR. Les mises à jour des versions majeures et mineures peuvent être traitées séparément.
1. Création du fichier Renovate.json
Le fichier de configuration cible un cas d'usage précis: la mise à jour de l'infrastructure CI/CD sur GitLab, avec une forte restriction sur le bruit (limite de PR) et l'utilisation de regex personnalisées
Créer le fichier
renovate.json à la racine du projet
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"enabledManagers": [
"gitlabci",
"custom.regex"
],
"baseBranches": [
"develop"
],
"prConcurrentLimit": 1,
"branchConcurrentLimit": 1,
"ignorePaths": [
"pom.xml",
"Dockerfile",
".github/**",
"src/**"
],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": [
"/^\\.gitlab\\/variables\\/global\\.yml$/"
],
"matchStrings": [
"#\\s*renovate:\\s*depName=(?[^\\s]+)\\s*\\r?\\n\\s*[A-Z][A-Z0-9_]*:\\s*[\"'](?[^:]+):(?[^@\"']+)(?:@(?sha256:[a-f0-9]+))?[\"']"
],
"datasourceTemplate": "docker",
"versioningTemplate": "docker"
}
],
"packageRules": [
{
"description": "Group all Docker image updates from global variables",
"matchFileNames": [
".gitlab/variables/global.yml"
],
"groupName": "CI global images",
"separateMajorMinor": true
}
]
}
Résumé des modules
Avec extends:["config:recommended"]: on fait hériter le pipeline des bonnes pratiques par défaut de Renovate
enabledManagers:["gitlabci","custom.regex"]
Limite les managers utilisés par Renovate à:
→ gitlab-ci, pour les dépendances dans le fichier gitlab-ci.yml
→ custom.regex, pour les dépendances à détecter dans variables/global.yml
Avec baseBranches:["develop"], Renovate utilise develop comme branche de référence et cible ses mises à jour dessus
Avec prConcurrentLimit: 1 et branchConcurrentLimit: 1, on limite le bot à une seule Merge Request ouverte à la fois.
Avec ignorePaths, on exclut les fichiers que nous ne voulons pas traiter.
customManagers permet, via une regex, de scanner le fichier global.yml
Son "customType": "regex", "managerFilePatterns": [..]
applique une expression régulière pour viser ce fichier
Son matchStrings: [ ... ] permet de cibler les paquets écrits sous un commentaire spécifique (via depName, packageName, ..) dans le fichier .yml, la regex détecte aussi les digests SHA256 (currentDigest)
Ses datasourceTemplate et versioningTemplate indiquent à Renovate que les dépendances trouvées dans ce fichier proviennent du Docker Hub (ou d'un registre Docker) et qu'il doit utiliser les règles de versioning de Docker
packageRules: {...} permet de regrouper les mises à jour des images présentes dans global.yml, sous un même groupe nommé "CI global images"
Avec separateMajorMinor: true, les mises à jour majeures et mineures sont traitées séparément afin d'éviter de mélanger des changements potentiellement plus importants avec les simples mises à jour de patch. (Il s'agit déjà du comportement par défaut de Renovate, mais l'écrire explicitement rend l'intention claire.)
En bref, ce fichier configure Renovate pour agir comme un gardien de la CI/CD GitLab.
Il ne touche pas au code source, regarde uniquement le fichier de variables globales et les fichiers GitLab CI, il recherche les images Docker définies par un commentaire # renovate: depName=..., et/ou un digest. Il met à jour toutes ces images en même temps dans une seule Merge Request, (sauf en cas de versions majeures), ciblant la branche develop.
2. Ajout des commentaires Renovate
.gitlab/variables/global.yml
variables:
JAVA_VERSION: "17"
MAVEN_USER_HOME: "$CI_PROJECT_DIR/.m2"
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
# renovate: depName=maven
MAVEN_IMAGE: "maven:3.9.11-eclipse-temurin-17@sha256:e4a7ace3dc0d645ed97f8d9ad0b0d3f0b14fa8d150138f27f116d7105a639b82"
# renovate: depName=maven
MAVEN_IMAGE_JAVA21: "maven:3.9.11-eclipse-temurin-21@sha256:6fdc855a6ed81d288ca7ca37ac6ff5e9308b612485c0801d70b25a858c83d237"
# renovate: depName=alpine
ALPINE_IMAGE: "alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8"
# renovate: depName=postgres
POSTGRES_IMAGE: "postgres:16.14-alpine3.23@sha256:42b8b8b29c8a4e933d88943e5b03001a78794905cf786e6e7634e9f2abd5a0d3"
# renovate: depName=docker
DOCKER_IMAGE: "docker:29.6.1@sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515"
# renovate: depName=docker
DOCKER_DIND_IMAGE: "docker:29.6.1-dind@sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515"
# renovate: depName=aquasec/trivy
TRIVY_IMAGE: "aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f"
# renovate: depName=grafana/k6
K6_IMAGE: "grafana/k6:2.1.0@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669"
Le commentaire # renovate: depName=... sert à indiquer explicitement à Renovate quelle dépendance doit être recherchée.
La valeur correspond au nom du repository Docker (maven, alpine, postgres, aquasec/trivy, etc.).
Il doit se trouver sur la ligne directement au-dessus de la variable: une ligne vide entre les deux casse la regex.
Tester la Regex et le fichier de variables
Tester la Regex en amont, depuis l'excellent site
regex101.com
- Dans le menu de gauche: "Editor", sélectionner <> Flavor: JavaScript
- Dans le Box "Regular Expression": Cliquez dans le coin en haut à droite et sélectionner Regex Options et ensuite Regex Flag cliquez sur global

3. Création du pipeline Renovate
flashcards
└── .gitlab/jobs/renovate.yml
renovate:
stage: quality
image: renovate/renovate:latest
variables:
RENOVATE_PLATFORM: gitlab
RENOVATE_ENDPOINT: "https://gitlab.com/api/v4"
RENOVATE_REPOSITORIES: "val7304/flashcards"
LOG_LEVEL: info
script:
- renovate
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- when: never
4. Création du Pipeline Schedule
Depuis le projet (menu de gauche): Build → Pipeline schedules
Description : Renovate
Target branch : develop
Cron: 0 3 * * *
Cron timezone : Europe/Paris
Il ne faut rien configurer comme variable dans le "Pipeline Schedule"
Le pipeline sera exécuté tous les jours à 03h (heure de Paris)
La variable prédéfinie CI_PIPELINE_SOURCE s'assure que le job ne s'exécutera que lorsqu'il sera lancé par le "Pipeline Schedule"
si on fait un "git push" : CI_PIPELINE_SOURCE != schedule Renovate ne tourne pas
si on ouvre une MR : CI_PIPELINE_SOURCE != schedule Renovate ne tourne pas
si on le Schedule : CI_PIPELINE_SOURCE = schedule Renovate tourne
C'est exactement ce qu'on veut. Il n'a pas besoin de s'exécuter à chaque commit.
5. Inclure les fichiers dans la CI
Le stage "quality" existe déjà, ajoutez simplement le job Renovate dans le fichier .gitlab-ci.yml
include:
- local: '.gitlab/jobs/renovate.yml'
6. Ajouter la branche Renovate aux règles
Ajouter "renovate" juste derrière la dernière valeur (chore) dans le fichier .gitlab\templates\rules.yml afin qu'il détecte les Merge Requests venant de Renovate vers la branche develop
nouvelle règle:
.develop_rules:
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_BRANCH =~ /^(feature|fix|refactor|chore|renovate)\/.+$/'
- when: never
7. Mise à jour du composant sast/semgrep
Dans ce même fichier, nous avions mis en place, depuis la branche develop, le composant sast de Semgrep.
Nous pouvons lui ajouter son digest afin que Renovate puisse également le mettre à jour
Renovate sur
gitlab.com/sast/-/tags?
Depuis
code / tags:
Cliquez sur le short-sha du commit de la dernière version:
https://gitlab.com/components/sast/-/commit/0a29..
Remplacez la version:
- component: gitlab.com/components/sast/sast@3.4.0
Par le SHA long du commit:
- component: gitlab.com/components/sast/sast@0a29cf13041d2693472a2ce536c126df209b1cd4 #3.4.0
Laisser la version #3.4.0 en commentaire permet de suivre les versions mises à jour par Renovate
8. Vérification du job et de la version
Avant de laisser Renovate tourner tous les jours, on fait un premier test manuel
Depuis Build → Pipeline schedules
Ouvrir "Schedule Renovate" et cliquer sur Run ()
Ainsi on constate:
- que le token fonctionne et Renovate trouve bien le dépôt
- qu'il détecte les dépendances du fichier GitLab-ci (via le manager gitlabci)
- qu'il détecte les images du fichier global.yml (via la custom.regex)
- qu'il peut éventuellement créer une première MR
D'après les logs:
$ renovate
INFO: Renovate started
"renovateVersion": "44.57.2"
INFO: Dependency extraction complete
(repository=val7304/flashcards, baseBranch=develop)
"stats": {
"managers": {
"gitlabci": {"fileCount": 9, "depCount": 12},
"regex": {"fileCount": 1, "depCount": 9}
},
"total": {"fileCount": 10, "depCount": 21}
}
9. Mise à jour de la version de l'image Renovate dans global
Maintenant que nous avons la version de l'image de Renovate, on peut rechercher son digest sur Docker Hub, l'ajouter dans le fichier "global.yml" et lui ajouter son commentaire Renovate, afin de lui faire bénéficier de ses propres mises à jour:
# renovate: depName=renovate/renovate
RENOVATE_IMAGE: "renovate/renovate:44.57.2@sha256:94230bf485d9f3b469eb3f63e05e960e7709084101acf159b132f2a3e2a225c7"
Dans le pipeline renovate.yml, remplacer:
image: renovate/renovate:latest
par:
image: $RENOVATE_IMAGE
9. Première Merge Request reçue
La branche est lancée depuis une branche Renovate vers develop, la stratégie Git est donc respectée
Renovate sur GitHub
Compatibilité multi-plateforme
Dependabot: intégré à l'écosystème GitHub
Renovate: compatible avec GitHub, GitLab, Bitbucket, Azure DevOps, Gitea, etc.
Renovate est particulièrement adapté si l'on souhaite utiliser le même outil sur plusieurs plateformes Git.
Je pourrais donc faire exactement la même chose sur GitHub, les faire cohabiter et ensuite supprimer Dependabot. Cela me permettra de n'avoir qu'un système à gérer.
Responsabilités entre plateformes
GitLab Renovate
.gitlab-ci.yml .gitlab/**/* renovate.json
GitHub Renovate
pom.xml Dockerfile .github/*
Cette répartition n'empêche pas certaines modifications d'être effectuées directement depuis une plateforme CI/CD lorsqu'une vulnérabilité est détectée.
Mise à jour des dépendances ou de l'image Docker
Le fichier
pom.xml peut être modifié dans les plateformes CI/CD utilisant des scans de vulnérabilités (Trivy):
- Si celles-ci concernent les dépendances: voir comment fixer-les-vulnerabilites-trivy-d-un-application-java
- Si la vulnérabilité concerne l'image Docker: seul GitHub prend en charge la mise à jour de l'image Distroless et possède son script et son pipeline de mise à jour.
Dans ce cas, il faut comparer les digests et, en cas de changement, mettre à jour le Dockerfile dans la plateforme concernée (GitLab).
En général, la mise à jour du digest de l'image résout la vulnérabilité sur l'image Docker.
Dans la majorité des cas, je retrouve les mêmes vulnérabilités sur les deux plateformes, celles-ci sont alors rapidement mises à jour.
Pour l'instant, j'ai encore Dependabot sur GitHub et rien ne presse pour l'installer progressivement ; la configuration de Renovate pourrait alors être partagée et harmonisée entre GitLab et GitHub afin de partager des paramètres communs, tout en conservant un périmètre de fichiers propre à chaque plateforme.
Conclusion
Avec ce refactoring, l'ensemble des pipelines devient entièrement reproductible grâce aux digests SHA, tout en restant simple à maintenir grâce à Renovate.
Cette combinaison permet de concilier le besoin de reproductibilité des pipelines avec la nécessité de maintenir régulièrement les images utilisées par la CI/CD.
Dans l'article suivant, nous poursuivrons le refactoring de la CI en externalisant les scripts CI. Chacun deviendra autonome, réutilisable et partageable entre plusieurs pipelines ou plateformes.