Kubernetes

Patcher tout un parc d'images sans rebuild : « pack rebase » après la graduation CNCF des Buildpacks — et ce que ça casse dans vos signatures et votre GitOps

La graduation des Cloud Native Buildpacks à la CNCF remet en lumière une opération peu connue : le « rebase », qui remplace la couche de base d'une image en quelques secondes, sans code source ni CI. Cet article détaille la mécanique, un runbook de remédiation à l'échelle d'un parc, et surtout ce que l'opération casse côté GitOps, signatures cosign et SBOM.

| | | 10 min de lecture · par
Schéma d'une image de conteneur dont la couche de base est remplacée, avec les couches applicatives conservées au-dessus

Le 11 août 2026, la CNCF a annoncé la graduation des Cloud Native Buildpacks (CNB), qui rejoignent le plus haut niveau de maturité de la fondation aux côtés d’OpenTelemetry (juillet 2026) et de Dragonfly (mars 2026). L’annonce met en avant trois capacités : la génération de SBOM, le layering automatique, et le rebase — la possibilité de remplacer la couche de base d’une image pour corriger une CVE système sans rebuild complet.

C’est ce troisième point qui mérite l’attention des équipes plateforme. Le rebase n’est pas une nouveauté technique — il existe depuis les débuts du lifecycle CNB — mais il change de statut : de curiosité, il devient un mécanisme de remédiation supporté par une spécification stable. Encore faut-il comprendre ce qu’il casse dans une chaîne Kubernetes moderne.

Ce que « graduated » change, et ce que ça ne change pas

La graduation est un signal de gouvernance, pas une release. Concrètement : les spécifications (Buildpack API, Platform API, Distribution API) sont considérées comme stables et versionnées de manière prévisible, le projet a passé un audit de sécurité indépendant et une revue de gouvernance, et il n’est pas contrôlé par un vendor unique. Pour une équipe plateforme, c’est ce qui permet de justifier un investissement à trois ans dans un builder maison.

Ce que ça ne change pas : CNB est une spécification et un lifecycle, pas une collection de buildpacks. Les implémentations que vous utiliserez (Paketo, Heroku, Google Cloud Buildpacks) restent des projets tiers, avec leurs propres cadences et leurs propres trous de couverture. Et la graduation ne réduit pas mécaniquement votre exposition aux CVE : elle vous donne l’outillage pour la réduire vite.

Le problème : une CVE glibc sur 300 microservices

Le modèle Dockerfile place le choix de l’image de base dans chaque dépôt applicatif, sur une ligne FROM. Quand une CVE critique tombe dans glibc, OpenSSL ou zlib, Renovate ou Dependabot ouvrent consciencieusement N pull requests. Mais un commit n’est pas une image : il faut N builds, N exécutions de suite de tests, N promotions, N redéploiements.

Prenons un modèle volontairement simple, à ajuster à votre contexte : 300 services, 8 minutes de pipeline moyen (build + tests + push + scan), 40 % de PR nécessitant une intervention humaine (test flaky, lockfile, image cache invalidé). On obtient ~40 heures de runner et, surtout, plusieurs jours de latence médiane entre la publication de l’image de base corrigée et la présence effective du correctif en production. C’est là que se joue la métrique qui compte : le délai de remédiation médian, pas le nombre de PR mergées.

Le rebase déplace le point de contrôle. L’image de base n’est plus une décision de dépôt applicatif ; elle est une propriété du builder détenu par l’équipe plateforme. Corriger le parc devient une boucle sur un inventaire d’images, pas une vague de rebuilds.

Comment fonctionne le rebase

Une build CNB manipule quatre objets :

  • la build image : l’environnement dans lequel s’exécutent les buildpacks (compilateurs, SDK) ;
  • la run image : la base de l’image finale, celle qui contient glibc, OpenSSL, les certificats CA ;
  • les buildpacks, empaquetés avec le lifecycle dans un builder ;
  • le lifecycle lui-même (detect, analyze, restore, build, export, et le binaire rebaser).

Le lifecycle garantit une propriété structurelle : les couches produites par les buildpacks (dépendances, JRE, artefact applicatif, métadonnées de lancement) sont écrites dans /layers et ne dérivent jamais du contenu de la run image. L’image finale est donc, littéralement, run image + couches applicatives, sans intrication.

Le rebase exploite cette propriété. Il ne télécharge rien : il lit le manifest de l’image existante, lit celui de la nouvelle run image, recalcule le manifest et le config JSON en substituant les couches de base, puis pousse le résultat en s’appuyant sur les cross-repository blob mounts du registry (spec OCI) pour que les blobs déjà présents ne transitent jamais par le poste client. D’où une opération en secondes, sans accès au code source, sans daemon Docker, sans CI.

