CI/CD : Supply Chain Security avec cosign, Trivy et SBOM

par

dans

Construire une image durcie ne sert à rien si l'on ne peut pas prouver qui l'a construite, depuis quoi, et qu'elle n'a pas changé depuis. Cet article documente la chaîne qui produit, signe, analyse et atteste les huit images durcies du parc, réparties sur six dépôts.

Ce que le pipeline garantit, et dans quel ordre

flowchart TB
    Lint["Analyse statique<br/>Dockerfile et scripts"] --> Build["Construction<br/>multi-architecture"]
    Build --> Sign["Signature sans cle<br/>identite du workflow"]
    Sign --> Scan["Analyse de vulnerabilites<br/>et attestation"]
    Scan --> Push["Publication<br/>sur deux registres"]
    VW["Veille de versions<br/>quotidienne"] -->|"nouvelle version amont"| Build
    SA["Audit de securite<br/>hebdomadaire"] -->|"CVE detectee"| Issue["Ticket automatique"]
    Push --> REG["Registre principal<br/>signatures et attestations"]
    Push --> MIR["Miroir public"]
    style Sign fill:#d4edda,stroke:#28a745
    style Scan fill:#d4edda,stroke:#28a745

Trois workflows tournent sur GitHub Actions, qui est la source de vérité du build. Le déploiement vers le homelab, lui, est déclenché séparément — les images publiées sont tirées depuis les registres, jamais reconstruites localement.

Analyse statique avant tout le reste

Deux contrôles passent avant qu'une seule couche ne soit construite : un analyseur de Dockerfile, qui refuse les anti-patrons — un tag latest, une invalidation de cache inutile — et un analyseur de scripts shell, sur les points d'entrée et les contrôles de santé.

L'ordre n'est pas cosmétique : ce sont les contrôles les moins chers de toute la chaîne, et les faire passer en premier évite d'occuper vingt minutes de compilation pour finir sur une faute de syntaxe.

Construction multi-architecture

Chaque image est construite pour deux architectures dans le même passage, avec génération de l'inventaire logiciel et de la provenance :

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    platforms: linux/amd64,linux/arm64
    push: true
    sbom: true
    provenance: mode=max
    cache-from: type=gha
    cache-to: type=gha,mode=max

Signer sans détenir de clé

L'image est signée immédiatement après publication, en mode sans clé. C'est le point le plus contre-intuitif de la chaîne, et le plus intéressant.

Aucune clé privée n'existe, donc aucune n'est stockée, tournée, ni compromise. La signature s'appuie sur le jeton d'identité du workflow, contre lequel une autorité éphémère émet un certificat de courte durée ; l'opération est inscrite dans un journal public infalsifiable.

L'identité du signataire n'est donc pas une personne ni un secret, c'est le workflow lui-même — vérifiable par n'importe qui :

cosign verify \
  --certificate-identity-regexp "https://github.com/jbsky/.*/.github/workflows/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/jbsky/nginx-waf-hardened:TAG

Ce que cette commande établit : l'image a bien été produite par un workflow de ce compte, via l'émetteur d'identité officiel. Pas « quelqu'un qui avait la clé ».

Analyser, et échouer sur ce qui est réparable

L'analyse de vulnérabilités porte sur les niveaux critique et élevé — une image FROM scratch n'a typiquement rien en dessous, faute de gestionnaire de paquets et de couche système.

La politique mérite d'être explicite, parce qu'elle détermine si le contrôle survit à six mois d'usage : tout est remonté dans l'onglet de sécurité, mais le build n'échoue que sur une vulnérabilité critique et déjà corrigée en amont.

Un rouge veut alors dire exactement une chose : une action existe. C'est ce qui évite le contrôle qu'on finit par contourner parce qu'il crie sans recours.

Prouver la provenance

Une attestation de provenance accompagne chaque image. Elle établit cryptographiquement qu'elle a été construite par ce workflow, depuis ce commit, sur cette infrastructure.

La provenance complète inclut l'empreinte du Dockerfile, les arguments de construction, les images de base avec leurs digests, et l'environnement de build. C'est ce qui permet, après coup, de répondre à « d'où sort exactement cette couche ».

S'y ajoute l'inventaire logiciel généré à chaque build : la liste exhaustive des composants, leurs licences, les dépendances transitives et les empreintes. Il est attaché à l'image comme attestation, pas déposé dans un fichier à côté qui divergerait.

Le suivi de versions vit dans l'image, pas dans un fichier

Chaque image embarque un label qui décrit les versions de ses composants internes. Une tâche quotidienne compare ce label aux versions publiées en amont, et déclenche une reconstruction dès qu'un écart apparaît.

L'intérêt est qu'il n'y a pas de second endroit à tenir à jour : la vérité voyage avec l'artefact. Une image qu'on retrouve six mois plus tard sait dire de quoi elle est faite, sans dépendre d'un dépôt qui a peut-être changé.

Un second analyseur, pour les faux négatifs

Un audit hebdomadaire relance l'analyse avec une base de vulnérabilités fraîche, et la double d'un second analyseur d'un autre éditeur.

L'objectif n'est pas la redondance mais la divergence : deux outils qui ne sont pas d'accord signalent soit un faux positif, soit un faux négatif, et les deux méritent d'être regardés. Un écart déclenche l'ouverture automatique d'un ticket.

Deux registres, une seule source de signatures

Les images sont publiées sur deux registres : le principal, qui porte aussi les signatures et les attestations, et un miroir public pour la découvrabilité.

Les signatures ne sont stockées qu'à un seul endroit, y compris pour les images du miroir, parce que le registre principal supporte nativement les conventions de nommage des artefacts de signature. Vérifier une image tirée du miroir revient donc à interroger le registre principal — une asymétrie qu'il vaut mieux connaître avant de conclure qu'une signature manque.

Le cache décide de la viabilité du pipeline

Certaines images compilent leur moteur depuis les sources — une vingtaine de minutes sans cache. Le cache de construction conserve les couches intermédiaires entre les exécutions, ce qui ramène une reconstruction après changement mineur à quelques minutes.

Sans lui, le pipeline resterait correct mais deviendrait dissuasif, et un pipeline qu'on évite de déclencher ne protège plus rien.

Construire derrière un proxy sans polluer l'image

Quand la construction passe par un proxy qui déchiffre, il faut lui faire confiance pendant le build sans laisser son certificat dans l'image. Le montage de secret répond exactement à ça :

RUN --mount=type=secret,id=ca-cert,target=/usr/local/share/ca-certificates/proxy.crt \
    update-ca-certificates && \
    apk add --no-cache build-base cmake git

Le certificat n'existe que pendant l'exécution de cette instruction. Il n'apparaît dans aucune couche de l'image finale — ce qu'un COPY suivi d'un rm n'aurait pas donné, la couche intermédiaire restant lisible.


Liens

Articles connexes