Aller au contenu
Edouard Topin's Blog
Sécurité Kubernetes en production / Série 03/04

Supply chain : Sigstore, SBOM, admission control

Kubernetes ne vérifie aucune signature d'image par lui-même. Signer avec Sigstore, inventorier avec un SBOM, refuser à l'admission — et ce que chacun de ces verbes recouvre réellement.

Edouard Topin
18 min de lecture
Illustration éditoriale abstraite d'un colis signé relié à une chaîne traçable de maillons géométriques.

L’auditeur ne demande pas la liste de vos outils. Il pose une question, une seule, et il attend une réponse avec un verbe : qu’est-ce qui empêche concrètement une image non signée de démarrer sur ce cluster ? Vous ouvrez la documentation Kubernetes pour trouver le mécanisme intégré qui répond. Il n’y en a pas.

Dans Kubernetes 1.35 comme dans 1.36, aucun mécanisme intégré ne vérifie une signature d’image. Le contrôleur d’admission prévu pour déléguer cette décision à un tiers, ImagePolicyWebhook, est désactivé par défaut. Le mécanisme de politique déclarative moderne, ValidatingAdmissionPolicy, est stable depuis la v1.30 mais s’exécute dans l’API server. Cet article prend le problème dans cet ordre : ce que chaque brique fait réellement d’après sa propre documentation, ce qu’elle ne prouve pas — Sigstore l’écrit lui-même —, et l’ordre de mise en place qui donne le plus de refus opposables pour le moins de composants à exploiter. Aucun lab n’a été exécuté : tout ce qui suit s’appuie sur une page publiée, ou est signalé comme non vérifié.

Kubernetes 1.35 → 1.36Sigstore · SBOM · admissionAucun lab exécuté

TL;DR

  • La décision. Sur les cinq points d’application possibles, un seul vérifie une signature, et il coûte un composant hors arbre à exploiter. Les quatre autres refusent un registre, une empreinte ou un pull — c’est utile, mais ce n’est pas la même phrase dans un rapport d’audit.
  • L’arbitrage qui coûte. Le mode keyless supprime la gestion de clés en la remplaçant par trois dépendances réseau au moment de vérifier — Fulcio, Rekor, le log de transparence de certificats — plus un fournisseur OIDC au moment de signer. « Keyless » ne veut pas dire « sans infrastructure ».
  • L’action de lundi matin. Poser une ValidatingAdmissionPolicy en admissionregistration.k8s.io/v1, stable depuis la v1.30, qui exige un registre unique et une image épinglée par empreinte. Zéro composant supplémentaire — et c’est le prérequis silencieux de toute politique de signature ultérieure.

Contrat de version

Cette série borne explicitement ce dont elle parle, parce qu’une affirmation d’API sans numéro de version est fausse quelques mois plus tard.

  • Plancher : Kubernetes 1.35. La 1.34 sort de support le 27 octobre 2026, avant la publication de cet article.
  • Cible : Kubernetes 1.36. La 1.36.2 est la dernière version amont publiée ; côté plateforme, VKS livre v1.36.1+vmware.4-vkr.5 depuis le 18 juin 2026.
  • Non traitée : 1.37, absente des pages de versions amont au 15 août 2026. Elle n’est donc pas anticipée ici.
  • Mécanisme retiré, nommé une seule fois : PodSecurityPolicy, déprécié en v1.21 et retiré en v1.25. Il n’apparaîtra plus dans cet article, et jamais au présent.

Voici l’état publié de chaque mécanisme cité, avec la question qui nous occupe en dernière colonne.

Mécanisme apiVersion / nature État publié Version Vérifie une signature ?
ValidatingAdmissionPolicy admissionregistration.k8s.io/v1 stable v1.30 non — évalue du CEL dans l’API server
ValidatingAdmissionPolicyBinding admissionregistration.k8s.io/v1 stable v1.30
MutatingAdmissionPolicy admissionregistration.k8s.io/v1 stable, activé par défaut v1.36 non
ValidatingAdmissionWebhook contrôleur d’admission activé par défaut 1.36 oui, par délégation
ImagePolicyWebhook contrôleur d’admission, API imagepolicy.k8s.io/v1alpha1 désactivé par défaut 1.36 par délégation, si vous écrivez le backend
ClusterImagePolicy (Sigstore) policy.sigstore.dev/v1beta1 composant « still actively under development! » hors arbre oui
ClusterPolicy / verifyImages (Kyverno) kyverno.io/v1 documentation affichée en v1.18.0 hors arbre oui
Gatekeeper livré par VKS CRD OPA Gatekeeper Standard Package 3.22.2+vmware.1-vks.1 hors arbre non — registre source, pas signature

