Seccomp dans Kubernetes : 7 choses à savoir dès le départ

Note de traduction.: Nous vous présentons la traduction d'un article d'un ingénieur senior en sécurité des applications de la société britannique ASOS.com. Il commence ainsi une série de publications consacrées à l'amélioration de la sécurité dans Kubernetes grâce à l'utilisation de seccomp. Si l'introduction plaît aux lecteurs, nous suivrons l'auteur et poursuivrons avec ses futurs travaux sur ce sujet.

Seccomp dans Kubernetes : 7 choses à savoir dès le départ

Cet article est le premier d'une série de publications sur la manière de créer des profils seccomp dans l'esprit du SecDevOps, sans recourir à la magie et aux sorcelleries. Dans cette première partie, je parlerai des principes de base et des détails internes de la mise en œuvre de seccomp dans Kubernetes.

L'écosystème Kubernetes offre une variété suffisante de moyens pour assurer la sécurité et l'isolation des conteneurs. Cet article est consacré au Secure Computing Mode, également connu sous le nom de seccomp. Son essence réside dans le filtrage des appels système disponibles pour l'exécution par les conteneurs.

Pourquoi est-ce important ? Un conteneur n'est rien d'autre qu'un processus lancé sur une certaine machine. Et il utilise le noyau au même titre que d'autres applications. Si les conteneurs pouvaient exécuter n'importe quel appel système, des programmes malveillants en profiteraient rapidement pour contourner l'isolation du conteneur et affecter d'autres applications : intercepter des informations, modifier des paramètres système, etc.

Les profils seccomp définissent quels appels système doivent être autorisés ou interdits. L'environnement d'exécution du conteneur les active lors de son démarrage, afin que le noyau puisse surveiller leur exécution. L'application de tels profils permet de limiter le vecteur d'attaque et de réduire les dommages en cas de comportement indésirable d'un programme à l'intérieur du conteneur (c'est-à-dire vos dépendances, ou celles de vos dépendances).

Comprendre les bases

Le profil seccomp de base comprend trois éléments : defaultAction, architectures ou archMapDeep Speech syscalls:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(medium-basic-seccomp.json)

defaultAction détermine le sort par défaut de tout appel système non spécifié dans la section syscalls. Pour simplifier les choses, concentrons-nous sur deux valeurs principales qui seront utilisées :

  • SCMP_ACT_ERRNO — bloque l'exécution de l'appel système,
  • SCMP_ACT_ALLOW — autorise.

Dans la section architectures les architectures cibles sont énumérées. Cela est important, car le filtre appliqué au niveau du noyau dépend des identifiants des appels système et non de leurs noms tels qu'indiqués dans le profil. Avant l'application, l'environnement d'exécution du conteneur fera correspondre ces identifiants. L'idée est que les appels système peuvent avoir des ID complètement différents selon l'architecture du système. Par exemple, l'appel système recvfrom (utilisé pour obtenir des informations d'un socket) a l'ID = 64 dans les systèmes x64 et l'ID = 517 dans les systèmes x86. Ici vous pouvez trouver la liste de tous les appels système pour les architectures x86-x64.

Dans la section syscalls tous les appels système sont énumérés et il est indiqué ce qu'il convient d'en faire. Par exemple, vous pouvez créer une liste blanche en définissant defaultAction sur SCMP_ACT_ERRNO, et attribuer aux appels dans la section syscalls le statut SCMP_ACT_ALLOW. Cela signifie que vous autorisez uniquement les appels énumérés dans la section syscalls, et interdisez tous les autres. Pour une liste noire, il suffit d'inverser les valeurs defaultAction et les actions.

Il convient maintenant de dire quelques mots sur les nuances qui ne sont pas si évidentes. Notez que les recommandations ci-dessous partent du principe que vous déployez une gamme d'applications professionnelles sur Kubernetes et qu'il est important pour vous qu'elles fonctionnent avec les privilèges les plus bas.

1. AllowPrivilegeEscalation=false

Dans securityContext le conteneur a le paramètre AllowPrivilegeEscalation. S'il est défini sur faux, les conteneurs seront exécutés avec le bit (on) no_new_priv. Le sens de ce paramètre est évident d'après son nom : il empêche le conteneur de lancer de nouveaux processus avec des privilèges supérieurs à ceux dont il dispose déjà.

L'effet secondaire de ce paramètre, lorsqu'il est réglé sur true (valeur par défaut), est que l'exécution du conteneur applique le profil seccomp dès le début du processus de démarrage. Ainsi, tous les appels système nécessaires pour démarrer les processus internes de l'environnement d'exécution (par exemple, la mise en place des identifiants utilisateur/groupe, l'abandon de certaines capacités) doivent être autorisés dans le profil.

Un conteneur exécutant un simple echo hi, aura besoin des autorisations suivantes :

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "capget",
                "capset",
                "chdir",
                "close",
                "execve",
                "exit_group",
                "fstat",
                "fstatfs",
                "futex",
                "getdents64",
                "getppid",
                "lstat",
                "mprotect",
                "nanosleep",
                "newfstatat",
                "openat",
                "prctl",
                "read",
                "rt_sigaction",
                "statfs",
                "setgid",
                "setgroups",
                "setuid",
                "stat",
                "uname",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-pod-seccomp.json)

… au lieu de cela :

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "close",
                "execve",
                "exit_group",
                "futex",
                "mprotect",
                "nanosleep",
                "stat",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-container-seccomp.json)

Mais encore une fois, pourquoi est-ce un problème ? Personnellement, je m'abstendrais d'inclure les appels système suivants dans la liste blanche (s'il n'y a pas de réelle nécessité) : capset, set_tid_address, setgid, setgroups et setuid. Cependant, la véritable complexité réside dans le fait qu'en autorisant des processus que vous ne contrôlez absolument pas, vous liez les profils à l'implémentation de l'environnement d'exécution des conteneurs. Autrement dit, vous pourriez vous retrouver dans une situation où, après une mise à jour de l'environnement d'exécution des conteneurs (par vous ou, plus probablement, par le fournisseur de services cloud), les conteneurs cessent soudainement de fonctionner.

Conseil n°1: Exécutez des conteneurs avec AllowPrivilegeEscaltion=false. Cela réduira la taille des profils seccomp et les rendra moins sensibles aux changements dans l'environnement d'exécution des conteneurs.

2. Définir des profils seccomp au niveau du conteneur

Le profil seccomp peut être défini au niveau du pod :

annotations:
  seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"

… ou au niveau du conteneur :

annotations:
  container.security.alpha.kubernetes.io/<nom-du-conteneur>: "localhost/profile.json"

Notez que la syntaxe ci-dessus changera lorsque Kubernetes seccomp deviendra GA (cet événement est attendu dans la prochaine version de Kubernetes — 1.18 — note de traduction).

Peu de gens savent qu'il y a toujours eu dans Kubernetes bug, ce qui a conduit à ce que les profils seccomp s'appliquent au conteneur pauseL'environnement d'exécution compense partiellement ce manque, cependant ce conteneur ne disparaît pas des pods, car il est utilisé pour configurer leur infrastructure.

Le problème est que ce conteneur démarre toujours avec AllowPrivilegeEscalation=true, ce qui entraîne les problèmes évoqués au point 1, et il est impossible de modifier cela.

En appliquant des profils seccomp au niveau du conteneur, vous évitez ce piège et pouvez créer un profil qui sera « taillé » pour un conteneur spécifique. Cela devra être fait jusqu'à ce que les développeurs corrigent le bug et qu'une nouvelle version (peut-être la 1.18 ?) soit disponible pour tous.

Conseil n°2: Configurez des profils seccomp au niveau du conteneur.

En pratique, cette règle sert généralement de réponse universelle à la question : « Pourquoi mon profil seccomp fonctionne avec docker run, mais ne fonctionne pas après déploiement dans un cluster Kubernetes ? ».

3. Utilisez runtime/default uniquement en dernier recours

Kubernetes propose deux options de profils intégrés : runtime/default et docker/default. Les deux sont implémentés par l'environnement d'exécution des conteneurs, et non par Kubernetes. Par conséquent, ils peuvent différer en fonction de l'environnement d'exécution utilisé et de sa version.

En d'autres termes, en changeant de runtime, le conteneur peut accéder à un autre ensemble d'appels système, qu'il peut utiliser ou ne pas utiliser. La plupart des environnements d'exécution utilisent la mise en œuvre de Docker. Si vous souhaitez utiliser ce profil, assurez-vous qu'il convient à vos besoins.

Le profil docker/default est considéré comme obsolète depuis Kubernetes 1.11, donc évitez de l'utiliser.

À mon avis, le profil runtime/default convient parfaitement aux objectifs pour lesquels il a été créé : protéger les utilisateurs contre les risques liés à l'exécution de commandes docker run sur leurs machines. Cependant, en ce qui concerne les applications commerciales fonctionnant dans des clusters Kubernetes, je me permettrai d'affirmer qu'un tel profil est trop ouvert et que les développeurs devraient se concentrer sur la création de profils pour leurs applications (ou types d'applications).

Conseil n°3: Créez des profils seccomp pour des applications spécifiques. Si cela n'est pas possible, travaillez sur des profils pour des types d'applications, par exemple, créez un profil étendu qui englobe toutes les API web de l'application en Golang. Utilisez runtime/default uniquement en dernier recours.

Dans de futures publications, je parlerai de la création de profils seccomp dans l'esprit de SecDevOps, de leur automatisation et de leur test dans les pipelines. En d'autres termes, vous n'aurez plus d'excuses pour ne pas passer aux profils spécifiques aux applications.

4. Unconfined n'est PAS une option

De premier audit de sécurité de Kubernetes il est apparu que par défaut seccomp est désactivé. Cela signifie que si vous ne spécifiez pas PodSecurityPolicy, qui l'activera dans le cluster, tous les pods pour lesquels aucun profil seccomp n'est défini fonctionneront en mode seccomp=unconfined.

