
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
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 — Nettoyer le code Python
Section intitulée « Étape 1 — Nettoyer le code Python »-
Ouvrez
app/config.py -
Remplacez la valeur par défaut par une valeur de développement sans conséquence
"""Configuration de l'application — lit les variables d'environnement."""import osclass 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 utilisable en développement local sans aucune configuration — SQLite crée un fichier local, pas de serveur, pas de mot de passe. En CI/CD et en production, la variable d’environnementDATABASE_URLsera définie par GitLab. -
Créez un fichier
.env.exampleà la racine du projetFenêtre de terminal # 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=falseCe fichier sert de documentation pour les nouveaux développeurs : il liste les variables d’environnement nécessaires avec des valeurs d’exemple, sans jamais contenir de vraies valeurs sensibles. Versionner
.env.example, jamais.env.
Étape 2 — Nettoyer le pipeline CI
Section intitulée « Étape 2 — Nettoyer le pipeline CI »-
Supprimez la variable
DATABASE_URLdu.gitlab-ci.ymlAvant :
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_DIRpeut rester dans le fichier car ce n’est pas un secret — c’est un chemin de répertoire. -
Supprimez aussi la ligne de debug dans le script
pytestCette ligne affichait la valeur de
DATABASE_URLdans les logs :script:- echo "Testing with DATABASE_URL=$DATABASE_URL" # ← à supprimer- pytest -v --junitxml=report.xmlAffichez
echouniquement pour du debug temporaire. Ne logguez jamais la valeur d’une variable potentiellement sensible.
Étape 3 — Déclarer la variable dans GitLab
Section intitulée « Étape 3 — Déclarer la variable dans GitLab »-
Allez dans votre fork sur gitlab.com
-
Naviguez dans Settings > CI/CD > Variables
Dans le menu de gauche de votre projet : Settings → CI/CD → déroulez la section Variables.
-
Ajoutez la variable
DATABASE_URLCliquez sur Add variable et remplissez :
- Key :
DATABASE_URL - Value :
postgresql://user:changeme@localhost:5432/app(une valeur de test, sans vrai mot de passe) - Type : Variable
- Protected : cochez cette case
- Masked : cochez cette case
- Expand variable reference : laissez coché
Cliquez sur Add variable.
- Key :
Pousser 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
Ce qui a changé
Section intitulée « Ce qui a changé » database_url: str = os.getenv( "DATABASE_URL", "postgresql://user:s3cret@db:5432/app" "DATABASE_URL", "sqlite:///./test.db" )
# .gitlab-ci.yml variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache" DATABASE_URL: "postgresql://user:s3cret@db:5432/app"
pytest: script: - echo "Testing with DATABASE_URL=$DATABASE_URL" - pytest -v --junitxml=report.xml
# .env.example (nouveau fichier) APP_VERSION=0.1.0 DATABASE_URL=postgresql://user:changeme@localhost:5432/app DEBUG=falsePiè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 starter/lab-05..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