Deux écarts amont méritent d’être signalés plutôt que lissés. La page de référence de MutatingAdmissionPolicy porte l’état stable et activé par défaut en v1.36, mais la liste des contrôleurs activés par défaut en 1.36, publiée sur une autre page chargée le même jour, ne le nomme pas. De même, le fichier source amont des contrôleurs d’admission décrit encore ValidatingAdmissionPolicy comme conditionné à un feature gate et à un groupe v1alpha1, alors que sa page de référence porte v1.30 stable en v1. Deux fichiers, deux états : cet article retient l’état de la page de référence et signale l’écart, sans trancher.

Quatre mots que le vocabulaire courant fusionne

C’est le tableau qui empêche la ligne de conformité fausse. Aucune page amont ne le publie ; c’est un assemblage propre à cet article.

Objet Ce qu’il contient Ce qu’il prouve Ce qu’il ne prouve pas Standard
SBOM l’inventaire des composants d’un artefact qu’un producteur déclare cette composition ni que la déclaration est exacte, ni que les composants sont sains SPDX (ISO/IEC 5962:2021), CycloneDX 1.7 (ECMA-424)
Signature une empreinte signée, un certificat, une entrée de journal qui a signé, et quand ni que le signataire aurait dû signer, ni que l’artefact est sain Cosign, Fulcio, Rekor
Attestation un prédicat signé, enveloppé en DSSE qu’une affirmation est signée par une identité rien de plus que ce que le prédicat affirme in-toto
Provenance une attestation décrivant le build comment l’artefact a été construit, et à quel niveau de garantie ni le contenu, ni l’absence de vulnérabilité SLSA v1.2, Build L0 à L3

Un tableau d’audit qui range « SBOM » et « signature » dans la même colonne « intégrité » est faux. L’un est une déclaration de contenu, l’autre une déclaration d’identité. On peut signer un SBOM mensonger, et la signature sera parfaitement conforme.

Sur les niveaux de provenance, la spécification SLSA v1.2 — statut « Approved », la v1.1 étant marquée « Retired » — se lit comme une échelle de résistance à la falsification, pas comme une note de qualité. Build L0 : « No guarantees ». Build L1 : la provenance existe, mais « trivial to bypass or forge ». Build L2 : la falsifier « requires an explicit ‘attack’ ». Build L3 : elle « requires exploiting a vulnerability beyond most adversaries’ capabilities ». Personne n’y promet un logiciel sain.

Côté formats d’inventaire, une précision s’impose parce que le site SPDX se contredit lui-même : sa page Specifications affiche 3.0 sous le titre « Current Version », tandis que sa page d’accueil annonce que la 3.1 est disponible en première candidate à la publication. Les deux pages ont été chargées le même jour. La formulation honnête cite les deux, et ne rattache pas le numéro d’ISO/IEC 5962:2021 à un numéro de version de spécification — le site ne le fait pas non plus. CycloneDX, de son côté, est en 1.7 depuis le 21 octobre 2025, standardisée ECMA-424 le 10 décembre 2025.

Sigstore : ce que « keyless » coûte réellement

Trois composants, et une phrase qui décide de tout. Cosign est le client : il crée la matière de signature et demande un certificat. Fulcio est l’autorité de certification, décrite comme « a free code signing Certificate Authority, built to make short-lived certificates available to anyone », qui émet des certificats X.509 valables 10 minutes. Rekor est le journal de transparence, « an immutable, append-only ledger », dont les entrées « are never mutated or removed ».

