[Sécurité] L'agent s'exécute en cluster-admin et exécute du code arbitraire issu des packages OCI #13

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

Constat

Le ServiceAccount vynil-agent est lié à cluster-admin et à un ClusterRole */*/* maison.

Dans box/vynil/systems/rbac.yaml.hbs :

kind: ClusterRole
metadata:
  name: {{instance.namespace}}-{{instance.appslug}}-admin
rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["*"]
---
kind: ClusterRoleBinding              # vynil-agent → cluster-admin
metadata:
  name: {{instance.namespace}}-vynil-agent-cluster-admin
subjects:
- kind: ServiceAccount
  name: vynil-agent
roleRef:
  kind: ClusterRole
  name: cluster-admin
---
kind: ClusterRoleBinding              # vynil-agent → -admin (*/*/*)
metadata:
  name: {{instance.namespace}}-vynil-agent-admin
subjects:
- kind: ServiceAccount
  name: vynil-agent
roleRef:
  name: {{instance.namespace}}-{{instance.appslug}}-admin

Le bootstrap fait de même (deploy/bootstrap/bootstrap.yaml) : vynil-bootstrap → ClusterRole */*/*.

Pourquoi c'est un risque

L'agent exécute du code arbitraire fourni par les packages : scripts Rhai unpackés depuis l'image OCI, plus, dans le moteur Rhai core, des primitives shell et système exposées à tous les packages (common/src/shellhandler.rs, common/src/rhaihandler.rs) :

.register_fn("shell_run", rhai_run)        // sh -c <string> arbitraire
.register_fn("shell_output", rhai_get_stdout)
.register_fn("get_env", ...)               // lecture des variables d'env de l'agent
.register_fn("file_read"/"file_write"/"file_copy"/"create_dir", ...)

Conséquence : installer un package = exécution de code arbitraire avec les droits cluster-admin. Un package malveillant ou compromis (ou une jukebox/registre compromis) obtient le contrôle total du cluster : lecture de tous les secrets, création de pods privilégiés, exfiltration, persistance. Il n'y a aucune isolation entre un package « tenant » (censé être cantonné à son namespace) et le reste du cluster au niveau RBAC : c'est le code Rhai qui est censé se restreindre, mais rien ne l'y contraint techniquement.

C'est cohérent avec le modèle « distribution intégrée » de Vynil, mais le niveau de privilège mérite d'être explicité et, si possible, réduit.

Pistes

  1. Principe de moindre privilège pour l'agent tenant : un job d'install/delete de package tenant ne devrait pas tourner en cluster-admin. Dériver un Role/ClusterRole calculé à partir des ressources réellement déclarées par le package (apiGroups/resources/namespaces effectivement manipulés), ou au minimum borner au(x) namespace(s) du tenant.
  2. Séparer les SA par type de package (system vs service vs tenant) avec des bindings distincts, plutôt qu'un seul vynil-agent cluster-admin partagé.
  3. Restreindre les primitives Rhai dangereuses (shell_run, shell_output, get_env, accès fichiers) aux seuls packages system/core de confiance ; les retirer du moteur exposé aux packages tenant. Le linter mentionne déjà des notions de « api mode » / « no tenant access in system packages » — étendre cette logique au runtime, pas seulement au lint.
  4. Documenter explicitement le modèle de menace : « tout package installé est de confiance équivalente à cluster-admin », pour que les opérateurs sachent qu'ils ne doivent installer que des packages de jukeboxes de confiance.

Note liée

L'efficacité de la signature Cosign (#issue dédiée) dépend directement de ce point : tant que n'importe quel package tourne en cluster-admin, la vérification de signature à l'installation est la seule barrière de défense en profondeur.

## Constat Le ServiceAccount `vynil-agent` est lié à `cluster-admin` **et** à un ClusterRole `*/*/*` maison. Dans [`box/vynil/systems/rbac.yaml.hbs`](box/vynil/systems/rbac.yaml.hbs) : ```yaml kind: ClusterRole metadata: name: {{instance.namespace}}-{{instance.appslug}}-admin rules: - apiGroups: ["*"] resources: ["*"] verbs: ["*"] --- kind: ClusterRoleBinding # vynil-agent → cluster-admin metadata: name: {{instance.namespace}}-vynil-agent-cluster-admin subjects: - kind: ServiceAccount name: vynil-agent roleRef: kind: ClusterRole name: cluster-admin --- kind: ClusterRoleBinding # vynil-agent → -admin (*/*/*) metadata: name: {{instance.namespace}}-vynil-agent-admin subjects: - kind: ServiceAccount name: vynil-agent roleRef: name: {{instance.namespace}}-{{instance.appslug}}-admin ``` Le bootstrap fait de même ([`deploy/bootstrap/bootstrap.yaml`](deploy/bootstrap/bootstrap.yaml)) : `vynil-bootstrap` → ClusterRole `*/*/*`. ## Pourquoi c'est un risque L'agent **exécute du code arbitraire fourni par les packages** : scripts Rhai unpackés depuis l'image OCI, plus, dans le moteur Rhai core, des primitives shell et système exposées à *tous* les packages ([`common/src/shellhandler.rs`](common/src/shellhandler.rs), [`common/src/rhaihandler.rs`](common/src/rhaihandler.rs)) : ```rust .register_fn("shell_run", rhai_run) // sh -c <string> arbitraire .register_fn("shell_output", rhai_get_stdout) .register_fn("get_env", ...) // lecture des variables d'env de l'agent .register_fn("file_read"/"file_write"/"file_copy"/"create_dir", ...) ``` Conséquence : **installer un package = exécution de code arbitraire avec les droits cluster-admin**. Un package malveillant ou compromis (ou une jukebox/registre compromis) obtient le contrôle total du cluster : lecture de tous les secrets, création de pods privilégiés, exfiltration, persistance. Il n'y a aucune isolation entre un package « tenant » (censé être cantonné à son namespace) et le reste du cluster au niveau RBAC : c'est le code Rhai qui est censé se restreindre, mais rien ne l'y contraint techniquement. C'est cohérent avec le modèle « distribution intégrée » de Vynil, mais le niveau de privilège mérite d'être explicité et, si possible, réduit. ## Pistes 1. **Principe de moindre privilège pour l'agent tenant** : un job d'install/delete de package *tenant* ne devrait pas tourner en cluster-admin. Dériver un Role/ClusterRole calculé à partir des ressources réellement déclarées par le package (apiGroups/resources/namespaces effectivement manipulés), ou au minimum borner au(x) namespace(s) du tenant. 2. **Séparer les SA par type de package** (system vs service vs tenant) avec des bindings distincts, plutôt qu'un seul `vynil-agent` cluster-admin partagé. 3. **Restreindre les primitives Rhai dangereuses** (`shell_run`, `shell_output`, `get_env`, accès fichiers) aux seuls packages `system`/`core` de confiance ; les retirer du moteur exposé aux packages `tenant`. Le linter mentionne déjà des notions de « api mode » / « no tenant access in system packages » — étendre cette logique au runtime, pas seulement au lint. 4. **Documenter explicitement** le modèle de menace : « tout package installé est de confiance équivalente à cluster-admin », pour que les opérateurs sachent qu'ils ne doivent installer que des packages de jukeboxes de confiance. ## Note liée L'efficacité de la signature Cosign (#issue dédiée) dépend directement de ce point : tant que n'importe quel package tourne en cluster-admin, la vérification de signature à l'installation est la seule barrière de défense en profondeur.
xmortelette added the Kind/Security
Reviewed
Confirmed
4
Priority
High
2
labels 2026-06-12 09:28:54 +02:00
shuss removed the
Reviewed
Confirmed
4
label 2026-06-12 10:27:18 +02:00
Owner

Techniquement, c'est une question de trust. Mais oui ce problème a été vu day 1 dans le design.

Le niveau de trust doit être porté par le rbac k8s :

  • Oui un package dans un jukebox est un risque de sécurité potentiel, de fait, les cluster-admin doivent être les seuls à pouvoir créer/configurer les jukebox. Les jukebox doivent pointer vers des distributions de confiance. C'est déjà le cas dans l'installation par défaut. Il faut peut-être le documenter clairement.
  • Les packages de types system et service ont, par design, besoin de ces droits de type cluster-admin. De la même manière, la stratégie doit rester le niveau de trust des administrateur du cluster. De fait, seuls eux doivent pouvoir installer ces 2 type de packages. C'est aussi le cas pour l'installation par défaut. Idem, peut-être qu'il faut documenter ca.

Au fond, vynil reproduit la structure debian : seul root peut déterminer quels sont les sources d'apt. Seul root peut installer des packages. Ok avec vynil, on permets aux utilisateurs d'installer leur propre applications, mais ces applications sont limité dans leur scope : seulement les packages de type tenant.

Pour le code, en rhai, il existe 2 modes d'installation, un mode spécifique pour le mode tenant et un mode pour les packages system/service qui permet tout (il faut pouvoir installer capsule, fluxcd ou argocd (et bien d'autres) qui eux aussi ont un clusterrolebinding cluster-admin, pour pouvoir créer ce clusterrolebinding il faut l'être aussi). Mais déjà, nous avons plusieurs package de type tenant, qui, pour de bonnes raisons, contournent le système automatique et utilisent des hooks pour installer 1-2 objets qui ne devrait pas être dans leur périmètre de droit. Ces packages par eux-même démontrent que ce n'est pas si simple.

Autre information, pour vynil, la seul définition d'un tenant est un ensemble de namespace qui partage un couple clé/valeur dans leur labels. Rien de plus rien de moins. (Ce comportement est customisable et est customisé dans les 2 distribution vynil existantes. les 2 customisations partent dans 2 directions différentes. On ne peux pas réduire le champs de la définition actuelle d'un tenant.

Enfin, mon idée initiale était de permettre la configuration d'un template pour définir quel est le service-account a utiliser. Et laisser à la distribution le soin de gérer les droits de ce ServiceAccount (via un package système qui matérialise la création du tenant, et donc gère ce service account). C'est encore le plan. Reste a le mettre en oeuvre, mais toute les briques sont là pour ca.

Pour autant, un quick fix intéressant serait d'enlever les méthodes suivante pour une installation/désinstallation :

.register_fn("shell_run", rhai_run)        // sh -c <string> arbitraire
.register_fn("shell_output", rhai_get_stdout)
.register_fn("get_env", ...)               // lecture des variables d'env de l'agent

Ces méthodes ne font du sens que pour aider le packaging ou pour les backup/restauration

Techniquement, c'est une question de trust. Mais oui ce problème a été vu day 1 dans le design. Le niveau de trust doit être porté par le rbac k8s : - Oui un package dans un jukebox est un risque de sécurité potentiel, de fait, les cluster-admin doivent être les seuls à pouvoir créer/configurer les jukebox. Les jukebox doivent pointer vers des distributions de confiance. C'est déjà le cas dans l'installation par défaut. Il faut peut-être le documenter clairement. - Les packages de types system et service ont, par design, besoin de ces droits de type cluster-admin. De la même manière, la stratégie doit rester le niveau de trust des administrateur du cluster. De fait, seuls eux doivent pouvoir installer ces 2 type de packages. C'est aussi le cas pour l'installation par défaut. Idem, peut-être qu'il faut documenter ca. Au fond, vynil reproduit la structure debian : seul root peut déterminer quels sont les sources d'apt. Seul root peut installer des packages. Ok avec vynil, on permets aux utilisateurs d'installer leur propre applications, mais ces applications sont limité dans leur scope : seulement les packages de type tenant. Pour le code, en rhai, il existe 2 modes d'installation, un mode spécifique pour le mode tenant et un mode pour les packages system/service qui permet tout (il faut pouvoir installer capsule, fluxcd ou argocd (et bien d'autres) qui eux aussi ont un clusterrolebinding cluster-admin, pour pouvoir créer ce clusterrolebinding il faut l'être aussi). Mais déjà, nous avons plusieurs package de type tenant, qui, pour de bonnes raisons, contournent le système automatique et utilisent des hooks pour installer 1-2 objets qui ne devrait pas être dans leur périmètre de droit. Ces packages par eux-même démontrent que ce n'est pas si simple. Autre information, pour vynil, la seul définition d'un tenant est un ensemble de namespace qui partage un couple clé/valeur dans leur labels. Rien de plus rien de moins. (Ce comportement est customisable et est customisé dans les 2 distribution vynil existantes. les 2 customisations partent dans 2 directions différentes. On ne peux pas réduire le champs de la définition actuelle d'un tenant. Enfin, mon idée initiale était de permettre la configuration d'un template pour définir quel est le service-account a utiliser. Et laisser à la distribution le soin de gérer les droits de ce ServiceAccount (via un package système qui matérialise la création du tenant, et donc gère ce service account). C'est encore le plan. Reste a le mettre en oeuvre, mais toute les briques sont là pour ca. Pour autant, un quick fix intéressant serait d'enlever les méthodes suivante pour une installation/désinstallation : ``` .register_fn("shell_run", rhai_run) // sh -c <string> arbitraire .register_fn("shell_output", rhai_get_stdout) .register_fn("get_env", ...) // lecture des variables d'env de l'agent ``` Ces méthodes ne font du sens que pour aider le packaging ou pour les backup/restauration
shuss added the
Reviewed
analysed
1
label 2026-06-12 11:07:53 +02:00
Author

Alignement après ton retour et l'analyse de packages en production :

  • La piste 1 de l'issue (RBAC calculé depuis le contenu du paquet) est retirée. L'observation de paquets tenant matures le confirme : plusieurs gèrent légitimement, via hooks, des objets hors de leur périmètre théorique (patch de CR d'opérateurs tiers, suppression de PVC créés indirectement). Un RBAC dérivé du contenu déclaré serait soit trop strict (casse ces paquets), soit troué au point d'être inutile. La direction « template de ServiceAccount par tenant, droits gérés par le paquet système qui matérialise le tenant » est la bonne — elle laisse chaque distribution définir sa propre frontière, ce qui colle au fait que la définition d'un tenant est volontairement minimale (un couple clé/valeur de labels) et déjà customisée différemment selon les distributions.

  • Quick fix retenu : retirer shell_run / shell_output / get_env du moteur Rhai pour les opérations install / delete / reconfigure, en les conservant pour le packaging (package build/update) et pour backup / restore (restic est piloté par shell). Concrètement : un mode d'enregistrement des primitives par opération dans le handler Rhai, plutôt qu'un moteur unique. Le lint connaît déjà la notion d'« api mode » — le runtime devient cohérent avec ce que le lint promet.

  • Documentation : le modèle de menace (« installer un paquet = confiance équivalente cluster-admin », jukebox réservées aux cluster-admins, types system/service réservés aux admins) est désormais explicité dans la doc (docs/operations/security.md), comme suggéré.


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

Alignement après ton retour et l'analyse de packages en production : - **La piste 1 de l'issue (RBAC calculé depuis le contenu du paquet) est retirée.** L'observation de paquets `tenant` matures le confirme : plusieurs gèrent légitimement, via hooks, des objets hors de leur périmètre théorique (patch de CR d'opérateurs tiers, suppression de PVC créés indirectement). Un RBAC dérivé du contenu déclaré serait soit trop strict (casse ces paquets), soit troué au point d'être inutile. La direction « template de ServiceAccount par tenant, droits gérés par le paquet système qui matérialise le tenant » est la bonne — elle laisse chaque distribution définir sa propre frontière, ce qui colle au fait que la définition d'un tenant est volontairement minimale (un couple clé/valeur de labels) et déjà customisée différemment selon les distributions. - **Quick fix retenu** : retirer `shell_run` / `shell_output` / `get_env` du moteur Rhai pour les opérations *install / delete / reconfigure*, en les conservant pour le packaging (`package build/update`) et pour *backup / restore* (restic est piloté par shell). Concrètement : un mode d'enregistrement des primitives par opération dans le handler Rhai, plutôt qu'un moteur unique. Le lint connaît déjà la notion d'« api mode » — le runtime devient cohérent avec ce que le lint promet. - **Documentation** : le modèle de menace (« installer un paquet = confiance équivalente cluster-admin », jukebox réservées aux cluster-admins, types system/service réservés aux admins) est désormais explicité dans la doc (`docs/operations/security.md`), comme suggéré. --- *Analyse et rédaction : Claude (assistant IA), publié via le compte de Xavier.*
shuss added the
Reviewed
Confirmed
4
label 2026-06-12 17:02:56 +02:00
Owner

Le mode d'installation pour les packages de type tenant qui utilise un SA dédié n'est en place que de manière théorique et que dans ma tête : ca n'a jamais été testé ni validé.
A mon sens, c'est la stratégie a suivre ici.

Il faut valider que le cablage est bien en place puis vérifier que ca marche.

Quand au Quick Fix, je suis pas sûr qu'il soit retenu, j'indiquais juste que c'est une piste. Si on regarde vraiment cette piste, c'est as un quick fix, c'est une solution pour rendre ca modulaire. Aujoud'hui au final on a 2 monolithes le monolithe de prod et le monolithe de test. Pour avancer dans cette piste ils faut 2 structure modulaire qui serait synchrones en fonction des états attendu en réél et en test. Non l'implémentation de cette piste n'est pas triviale

Le mode d'installation pour les packages de type tenant qui utilise un SA dédié n'est en place que de manière théorique et que dans ma tête : ca n'a jamais été testé ni validé. A mon sens, c'est la stratégie a suivre ici. Il faut valider que le cablage est bien en place puis vérifier que ca marche. Quand au Quick Fix, je suis pas sûr qu'il soit retenu, j'indiquais juste que c'est une piste. Si on regarde vraiment cette piste, c'est as un quick fix, c'est une solution pour rendre ca modulaire. Aujoud'hui au final on a 2 monolithes le monolithe de prod et le monolithe de test. Pour avancer dans cette piste ils faut 2 structure modulaire qui serait synchrones en fonction des états attendu en réél et en test. Non l'implémentation de cette piste n'est pas triviale
shuss removed the
Reviewed
Confirmed
4
label 2026-06-12 17:37:00 +02:00
Sign in to join this conversation.