[Sécurité] Les images de packages sont signées (Cosign) mais jamais vérifiées au pull/install #14

Open
opened 2026-06-12 09:29:13 +02:00 by xmortelette · 3 comments

Constat

La signature Cosign a été ajoutée côté push (commits cf6ba49 / ee1747e), via common/src/ocihandler.rs :

pub fn sign_image(&mut self, repository, tag, digest, key_path) -> RhaiRes<()> {
    ...
    std::process::Command::new("cosign").args(["sign", "--yes", "--key", &key_path, &image_ref])
    ...
}

Mais aucune vérification de signature n'est faite au moment du pull / de l'installation. pull_image() et verify_tag_in_registry() ne font que récupérer le manifest/les layers :

pub fn pull_image(&mut self, ...) {
    let client = Client::new(client::ClientConfig::default());
    ...
    client.pull(&reference, &self.auth, vec![...]).await   // aucun cosign verify
}

Le scan de jukebox et le unpack (initContainer du job d'install) téléchargent et exécutent donc le contenu du package sans valider qu'il provient bien d'une source de confiance.

Pourquoi c'est un risque

La signature n'apporte aujourd'hui aucune protection à la consommation : un attaquant capable de pousser un tag dans le registre (registre compromis, MITM si un registre HTTP est utilisé, ou simple typosquatting de jukebox) sert un package non signé / signé par une autre clé, et il est installé sans broncher. Combiné au fait que l'agent tourne en cluster-admin (#13), c'est une chaîne d'approvisionnement non protégée.

Pistes

  1. Vérifier la signature avant unpack : cosign verify --key <pub> (ou keyless/Fulcio selon le modèle retenu) sur le digest résolu, dans l'initContainer unpack et dans le scan. Échec de vérification ⇒ refus d'installer.
  2. Épingler par digest : résoudre tag→digest une fois (déjà fait au push, push_response.manifest_url) et installer par @sha256:…, pas par tag mutable.
  3. Clé publique de confiance configurable par jukebox (champ spec de JukeBox) ou globalement dans la config opérateur, avec un mode maturity/verify: required|warn|off pour migration progressive.
  4. TLS : s'assurer que les pulls refusent le HTTP en clair (vérifier la ClientConfig d'oci_client, par défaut ClientProtocol::Https mais à confirmer/forcer).

Tests

  • Unitaire : une fonction verify_signature(reference, digest, pubkey) renvoyant Err quand cosign échoue (analogue au test existant sign_image_cosign_not_found_returns_error).
  • Intégration : pousser un package signé avec clé A, configurer la vérification avec clé B → l'install doit être refusée.
## Constat La signature Cosign a été ajoutée côté **push** (commits `cf6ba49` / `ee1747e`), via [`common/src/ocihandler.rs`](common/src/ocihandler.rs) : ```rust pub fn sign_image(&mut self, repository, tag, digest, key_path) -> RhaiRes<()> { ... std::process::Command::new("cosign").args(["sign", "--yes", "--key", &key_path, &image_ref]) ... } ``` Mais **aucune vérification de signature n'est faite au moment du pull / de l'installation**. `pull_image()` et `verify_tag_in_registry()` ne font que récupérer le manifest/les layers : ```rust pub fn pull_image(&mut self, ...) { let client = Client::new(client::ClientConfig::default()); ... client.pull(&reference, &self.auth, vec![...]).await // aucun cosign verify } ``` Le `scan` de jukebox et le `unpack` (initContainer du job d'install) téléchargent et exécutent donc le contenu du package sans valider qu'il provient bien d'une source de confiance. ## Pourquoi c'est un risque La signature n'apporte aujourd'hui **aucune protection à la consommation** : un attaquant capable de pousser un tag dans le registre (registre compromis, MITM si un registre HTTP est utilisé, ou simple typosquatting de jukebox) sert un package non signé / signé par une autre clé, et il est installé sans broncher. Combiné au fait que l'agent tourne en cluster-admin (#13), c'est une chaîne d'approvisionnement non protégée. ## Pistes 1. **Vérifier la signature avant unpack** : `cosign verify --key <pub>` (ou keyless/Fulcio selon le modèle retenu) sur le digest résolu, dans l'initContainer `unpack` et dans le `scan`. Échec de vérification ⇒ refus d'installer. 2. **Épingler par digest** : résoudre tag→digest une fois (déjà fait au push, `push_response.manifest_url`) et installer par `@sha256:…`, pas par tag mutable. 3. **Clé publique de confiance configurable** par jukebox (champ `spec` de `JukeBox`) ou globalement dans la config opérateur, avec un mode `maturity`/`verify: required|warn|off` pour migration progressive. 4. **TLS** : s'assurer que les pulls refusent le HTTP en clair (vérifier la `ClientConfig` d'`oci_client`, par défaut `ClientProtocol::Https` mais à confirmer/forcer). ## Tests - Unitaire : une fonction `verify_signature(reference, digest, pubkey)` renvoyant `Err` quand cosign échoue (analogue au test existant `sign_image_cosign_not_found_returns_error`). - Intégration : pousser un package signé avec clé A, configurer la vérification avec clé B → l'install doit être refusée.
xmortelette added the Kind/Security
Reviewed
Confirmed
4
Priority
Medium
3
labels 2026-06-12 09:29:13 +02:00
Author