En mode keyless, l’identité vient d’un jeton OIDC : « The OIDC token contains a user’s email address or service account, or workflow information in the case of CI signing. » Le certificat expirant en dix minutes, la preuve de fraîcheur ne vient plus de lui mais de l’horodatage signé de l’entrée de journal. Et c’est là que la facture arrive, écrite noir sur blanc par le projet : « Verifying keyless signatures require verifying signatures from Rekor, material (SCTs) from the CT log, and certificates that chain up to Fulcio. »

Un trou assumé, parce qu’il pèse sur cette décision : aucun engagement de disponibilité n’a pu être relevé pour l’instance publique Sigstore. La page Public Deployment documente explicitement l’absence de garantie pour l’environnement de staging — « neither SLO guarantees nor the same protection of the root key material » —, mais la section consacrée à l’instance publique a rendu vide au chargement. Ni « l’instance publique est fiable » ni l’inverse ne sont écrivables ici. La question, elle, se pose avant de bâtir une chaîne de production dessus.

Enfin, le raccourci le plus tentant, et le projet lui-même le refuse. Sa page Threat Model écrit : « not everything that’s signed is secure ». Une signature keyless valide garantit qu’un signataire s’est authentifié auprès de ce fournisseur d’identité à ce moment-là. Elle ne garantit pas qu’il aurait dû pouvoir s’authentifier — « in the event of a compromised account, or compromised identity provider » —, ni qu’il aurait dû signer ce message, ni « that the software artifact in question is ‘good’ ». Toute phrase de la forme « Sigstore protège de telle classe d’attaque » sort de ce que la documentation autorise à dire.

Cinq points d’application, et ce que chacun refuse

Point d’application Où il s’exécute Registre source Empreinte Signature Attestation Coût
ValidatingAdmissionPolicy (CEL) API server, en processus oui oui non non aucun composant
Gatekeeper — image registry policy VKS webhook installé par la plateforme oui, c’est son objet non publié non non maintenu par VKS
Sigstore Policy Controller webhook hors arbre oui, par glob par résolution de tag oui oui un composant, « actively under development »
Kyverno — verifyImages webhook hors arbre oui, par imageReferences oui, mutateDigest par défaut oui oui un composant
Harbor — content trust hors du cluster, au pull par projet non oui non publié livré en Standard Package

Pourquoi CEL ne conclut pas sur une signature ? C’est un raisonnement, pas une citation, et il appartient à cet article. Ce que les pages amont écrivent, c’est que « CEL expressions are evaluated directly in the API server, making CEL a convenient alternative to out-of-process mechanisms, such as webhooks », sous un budget de coût d’exécution publié. Or vérifier une signature keyless exige de joindre Rekor, le log de transparence de certificats et Fulcio. Un mécanisme qui ne sort pas de l’API server ne peut donc pas conclure. La formule « without relying on any external HTTP callouts », que l’on retrouve partout attribuée à la documentation, n’a pas été retrouvée dans les fichiers source amont chargés : elle n’est pas citée ici, et le raisonnement se présente comme tel.

Le mécanisme intégré prévu pour cet usage existe pourtant. Le fichier source amont écrit trois choses sur ImagePolicyWebhook : « Type: Validating. », « allows a backend webhook to make admission decisions. », et « This admission controller is disabled by default. » Sa configuration expose kubeConfigFile, allowTTL, denyTTL, retryBackoff et defaultAllow, et son API d’échange est encore en imagepolicy.k8s.io/v1alpha1, dix versions mineures après notre plancher. La phrase « You must implement a webhook and deploy it separately », largement recopiée, ne figure pas non plus dans le fichier chargé — les trois phrases ci-dessus disent la même chose, et elles, elles sont vérifiables.

Reste la ligne Harbor, qui n’est pas dans le même plan que les autres. La documentation Harbor 2.14.0 décrit la procédure — onglet Configuration du projet, case Cosign ou Notation, Save — et publie l’effet exact : « Harbor will only allow verified images to be pulled from the project. » Pulled. Pas deployed. Un pod dont l’image est déjà dans le cache du nœud ne repasse pas par le registre. À noter que le billet d’éditeur commercial Securing Your Software Supply Chain with Harbor, signé Dhruv Tyagi et publié le 30 janvier 2026 sur blogs.vmware.com, écrit de son côté que seules des images signées « can be deployed ». L’écart entre les deux verbes est exactement le sujet de cette section : on cite les deux, on borne, on ne juge pas.

