TP12 : Software Supply Chain (Harbor, SBOM, Cosign, provenance, SLSA)

Scanner son code ne suffit plus : des attaques comme SolarWinds, Codecov ou la porte dérobée de xz-utils ont compromis la chaîne de fabrication elle-même (dépendance, outil de build, registre). Ce TP met en place les briques qui permettent de savoir ce que contient une image (SBOM), de prouver qui l’a construite et comment (signature, provenance), et de refuser ce qui n’est pas prouvé. Le tout autour d’un registre privé Harbor.


Fichiers de départ : workflow-starter.zip

1. Objectifs pédagogiques

À l’issue du TP, vous serez capable de :

  • installer Harbor et y configurer projets, RBAC, robot accounts, scan automatique et rétention ;
  • générer un SBOM (SPDX, CycloneDX) avec Syft et l’analyser avec Grype ;
  • signer et vérifier une image avec Cosign, avec une paire de clés puis en mode keyless (OIDC) ;
  • produire une attestation de provenance dans GitHub Actions et la vérifier ;
  • situer un pipeline sur les niveaux SLSA et expliquer le rôle d’in-toto.

Les images de cette page sont des schémas explicatifs, pas des captures d’écran.

2. Environnement du TP

  • Une VM dédiée Ubuntu Server 26.04 pour Harbor (≥ 2 vCPU, 4 Go de RAM, 40 Go de disque), par exemple créée avec Vagrant (TP5). Harbor occupe le port 80.
  • La VM de lab (Docker) comme poste client. Les deux VM se joignent par IP privée.
  • Dans le fichier /etc/hosts des deux VM : <IP-harbor> harbor.lab.local.
  • L’image contact-api des TP précédents (ou toute image que vous avez construite).
Terme Signification
SBOM Software Bill of Materials : inventaire des composants d’un logiciel (paquets, versions, licences).
Digest Empreinte sha256:… du manifest d’une image. Immuable, contrairement à un tag.
Signature Preuve cryptographique qu’un digest a été approuvé par le détenteur d’une clé ou d’une identité.
Attestation Déclaration signée sur un artefact (provenance, SBOM, résultat de scan), au format in-toto.
Provenance Attestation qui décrit où, quand, comment et à partir de quelles sources un artefact a été construit.
SLSA Supply-chain Levels for Software Artifacts : niveaux d’exigence croissants pour la fabrication d’un artefact.

Chaîne : build, SBOM, scan, push, signature, attestation, vérification Chaîne : build, SBOM, scan, push, signature, attestation, vérification

3. Partie 1 : Installer Harbor

Harbor dans le lab : projets, RBAC, robot accounts, scan Trivy, rétention Harbor dans le lab : projets, RBAC, robot accounts, scan Trivy, rétention

  • Sur la VM Harbor, installer Docker et Compose, puis télécharger l’installeur en ligne depuis les releases Harbor :

    wget https://github.com/goharbor/harbor/releases/download/<version>/harbor-online-installer-<version>.tgz
    tar xzf harbor-online-installer-*.tgz && cd harbor
    cp harbor.yml.tmpl harbor.yml
  • Dans harbor.yml : hostname: harbor.lab.local, commenter tout le bloc https: (pas de certificat dans le lab), changer harbor_admin_password.

  • Installer avec le scanner Trivy :

    sudo ./install.sh --with-trivy
    docker compose ps
  • Sur la VM client, autoriser ce registre HTTP dans /etc/docker/daemon.json, puis redémarrer Docker :

    { "insecure-registries": ["harbor.lab.local"] }

    En production, un registre est toujours servi en HTTPS. Le mode HTTP n’est acceptable que dans un lab isolé.

4. Partie 2 : Projets, RBAC, scan et rétention

  • Dans l’interface http://harbor.lab.local (admin) :

    • créer un projet privé devsecops ;
    • créer un utilisateur dev1 avec le rôle Developer sur ce projet, et un utilisateur audit1 avec le rôle Guest ;
    • créer un robot account ci-push limité au projet devsecops (push + pull uniquement).
  • Pousser une image avec le robot account :

    docker login harbor.lab.local -u 'robot$devsecops+ci-push'
    docker tag contact-api:latest harbor.lab.local/devsecops/contact-api:1.0.0
    docker push harbor.lab.local/devsecops/contact-api:1.0.0
  • Vérifier le RBAC : audit1 peut tirer l’image mais pas la pousser (denied: requested access to the resource is denied).

  • Projet → Configuration : activer Automatically scan images on push et Prevent vulnerable images from running (gravité Critical). Pousser une image vulnérable (celle du TP10 avant correction) et constater que le docker pull est refusé.

  • Policy → Tag Retention : conserver les 5 derniers tags poussés, puis lancer un dry run après avoir poussé 7 tags. Policy → Tag Immutability : rendre immuables les tags v*.

  • Comparer GitHub Container Registry et Harbor (hébergement, RBAC, scan, réplication, coût, usage hors ligne).