Une piste à explorer, pour renforcer la défense en profondeur, serait aussi de faire vérifier en plus au cluster kubernetes la signature des images des packages

Une piste à explorer, pour renforcer la défense en profondeur, serait aussi de faire vérifier en plus au cluster kubernetes la signature des images des packages
shuss removed the
Reviewed
Confirmed
4
label 2026-06-12 10:27:57 +02:00
Author

Deux compléments pour garder l'issue alignée avec les évolutions en cours :

  1. Vérification côté cluster (commentaire précédent) : pertinent en défense en profondeur (policy-controller/Kyverno verifyImages sur les images des Jobs d'agent), mais ça ne remplace pas la vérification par vynil lui-même : le scan lit les annotations des manifests sans jamais pull l'image, et c'est lui qui alimente le catalogue. Une signature vérifiée seulement à l'admission ne protège pas le catalogue d'entrées forgées.

  2. Index statiques (sources http/s3) : si la proposition d'index statique pré-calculé avance, la chaîne de confiance doit la couvrir aussi — les fichiers d'index doivent embarquer les digests des images (épinglage tag→digest au moment du file-scan, vérification du digest à l'unpack) et idéalement être signés eux-mêmes. Sinon l'index devient le maillon faible : il court-circuite la lecture des annotations OCI.

L'épinglage par digest (piste 2 de l'issue) est le socle commun aux deux points : résoudre une fois au scan, stocker dans box.status.packages, et installer par @sha256:….


Analyse et rédaction : Claude (assistant IA), publié via le compte de Xavier.

Deux compléments pour garder l'issue alignée avec les évolutions en cours : 1. **Vérification côté cluster (commentaire précédent)** : pertinent en défense en profondeur (policy-controller/Kyverno `verifyImages` sur les images des Jobs d'agent), mais ça ne remplace pas la vérification par vynil lui-même : le scan lit les annotations des manifests sans jamais pull l'image, et c'est lui qui alimente le catalogue. Une signature vérifiée seulement à l'admission ne protège pas le catalogue d'entrées forgées. 2. **Index statiques (sources http/s3)** : si la proposition d'index statique pré-calculé avance, la chaîne de confiance doit la couvrir aussi — les fichiers d'index doivent embarquer les digests des images (épinglage tag→digest au moment du `file-scan`, vérification du digest à l'unpack) et idéalement être signés eux-mêmes. Sinon l'index devient le maillon faible : il court-circuite la lecture des annotations OCI. L'épinglage par digest (piste 2 de l'issue) est le socle commun aux deux points : résoudre une fois au scan, stocker dans `box.status.packages`, et installer par `@sha256:…`. --- *Analyse et rédaction : Claude (assistant IA), publié via le compte de Xavier.*
Owner

Je vois bien que la réflexion sur le sujet est encore en maturation. Je trouves l'idée de feature intéressante, mais elle ne fonctionne que temps qu'on utilise pas le montage OCI pour les images des packages Vynil. Hors c'est dans la roadmap. en fait, c'est une feature prévu dès le début, mais qui attends que tous nos clusters soit a k8s 1.36+ pour le cabler. Tout est déjà prêt dans le code, il n'y a plus qu'a activer si le cluster est en version 1.36 ou supérieur et tous ce qu'il faut pour implémenter ce test est déjà présent.

Au final, il va falloir déléguer la vérification cosign au cluster

Je vois bien que la réflexion sur le sujet est encore en maturation. Je trouves l'idée de feature intéressante, mais elle ne fonctionne que temps qu'on utilise pas le montage OCI pour les images des packages Vynil. Hors c'est dans la roadmap. en fait, c'est une feature prévu dès le début, mais qui attends que tous nos clusters soit a k8s 1.36+ pour le cabler. Tout est déjà prêt dans le code, il n'y a plus qu'a activer si le cluster est en version 1.36 ou supérieur et tous ce qu'il faut pour implémenter ce test est déjà présent. Au final, il va falloir déléguer la vérification cosign au cluster
shuss added the
Reviewed
need-more-info
5
label 2026-06-12 17:36:36 +02:00
Sign in to join this conversation.