L’ordre de mise en place, phase par phase

L’ordre proposé n’est pas celui des tutoriels, qui commencent par la signature. Il est classé par rapport entre refus obtenus et composants à exploiter, pour produire une réponse d’audit dès la première semaine.

Phase 0 — refuser sans rien installer. Deux règles se posent avec le seul ValidatingAdmissionPolicy : le registre source et l’épinglage par empreinte. Aucune des deux ne vérifie une signature, mais l’épinglage est le prérequis silencieux de tout le reste — une signature porte sur une empreinte, jamais sur un tag.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: images-registre-et-empreinte.k8s-sec.corp.example
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   [""]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["pods"]
  validations:
    - expression: >-
        object.spec.containers.all(c,
          c.image.startsWith('harbor.corp.example/webshop/'))
      message: "image hors du registre autorise harbor.corp.example/webshop/"
    # motif de chaine, PAS une empreinte : aucune valeur sha256 n'est ecrite ici
    - expression: >-
        object.spec.containers.all(c, c.image.contains('@sha256:'))
      message: "image non epinglee par empreinte"
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: images-registre-et-empreinte-webshop
spec:
  policyName: images-registre-et-empreinte.k8s-sec.corp.example
  validationActions: ["Deny"]      # Deny et Warn ne sont pas combinables — contrainte publiee
  matchResources:
    namespaceSelector:
      matchLabels:
        supply-chain.corp.example/enforce: "true"

Trois réserves accompagnent cet exemple, et elles font partie du contenu : il ne couvre que spec.containers, alors qu’une politique réelle doit aussi couvrir spec.initContainers et spec.ephemeralContainers ; il porte sur des pods, donc un Deployment non conforme est accepté et ses pods rejetés ; et ces deux expressions CEL n’ont pas été exécutées.

Phase 1 — signer dans la CI, et trancher le mode de confiance. Avec clé : cosign attest --predicate <fichier> --key cosign.key <image>, vérifié par cosign verify-attestation --key cosign.pub <image>, avec un cycle de vie de clé complet à la charge de l’équipe. Sans clé : le jeton OIDC de l’exécuteur devient l’identité, et la dépendance décrite plus haut s’installe des deux côtés de la chaîne. Les valeurs d’instance restent des trous volontaires — REPLACE_WITH_OIDC_ISSUER, REPLACE_WITH_OIDC_IDENTITY, REPLACE_WITH_REKOR_URL — et aucune clé, aucune identité, aucune empreinte d’exemple n’apparaîtra dans cet article : une valeur d’exemple dans une procédure de signature finit copiée-collée dans une politique réelle le lendemain.

Phase 2 — vérifier à l’admission, en mode avertissement d’abord. Le Sigstore Policy Controller s’installe par Helm dans un namespace cosign-system et ne valide que les namespaces qui ont opté, par le label policy.sigstore.dev/include: "true". Il « resolves the image tags to ensure the image being ran is not different from when it was admitted ». Son champ décisif est publié : « enforce - Reject (default) / warn - allow but warn ».

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: webshop-images-signees
spec:
  mode: warn                       # enforce = rejet (defaut) ; basculer apres releve
  images:
    - glob: "harbor.corp.example/webshop/**"
  authorities:
    - name: ci-keyless
      keyless:
        url: "https://fulcio.sigstore.dev"
        identities:
          - issuer: REPLACE_WITH_OIDC_ISSUER
            subject: REPLACE_WITH_OIDC_IDENTITY

Deux réserves de citation, à porter plutôt qu’à masquer. D’abord, les exemples officiels de politiques publiés par Sigstore portent apiVersion: policy.sigstore.dev/v1alpha1, alors que la page d’aperçu du même site donne ClusterImagePolicy en v1beta1 — deux pages du même site, chargées le même jour. Cet article retient v1beta1 pour ses manifestes et signale l’écart, sans trancher. Ensuite, la référence d’API publie une réserve de maturité sur le langage de politique : « only rego or cue are supported. Furthermore, only cue is tested ». CUE est donc recommandé ici parce que le projet le dit, pas par préférence.

