Publier et signer un chart
Pourquoi
Un chart qui vit dans le dossier d'un dépôt Git se déploie bien tant qu'une seule équipe le consomme. Dès qu'on veut qu'Argo CD, une autre équipe ou un client le tire, il faut le publier : en faire un artefact versionné, immuable, adressable. Et à partir du moment où ce que vous publiez décrit ce qui tourne en production, la question suivante arrive vite : comment celui qui l'installe sait-il que c'est bien votre chart, et pas une copie modifiée ?
Un chart est du code exécuté par votre cluster. Il crée des rôles, des Jobs, des conteneurs privilégiés si on le laisse faire. Un registre compromis, un compte de CI volé, une étiquette réécrite : n'importe lequel de ces incidents peut remplacer le contenu que vos clusters vont tirer à la prochaine synchronisation. La publication et la signature sont la réponse de la chaîne d'approvisionnement : un nom immuable (une version, une empreinte), et une preuve vérifiable de l'origine.
Cette leçon suit le chemin d'un chart de bout en bout. D'abord la version : ce qu'elle promet, ce que change appVersion. Ensuite l'empaquetage, qui produit un fichier avec une vraie propriété surprenante (il n'est pas reproductible). Puis le registre : OCI aujourd'hui, les dépôts HTTP en déclin. Enfin la signature, avec deux mécanismes aux garanties différentes : la provenance GPG de Helm, dont on rejoue ici la création et la vérification, et la signature cosign de l'artefact OCI. La leçon se termine par la chaîne GitHub Actions qui enchaîne tout.
La leçon 3 a défini ce qu'est une release, et la leçon 4 a fabriqué le chart signalements ; on le publie ici dans sa version 0.3.0. Les commandes qui parlent à un registre (helm registry login, helm push, helm pull) ne sont pas exécutées dans ce cours : elles sont décrites d'après la documentation. L'empaquetage, la provenance et la vérification locale, eux, ont été exécutés pour de bon.
Les concepts
Deux versions, deux promesses
Le Chart.yaml porte deux numéros que l'on confond souvent :
version: la version du chart, c'est-à-dire de l'emballage : modèles, valeurs, schéma. C'est un numéro de versionnement sémantique obligatoire, et c'est lui que Helm et les registres utilisent pour identifier le paquet.appVersion: la version de l'application que le chart déploie. Elle est informative : Helm ne l'interprète pas. Pour Signalements, c'est le tag d'image par défaut (1.2.0), comme on l'a vu avecsignalements.image.
Les deux évoluent indépendamment. Une correction d'un modèle sans changement d'application : version passe de 0.3.0 à 0.3.1, appVersion reste 1.2.0. Une nouvelle version de l'application sans changement de chart : on publie quand même un nouveau chart (0.3.1, appVersion: 1.3.0), puisque le contenu du paquet a changé.
Le versionnement sémantique (MAJEUR.MINEUR.CORRECTIF) prend ici un sens précis pour un chart, en reprenant le tableau de la leçon 6 :
| Version | Quand | Exemples |
|---|---|---|
| Majeur | rupture pour qui utilise le chart | clé de valeurs renommée ou supprimée, schéma resserré, sélecteur modifié, changement de la forme des objets qui exige une recréation |
| Mineur | ajout compatible | nouvelle valeur optionnelle, nouvel objet optionnel (désactivé par défaut) |
| Correctif | correction sans changement d'interface | fix d'un modèle, d'un commentaire, d'une valeur par défaut qui était fausse |
Les préversions (0.4.0-rc.1) servent à tester avant de publier une version stable. Le suffixe de métadonnées (+build.5) est accepté par helm package, mais, comme on le verra, il pose un problème avec les registres OCI.
Une règle d'or : une version publiée est immuable. On ne republie jamais 0.3.0 avec un contenu différent. Toute correction, même d'une virgule, donne une nouvelle version.
Le paquet
helm package transforme le répertoire du chart en une archive .tgz nommée <nom>-<version>.tgz. L'archive contient tout ce que .helmignore ne retire pas : Chart.yaml, values.yaml, values.schema.json, templates/, charts/ (les dépendances de la leçon 7), LICENSE, README.md. C'est ce fichier qu'on pousse, qu'on signe et qu'on installe.
Les registres de charts
Un chart se distribue de deux façons.
Les registres OCI. Le même type de registre que pour les images de conteneurs (Docker Hub, GitHub Container Registry, Harbor, Container Registry de Scaleway, Amazon ECR) accepte aussi les charts : un chart devient un artefact OCI, adressé comme une image (registre/chemin/nom:version, et une empreinte @sha256:...). Helm 3 les gère depuis la version 3.8 comme fonctionnalité stable, et c'est le mode de distribution actuel. L'avantage : un seul registre, un seul système d'authentification, et un nom immuable par empreinte.
Les dépôts HTTP. L'ancien modèle : un serveur de fichiers (un bucket, des pages web) qui expose les archives .tgz et un fichier index.yaml qui les liste. Les clients ajoutent le dépôt avec helm repo add, mettent à jour l'index avec helm repo update, et cherchent avec helm search repo. Ce modèle reste en usage (beaucoup de charts de la communauté y sont encore publiés), mais il décline : l'index est un fichier à régénérer, sans notion d'empreinte ou d'authentification standard. Pour un nouveau projet, choisissez OCI.
Deux mécanismes de signature
La provenance de Helm. helm package --sign produit, à côté du paquet, un fichier .prov : les métadonnées du chart, l'empreinte SHA-256 de l'archive, le tout signé avec une clé GPG (OpenPGP). helm verify (ou l'option --verify des commandes d'installation et de téléchargement) contrôle l'empreinte et la signature avec la clé publique du signataire. Ce que cela prouve, d'après la documentation de Helm : que le paquet n'a pas été altéré, et que la personne qui l'a signé est connue (celle dont la clé figure dans votre trousseau).
La signature d'un artefact OCI. Un chart publié dans un registre OCI est un artefact comme un autre : on peut le signer avec cosign (projet Sigstore), comme une image (Signer et vérifier ses images). La signature s'attache à l'empreinte du manifeste, et se stocke dans le même registre. Avec la signature « sans clé », l'identité du signataire est celle d'un workflow GitHub (un certificat éphémère lié au dépôt et à la branche), et non une clé que l'on garde.
Les deux se complètent plus qu'elles ne s'opposent. Le tableau les compare :
| Provenance Helm (GPG) | cosign sur l'artefact OCI | |
|---|---|---|
| Ce qui est signé | l'archive .tgz (empreinte du fichier) | l'empreinte du manifeste OCI |
| Où est la signature | un fichier .prov à côté (et dans le registre, voir plus bas) | un artefact attaché dans le registre |
| Gestion des clés | une clé GPG à garder, distribuer, renouveler | des clés cosign, ou aucune clé (identité OIDC du workflow) |
| Vérificateur | helm verify, --verify | cosign verify |
| Intégré à Helm | oui | non : un outil à part |
| Journal de transparence | non | oui, avec la signature sans clé (Rekor) |
Helm 3 ne vérifie pas les signatures cosign de lui-même, et Helm 4 non plus, d'après sa documentation : il apporte l'installation par empreinte (helm install ... oci://registre/chart@sha256:...), une signature et une vérification des greffons (pas des charts), mais pas d'intégration de cosign. La vérification d'un chart signé par cosign se fait donc par un appel explicite à cosign verify, dans la CI ou à l'admission.
En pratique
Empaqueter
$ helm package signalements -d paquets
Successfully packaged chart and saved it to: paquets/signalements-0.3.0.tgz
$ tar tzf paquets/signalements-0.3.0.tgz
signalements/Chart.yaml
signalements/values.yaml
signalements/values.schema.json
signalements/templates/_helpers.tpl
signalements/templates/deployment.yaml
signalements/templates/externalsecret.yaml
signalements/templates/httproute.yaml
signalements/templates/migration-job.yaml
signalements/templates/service.yaml
signalements/templates/tests/test-sante.yaml
$ helm show chart paquets/signalements-0.3.0.tgz
apiVersion: v2
appVersion: 1.2.0
description: Signalements, l'application de démonstration des cours Lyneko
name: signalements
type: application
version: 0.3.0
Le nom du paquet vient du Chart.yaml. Deux options changent les numéros au moment de l'empaquetage, sans toucher au fichier : --version et --app-version. C'est le moyen de produire une préversion dans une CI :
$ helm package signalements --version 0.3.1-rc.1 --app-version 1.3.0 -d paquets
Successfully packaged chart and saved it to: paquets/signalements-0.3.1-rc.1.tgz
Le paquet n'est pas reproductible
Un détail qui change la façon de signer. Empaquetez deux fois le même chart, à deux secondes d'intervalle :
$ helm package signalements -d p1; sleep 2; helm package signalements -d p2
$ sha256sum p1/*.tgz p2/*.tgz | cut -c1-16
16a1d6ab5fa9edad
4c715dbe0ea22a4b
Les deux archives ont des empreintes différentes, alors que le contenu des fichiers est identique : l'archive enregistre l'heure de création des fichiers (tar tvzf montre 2026-10-05 17:25 sur chaque entrée). Conséquence pratique : le paquet est un artefact, pas une recette. On l'empaquette une fois, et c'est ce fichier que l'on signe, pousse, et référence. Ré-empaqueter plus tard et republier sous le même numéro produirait un contenu identique au sens des fichiers mais d'empreinte différente : un registre qui refuse la réécriture refuserait, un registre qui l'accepte donnerait deux empreintes sous un même tag.
Signer par provenance (GPG)
On génère une clé de démonstration (dans un trousseau isolé, jetable), puis on signe :
$ gpg --batch --pinentry-mode loopback --passphrase '' \
--quick-gen-key "Lyneko Charts (demo) <charts@lyneko.example>" rsa3072 sign never
$ gpg --batch --pinentry-mode loopback --passphrase '' --export-secret-keys > secring.gpg
$ gpg --export > pubring.gpg
$ helm package signalements --sign --key 'Lyneko Charts' --keyring secring.gpg \
--passphrase-file <(echo) -d signe
Successfully packaged chart and saved it to: signe/signalements-0.3.0.tgz
$ ls signe
signalements-0.3.0.tgz signalements-0.3.0.tgz.prov
Helm 3 attend un trousseau au format historique (fichier secring.gpg), que l'on exporte explicitement de GnuPG moderne, d'où les deux --export. L'argument de --key est une sous-chaîne de l'identité de la clé. Le fichier .prov est un message OpenPGP signé en clair :
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
apiVersion: v2
appVersion: 1.2.0
description: Signalements, l'application de démonstration des cours Lyneko
name: signalements
type: application
version: 0.3.0
...
files:
signalements-0.3.0.tgz: sha256:3f997aec5f7b5eb4e6f5c772bbb2843bc3419b4b74415907e11dc9edf490f411
-----BEGIN PGP SIGNATURE-----
wsDcBAEBCgAQBQJqw8EqCRBm... (signature, abrégée)
-----END PGP SIGNATURE-----Trois parties, comme le dit la documentation : les métadonnées du Chart.yaml, l'empreinte SHA-256 du paquet, et la signature de l'ensemble.
Vérifier
$ helm verify signe/signalements-0.3.0.tgz --keyring pubring.gpg
Signed by: Lyneko Charts (demo) <charts@lyneko.example>
Using Key With Fingerprint: 0B573BE8DE90FF9B2552AD2766AB2B1A0F840B31
Chart Hash Verified: sha256:3f997aec5f7b5eb4e6f5c772bbb2843bc3419b4b74415907e11dc9edf490f411
Trois échecs qui protègent, tous produits pour de vrai. D'abord, on modifie une valeur dans le paquet (replicaCount: 2 devient 9) et on le ré-archive sans re-signer :
$ helm verify alt/signalements-0.3.0.tgz --keyring pubring.gpg
Error: sha256 sum does not match for signalements-0.3.0.tgz: "sha256:3f997aec5f7b5eb4e6f5c772bbb2843bc3419b4b74415907e11dc9edf490f411" != "sha256:4ef24e8e6dab5f6f35b2200610261694c949e6b82d33b918c8f72546ea3634a1"
La signature elle-même est intacte, mais l'empreinte de l'archive ne correspond plus à celle qu'elle atteste : une altération du paquet est détectée. Ensuite, on vérifie avec un trousseau qui ne contient pas la clé du signataire :
$ helm verify signe/signalements-0.3.0.tgz --keyring /dev/null
Error: openpgp: signature made by unknown entity
La signature est valide, mais le signataire n'est dans la liste de personne que vous connaissez : le contrôle échoue. C'est le point central de toute signature : elle ne vaut que par la façon dont vous avez obtenu la clé publique. Enfin, l'absence de fichier .prov fait échouer helm verify : un paquet non signé ne passe pas un --verify.
Ce que la provenance ne dit pas
La provenance garantit l'intégrité de l'archive et l'identité de la clé. Elle ne dit pas que le chart est sûr, ni qu'il est récent : un ancien chart, signé légitimement, mais qui contient une faille, vérifie parfaitement. Elle ne dit pas non plus comment le paquet a été construit (quel dépôt, quel commit, quelle CI) : c'est ce que la signature sans clé, liée à l'identité d'un workflow, ajoute.
Publier dans un registre OCI
Pour Container Registry de Scaleway, l'espace de noms est celui de la leçon 7 du cours Scaleway, dans lequel on range les charts sous un chemin dédié. Les commandes, d'après la documentation de Helm (non exécutées ici) :
$ echo "$SCW_SECRET_KEY" | helm registry login rg.fr-par.scw.cloud \
--username nologin --password-stdin
$ helm push signalements-0.3.0.tgz oci://rg.fr-par.scw.cloud/<espace>/charts
Quelques points précis, tous tirés de la documentation de Helm :
helm registry loginprend un nom d'hôte seul, éventuellement avec un port, sans schéma ni chemin. L'identifiantnologinet la clé secrète sont ceux que le cours Scaleway utilise avecdocker login. Lisez la clé sur l'entrée standard (--password-stdin) plutôt que sur la ligne de commande, où elle se retrouve dans l'historique du shell et la liste des processus.helm pushprend le fichier.tgz(pas le répertoire) et une destinationoci://hôte/chemin, sans le nom du chart : Helm le déduit du paquet, et utilise la version comme étiquette. Le chart ci-dessus est donc publié enrg.fr-par.scw.cloud/<espace>/charts/signalements:0.3.0.- Si un fichier
.provexiste à côté du paquet,helm pushl'envoie aussi. - Une version avec métadonnées de construction (
+build.5) n'est pas un nom d'étiquette valide dans OCI (le+n'y est pas autorisé) : Helm la transforme en_. Évitez ce suffixe, ou sachez que le+devient_.
Pour l'utiliser : helm install signalements oci://rg.fr-par.scw.cloud/<espace>/charts/signalements --version 0.3.0 (le nom du chart est dans la référence, contrairement à push), helm show values oci://..., helm pull oci://.... Il n'y a pas de helm search dans un registre OCI : on ne découvre pas les charts par Helm, il faut connaître leur adresse. Le registre n'a pas de notion d'index.
Note
La documentation de Scaleway consultée ne mentionne pas explicitement les charts Helm, et le cours sur Container Registry rappelle que la prise en charge de l'API referrers d'OCI 1.1 n'y est pas documentée. Testez sur votre espace de noms, avant de bâtir une chaîne dessus : un helm push d'un chart d'essai, un helm pull, et un cosign sign suivi d'un cosign tree sur l'empreinte obtenue.
Installer par empreinte
Une version est une étiquette, et une étiquette peut être déplacée par qui a le droit d'écrire dans le registre. L'empreinte, non. Helm 4 permet d'installer un chart directement par empreinte, d'après sa documentation :
$ helm install signalements oci://rg.fr-par.scw.cloud/<espace>/charts/signalements@sha256:<empreinte>
C'est la forme à privilégier en production, et celle que la chaîne ci-dessous calcule. Le fichier de documentation de Helm sur les registres OCI donne cette syntaxe @sha256: pour helm install ; vérifiez qu'elle est acceptée par la version de Helm que vous utilisez.
Signer avec cosign
La signature s'applique à l'empreinte de l'artefact dans le registre. Sans clé, depuis un workflow GitHub Actions :
$ cosign sign --yes rg.fr-par.scw.cloud/<espace>/charts/signalements@sha256:<empreinte>
$ cosign verify \
--certificate-identity "https://github.com/lyneko-formation/signalements-chart/.github/workflows/publier.yml@refs/tags/chart-v0.3.0" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
rg.fr-par.scw.cloud/<espace>/charts/signalements@sha256:<empreinte>
Le verify exige deux choses : l'identité du signataire (ce workflow, dans ce dépôt, sur cette étiquette de Git) et l'émetteur de cette identité (GitHub Actions). Ces deux contraintes sont ce qui fait la valeur de la signature : sans elles, n'importe quelle signature valide d'un certificat de Sigstore serait acceptée. Les mécanismes (Fulcio, Rekor, schéma de repli quand le registre n'a pas l'API referrers) sont ceux du cours sur les images ; ils ne dépendent pas du type de l'artefact.
La chaîne de publication
Le workflow suivant publie, signe et vérifie. Il n'a pas été exécuté ici : il suit la documentation de Helm et de cosign, et le cours GitHub Actions pour l'épinglage.
name: publier-chart
on:
push:
tags: ["chart-v*"]
permissions:
contents: read
jobs:
publier:
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write # identité OIDC pour la signature sans clé
env:
REGISTRE: rg.fr-par.scw.cloud
DEPOT: oci://rg.fr-par.scw.cloud/<espace>/charts
steps:
- uses: actions/checkout@<sha-du-commit> # épingler par empreinte de commit
- uses: azure/setup-helm@<sha-du-commit>
with: {version: v3.16.3}
- uses: sigstore/cosign-installer@<sha-du-commit>
- name: Contrôles
run: |
helm lint chart --strict
for f in environnements/*/values.yaml; do helm template s chart -f "$f" > /dev/null; done
- name: Vérifier que la version correspond à l'étiquette
run: |
v=$(helm show chart chart | awk '/^version:/ {print $2}')
test "chart-v$v" = "$GITHUB_REF_NAME"
- name: Empaqueter une seule fois
run: helm package chart -d paquet
- name: Connexion au registre
env:
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
run: echo "$SCW_SECRET_KEY" | helm registry login "$REGISTRE" --username nologin --password-stdin
- name: Publier et relever l'empreinte
id: push
run: |
sortie=$(helm push paquet/*.tgz "$DEPOT" 2>&1)
echo "$sortie"
echo "digest=$(echo "$sortie" | awk '/^Digest:/ {print $2}')" >> "$GITHUB_OUTPUT"
- name: Signer l'empreinte
run: cosign sign --yes "$REGISTRE/<espace>/charts/signalements@${{ steps.push.outputs.digest }}"
- name: Vérifier ce qui vient d'être publié
run: |
cosign verify \
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$REGISTRE/<espace>/charts/signalements@${{ steps.push.outputs.digest }}"Quelques choix à expliquer. Le workflow n'est déclenché que par une étiquette chart-v* : on ne publie jamais depuis une branche, ce qui rend l'identité signée (@refs/tags/chart-v0.3.0) précise. La version du Chart.yaml doit correspondre à l'étiquette, pour éviter de publier 0.3.0 avec le fichier de la 0.2.9. L'empaquetage se fait une fois (le paquet n'est pas reproductible), et c'est l'empreinte relevée à la sortie de helm push qui est signée, jamais l'étiquette. La clé du registre est un secret du dépôt : Scaleway n'offre pas de fédération OIDC (voir le cours OIDC), on garde donc un secret, d'où l'importance de limiter ses droits à l'espace de noms des charts. La signature, elle, n'a besoin d'aucun secret : c'est l'identité OIDC du workflow (id-token: write). Le dernier pas vérifie juste après avoir signé : une signature qui n'est jamais vérifiée n'est qu'un fichier de plus.
Le format de la ligne Digest: affichée par helm push est celui de Helm 3 ; contrôlez-le sur la version que vous utilisez avant de bâtir un awk dessus.
Sous le capot
Ce qu'est un chart dans un registre. Un chart OCI est un manifeste OCI qui référence une configuration (le contenu de Chart.yaml, au format JSON, avec un type de média propre à Helm) et une couche unique : l'archive .tgz, avec son type de média. Un fichier .prov, s'il existe, est une seconde couche. Le manifeste a une empreinte, qui est une fonction du contenu : changer un octet change l'empreinte. L'étiquette (0.3.0) est un simple pointeur vers cette empreinte, que le registre peut laisser déplacer.
Pourquoi signer l'empreinte et pas l'étiquette. Signer « signalements:0.3.0 » serait signer un pointeur mobile : la signature resterait valable après que quelqu'un a repointé l'étiquette vers un autre contenu, mais cosign la lie à l'empreinte, qui ne bouge pas. Si l'étiquette est déplacée, la vérification de l'ancienne empreinte réussit toujours (c'est le bon contenu), et celle de la nouvelle échoue (no signatures found). C'est pourquoi on déploie, ou au moins on vérifie, par empreinte.
Où cosign range la signature. Avec un registre qui prend en charge l'API referrers d'OCI 1.1, la signature est un artefact qui référence l'empreinte signée, et que l'on retrouve en interrogeant les « référents » du manifeste. Sans cette API, cosign range la signature sous une étiquette de repli sha256-<empreinte>.sig dans le même dépôt. Ce second schéma pollue la liste des étiquettes du dépôt du chart, ce qui compte si une CI de nettoyage supprime « les étiquettes qui ne sont pas des versions » : elle supprimerait les signatures.
Comment helm verify fait son contrôle. Il lit le .prov, vérifie la signature OpenPGP avec le trousseau public indiqué, recalcule le SHA-256 du .tgz, le compare à celui du .prov, et compare les métadonnées de Chart.yaml à celles du paquet. Les trois doivent concorder. La documentation de Helm ne détaille pas, dans la page de référence de helm pull, si l'option --verify s'applique aux références oci:// ; sa page sur les registres renvoie, pour signer des charts OCI, vers le greffon helm-sigstore. C'est une raison de plus de tester votre chaîne sur votre registre.
Le .prov et l'OCI. helm push envoie le .prov s'il existe (couche de provenance), mais les outils d'admission et de déploiement les plus courants s'appuient sur des signatures cosign ou Notation (Argo CD, on l'a vu, ne mentionne que les commits GnuPG). D'où la tendance actuelle : on garde helm package --sign pour les consommateurs de Helm qui ont le trousseau, et cosign pour la vérification automatisée.
Pièges courants
Republier une version existante. Le registre accepte parfois l'écrasement : 0.3.0 pointe alors vers un contenu différent, et les clusters qui ont déjà tiré l'ancien et ceux qui tirent le nouveau divergent sans que rien le signale. Faites échouer la CI si la version existe déjà (un helm pull de 0.3.0 qui réussit avant le push est un signal d'arrêt), et protégez les étiquettes de version dans le registre quand il le permet.
helm push d'un répertoire. helm push veut un .tgz. Si vous avez un répertoire, lancez d'abord helm package.
Signer l'étiquette. Voir plus haut : toute signature par étiquette est une signature d'un pointeur. Utilisez l'empreinte retournée par le push.
+ dans la version. Accepté à l'empaquetage, transformé en _ pour l'étiquette OCI : la version que vous annoncez ne correspond plus au nom que le registre connaît. Pas de métadonnées de construction dans les versions de charts.
Un trousseau GPG introuvable. helm package --sign attend un trousseau secret au format historique ; les clés d'un GnuPG récent ne s'y trouvent pas sans exportation explicite. Exportez-les avec gpg --export-secret-keys > secring.gpg, comme dans la démonstration.
Vérifier avec le mauvais trousseau, ou ne pas vérifier. Une signature n'a de valeur que si la clé publique vient par un canal sûr : pas le même dépôt que le paquet. Et le piège le plus courant n'est pas technique : on signe, mais plus personne ne vérifie.
Une identité cosign trop large. --certificate-identity-regexp '.*' accepte n'importe quel workflow de n'importe quel dépôt qui a signé via Sigstore. Écrivez l'identité complète, ou une expression qui ancre le dépôt et l'étiquette.
Le chart signé, mais pas ses dépendances. Un sous-chart tiré d'un autre dépôt est empaqueté dans le chart ; sa provenance d'origine n'est pas conservée. Signer le chart parent atteste ce que vous avez publié, pas l'innocuité du contenu qu'il embarque.
Sécurité
Ce qui se défend. Quatre attaques sont visées. L'altération du paquet en transit ou dans le registre : l'empreinte et la signature la détectent. Le remplacement d'une étiquette : on déploie par empreinte. L'usurpation d'un chart par un tiers : l'identité du signataire est contrôlée. La publication par un compte volé : avec la signature sans clé, le voleur de la clé du registre peut publier, mais pas produire la signature d'un workflow qu'il ne contrôle pas ; la vérification en aval la refuse.
Ce qui ne se défend pas. Un chart légitime et signé, mais vulnérable ou malveillant dès la source (revue de code absente, compte de mainteneur compromis avant la signature). La signature atteste l'origine, pas la qualité : la revue des modifications du chart, les droits de fusion dans le dépôt et la protection de la branche restent le premier rempart.
Moindres droits. La clé du registre utilisée par la CI ne doit pouvoir écrire que dans l'espace de noms des charts, rien d'autre. Les clusters, eux, ne reçoivent qu'un accès en lecture à ce même espace.
Les clés GPG. Une clé GPG sans phrase secrète sur un poste de CI est un secret de longue durée à protéger comme un mot de passe. C'est l'argument principal pour la signature sans clé de cosign, qui n'a rien à garder. Si vous gardez GPG, la clé vit dans un coffre, jamais dans le dépôt, avec une procédure de renouvellement et une liste de qui détient la clé publique de confiance.
Où vérifier. Argo CD (3.5) vérifie la signature GPG des commits de Git (Source Integrity, d'après sa documentation) ; elle ne mentionne ni cosign ni les signatures d'artefacts OCI. Pour un chart OCI signé par cosign, la vérification se place donc avant : dans la CI qui écrit la nouvelle version dans le dépôt de déploiement (la promotion de la leçon 10), qui exécute cosign verify sur l'empreinte qu'elle s'apprête à écrire, et refuse sinon. Le contrôle ne dépend alors pas d'une fonctionnalité du cluster.
En production
Un registre dédié, des droits distincts. Un espace de noms charts séparé de celui des images : les équipes qui poussent des images n'ont pas le droit d'y écrire, et les clusters n'y lisent que ce qu'on y a publié. Les droits de publication appartiennent à la CI du dépôt de charts, derrière une branche protégée et une étiquette protégée.
Un miroir des charts tiers. Les charts de la communauté (Traefik, cert-manager, Argo CD) sont tirés de leurs dépôts d'origine : copiez-les dans votre registre (helm pull puis helm push), après relecture du rendu, et déployez depuis la copie. Vous maîtrisez ce qui est servi, vous ne dépendez plus de la disponibilité du site amont, et vous signez la copie avec votre identité : cela atteste que vous l'avez relue.
Les versions et les environnements. Un chart publié est promu d'un environnement à l'autre en changeant la version référencée, jamais en le republiant. Les préversions (-rc.N) servent à la recette ; la production ne tire que des versions stables, par empreinte.
La rétention. Un registre qui garde toutes les versions de tous les charts grossit. Une règle de rétention sur les préversions, sans toucher aux versions stables, évite de supprimer ce qu'un cluster, un retour arrière ou une enquête réclame. Souvenez-vous que sans API referrers, supprimer des étiquettes .sig supprime des signatures.
Une version de Helm commune. Le helm package de la CI, le Helm des postes et celui d'Argo CD (qui embarque Helm 4 dans la 3.5) ne sont pas forcément les mêmes : testez le rendu du paquet publié avec la version qui le déploie. La leçon 10 y revient.
Journaliser. Gardez la trace de ce qui est publié (version, empreinte, workflow, date) : la sortie de la CI suffit si elle est conservée. En cas d'enquête, savoir quelle empreinte tournait à telle date est la première question.
Exercices
Exercice 1 : quelle version ?
Le chart signalements est en 0.3.0 (application 1.2.0). Donnez le numéro de chart (et l'appVersion) pour chacun de ces changements : (a) correction d'une faute dans un commentaire de values.yaml ; (b) nouvelle valeur optionnelle autoscaling désactivée par défaut ; (c) l'image passe en 1.3.0, sans autre changement ; (d) replicaCount est renommé replicas sans compatibilité ; (e) test de la prochaine version, avant publication.
Solution
(a) 0.3.1, appVersion 1.2.0 (correctif). (b) 0.4.0 (mineur : ajout compatible, désactivé par défaut). (c) 0.3.1 ou 0.4.0 selon votre convention, avec appVersion: "1.3.0" ; le chart a changé, donc on publie une nouvelle version (un correctif si l'on considère que seul le défaut d'image bouge). (d) 1.0.0 (majeur : rupture pour qui surcharge replicaCount, et le schéma le rejettera désormais). (e) 0.4.0-rc.1 : une préversion, produite par --version 0.4.0-rc.1, qui n'atteint jamais la production.
Exercice 2 : lire un échec
Une CI exécute helm verify signalements-0.3.0.tgz --keyring pubring.gpg et obtient Error: sha256 sum does not match. Qu'est-ce que cela signifie, et qu'est-ce que cela ne signifie pas ? Quelles hypothèses testez-vous, dans l'ordre ?
Solution
Le fichier .tgz que l'on vérifie n'a pas l'empreinte que le .prov atteste : le paquet a été modifié, ou remplacé par un autre empaquetage du même chart (le paquet n'est pas reproductible : deux helm package successifs donnent deux empreintes). Cela ne dit rien sur la validité de la signature elle-même, ni sur la clé. On teste : (1) a-t-on récupéré le .tgz et le .prov qui vont ensemble (même exécution de helm package) ? (2) le paquet a-t-il été ré-archivé après la signature (un outil qui décompresse et recompresse) ? (3) l'archive a-t-elle été altérée en route, auquel cas on compare avec l'empreinte relevée à la publication. Si les deux fichiers sont ceux de la publication et que l'écart persiste, on traite l'artefact comme compromis.
Exercice 3 : provenance ou cosign ?
Argo CD tire le chart de Signalements depuis le registre OCI de Lyneko. Vous voulez qu'un chart non signé par votre workflow de publication ne soit jamais promu en production. Quel mécanisme choisissez-vous, où placez-vous la vérification, et pourquoi pas dans Argo CD ?
Solution
La signature cosign sans clé sur l'empreinte de l'artefact, avec la vérification de l'identité du workflow (--certificate-identity) et de l'émetteur : elle ne demande aucune clé à garder, et lie le chart à un dépôt et une étiquette. La vérification se place dans la CI qui écrit la nouvelle version dans le dépôt de déploiement : elle exécute cosign verify sur l'empreinte à promouvoir, et refuse de créer le commit si la vérification échoue. Argo CD (3.5) ne vérifie, d'après sa documentation de Source Integrity, que les signatures GnuPG des commits Git, et ne mentionne pas les signatures cosign d'artefacts OCI : la vérification ne peut donc pas s'y appuyer. La provenance GPG de Helm conviendrait pour des consommateurs qui exécutent helm install --verify eux-mêmes, ce qui n'est pas le cas ici.
Récapitulatif
versionidentifie le chart (versionnement sémantique, immuable une fois publié),appVersionidentifie l'application. Un changement de l'un n'implique pas l'autre.helm packageproduit un.tgznon reproductible : on l'empaquette une fois, et c'est ce fichier que l'on signe, pousse et référence.- Les charts se publient dans un registre OCI (
helm registry login,helm push fichier.tgz oci://hôte/chemin, installation paroci://.../nom --versionou par empreinte). Les dépôts HTTP àindex.yamldéclinent. helm package --signproduit un.prov(métadonnées, SHA-256, signature GPG) ;helm verifydétecte une altération (sha256 sum does not match) et un signataire inconnu (signature made by unknown entity).- La signature cosign d'un artefact OCI se lie à l'empreinte, s'obtient sans clé par l'identité d'un workflow, et se vérifie avec l'identité et l'émetteur attendus. Helm ne la vérifie pas de lui-même : la vérification se place dans la CI ou à l'admission.
- La chaîne GitHub Actions publie sur étiquette, empaquette une fois, relève l'empreinte, signe, et vérifie aussitôt. Une signature n'a de valeur que si quelqu'un la vérifie.
Pour aller plus loin
- Helm, Provenance and Integrity et Using OCI-based registries.
- Sigstore, documentation de cosign : signature par empreinte, attestations.
- Dans ce cours : Valeurs, schéma et fonctions d'aide (compatibilité des valeurs, base de la version majeure) et Helm et GitOps (promouvoir une version signée).
- Signer et vérifier ses images, OIDC : des identités sans secret, Sécuriser ses workflows, Container Registry.
Sources
- Helm, Using OCI-based registries
- Helm, Helm Provenance and Integrity
- Helm, helm push et helm pull (--verify, --keyring, fichier de provenance)
- Helm 4, présentation et journal des changements (installation par empreinte, greffons signés)
- Sigstore, Signing Containers with Cosign (signer par empreinte, identité sans clé)
- Semantic Versioning 2.0.0
- OCI Distribution Specification 1.1 (referrers, schéma de repli)
- Argo CD, Source Integrity