La condition de validité : compatibilité ABI et cohérence de l’environnement (mêmes chemins, même UID/GID d’exécution, mêmes paquets système requis par les couches applicatives). C’est ce que matérialisent les identifiants de base (historiquement le stack ID, aujourd’hui les targets de la Platform API 0.12+) : le rebaser refuse par défaut de croiser deux bases dont les identifiants diffèrent.

Runbook : mettre la chaîne en place

1. Un builder détenu par la plateforme

# builder.toml
description = "Builder interne — plateforme"

[[buildpacks]]
uri = "urn:cnb:registry:paketo-buildpacks/java"

[[buildpacks]]
uri = "urn:cnb:registry:paketo-buildpacks/nodejs"

[[order]]
  [[order.group]]
  id = "paketo-buildpacks/java"
  version = "latest"

[[order]]
  [[order.group]]
  id = "paketo-buildpacks/nodejs"
  version = "latest"

[build]
image = "registry.interne/base/build:jammy"

[[run.images]]
image = "registry.interne/base/run:jammy"
pack builder create registry.interne/platform/builder:2026.08 \
  --config builder.toml --publish

Le point important n’est pas la syntaxe : c’est que registry.interne/base/run devient une ressource gouvernée, avec sa propre cadence de mise à jour, son propre scan et son propre SLA. Plusieurs fournisseurs commerciaux se positionnent explicitement sur ce créneau — BellSoft a publié en juillet 2026 un « Hardened Builder » zéro-CVE pour Paketo, et les fournisseurs d’images de base affichent désormais des SLA de remédiation de quelques jours sur les CVE critiques.

2. Le build applicatif

pack build registry.interne/apps/orders:1.4.2 \
  --builder registry.interne/platform/builder:2026.08 \
  --publish

En pratique, on n’utilise pas pack en CI pour tout : le plugin Spring Boot (./gradlew bootBuildImage) couvre le monde JVM sans installer d’outillage supplémentaire, et kpack automatise le tout en cluster. kpack a un atout décisif ici : lorsqu’une ClusterStack / run image est mise à jour et que rien d’autre n’a changé, il déclenche un rebase et non un rebuild.

3. L’inventaire : quelles images sont rebasables ?

Toute image produite par le lifecycle porte le label io.buildpacks.lifecycle.metadata, qui contient notamment la référence et le digest de la run image utilisée.

crane config registry.interne/apps/orders:1.4.2 \
  | jq -r '.config.Labels["io.buildpacks.lifecycle.metadata"]' \
  | jq '{runImage: .runImage.reference, base: .stack.runImage}'

Un inventaire fiable se construit à partir du registry (catalogue + tags) filtré sur la présence de ce label, croisé avec ce qui tourne réellement (kubectl get pods -A -o jsonpath sur les image des containers). Une image sans le label n’est pas rebasable : elle reste sur le circuit Dockerfile.

4. La remédiation

NEW_RUN="registry.interne/base/run:jammy-20260812"

while read -r IMG; do
  pack rebase "$IMG" --run-image "$NEW_RUN" --publish \
    || echo "ECHEC $IMG" >> failures.txt
done < inventory.txt

Puis vérification : le digest de la run image dans le label doit avoir changé, et un scan de contrôle doit confirmer la disparition du paquet vulnérable.

Les pièges côté Kubernetes

C’est la partie que les annonces passent sous silence. Un rebase produit une nouvelle image, donc un nouveau digest. Tout ce qui est indexé sur le digest devient faux.

GitOps pinné par digest. Si vos manifests référencent ...@sha256:abc…, le rebase ne déploie rien : Argo CD ou Flux continuent de réconcilier l’ancien digest, et vous croirez le parc patché alors qu’il ne l’est pas. Il faut boucler la chaîne : Flux ImageRepository/ImagePolicy + ImageUpdateAutomation, ou Argo CD Image Updater, écrivent le nouveau digest dans Git après le rebase. Le rebase doit donc s’accompagner d’un tag mouvant (ou d’un tag de révision) que la policy sait ordonner — un tag purement immuable par version applicative ne suffit pas.

Signatures et attestations invalidées. Une signature cosign porte sur un digest. Après rebase, l’image en production n’est plus signée, et un policy-controller Sigstore ou une règle Kyverno verifyImages bloquera l’admission au prochain redémarrage de pod — potentiellement au pire moment, pendant un incident. Le rebase doit être suivi immédiatement d’une re-signature et d’une ré-attestation :

NEW_DIGEST=$(crane digest "$IMG")
cosign sign --yes "${IMG%%:*}@${NEW_DIGEST}"