L’alternative Kyverno couvre le même besoin avec d’autres attributs : imageReferences, required, mutateDigest — activé par défaut, il ajoute l’empreinte quand elle manque — et un bloc attestors. La structure interne de ce bloc n’a pas pu être relevée sur la page chargée : elle reste un trou déclaré, et le placeholder ne sera pas rempli par analogie avec la syntaxe Sigstore, les deux projets ne partageant pas leur schéma.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verifier-signature-webshop
spec:
  rules:
    - name: verify-webshop
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "harbor.corp.example/webshop/*"
          mutateDigest: true        # defaut publie
          required: true
          attestors: REPLACE_WITH_ATTESTOR_BLOCK   # schema exact non releve

Un webhook hérite des bonnes pratiques amont : timeout court, portée limitée, filtrage par matchConditions, déploiement hautement disponible, et surtout « prevent your webhook from triggering itself ». L’amont ajoute un conseil de sobriété : « The Kubernetes project recommends that you use CEL-based admission control when possible. » Pour la signature, ce n’est pas possible — le webhook n’est donc pas un choix de confort, c’est le prix documenté d’une vérification cryptographique, avec un mode de panne que CEL n’a pas.

Phase 3 — attester, en dernier. L’attestation arrive en fin de parcours parce qu’elle est la plus coûteuse à produire, la plus coûteuse à vérifier, et la moins contraignante : une politique qui exige la présence d’un SBOM n’exige rien sur son contenu. Le drapeau --type de cosign attest accepte neuf types nommés — slsaprovenance, slsaprovenance02, slsaprovenance1, link, spdx, spdxjson, cyclonedx, vuln, openvex, custom — ou une URI, avec custom par défaut. Et la même documentation prévient : « Note that there are also SBOM predicate types, but they are not recommended », parce que le SBOM entier se retrouve alors embarqué dans le bundle de signature, à retélécharger à chaque vérification. Un article qui recommande --type spdxjson sans cette phrase contredit sa propre source.

Phase 4 — refermer côté registre, sans confondre les plans. Le content trust d’Harbor est un second refus, pas le même : il agit avant que le nœud n’obtienne l’image, l’admission agit avant que l’objet n’existe. Les deux se documentent séparément dans la réponse d’audit, avec leur verbe exact.

Ce que VKS installe déjà — et ce qu’il ne vérifie pas

L’ancrage plateforme mérite une formulation précise, parce que les deux raccourcis opposés sont faux. VKS installe bien un moteur d’admission : « When you implement one of these policies, VKS cluster management installs Gatekeeper on the cluster and maintains that installation ». Les types publiés incluent « security policy, image registry policy, and mutation policy ». Et ce que la politique d’image fait exactement, c’est définir « the registries from which an image can be pulled », avec héritage organisation → projet → cluster, et aucune restriction par défaut.

Ni signature, ni empreinte, ni tag ne sont nommés sur ces pages. La formule exacte est donc : VKS fournit le moteur et la politique de provenance de registre ; la vérification cryptographique reste à ajouter. Détail plus discret et plus parlant : les procédures VKS de registre privé emploient explicitement imgpkg copy ... --cosign-signatures, ce qui montre que la plateforme préserve les signatures Cosign de ses propres paquets à la copie — sans qu’aucune page chargée ne documente un mécanisme qui les vérifierait ensuite à l’admission. À noter, sans en tirer de conclusion : une autre procédure de copie publiée ne porte pas ce drapeau.

Harbor est disponible des deux côtés — en Standard Package 2.14.3+vmware.2-vks.1, et en Supervisor Service, avec la commodité que les clusters font automatiquement confiance à ses certificats. Aucune de ces deux pages ne nomme Cosign, Notation ni le content trust : la procédure de content trust citée plus haut vient de la documentation Harbor, pas de la documentation de plateforme. C’est un trou assumé, pas un oubli.