5. Partie 3 : SBOM avec Syft, scan avec Grype

  • Générer un SBOM de l’image dans deux formats :

    docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:/out" anchore/syft:latest \
      contact-api:latest -o spdx-json=/out/contact-api.spdx.json -o cyclonedx-json=/out/contact-api.cdx.json
  • Ouvrir le SBOM : combien de paquets ? Retrouver la version d’express et celle d’openssl.

  • Analyser le SBOM (et non l’image) avec Grype :

    docker run --rm -v "$PWD:/in" anchore/grype:latest sbom:/in/contact-api.spdx.json

    Intérêt : conserver le SBOM de chaque release permet de répondre en quelques secondes à la question « suis-je concerné par la nouvelle CVE X ? » sans reconstruire ni retélécharger les images.

6. Partie 4 : Signer et vérifier avec Cosign

  • Installer Cosign (binaire depuis les releases Sigstore) puis générer une paire de clés :

    cosign generate-key-pair       # crée cosign.key (privée, chiffrée) et cosign.pub

    cosign.key ne doit jamais être commitée (elle est dans le .gitignore du dépôt).

  • Récupérer le digest de l’image poussée, puis la signer :

    DIGEST=$(docker buildx imagetools inspect harbor.lab.local/devsecops/contact-api:1.0.0 --format '{{json .Manifest.Digest}}' | tr -d '"')
    cosign sign --key cosign.key --allow-http-registry --allow-insecure-registry \
      harbor.lab.local/devsecops/contact-api@$DIGEST
  • Vérifier la signature, puis vérifier une image non signée (tag 1.0.1 poussé sans signature) :

    cosign verify --key cosign.pub --allow-http-registry --allow-insecure-registry \
      harbor.lab.local/devsecops/contact-api@$DIGEST
  • Attacher le SBOM comme attestation signée, puis la vérifier :

    cosign attest --key cosign.key --type spdxjson --predicate contact-api.spdx.json \
      --allow-http-registry --allow-insecure-registry harbor.lab.local/devsecops/contact-api@$DIGEST
    cosign verify-attestation --key cosign.pub --type spdxjson \
      --allow-http-registry --allow-insecure-registry harbor.lab.local/devsecops/contact-api@$DIGEST
  • Dans Harbor, constater que l’image affiche une signature et des accessoires (attestation).

7. Partie 5 : Keyless et provenance dans GitHub Actions

Harbor n’est pas joignable depuis GitHub : cette partie publie dans GHCR (ghcr.io).

  • Copier workflow-starter/release.yml dans .github/workflows/ du dépôt contact-api et compléter les TODO : SBOM (anchore/sbom-action), signature Cosign keyless, attestation de provenance (actions/attest-build-provenance), SBOM attaché à la release.

    En mode keyless, aucune clé n’est stockée : GitHub fournit un jeton OIDC qui prouve l’identité du workflow, Fulcio émet un certificat éphémère, et la signature est inscrite dans le journal public de transparence Rekor.

  • Créer un tag v1.0.0 et le pousser. Une fois le workflow terminé, vérifier depuis la VM :

    cosign verify ghcr.io/<utilisateur>/contact-api:v1.0.0 \
      --certificate-identity-regexp 'https://github.com/<utilisateur>/contact-api/.*' \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com
    gh attestation verify oci://ghcr.io/<utilisateur>/contact-api:v1.0.0 --owner <utilisateur>
  • Que se passe-t-il si --certificate-identity-regexp désigne un autre dépôt ? Pourquoi est-ce important ?

8. Partie 6 : SLSA, in-toto et attaque simulée

  • Lire la spécification SLSA v1.0 (Build track) et situer : (a) un build sur le poste du développeur, (b) votre pipeline Jenkins, (c) votre workflow GitHub Actions avec attestation.
Niveau Exigence principale Atteint par
Build L1 Provenance existante
Build L2 Provenance signée, générée par une plateforme hébergée
Build L3 Plateforme de build durcie, provenance infalsifiable par le projet
  • Expliquer en quelques lignes le rôle d’in-toto (layout, étapes, links) et le lien avec les attestations produites par Cosign et GitHub.
  • Attaque simulée : un « attaquant » pousse une image contact-api:1.0.0 modifiée sous le même tag. Montrer que cosign verify sur le nouveau digest échoue, alors qu’un simple docker pull ne voit rien.
  • Écrire un script verify-before-run.sh qui refuse de lancer (docker run) une image dont la signature n’est pas valide.

Défi

  1. Configurer une réplication Harbor (Registries → New Endpoint Docker Hub, règle pull-based) pour mettre en cache une image publique.
  2. Ne plus garder la clé privée dans un fichier : importez-la dans le pipeline sous forme de credential Jenkins ou de secret GitHub (COSIGN_PRIVATE_KEY, COSIGN_PASSWORD) et signez depuis la CI. La gestion centralisée des secrets sera approfondie au TP18.

9. Livrables

Captures à déposer dans captures/ :

Fichier Contenu attendu
harbor-project.png Projet devsecops avec ses membres et le robot account
harbor-rbac-denied.png Push refusé pour audit1
harbor-scan.png Rapport de scan Trivy dans Harbor et pull bloqué
harbor-retention.png Dry run de la politique de rétention
sbom-grype.png Extrait du SBOM et résultat de Grype
cosign-verify.png Vérification réussie puis échec sur une image non signée
keyless-provenance.png cosign verify keyless et gh attestation verify réussis
attack-detected.png Image remplacée détectée par cosign verify

Ainsi que release.yml, verify-before-run.sh, le tableau SLSA et la comparaison GHCR / Harbor. Jamais cosign.key.