Supply chain et signatures¶
Cette page explique le pourquoi des deux mécanismes de supply chain greffés sur la pipeline de release Kirexo : la signature keyless de l'image kirexo-prod (job docker:sign-prod) et le SBOM attaché à chaque release GitLab (job release:sbom). Pour la mécanique factuelle, voir la Référence Pipeline GitLab CI ; pour la vérification côté serveur, voir Vérifier la signature de l'image avant de la pull.
Le problème à résoudre¶
Un déploiement de prod typique enchaîne docker compose pull puis up -d. La seule chose que le démon Docker garantit, c'est l'intégrité du transport : l'image reçue est bien celle annoncée par le registry. Mais rien ne dit qui a poussé l'image, ni à partir de quel commit, ni avec quelle toolchain. Trois scénarios concrets que ce trou de confiance ouvre :
- Un compte d'écriture sur le registry est compromis. Une image
kirexo-prod:202605231742retaggée à la main par un attaquant atterrit sur la prod sans qu'aucun contrôle ne déclenche d'alarme. - Une dépendance Composer dans
vendor/est rétrocompromise (typosquat, prise de contrôle du mainteneur upstream). L'audit de sécurité hebdo arrive trop tard pour les déploiements déjà partis avec cette version. - Six mois après une release, un auditeur conformité demande la liste exhaustive des composants embarqués dans l'image alors déployée. La pipeline a déjà recyclé les caches Docker, l'image elle-même peut avoir été expirée par la cleanup policy. Personne ne peut produire l'inventaire de manière fiable.
Les deux mécanismes répondent chacun à une question distincte : « cette image est-elle bien sortie de notre pipeline ? » (signature) et « qu'y a-t-il exactement dedans ? » (SBOM).
Pourquoi la signature keyless plutôt qu'une PKI dédiée¶
L'approche historique de la signature d'image consistait à générer une paire de clés, stocker la privée dans un coffre-fort (Vault, KMS), distribuer la publique aux consommateurs, et faire signer chaque image par la CI. Trois charges opérationnelles non triviales : rotation des clés, contrôle d'accès au coffre, distribution de la publique à jour à tous les serveurs.
Le mode keyless de cosign / sigstore remplace cette PKI par une chaîne de confiance courte-vie :
- La CI demande à GitLab un JWT OIDC pour le job courant, signé par GitLab. Ce JWT porte l'identité du pipeline (URL du projet, ref, sha du commit) et a une durée de vie de quelques minutes.
- La CI échange ce JWT contre un certificat X.509 éphémère auprès de Fulcio, la CA publique de sigstore. Fulcio valide le JWT auprès du provider OIDC GitLab, puis émet un cert lié à cette identité, valide ~10 minutes.
- La CI signe le digest de l'image avec la clé privée associée à ce cert, et publie l'attestation dans Rekor, un log de transparence public immuable (modèle Merkle, à la manière des Certificate Transparency Logs HTTPS).
Aucune clé privée à long terme n'est jamais matérialisée — la clé est générée à la volée dans le runner, le cert expire avant qu'on ait le temps de l'exfiltrer, et la révocation se fait par expiration plutôt que par CRL. Côté Kirexo, le seul coût opérationnel résiduel est de suivre les versions de cosign et syft, épinglées via le tag des FROM de supply-chain/Dockerfile (cf. section ci-dessous) — le manager dockerfile natif de Renovate suit ces tags automatiquement, sans customManager à maintenir.
Le compromis assumé est la dépendance à un service tiers public (Fulcio + Rekor, opérés par la Linux Foundation). En contrepartie, l'audit a posteriori devient gratuit : n'importe qui peut interroger Rekor pour vérifier qu'une signature a bien été publiée, sans demander quoi que ce soit à GitLab ni à Kirexo. C'est cette traçabilité publique qui rend la chaîne « pipeline GitLab → image en prod » réellement attaquable par un auditeur — pas seulement par nous.
Pourquoi vérifier la signature côté serveur, pas seulement en CI¶
La signature n'a de valeur que si quelqu'un la vérifie au moment du déploiement. Vérifier en CI uniquement reviendrait à dire « c'est nous qui avons buildé, on se croit sur parole » — exactement le trou de confiance qu'on cherche à fermer. C'est le cosign verify côté serveur, avec une regex d'identité épinglée sur ^https://gitlab\.com/shaurifr/kirexo, qui ferme la boucle. Si une image présente sur le registry ne porte pas de signature compatible avec cette regex, le verify échoue et le déploiement est refusé.
La regex est volontairement stricte (pas un préfixe lâche kirexo) pour qu'une image émise par un fork ou un autre namespace soit rejetée par défaut. Assouplir cette regex revient à élargir la surface de confiance — décision à ne jamais prendre à la légère sous prétexte de débloquer un déploiement urgent.
La discipline « pas de pull avant verify » est documentée dans le how-to Vérifier la signature de l'image avant de la pull. L'idéal est d'encapsuler cette vérification dans le même script shell qui pilote le déploiement, pour qu'un oubli soit physiquement impossible.
Pourquoi un SBOM (et pourquoi deux formats)¶
Le SBOM (Software Bill of Materials) est l'inventaire exhaustif des composants embarqués dans une image : paquets OS Debian, extensions PHP, dépendances Composer présentes dans vendor/. C'est le pendant statique des CVE — une fois qu'on connaît la liste, un outil comme Dependency-Track peut alerter chaque fois qu'une CVE est publiée pour un de ces composants, sans avoir à rescanner l'image.
La pipeline Kirexo génère le SBOM de chaque tag de release via syft et l'attache à la release GitLab en deux formats :
- SPDX JSON (norme ISO 5962) est le format de référence des juristes et des auditeurs conformité (OpenChain, NTIA Minimum Elements). C'est ce que demande un audit logiciel formel, ou un service achats qui exige un inventaire signé pour la couverture juridique.
- CycloneDX JSON (OWASP) est le format de référence des outils de sécurité opérationnels (Dependency-Track, Trivy en mode SBOM, GitHub Advanced Security, Snyk). C'est ce qu'on alimente à un pipeline d'audit continu pour corréler la composition de l'image avec les CVE publiées.
Les deux formats portent en grande partie la même information ; les générer en parallèle évite à l'équipe consommatrice de devoir convertir, et garantit que la conformité juridique et l'audit sécurité parlent du même SBOM produit par la même pipeline.
Le SBOM est conservé un an comme asset de release. Au-delà, il est de moins en moins représentatif des CVE connues du moment (Dependency-Track le rejoue lui-même de toute façon), et il peut toujours être régénéré à la demande depuis l'image taggée tant qu'elle reste dans le registry. Cette rétention est alignée sur un cycle d'audit annuel typique — pas un choix de coût.
Pourquoi une image custom kirexo-supply-chain¶
Les deux outils — cosign et syft — sont distribués par leurs auteurs sous forme d'images officielles (gcr.io/projectsigstore/cosign, anchore/syft). Ces images sont distroless : pas de shell, pas d'utilitaires Unix, juste le binaire et ses dépendances dynamiques. Choix défendable côté upstream — surface d'attaque minimale pour un outil de supply chain — mais incompatible avec le GitLab Runner SaaS, qui enveloppe systématiquement le script d'un job dans sh -c "step_script". Sans sh dans l'image, le job échoue avant même d'exécuter sa première instruction (exec: "sh": executable file not found in $PATH).
La parade retenue est une image custom kirexo-supply-chain (supply-chain/Dockerfile), buildée par la CI au même titre que kirexo-docs (cf. Pipeline GitLab CI — docker:build-supply-chain) : une base Alpine minimale qui fournit sh, et au-dessus de laquelle on copie les binaires officiels depuis les images upstream via un multi-stage Docker (COPY --from=gcr.io/projectsigstore/cosign:<tag> et COPY --from=anchore/syft:<tag>). Les versions vivent dans le tag des FROM du Dockerfile ; un bump = éditer le tag + commit, et docker:build-supply-chain rebuild automatiquement l'image dès que supply-chain/Dockerfile change sur main.
Trois alternatives envisagées et écartées :
| Alternative | Raison du refus |
|---|---|
| Garder les images officielles distroless | Échec système immédiat sur GitLab Runner SaaS — non négociable. |
Télécharger cosign et syft depuis les releases GitHub upstream avec vérification SHA256 dans le Dockerfile |
Les runners GitLab SaaS bloquent régulièrement les sorties HTTP vers les CDN GitHub Releases (objects.githubusercontent.com → curl: (28) Connection timed out). Première version livrée sous cette forme, KO en production de la CI. Les pulls Docker (gcr.io, Docker Hub) marchent sans problème depuis le même runner. |
Forker les images officielles upstream pour y ajouter sh |
Maintenance d'un fork sur deux projets externes, sans intérêt fonctionnel — autant assembler nous-mêmes une image minimale. |
Le compromis assumé est le transfert de la confiance du couple (release GitHub + SHA256 figé localement) vers les registries source (gcr.io/projectsigstore pour cosign, Docker Hub anchore/syft pour syft). En contrepartie, Sigstore et Anchore publient eux-mêmes leurs images sur ces registries — c'est la même chaîne de confiance que celle qui sécurise n'importe quel FROM du Dockerfile racine du projet, et c'est précisément la propriété qu'exploite le manager dockerfile natif de Renovate pour proposer les bumps automatiquement. L'épinglage se fait par tag (v2.6.3 côté cosign — on reste volontairement sur la branche v2.6.x maintenue en parallèle de v3.x par Sigstore plutôt que de migrer en v3 dès maintenant —, v1.45.1 côté syft), pas par digest immuable @sha256:… : si on voulait fermer la fenêtre théorique où un mainteneur de l'image source pourrait retagger une version déjà publiée, il faudrait passer aux digests — au prix de bumps à la main (Renovate gère les tags, pas les digests par défaut). Tant qu'on accepte de faire confiance à Sigstore et Anchore pour ne pas réécrire un tag historique, l'épinglage par tag est suffisant pour notre modèle de menace et garde le bénéfice du suivi automatique.
Le triangle traçabilité, conformité, audit¶
Les trois angles que la combinaison signature + SBOM ferme :
| Question posée | Réponse | Mécanisme |
|---|---|---|
| Traçabilité — Qui a buildé cette image, à partir de quel commit, à quel moment ? | Le JWT capturé par Fulcio porte URL du projet, ref, sha. Rekor publie cette attestation immuable. | Signature keyless + log Rekor. |
| Conformité — Quel inventaire logiciel cette image embarque-t-elle ? Sous quelles licences ? | SBOM SPDX (norme ISO, accepté par OpenChain et NTIA). | release:sbom → asset SPDX. |
| Audit sécurité — Quelles CVE concernent les composants de cette image ? | SBOM CycloneDX consommé par Dependency-Track / Trivy. | release:sbom → asset CycloneDX. |
Chaque angle est résolu par un mécanisme distinct, mais ils partagent la même propriété d'audit a posteriori : la signature reste interrogeable via Rekor indépendamment de Kirexo, et le SBOM reste lisible sans avoir à pull l'image d'origine. Si demain le projet est archivé, ces artefacts restent vérifiables tant que les services publics sigstore et le registry GitLab existent.
Voir aussi¶
- Pipeline GitLab CI — Signature keyless de
kirexo-prod— détail factuel du jobdocker:sign-prod. - Pipeline GitLab CI — Job
release:sbom— détail factuel de la génération SBOM. - Vérifier la signature de l'image avant de la pull — procédure côté serveur.
- SBOM disponible en asset de la release — comment récupérer les SBOM côté consommateur.