Aller au contenu
CI/CD & Automatisation medium

Lab 05 — Sortir les secrets du code

15 min de lecture

logo gitlab

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.

  • 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

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.

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
  1. Passez sur la branche de départ

    Fenêtre de terminal
    cd pipeline-craft
    git checkout starter/lab-05
  2. 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 s3cret est visible dans deux endroits. Si ce dépôt était public, n'importe qui pourrait le lire.

  3. Poussez pour déclencher le pipeline

    Fenêtre de terminal
    git push origin starter/lab-05

    Dans les logs du job pytest, cherchez la ligne Testing with DATABASE_URL=... — vous verrez le mot de passe s'afficher en clair dans les logs GitLab.

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.

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 :

  1. 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
  2. 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
  3. Comment allez-vous documenter ce changement aux autres développeurs ?

    • 🔍 Indice : Ajoutez un commentaire au début de config.py expliquant que les vraies valeurs doivent être en variables d'environnement
👉 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

À la racine du projet, créez ce fichier :

Fenê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.0
DATABASE_URL=postgresql://user:changeme@localhost:5432/app
DEBUG=false

Remarque importante :

  • Versionnez .env.example (pas de secrets)
  • Ignorez .env (confidentiel, ajoutez à .gitignore s'il existe)

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

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 :

  1. Qu'allez-vous supprimer du bloc variables: global ?

    • 🔍 Indice : Repérez la ligne qui contient DATABASE_URL et le mot de passe
  2. Qu'allez-vous supprimer du script du job pytest ?

    • 🔍 Indice : Cherchez une ligne qui commence par echo et affiche DATABASE_URL
    • 🔍 Indice 2 : Règle générale : ne jamais logger les variables sensibles, même "par accident"
  3. Qu'allez-vous garder dans variables: ?

    • 🔍 Indice : La seule variable globale utile est PIP_CACHE_DIR (ce n'est pas un secret)
👉 Voir le résultat attendu de l'étape 2 (masqué)

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

Après :

pytest:
script:
- pytest -v --junitxml=report.xml

Ne 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 :

  1. Où allez-vous créer cette variable dans l'interface GitLab ?

    • 🔍 Indice : Menu Settings, puis CI/CD, puis Variables
  2. Quels champs allez-vous remplir ?

    • 🔍 Indice : Key, Value, Type, Protected, Masked
  3. Pourquoi cocher la case Protected ?

    • 🔍 Indice : Empêcher une branche malveillante de voler la variable
  4. Pourquoi cocher Masked ?

    • 🔍 Indice : Si quelqu'un oublie un echo, GitLab remplace la valeur par [MASKED] dans les logs
  5. 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é)
  1. Allez dans votre fork sur gitlab.com
  2. Menu gauche : Settings → CI/CD → Variables
  3. Cliquez sur Add variable
ChampValeur
KeyDATABASE_URL
Valuepostgresql://user:changeme@localhost:5432/app
TypeVariable
Protected✅ Coché
Masked✅ Coché
Expand variable reference✅ Laissez coché (défaut)

Cliquez sur Add variable.

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.

📄 Voir la solution complète du Lab 05 (masquée)
"""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()
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
Fenê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.0
DATABASE_URL=postgresql://user:changeme@localhost:5432/app
DEBUG=false
  1. Committez les modifications

    Fenêtre de terminal
    git add app/config.py .env.example .gitlab-ci.yml
    git commit -m "fix: remove hardcoded credentials, use CI/CD variables"
    git push origin starter/lab-05
  2. Observez les logs du job pytest

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

  3. Vérification rapide : cherchez s3cret dans les logs

    Si vous ne trouvez pas s3cret nulle part dans les logs, c'est bon signe. Le secret ne transite plus en clair.

  • app/config.py : plus de mot de passe dans la valeur par défaut
  • .gitlab-ci.yml : DATABASE_URL supprimée du bloc variables:
  • .env.example créé avec des valeurs d'exemple (sans vrai secret)
  • La variable DATABASE_URL est déclarée dans Settings > CI/CD > Variables (Protected + Masked)
  • Les logs du pipeline ne contiennent plus s3cret
SymptômeCauseSolution
La variable ne s'injecte pas dans le jobVariable déclarée comme Protected mais la branche n'est pas protégéeSoit déprotéger la variable pour le test, soit protéger la branche
[MASKED] n'apparaît pas malgré le masquageLa valeur est trop courte (< 8 caractères) ou contient des caractères spéciaux non supportésChoisir une valeur de test plus longue pour tester le masquage
Le pipeline échoue avec DATABASE_URL non définieLa variable est Protected et le job tourne sur une branche non protégéeTester avec une variable non-protected en premier
  • git log -p --all -S "s3cret" : cette commande cherche dans tout l'historique Git les commits qui ont ajouté ou supprimé le mot s3cret. 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-05 montre exactement les modifications attendues.
  • 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.example versionné documente les variables nécessaires sans les exposer

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn