Skip to main content

10 paramètres securityContext Kubernetes à connaître

10 mars 2021

0 minutes de lecture

Exécuter des charges de travail en toute sécurité dans Kubernetes peut s’avérer difficile. De nombreux paramètres ont une incidence sur la sécurité de l’API Kubernetes, et leur configuration correcte exige des connaissances approfondies. Parmi les outils les plus puissants que Kubernetes met à votre disposition figurent les paramètres securityContext, que vous pouvez utiliser dans chaque manifeste de Pod et de conteneur. Dans cette fiche pratique, nous allons passer en revue les différents paramètres securityContext, expliquer leur signification et vous montrer comment les utiliser.

Aide-mémoire présentant 10 paramètres de contexte de sécurité Kubernetes, dont runAsNonRoot, seccompProfile, capabilities, procMount et sysctls.

Télécharger la fiche pratique.

  1. runAsNonRoot

  2. runAsUser / runAsGroup

  3. seLinuxOptions

  4. seccompProfile

  5. privileged / allowPrivilegeEscalation

  6. capabilities

  7. readonlyRootFilesystem

  8. procMount

  9. fsGroup / fsGroupChangePolicy

  10. sysctls

Paramètres Pod et conteneur

Les paramètres Kubernetes securityContext sont définis dans les API PodSpec et ContainerSpec. Leur portée est indiquée dans ce document par les annotations [P] et/ou [C] placées à côté de chacun d’eux. Notez que si un paramètre est disponible et configuré aux deux niveaux, celui du conteneur est prioritaire.

Sans ordre particulier, passons maintenant en revue les paramètres securityContext :

1. runAsNonRoot [P/C]

Même si un conteneur utilise des espaces de noms et des cgroups pour limiter ses processus, une seule erreur de configuration dans ses paramètres de déploiement peut suffire à leur donner accès aux ressources de l’hôte. Si ce processus s’exécute en tant que root, il dispose des mêmes accès à ces ressources que le compte root de l’hôte. De plus, si d’autres paramètres de Pod ou de conteneur sont utilisés pour réduire les contraintes (par exemple procMount ou capabilities), un UID root aggrave les risques liés à leur exploitation. À moins d’avoir une très bonne raison, vous ne devriez jamais exécuter un conteneur en tant que root.

Alors, que faire si vous devez déployer une image qui utilise le compte root ?

Option 1 : utiliser l’utilisateur fourni dans l’image de base

Les images de base incluent souvent un utilisateur déjà créé, mais laissent aux équipes de développement ou de déploiement le soin de l’utiliser. Par exemple, l’image officielle Node.js comprend un utilisateur nommé node, avec l’UID 1000, que vous pouvez utiliser pour exécuter le processus. Cependant, le Dockerfile ne définit pas explicitement cet utilisateur comme utilisateur courant. Vous devrez soit le configurer à l’exécution avec le paramètre runAsUser, soit remplacer l’utilisateur courant dans l’image à l’aide d’un Dockerfile dérivé. La première option suppose que l’UID 1000 peut lire les fichiers du répertoire de l’application. Voyons plutôt un exemple où nous créons notre propre image à l’aide d’un Dockerfile dérivé.

Sans entrer dans les détails de la création d’images, supposons que nous disposions d’une application npm précompilée. Voici un Dockerfile minimal pour créer une image basée sur [**node:slim**](https://hub.docker.com/_/node) et l’exécuter avec l’utilisateur node fourni.

FROM node:slim
COPY --chown=node . /home/node/app/   # <--- Copy app into the home directory with right ownership
USER 1000                             # <--- Switch active user to “node” (by UID)
WORKDIR /home/node/app                # <--- Switch current directory to app
ENTRYPOINT ["npm", "start"]           # <--- This will now exec as the “node” user instead of root

La ligne clé commence par USER : elle définit node comme utilisateur par défaut dans tout conteneur démarré à partir de cette image. Nous utilisons l’UID plutôt que le nom de l’utilisateur, car Kubernetes ne peut pas associer le nom d’utilisateur par défaut de l’image à son UID avant de démarrer le conteneur. Si vous déployez avec runAsNotRoot: true, Kubernetes renverra une erreur à ce sujet.

Option 2 : l’image de base ne fournit aucun utilisateur

Que faire si l’image de base node ne fournit aucun utilisateur ? Pour de nombreux processus, il suffit d’en créer un dans un Dockerfile dérivé et de l’utiliser. Reprenons l’exemple précédent et adaptons-le :

FROM node:slim
RUN useradd somebody -u 10001 --create-home --user-group  # <--- Create a user
COPY --chown=somebody . /home/somebody/app/
USER 10001
WORKDIR /home/somebody/app
ENTRYPOINT ["npm", "start"]

Comme vous pouvez le constater, le seul ajout est la ligne RUN, qui crée un utilisateur. La syntaxe peut varier selon la distribution de l’image de base. J’ai ensuite modifié les références à l’utilisateur et aux chemins en conséquence.

REMARQUE : cette méthode fonctionne parfaitement avec node.js et npm, mais d’autres outils peuvent nécessiter de modifier les propriétaires d’autres éléments du système de fichiers. Si vous rencontrez des problèmes, consultez la documentation de votre outil.

2. runAsUser / runAsGroup [P/C]

Les images de conteneur peuvent définir un utilisateur et/ou un groupe spécifique pour l’exécution du processus. Vous pouvez remplacer ces paramètres à l’aide des options de configuration runAsUser et runAsGroup. Elles sont souvent configurées avec des volumes contenant des fichiers dont les identifiants de propriétaire correspondent.

...
spec:
  containers:
  - name: web
    image: mycorp/webapp:1.2.3
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
...

L’utilisation de ces paramètres comporte un risque : vous prenez des décisions à l’exécution pour le conteneur qui peuvent être incompatibles avec l’image d’origine. Par exemple, l’image officielle du serveur CI jenkins/jenkins s’exécute sous le groupe et l’utilisateur jenkins:jenkins, qui possède tous les fichiers de l’application. Si vous configurez un autre utilisateur, le conteneur ne démarrera pas, car cet utilisateur n’existe pas dans le fichier /etc/passwd de l’image. Et même s’il y figurait, il aurait probablement des difficultés à lire et à écrire les fichiers appartenant à jenkins:jenkins. Une simple commande docker run vous permettra de vérifier cela :

$ docker run --rm -it -u eric:eric jenkins/jenkins
docker: Error response from daemon: unable to find user eric: no matching entries in passwd file.

Comme nous l’avons indiqué plus haut, il est fortement recommandé de veiller à ce que les processus du conteneur ne s’exécutent pas en tant qu’utilisateur root. Mais ne comptez pas uniquement sur les paramètres runAsUser ou runAsGroup pour le garantir : quelqu’un pourrait les supprimer ultérieurement. Veillez également à définir runAsNonRoot sur true.

3. seLinuxOptions [P/C]

SELinux est un système fondé sur des politiques, qui contrôle l’accès aux applications, aux processus et aux fichiers sur un système Linux. Il implémente le framework Linux Security Modules dans le noyau Linux. SELinux repose sur le principe des étiquettes. Il attribue ces étiquettes à tous les éléments du système afin de les regrouper. Ces étiquettes sont appelées contextes de sécurité — à ne pas confondre avec le securityContext de Kubernetes — et comprennent un user, un role, un type et un champ facultatif level, au format user:role:type:level.

SELinux utilise ensuite des politiques pour définir quels processus d’un contexte donné peuvent accéder aux autres objets étiquetés du système. SELinux peut être appliqué strictement, auquel cas l’accès est refusé, ou configuré en mode permissif, auquel cas les accès sont consignés. Dans les conteneurs, SELinux étiquette généralement le processus et l’image du conteneur de façon à limiter l’accès du processus aux seuls fichiers de l’image.

Le runtime de conteneur applique les étiquettes SELinux par défaut lors de la création d’un conteneur. Le paramètre seLinuxOptions dans securityContext permet d’appliquer des étiquettes SELinux personnalisées. Attention : modifier les étiquettes SELinux d’un conteneur peut permettre au processus qu’il héberge de sortir de l’image du conteneur et d’accéder au système de fichiers de l’hôte.

Notez que cette fonctionnalité ne s’applique que si le système d’exploitation de l’hôte prend en charge SELinux.

4. seccompProfile [P/C]

Seccomp signifie secure computing mode. Il s’agit d’une fonctionnalité du noyau Linux qui permet de limiter les appels qu’un processus donné peut effectuer depuis l’espace utilisateur vers le noyau. Un profil seccomp est une définition JSON qui comprend généralement un ensemble d’appels système et l’action par défaut à exécuter si l’un de ces appels se produit.

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "name": "accept",
            "action": "SCMP_ACT_ALLOW",
            "args": []
        },
        {
            "name": "accept4",
            "action": "SCMP_ACT_ALLOW",
            "args": []
        },
        ...
    ]
}

Kubernetes permet d’utiliser des profils personnalisés grâce au paramètre seccompProfile de securityContext.

seccompProfile:
      type: Localhost
      localhostProfile: profiles/myprofile.json

Le champ type peut prendre trois valeurs :

  • Localhost : le paramètre localhostProfile indique un chemin d’accès à un profil seccomp dans le conteneur.

  • Unconfined : aucun profil n’est appliqué.

  • RuntimeDefault : le profil par défaut du runtime de conteneur est utilisé. C’est la valeur par défaut si le type n’est pas spécifié.

Vous pouvez appliquer ces paramètres dans un PodSecurityContext ou un securityContext. Si les deux sont définis, les paramètres de niveau conteneur dans securityContext sont utilisés. Notez que l’API de configuration securityContext a été publiée dans Kubernetes v1.19. Si vous déployez sur une version antérieure, la syntaxe est différente. Consultez le site de documentation Kubernetes pour plus de détails et des exemples.

Comme pour la plupart des paramètres de sécurité, le principe du moindre privilège s’applique ici. N’accordez à votre conteneur que les privilèges dont il a besoin, et pas davantage. Commencez par créer un profil qui consigne simplement les appels système effectués, puis testez votre application pour établir la liste des appels système autorisés. Pour en savoir plus sur cette démarche, consultez les tutoriels Kubernetes.

5. Éviter les conteneurs privilégiés et l’escalade de privilèges [C]

Accorder des privilèges à un conteneur est dangereux. Cette pratique est souvent utilisée pour obtenir plus facilement des autorisations spécifiques, alors qu’il est possible de les gérer autrement en accordant l’accès aux capabilities nécessaires. Le runtime de conteneur contrôle la mise en œuvre exacte de l’indicateur privileged, mais celui-ci accorde en pratique tous les privilèges au conteneur et lève les restrictions imposées par le contrôleur de cgroup des périphériques. Il peut également modifier la configuration du Linux Security Module et permettre aux processus du conteneur de s’en échapper.

Les conteneurs isolent les processus sur l’hôte. Ainsi, même lorsqu’un conteneur s’exécute en tant que root, le runtime ne lui accorde pas certaines capabilities. Lorsque l’indicateur privileged est activé, le runtime accorde au conteneur toutes les capabilities du compte root du système. Cette pratique est extrêmement dangereuse du point de vue de la sécurité, car elle donne un accès complet au système hôte sous-jacent.

Évitez d’utiliser l’indicateur privileged. Si votre conteneur a besoin de capabilities supplémentaires, ajoutez uniquement celles dont il a besoin via les paramètres capabilities. Un conteneur n’a besoin de l’indicateur privileged que s’il doit contrôler des paramètres du noyau de l’hôte — par exemple, accéder à du matériel spécifique ou reconfigurer des réseaux — et accéder au système de fichiers de l’hôte.

Pour en savoir plus sur les conteneurs privilégiés, consultez l’article de blog de Matt : Conteneurs Docker privilégiés : en avez-vous vraiment besoin ?

6. Capabilities du noyau Linux [C]

Les capabilities sont des autorisations au niveau du noyau qui permettent de contrôler plus finement les autorisations d’appel au noyau que l’exécution de tous les processus en tant que root. Elles incluent notamment la possibilité de modifier les permissions des fichiers, de contrôler le sous-système réseau et d’effectuer des tâches d’administration à l’échelle du système. Dans securityContext, Kubernetes permet de supprimer ou d’ajouter des capabilities. Vous pouvez indiquer une capability individuelle ou une liste séparée par des virgules sous forme de tableau de chaînes. Vous pouvez aussi utiliser le raccourci -all pour ajouter ou supprimer toutes les capabilities. Cette configuration est transmise au runtime de conteneur, qui définit l’ensemble des capabilities lors de la création du conteneur. Si la section capabilities est absente de securityContext, le conteneur reçoit l’ensemble de capabilities par défaut fourni par le runtime.

securityContext:
      capabilities:
        drop:
          - ALL
        add: ["MKNOD"]

La pratique recommandée consiste à supprimer toutes les capabilities, puis à rétablir uniquement celles dont votre application a réellement besoin. Dans bien des cas, les applications n’ont besoin d’aucune capability en fonctionnement normal. Vérifiez-le en les supprimant toutes, puis analysez les journaux d’audit pour repérer les capabilities bloquées et diagnostiquer les problèmes éventuels.

Notez que, lorsque vous indiquez les capabilities à supprimer ou à ajouter dans securityContext, vous devez omettre le préfixe CAP_ utilisé par le noyau dans leur nom. Pour le débogage, l’outil capsh affiche dans un format lisible les capabilities activées dans votre conteneur. Il est disponible pour la plupart des distributions. Ne le laissez pas à disposition dans les conteneurs de production : un attaquant pourrait facilement déterminer quelles capabilities sont activées ! Si vous préférez lire les bitmaps, vous pouvez également consulter les capabilities activées dans le fichier /proc/1/status.

Découvrez comment renforcer la sécurité Kubernetes en supprimant les capabilities par défaut d’un conteneur.

7. Utiliser un système de fichiers en lecture seule [C]

Si votre conteneur est compromis et qu’il dispose d’un système de fichiers en lecture-écriture, un attaquant peut modifier sa configuration, installer des logiciels et potentiellement lancer d’autres exploits. Un système de fichiers en lecture seule contribue à prévenir ce type d’escalade en limitant les actions qu’un attaquant peut effectuer. En règle générale, les conteneurs ne devraient pas avoir besoin d’écrire dans leur système de fichiers. Si votre application comporte des données persistantes, vous devriez utiliser une méthode de persistance externe, comme une base de données, un volume ou un autre service. Veillez également à ce que tous les journaux soient écrits dans stdout et/ou transmis à un agrégateur de journaux centralisé.

8. procMount [C]

Par défaut, les environnements d’exécution de conteneurs masquent certaines parties du système de fichiers /proc à l’intérieur d’un conteneur afin d’éviter d’éventuels problèmes de sécurité. Toutefois, l’accès à ces parties de /proc est parfois nécessaire, notamment lors de l’utilisation de conteneurs imbriqués, comme c’est souvent le cas dans le cadre d’un processus de build au sein d’un cluster. Deux valeurs seulement sont valides pour cette entrée : Default, qui conserve le comportement standard de l’environnement d’exécution des conteneurs, ou Unmasked, qui supprime tous les masquages du système de fichiers **/proc**.

Bien entendu, vous ne devriez utiliser cette option que si vous savez vraiment ce que vous faites. Si vous l’utilisez pour créer des images, vérifiez la dernière version de votre outil de build : beaucoup n’en ont plus besoin. Mettez-le à niveau et rétablissez la valeur par défaut de procMount adaptée à l’outil que vous utilisez.

Enfin, si vous constatez que vous devez utiliser cette option, faites-le uniquement pour un conteneur imbriqué ; n’exposez jamais le système de fichiers /proc de votre système hôte à un conteneur.

9. fsGroup / fsGroupChangePolicy [P]

Le paramètre fsGroup définit un groupe auquel Kubernetes attribue les permissions de tous les fichiers des volumes lorsque ceux-ci sont montés par un pod. Le comportement est également contrôlé par fsGroupChangePolicy, qui peut être défini sur onRootMismatch ou Always. Avec la valeur onRootMismatch, les permissions ne sont modifiées que si elles ne correspondent pas déjà à celles de la racine du conteneur.

Soyez prudent lorsque vous utilisez fsGroup. La modification du groupe propriétaire de l’ensemble d’un volume peut ralentir le démarrage des pods si le système de fichiers est lent et/ou volumineux. Elle peut également nuire à d’autres processus qui partagent le même volume si ceux-ci n’ont pas les permissions d’accès au nouveau GID. C’est pourquoi certains fournisseurs de systèmes de fichiers partagés, comme NFS, ne prennent pas en charge cette fonctionnalité. Ces paramètres n’ont pas non plus d’effet sur les volumes éphémères.

10. sysctls [P]

Les sysctls sont une fonctionnalité du noyau Linux qui permet aux administrateurs de modifier sa configuration. Dans un système d’exploitation Linux complet, ces paramètres sont définis dans /etc/sysctl.conf et peuvent également être modifiés à l’aide de l’utilitaire **sysctl**.

Le paramètre sysctls de securityContext permet de modifier certains sysctls dans le conteneur. Seul un petit sous-ensemble de sysctls du système d’exploitation peut être modifié par conteneur ; ces paramètres sont isolés dans des espaces de noms du noyau. Certains éléments de ce sous-ensemble sont considérés comme sûrs. Un ensemble bien plus vaste est considéré comme dangereux, en fonction du risque d’impact sur les autres pods. Les sysctls dangereux sont généralement désactivés dans les clusters et doivent être explicitement activés par l’administrateur du cluster.

Compte tenu du risque de déstabilisation du système d’exploitation sous-jacent, évitez de modifier les paramètres du noyau via sysctls, sauf si vous avez des exigences très précises. Vous devriez également examiner ces modifications avec l’opérateur de votre cluster.

Remarque sur securityContext à l’exécution

Dans de nombreux cas, les paramètres de sécurité décrits ici sont associés à un contrôle d’admission fondé sur des règles afin de garantir que les paramètres requis sont bien configurés avant le lancement des conteneurs dans le cluster. En combinant les paramètres de securityContext avec une PodSecurityPolicy, vous pouvez veiller à ce que seuls les conteneurs conformes à la règle soient lancés, en imposant des paramètres securityContext spécifiques. Les paramètres securityContext peuvent également être ajoutés à la configuration des conteneurs au lancement grâce au contrôle d’admission dynamique et à l’utilisation de webhooks de mutation.

Conclusion

Il faut tenir compte de nombreux éléments pour renforcer la sécurité de vos déploiements d’applications à l’aide des paramètres securityContext. Utilisés correctement, ils constituent un outil très efficace. Nous espérons que cette liste aidera vos équipes à choisir les bonnes options pour leurs workloads et leurs environnements. Snyk peut vous aider à faire ces choix en analysant vos fichiers YAML Kubernetes pour détecter les erreurs de configuration courantes. Créez gratuitement votre compte à l’aide du bouton ci-dessous.

Sécurisez votre infrastructure dès la source

Snyk automatise la sécurité et la conformité de l’IaC dans vos workflows, et détecte les ressources dont la configuration a dérivé ou qui sont manquantes.