Pièges & points de vigilance

  • Confondre pulled et admitted. Répondre « nous avons activé le content trust Harbor » à la question « qu’est-ce qui empêche un pod non signé de démarrer » est une réponse fausse, et la documentation Harbor donne le verbe exact pour s’en apercevoir.
  • Écrire une politique de signature avant d’avoir épinglé les empreintes. Tant que le déploiement utilise des tags, la politique vérifie la signature de ce que le registre veut bien résoudre au moment du pull. C’est aussi pourquoi Kyverno active mutateDigest par défaut et pourquoi le Policy Controller résout les tags.
  • Oublier initContainers et ephemeralContainers. Le défaut le plus fréquent de ce type de règle, et il est invisible en essai parce qu’on essaie avec un pod simple.
  • Reproduire la limite de portée déjà rencontrée à l’article 1. Une politique cadrée sur resources: ["pods"] accepte le Deployment et rejette ses pods : l’équipe voit un déploiement « réussi » avec zéro pod en cours d’exécution. Mécanisme différent, erreur de lecture identique.
  • Empiler deux moteurs sans savoir lequel refuse en premier. Politique de registre héritée de la plateforme d’un côté, politique CEL dans le cluster de l’autre : choisissez lequel fait autorité, et écrivez-le.
  • Supposer la couverture du Policy Controller. Les types de ressources Kubernetes qu’il valide réellement — pods seuls, ou objets de charge — ne sont publiés ni sur les pages de documentation ni sur le dépôt chargés. C’est à relever sur votre plateforme avant d’en dépendre.

Conclusion

La réponse d’audit défendable n’est pas « nous utilisons Sigstore ». C’est une phrase par mécanisme, avec son verbe et sa limite : ce registre est le seul autorisé, cette image est épinglée par empreinte, cette signature est vérifiée à l’admission par ce composant, et ce refus-là s’applique au pull et non au démarrage. Chacune tient dans une ligne, chacune s’adosse à une page publiée — et c’est précisément parce qu’aucune ne prétend garantir plus qu’elle ne peut qu’elles tiennent devant un auditeur.

Rien n’est intégré

Kubernetes 1.35 comme 1.36 ne vérifient aucune signature d’image. Le seul contrôleur prévu pour déléguer la décision est désactivé par défaut, et son API d’échange est toujours en v1alpha1.

Commencez par l’empreinte

Registre unique et épinglage par empreinte en ValidatingAdmissionPolicy, stable depuis la v1.30 : zéro composant, un refus opposable, et le prérequis de tout ce qui suit.

Une signature dit qui, pas quoi

« not everything that’s signed is secure » — le projet l’écrit lui-même. La provenance rend une alerte interprétable ; elle ne la remplace pas.

Cet article ferme la question « qu’est-ce qui a le droit de s’exécuter ». Il ne dit rien de « qu’est-ce qui s’est réellement passé » : un binaire de provenance parfaitement établie peut faire, à l’exécution, exactement ce qu’un binaire anonyme aurait fait. C’est le sujet du dernier article de la série.

Reçois le prochain par email

Les nouveaux articles et séries, envoyés à leur publication. Aucun autre courrier.

Désabonnement en un clic, à tout moment.

Retour au blog
Partager

Articles similaires

  1. 23 min de lecture

    Network policies et Cilium : construire un default-deny défendable

    L'API NetworkPolicy est livrée avec Kubernetes ; l'appliquer est le travail du CNI. Ce que Cilium ajoute, ce qui reste standard, et comment atteindre le default-deny sans casser la production.

  2. 18 min de lecture

    RBAC Kubernetes : les fondations, et les pièges qui survivent à l'audit

    Chacun de ces pièges est publié sur kubernetes.io. Ce qui manque, c'est leur mise en ordre — et le chemin qui mène d'un Namespace vSphere jusqu'à cluster-admin.

  3. 18 min de lecture

    Sécurité runtime : Falco et Tetragon, sur quel critère trancher

    Falco et Tetragon collectent tous deux en eBPF. Ce qui les sépare est ailleurs : périmètre d’événements, modèle de règles, et surtout ce que chacun peut réellement empêcher.

Suivre le blog

Nouveaux articles, réflexions et mises à jour.