Idem pour la provenance SLSA : l’attestation d’origine décrit un build qui n’a pas produit cette image. Soit vous émettez une attestation de rebase distincte (prédicat maison décrivant l’opération, l’ancien et le nouveau digest, la run image source), soit vous acceptez explicitement une rupture de chaîne — mais alors documentez-la dans votre politique d’admission.

SBOM périmés. Les buildpacks émettent un SBOM des couches applicatives ; l’inventaire des paquets OS, lui, vient de la run image. Si votre SBOM a été produit par un scan de l’image complète au moment du build et stocké en attestation, il décrit l’ancienne base : vos scanners rapporteront une CVE déjà corrigée, ou rateront une nouvelle. Fusionnez le SBOM applicatif conservé avec celui de la nouvelle run image, ou basculez sur des scanners qui lisent l’image à la demande plutôt que l’attestation.

Tags mutables et admission par registre. Si vos Deployments référencent un tag mutable avec imagePullPolicy: IfNotPresent, les pods existants ne verront jamais le correctif, et deux nœuds pourront servir deux contenus différents sous le même tag. Le rebase rend ce problème latent immédiatement visible.

Le rebase ne redémarre rien. Corriger l’image dans le registry ne remplace pas le processus qui tourne. Il faut un rollout :

kubectl rollout restart deployment/orders -n prod

À l’échelle de 300 services, ce rollout est l’opération risquée, pas le rebase. Prévoyez des vagues, vérifiez que les PodDisruptionBudgets ne bloquent pas, et traitez à part les charges à état (StatefulSets, brokers, bases) dont le redémarrage a un coût opérationnel réel.

Quand rester sur un Dockerfile

Le rebase n’est pas universel. Quelques cas où il faut passer son chemin :

  • binaires natifs / GraalVM : un exécutable lié statiquement rend la base largement inopérante — le gain du rebase disparaît, et un binaire lié dynamiquement à une glibc spécifique demande une vigilance ABI accrue ;
  • images distroless minimales : si votre run image ne contient presque rien, la surface de CVE est déjà proche de zéro et le rebuild est bon marché ;
  • dépendances système exotiques (drivers, bibliothèques propriétaires, agents) : les buildpacks les couvrent mal, et vous finirez par écrire un buildpack maison — ce qui est un projet à part entière ;
  • debug : diagnostiquer une image dont personne dans l’équipe n’a écrit la recette est un coût réel, surtout à 3 h du matin ;
  • verrouillage : votre builder devient un SPOF organisationnel. Il lui faut un owner, un cycle de release et un plan de rollback.

Comparaison honnête avec les alternatives : ko (Go) et Jib (JVM) produisent des images sans daemon avec un layering propre et un rebuild extrêmement rapide — pour un parc mono-langage, changer la base et relancer 300 builds ko peut coûter moins cher qu’introduire CNB. apko/melange (Wolfi) prennent le problème par l’autre bout : des images entièrement déclaratives et reconstruites en continu, où « patcher » signifie « rebuild », mais un rebuild de quelques secondes. Les images « zéro CVE » commerciales déplacent simplement le SLA chez un fournisseur.

Grille rapide : service Go unique → ko ; parc JVM homogène avec build tooling maîtrisé → Jib, éventuellement CNB pour la centralisation de la base ; parc polyglotte de plusieurs dizaines à centaines de services, équipes produit sans expertise conteneur → CNB + rebase ; images système / sidecars / outillage → apko ou Dockerfile assumé.

Décider, puis migrer

Le seuil de rentabilité est moins une question de nombre de services que de produit nombre de services × fréquence des CVE de base × coût unitaire d'un rebuild. En dessous d’une trentaine de services, ou avec un pipeline de rebuild à deux minutes, la centralisation apporte surtout de la complexité. Au-delà de la centaine, avec des pipelines à dix minutes et des équipes produit qui n’ont pas envie de posséder un Dockerfile, l’arbitrage bascule.

La migration se fait service par service, jamais en big bang : commencez par les services stateless les plus standards (une PR qui supprime le Dockerfile et ajoute une config de build), faites cohabiter les deux circuits pendant plusieurs mois, et n’exécutez le premier rebase de masse qu’après avoir validé de bout en bout la chaîne signature → admission → rollout sur un environnement de pré-production.

Trois métriques suffisent à piloter : le délai de remédiation médian entre publication de la run image corrigée et rollout effectif, le pourcentage du parc rebasable (images portant io.buildpacks.lifecycle.metadata), et les minutes de CI économisées par campagne. Si la première ne descend pas franchement sous ce qu’elle était avec les Dockerfiles, le problème n’est pas dans le build : il est dans le rollout et l’admission — et c’est précisément là que le rebase déplace le travail, plutôt que de le supprimer.

Commentaires (0)

Veuillez vous connecter pour laisser un commentaire.

Se connecter

Pas encore de commentaires. Soyez le premier à commenter !