Travailler dans ce mode signifie qu'un niveau entier d'isolation, garantissant la protection du cluster, est perdu. Une telle approche n'est pas recommandée par les spécialistes en sécurité.

Conseil n°4: Aucun conteneur dans le cluster ne doit fonctionner en mode seccomp=unconfined, en particulier dans les environnements de production.

5. « Mode audit »

Ce point n'est pas unique à Kubernetes, mais il mérite d'être mentionné avant de commencer.

Traditionnellement, la création de profils seccomp a toujours été une tâche difficile et se basait en grande partie sur la méthode des essais et des erreurs. En effet, les utilisateurs n'ont tout simplement pas la possibilité de les tester dans des environnements de production sans risquer de « faire planter » l'application.

Avec l'apparition du noyau Linux 4.14, il est devenu possible d'exécuter des parties du profil en mode audit, enregistrant dans syslog des informations sur tous les appels système, sans les bloquer. Ce mode peut être activé à l'aide du paramètre SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp n'affectera pas le fonctionnement du线程 effectuant l'appel système, à moins qu'il ne tombe sous une règle de filtre, mais les informations sur l'appel système seront consignées.

Voici une stratégie typique pour utiliser cette possibilité :

  1. Autoriser les appels système nécessaires.
  2. Bloquer les appels système dont l'on sait qu'ils ne seront pas utiles.
  3. Consigner les informations sur tous les autres appels.

Un exemple simplifié est le suivant :

{
    "defaultAction": "SCMP_ACT_LOG",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": [
                "add_key",
                "keyctl",
                "ptrace"
            ],
            "action": "SCMP_ACT_ERRNO"
        }
    ]
}

(medium-mixed-seccomp.json)

Mais rappelez-vous qu'il est nécessaire de bloquer tous les appels que l'on sait ne pas être utilisés et qui peuvent potentiellement nuire au cluster. Une bonne base pour établir une liste est la documentation officielle de Docker. Elle explique en détail quels appels système sont bloqués par défaut et pourquoi.

Cependant, il y a une subtilité. Bien que SCMT_ACT_LOG soit supporté par le noyau Linux depuis fin 2017, il est entré dans l'écosystème Kubernetes relativement récemment. Ainsi, pour utiliser cette méthode, un noyau Linux 4.14 et une version de runC d'au moins v1.0.0-rc9.

Conseil n°5: Un profil de mode audit pour tester en production peut être créé en combinant des listes noires et blanches, toutes les exceptions étant enregistrées dans un journal.

6. Utilisez des listes blanches

La formation de listes blanches nécessite des efforts supplémentaires, car il faut identifier chaque appel qui peut être nécessaire à l'application, cependant cette approche augmente considérablement la sécurité :

Il est fortement recommandé d'utiliser une approche basée sur des listes blanches, car elle est plus simple et plus fiable. Une liste noire devra être mise à jour chaque fois qu'un nouvel appel système potentiellement dangereux (ou un drapeau/option dangereux figurant sur la liste noire) est ajouté. De plus, il est souvent possible de modifier la présentation du paramètre sans changer son essence, contournant ainsi les restrictions de la liste noire.

Pour les applications Go, j'ai conçu un outil spécifique qui accompagne l'application et collecte tous les appels effectués pendant l'exécution. Par exemple, pour l'application suivante :

package main

import "fmt"

func main() {
	fmt.Println("test")
}

… exécutons gosystract donc :

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf "%s", .Name}}{{- end}}' application-path

… et nous obtiendrons le résultat suivant :

"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",

Ceci n'est qu'un exemple - plus de détails sur les outils viendront plus tard.

Conseil n°6: N'autorisez que les appels dont vous avez vraiment besoin et bloquez tous les autres.

7. Posez de bonnes bases (ou préparez-vous à des comportements imprévus)

Le noyau veillera à respecter le profil, peu importe ce que vous y avez inscrit. Même si ce n'est pas exactement ce que vous souhaitez. Par exemple, si vous bloquez l'accès à des appels comme exit ou exit_group, le conteneur ne pourra pas s'arrêter correctement et même une simple commande comme echo hi suspendra sonfonctionnement indéfiniment. Vous obtiendrez ainsi une charge CPU élevée dans le cluster :

Seccomp dans Kubernetes : 7 choses à savoir dès le départ

Dans de tels cas, l'outil strace peut vous venir en aide :

Seccomp dans Kubernetes : 7 choses à savoir dès le départ
sudo strace -c -p 9331

Assurez-vous que les profils contiennent tous les appels systèmes nécessaires au bon fonctionnement de l'application.

Conseil n°7: Soyez attentif aux détails et vérifiez que tous les appels systèmes nécessaires sont inclus dans la liste blanche.

Ceci conclut la première partie du cycle d'articles sur l'utilisation de seccomp dans Kubernetes en mode SecDevOps. Dans les prochaines parties, nous parlerons de l'importance de cela et de la manière d'automatiser le processus.

P.S. de l'auteur

Lisez aussi dans notre blog :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster