
La branche de départ de ce lab contient un mot de passe écrit en clair dans le code. La chaîne postgresql://user:s3cret@db:5432/app est codée en dur dans app/config.py — et elle est aussi présente dans le .gitlab-ci.yml. Quiconque clone le dépôt ou lit les logs du pipeline peut la lire. C'est la faille de sécurité numéro un dans les pipelines CI/CD débutants. Dans ce lab, vous allez sortir ce secret du code et le gérer correctement via les variables CI/CD de GitLab.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Identifier un secret codé en dur dans du code Python et un pipeline CI/CD
- Utiliser
os.getenv()pour lire une valeur depuis l'environnement au lieu de la coder en dur - Créer une variable CI/CD dans GitLab (protégée + masquée)
- Comprendre la différence entre variable protégée, masquée, et non protégée
Quel lab commencer ?
Section intitulée « Quel lab commencer ? »Ce lab s'adresse encore aux apprenants ayant complété Lab 04. La gestion des secrets est fondamentale pour toute pipeline, introduit tôt serait prématuré. Si vous êtes en avancé, vous pouvez commencer par Lab 05 directement depuis starter/lab-05.
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »Les secrets codés en dur dans le code source sont une des vulnérabilités les plus fréquentes dans les projets réels. Un développeur pressé colle directement un mot de passe dans le fichier de configuration « juste pour tester » — et ça finit dans un commit, dans l'historique Git, et parfois dans un dépôt public.
Situations réelles où ce lab vous aide :
- Un audit de sécurité détecte des credentials dans l'historique Git de votre projet
- Un développeur a poussé
DATABASE_URL: "postgresql://user:s3cret@prod:5432/db"dans le CI « pour faire vite » - Les logs de votre pipeline affichent un token d'API en clair quand vous le passez en argument
- Une rotation de mot de passe nécessite de modifier un fichier versionné et de pousser un commit
Prérequis
Section intitulée « Prérequis »- Lab 04 complété — ou partir directement de la branche
starter/lab-05 - Avoir lu Variables GitLab CI/CD (conseillé)
Point de départ
Section intitulée « Point de départ »-
Passez sur la branche de départ
Fenêtre de terminal cd pipeline-craftgit checkout starter/lab-05 -
Observez les secrets en place
Ouvrez
app/config.py:class Settings:database_url: str = os.getenv("DATABASE_URL", "postgresql://user:s3cret@db:5432/app")Et regardez le
.gitlab-ci.yml:variables:DATABASE_URL: "postgresql://user:s3cret@db:5432/app"Le mot de passe
s3cretest visible dans deux endroits. Si ce dépôt était public, n'importe qui pourrait le lire. -
Poussez pour déclencher le pipeline
Fenêtre de terminal git push origin starter/lab-05Dans les logs du job
pytest, cherchez la ligneTesting with DATABASE_URL=...— vous verrez le mot de passe s'afficher en clair dans les logs GitLab.
Le problème
Section intitulée « Le problème »Il y a deux problèmes distincts dans cette branche.
Le premier est dans app/config.py. La valeur par défaut de DATABASE_URL contient un mot de passe : "postgresql://user:s3cret@db:5432/app". En Python, une valeur par défaut dans le code source est acceptable pour le développement local (une valeur sans conséquence), mais elle ne doit jamais contenir un vrai credential. Si demain vous mettez ce projet en production avec cette valeur, et que quelqu'un accède à votre code, il a le mot de passe.
Le second est dans .gitlab-ci.yml. La variable DATABASE_URL est déclarée avec sa valeur en clair dans le fichier de configuration. Cette valeur s'affiche dans les logs. Elle est dans le dépôt Git, donc dans l'historique, donc potentiellement dans des forks et des archives.
La bonne pratique est de ne jamais stocker de secret dans un fichier versionné. Les valeurs sensibles doivent être déclarées dans GitLab (Settings > CI/CD > Variables) et injectées automatiquement dans les jobs.
L'exercice
Section intitulée « L'exercice »Étape 1 — À vous de nettoyer le code Python
Section intitulée « Étape 1 — À vous de nettoyer le code Python »Ouvrez app/config.py. Vous verrez que la valeur par défaut de DATABASE_URL contient un mot de passe : "postgresql://user:s3cret@db:5432/app".
À vous de proposer :
-
Par quelle chaîne allez-vous remplacer ce mot de passe dans la valeur par défaut ?
- 🔍 Indice : Utilisez une base de données qui fonctionne sans configuration (SQLite) et un chemin local inoffensif
- 🔍 Indice 2 : Format SQLite :
sqlite:///chemin/vers/fichier.db
-
Créez un fichier
.env.exampleà la racine du projet. Qu'allez-vous y mettre ?- 🔍 Indice : Listez les variables d'environnement nécessaires
- 🔍 Indice 2 : Les valeurs doivent être des exemples sans secrets réels
-
Comment allez-vous documenter ce changement aux autres développeurs ?
- 🔍 Indice : Ajoutez un commentaire au début de
config.pyexpliquant que les vraies valeurs doivent être en variables d'environnement
- 🔍 Indice : Ajoutez un commentaire au début de
👉 Voir le résultat attendu de l'étape 1 (masqué)
1️⃣ Remplacer la valeur par défaut dans app/config.py
Section intitulée « 1️⃣ Remplacer la valeur par défaut dans app/config.py »"""Configuration de l'application — lit les variables d'environnement."""
import os
class Settings: """Paramètres de l'application, lus depuis l'environnement."""
app_version: str = os.getenv("APP_VERSION", "0.1.0") database_url: str = os.getenv("DATABASE_URL", "sqlite:///./test.db") debug: bool = os.getenv("DEBUG", "false").lower() == "true"
settings = Settings()La valeur par défaut "sqlite:///./test.db" est complètement inoffensive :
- Fonctionne en développement local sans aucune configuration
- SQLite crée un fichier dans le répertoire courant
- Pas de serveur, pas d'authentification, pas de mot de passe
- En CI/CD et en production, GitLab injectera la vraie valeur via la variable
DATABASE_URL
2️⃣ Créer .env.example pour la documentation
Section intitulée « 2️⃣ Créer .env.example pour la documentation »À la racine du projet, créez ce fichier :
# Copier ce fichier en .env et adapter les valeurs# En CI/CD : configurer via Settings > CI/CD > Variables (Protected + Masked)APP_VERSION=0.1.0DATABASE_URL=postgresql://user:changeme@localhost:5432/appDEBUG=falseRemarque importante :
- Versionnez
.env.example(pas de secrets) - Ignorez
.env(confidentiel, ajoutez à.gitignores'il existe)
3️⃣ Documenter dans le code
Section intitulée « 3️⃣ Documenter dans le code »Ajoutez un commentaire en première ligne de config.py :
"""Configuration de l'application — lit les variables d'environnement.
Les secrets (DATABASE_URL, tokens API, etc.) doivent JAMAIS être codés en dur.Les valeurs par défaut ici sont des exemples inoffensifs pour le développement local.En production et en CI/CD, les vraies valeurs sont injectées via des variables protégées.Voir .env.example pour la liste des variables requises."""Étape 2 — À vous de nettoyer le pipeline CI
Section intitulée « Étape 2 — À vous de nettoyer le pipeline CI »Vous allez supprimer le secret du .gitlab-ci.yml. Deux modifications à faire : une dans le bloc variables: global, et une dans le script du job pytest (une ligne de debug).
À vous de proposer :
-
Qu'allez-vous supprimer du bloc
variables:global ?- 🔍 Indice : Repérez la ligne qui contient
DATABASE_URLet le mot de passe
- 🔍 Indice : Repérez la ligne qui contient
-
Qu'allez-vous supprimer du script du job
pytest?- 🔍 Indice : Cherchez une ligne qui commence par
echoet afficheDATABASE_URL - 🔍 Indice 2 : Règle générale : ne jamais logger les variables sensibles, même "par accident"
- 🔍 Indice : Cherchez une ligne qui commence par
-
Qu'allez-vous garder dans
variables:?- 🔍 Indice : La seule variable globale utile est
PIP_CACHE_DIR(ce n'est pas un secret)
- 🔍 Indice : La seule variable globale utile est
👉 Voir le résultat attendu de l'étape 2 (masqué)
1️⃣ Supprimer DATABASE_URL du bloc variables:
Section intitulée « 1️⃣ Supprimer DATABASE_URL du bloc variables: »Avant :
variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache" DATABASE_URL: "postgresql://user:s3cret@db:5432/app"Après :
variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"PIP_CACHE_DIR reste car ce n'est pas un secret — c'est juste un chemin.
2️⃣ Supprimer la ligne de debug dans le job pytest
Section intitulée « 2️⃣ Supprimer la ligne de debug dans le job pytest »Avant :
pytest: script: - echo "Testing with DATABASE_URL=$DATABASE_URL" - pytest -v --junitxml=report.xmlAprès :
pytest: script: - pytest -v --junitxml=report.xmlNe loggez rien qui puisse contenir une valeur sensible, même "juste pour debug". Les logs sont souvent consultables par d'autres personnes et conservés longtemps.
Étape 3 — À vous de déclarer la variable dans GitLab
Section intitulée « Étape 3 — À vous de déclarer la variable dans GitLab »Vous allez créer une variable CI/CD dans GitLab, avec deux protections essentielles : Protected et Masked.
À vous de proposer :
-
Où allez-vous créer cette variable dans l'interface GitLab ?
- 🔍 Indice : Menu Settings, puis CI/CD, puis Variables
-
Quels champs allez-vous remplir ?
- 🔍 Indice : Key, Value, Type, Protected, Masked
-
Pourquoi cocher la case Protected ?
- 🔍 Indice : Empêcher une branche malveillante de voler la variable
-
Pourquoi cocher Masked ?
- 🔍 Indice : Si quelqu'un oublie un
echo, GitLab remplace la valeur par[MASKED]dans les logs
- 🔍 Indice : Si quelqu'un oublie un
-
Quelle valeur allez-vous mettre dans le champ Value ?
- 🔍 Indice : Une valeur de test, pas le vrai mot de passe de production
- 🔍 Indice 2 : Format :
postgresql://user:changeme@localhost:5432/app
👉 Voir le résultat attendu de l'étape 3 (masqué)
Navigation et création
Section intitulée « Navigation et création »- Allez dans votre fork sur gitlab.com
- Menu gauche : Settings → CI/CD → Variables
- Cliquez sur Add variable
Remplir le formulaire
Section intitulée « Remplir le formulaire »| Champ | Valeur |
|---|---|
| Key | DATABASE_URL |
| Value | postgresql://user:changeme@localhost:5432/app |
| Type | Variable |
| Protected | ✅ Coché |
| Masked | ✅ Coché |
| Expand variable reference | ✅ Laissez coché (défaut) |
Cliquez sur Add variable.
Explications de sécurité
Section intitulée « Explications de sécurité »Protected : La variable n'est injectée que dans les jobs qui tournent sur des branches/tags protégés (ex : main). Cela empêche un développeur malveillant de :
- Créer une branche
steal-secrets - Ajouter un job
echo $DATABASE_URL - Voir le secret dans les logs publics
Masked : Si quelqu'un ajoute accidentellement un echo $DATABASE_URL dans le pipeline après votre nettoyage, GitLab remplace automatiquement à l'affichage :
✗ Affichage avant → echo postgresql://user:changeme@localhost:5432/app✓ Affichage après → echo [MASKED]Les deux se complètent : Protected = contrôle d'accès en amont, Masked = filet de sécurité en aval.
Le fichier complet
Section intitulée « Le fichier complet »📄 Voir la solution complète du Lab 05 (masquée)
app/config.py
Section intitulée « app/config.py »"""Configuration de l'application — lit les variables d'environnement."""
import os
class Settings: """Paramètres de l'application, lus depuis l'environnement."""
app_version: str = os.getenv("APP_VERSION", "0.1.0") database_url: str = os.getenv("DATABASE_URL", "sqlite:///./test.db") debug: bool = os.getenv("DEBUG", "false").lower() == "true"
settings = Settings().gitlab-ci.yml
Section intitulée « .gitlab-ci.yml »stages: - lint - test - build
variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
ruff-lint: stage: lint image: python:3.12-slim cache: key: files: - requirements-dev.txt paths: - .pip-cache/ policy: pull before_script: - pip install ruff script: - ruff check app/ tests/
pytest: stage: test image: python:3.12-slim cache: key: files: - requirements-dev.txt paths: - .pip-cache/ before_script: - pip install -r requirements-dev.txt script: - pytest -v --junitxml=report.xml artifacts: when: always paths: - report.xml reports: junit: report.xml expire_in: 7 days
docker-build: stage: build image: docker:27 services: - docker:27-dind variables: DOCKER_TLS_CERTDIR: "/certs" script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . - docker build -t $CI_REGISTRY_IMAGE:latest . - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - docker push $CI_REGISTRY_IMAGE:latest.env.example
Section intitulée « .env.example »# Copier ce fichier en .env et adapter les valeurs# En CI/CD : configurer via Settings > CI/CD > Variables (Protected + Masked)APP_VERSION=0.1.0DATABASE_URL=postgresql://user:changeme@localhost:5432/appDEBUG=falsePousser et vérifier
Section intitulée « Pousser et vérifier »-
Committez les modifications
Fenêtre de terminal git add app/config.py .env.example .gitlab-ci.ymlgit commit -m "fix: remove hardcoded credentials, use CI/CD variables"git push origin starter/lab-05 -
Observez les logs du job
pytestCherchez la ligne qui affichait
DATABASE_URL=postgresql://user:s3cret@.... Elle a disparu — la variable n'est plus logguée, et même si elle l'était,[MASKED]s'afficherait à la place. -
Vérification rapide : cherchez
s3cretdans les logsSi vous ne trouvez pas
s3cretnulle part dans les logs, c'est bon signe. Le secret ne transite plus en clair.
Vérification
Section intitulée « Vérification »-
app/config.py: plus de mot de passe dans la valeur par défaut -
.gitlab-ci.yml:DATABASE_URLsupprimée du blocvariables: -
.env.examplecréé avec des valeurs d'exemple (sans vrai secret) - La variable
DATABASE_URLest déclarée dans Settings > CI/CD > Variables (Protected + Masked) - Les logs du pipeline ne contiennent plus
s3cret
Pièges fréquents
Section intitulée « Pièges fréquents »| Symptôme | Cause | Solution |
|---|---|---|
| La variable ne s'injecte pas dans le job | Variable déclarée comme Protected mais la branche n'est pas protégée | Soit déprotéger la variable pour le test, soit protéger la branche |
[MASKED] n'apparaît pas malgré le masquage | La valeur est trop courte (< 8 caractères) ou contient des caractères spéciaux non supportés | Choisir une valeur de test plus longue pour tester le masquage |
Le pipeline échoue avec DATABASE_URL non définie | La variable est Protected et le job tourne sur une branche non protégée | Tester avec une variable non-protected en premier |
Pour aller plus loin
Section intitulée « Pour aller plus loin »git log -p --all -S "s3cret": cette commande cherche dans tout l'historique Git les commits qui ont ajouté ou supprimé le mots3cret. Si votre dépôt est public, un secret qui a traversé l'historique est compromis même après suppression — il faut changer le mot de passe.- Comparez avec
solution/lab-05:git diff origin/starter/lab-05..origin/solution/lab-05montre exactement les modifications attendues.
À retenir
Section intitulée « À retenir »- Un secret ne doit jamais figurer dans un fichier versionné — ni dans le code, ni dans le CI, ni dans les commentaires
os.getenv("VAR", "valeur_par_defaut")est le pattern correct en Python : la valeur par défaut doit être inoffensive (SQLite, localhost, false)- Protected protège contre le vol de la variable via une branche malveillante. Masked protège contre l'affichage accidentel dans les logs. Utilisez les deux.
- Un
.env.exampleversionné documente les variables nécessaires sans les exposer