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

Noter. trad.: Nous présentons à votre attention la traduction d'un article d'un ingénieur senior en sécurité des applications de la société britannique ASOS.com. Avec cela, il commence une série de publications consacrées à l'amélioration de la sécurité dans Kubernetes grâce à l'utilisation de seccomp. Si les lecteurs aiment l’introduction, nous suivrons l’auteur et poursuivrons ses futurs documents sur ce sujet.

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

Cet article est le premier d'une série d'articles sur la façon de créer des profils seccomp dans l'esprit de SecDevOps, sans recourir à la magie et à la sorcellerie. Dans la première partie, je couvrirai les bases et les détails internes de l'implémentation de seccomp dans Kubernetes.

L'écosystème Kubernetes offre une grande variété de façons de sécuriser et d'isoler les conteneurs. L'article concerne le mode informatique sécurisé, également connu sous le nom de seccomp. Son essence est de filtrer les appels système disponibles pour exécution par conteneurs.

Pourquoi c'est important? Un conteneur est simplement un processus exécuté sur une machine spécifique. Et il utilise le noyau comme les autres applications. Si les conteneurs pouvaient effectuer des appels système, les logiciels malveillants en profiteraient très vite pour contourner l'isolation des conteneurs et affecter d'autres applications : intercepter des informations, modifier les paramètres du système, etc.

Les profils seccomp définissent quels appels système doivent être autorisés ou désactivés. Le runtime du conteneur les active au démarrage afin que le noyau puisse surveiller leur exécution. L'utilisation de tels profils vous permet de limiter le vecteur d'attaque et de réduire les dégâts si un programme à l'intérieur du conteneur (c'est-à-dire vos dépendances ou leurs dépendances) commence à faire quelque chose qu'il n'est pas autorisé à faire.

Comprendre les bases

Le profil seccomp de base comprend trois éléments : defaultAction, architectures (ou archMap) Et 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 faciliter les choses, concentrons-nous sur les deux valeurs principales qui seront utilisées :

  • SCMP_ACT_ERRNO — bloque l'exécution d'un appel système,
  • SCMP_ACT_ALLOW - permet.

Dans la section architectures les architectures cibles sont répertoriées. Ceci est important car le filtre lui-même, appliqué au niveau du noyau, dépend des identifiants des appels système, et non de leurs noms spécifiés dans le profil. Le runtime du conteneur les fera correspondre aux identifiants avant utilisation. L'idée est que les appels système peuvent avoir des identifiants complètement différents selon l'architecture du système. Par exemple, l'appel système recvfrom (utilisé pour recevoir des informations du socket) a un ID = 64 sur les systèmes x64 et un ID = 517 sur x86. il est vous pouvez trouver une liste de tous les appels système pour les architectures x86-x64.

Dans la section syscalls répertorie tous les appels système et précise quoi en faire. Par exemple, vous pouvez créer une liste blanche en définissant defaultAction sur SCMP_ACT_ERRNO, et appelle dans la section syscalls attribuer SCMP_ACT_ALLOW. Ainsi, vous autorisez uniquement les appels spécifiés dans la section syscalls, et interdire tous les autres. Pour la liste noire, vous devez modifier les valeurs defaultAction et des actions à l'opposé.

Il convient maintenant de dire quelques mots sur des nuances qui ne sont pas si évidentes. Veuillez noter que les recommandations ci-dessous supposent que vous déployez une ligne d'applications métier sur Kubernetes et que vous souhaitez qu'elles s'exécutent avec le moins de privilèges possible.

1. AllowPrivilegeEscalation=false

В securityContext le conteneur a un paramètre AllowPrivilegeEscalation. S'il est installé dans false, les conteneurs commenceront par (on) peu no_new_priv. La signification de ce paramètre ressort clairement de son nom : il empêche le conteneur de lancer de nouveaux processus avec plus de privilèges qu'il n'en a lui-même.

Un effet secondaire de cette option étant définie sur true (par défaut) est que le runtime du conteneur applique le profil seccomp au tout début du processus de démarrage. Ainsi, tous les appels système requis pour exécuter les processus d'exécution internes (par exemple, définition des identifiants d'utilisateur/groupe, suppression de certaines fonctionnalités) doivent être activés dans le profil.

Vers un conteneur qui fait des choses triviales echo hi, les autorisations suivantes seront requises :

{
    "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 ceux-ci :

{
    "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"
        }
    ]
}

(salut-conteneur-seccomp.json)

Mais encore une fois, pourquoi est-ce un problème ? Personnellement, j'éviterais de mettre sur liste blanche les appels système suivants (à moins qu'ils ne soient réellement nécessaires) : capset, set_tid_address, setgid, setgroups и setuid. Cependant, le véritable défi est qu'en autorisant des processus sur lesquels vous n'avez absolument aucun contrôle, vous liez les profils à l'implémentation du runtime du conteneur. En d’autres termes, vous constaterez peut-être un jour qu’après avoir mis à jour l’environnement d’exécution du conteneur (soit par vous, soit, plus probablement, par le fournisseur de services cloud), les conteneurs cessent soudainement de fonctionner.

Astuce # 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 du conteneur.

2. Définition 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/<container-name>: "localhost/profile.json"

Veuillez noter que la syntaxe ci-dessus changera lorsque Kubernetes seccomp deviendra GA (cet événement est attendu dans la prochaine version de Kubernetes - 1.18 - environ trad.).

Peu de gens savent que Kubernetes a toujours eu bugce qui a provoqué l'application des profils seccomp à conteneur de pause. L'environnement d'exécution compense en partie cette lacune, mais ce conteneur ne disparaît pas des pods, puisqu'il sert à configurer leur infrastructure.

Le problème est que ce conteneur commence toujours par AllowPrivilegeEscalation=true, ce qui conduit aux problèmes évoqués au paragraphe 1, et cela ne peut pas être modifié.

En utilisant des profils seccomp au niveau du conteneur, vous évitez cet écueil et pouvez créer un profil adapté à un conteneur spécifique. Cela devra être fait jusqu'à ce que les développeurs corrigent le bug et que la nouvelle version (peut-être la 1.18 ?) soit disponible pour tout le monde.

Astuce # 2: Définissez les profils seccomp au niveau du conteneur.

D'un point de vue pratique, cette règle sert généralement de réponse universelle à la question : « Pourquoi mon profil seccomp fonctionne-t-il avec docker runmais cela ne fonctionne pas après le déploiement sur un cluster Kubernetes ?

3. Utilisez le runtime/default uniquement en dernier recours

Kubernetes propose deux options pour les profils intégrés : runtime/default и docker/default. Les deux sont implémentés par le runtime du conteneur, et non par Kubernetes. Ils peuvent donc différer selon l’environnement d’exécution utilisé et sa version.

En d’autres termes, suite à un changement d’environnement d’exécution, le conteneur peut avoir accès à un ensemble différent d’appels système, qu’il peut ou non utiliser. La plupart des environnements d'exécution utilisent Implémentation de Docker. Si vous souhaitez utiliser ce profil, assurez-vous qu'il vous convient.

Profil docker/default est obsolète depuis Kubernetes 1.11, évitez donc de l'utiliser.

À mon avis, le profil runtime/default parfaitement adapté à l'objectif pour lequel il a été créé : protéger les utilisateurs des risques liés à l'exécution d'une commande docker run sur leurs voitures. Cependant, lorsqu'il s'agit d'applications métier exécutées sur des clusters Kubernetes, j'oserais 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).

Astuce # 3: Créez des profils seccomp pour des applications spécifiques. Si cela n'est pas possible, créez des profils pour les types d'applications, par exemple, créez un profil avancé qui inclut toutes les API Web de l'application Golang. N'utilisez le runtime/default qu'en dernier recours.

Dans les prochains articles, j'expliquerai comment créer des profils seccomp inspirés de SecDevOps, les automatiser et les tester dans des pipelines. En d’autres termes, vous n’aurez aucune excuse pour ne pas passer à des profils spécifiques à une application.

4. Le déconfinement n’est PAS une option.

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

Fonctionner dans ce mode signifie qu’une couche entière d’isolation qui protège le cluster est perdue. Cette approche n'est pas recommandée par les experts en sécurité.

Astuce # 4 : Aucun conteneur du cluster ne doit être exécuté dans seccomp=unconfined, en particulier dans les environnements de production.

5. "Mode audit"

Ce point n’est pas propre à Kubernetes, mais rentre tout de même dans la catégorie « choses à savoir avant de commencer ».

Il se trouve que la création de profils seccomp a toujours été un défi et repose en grande partie sur des essais et des erreurs. Le fait est que les utilisateurs n'ont tout simplement pas la possibilité de les tester dans des environnements de production sans risquer de « laisser tomber » l'application.

Après la sortie du noyau Linux 4.14, il est devenu possible d'exécuter des parties d'un profil en mode audit, en enregistrant des informations sur tous les appels système dans syslog, mais sans les bloquer. Vous pouvez activer ce mode à l'aide du paramètre SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp n'affectera pas le thread effectuant l'appel système s'il ne correspond à aucune règle du filtre, mais les informations sur l'appel système seront enregistrées.

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

  1. Autorisez les appels système nécessaires.
  2. Bloquez les appels du système dont vous savez qu’ils ne seront pas utiles.
  3. Enregistrez les informations sur tous les autres appels dans le journal.

Un exemple simplifié ressemble à ceci :

{
    "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"
        }
    ]
}

(moyen-mixte-seccomp.json)

Mais n'oubliez pas que vous devez bloquer tous les appels dont vous savez qu'ils ne seront pas utilisés et qui pourraient potentiellement nuire au cluster. Une bonne base pour dresser une liste est le document officiel Documentation Docker. Il explique en détail quels appels système sont bloqués dans le profil par défaut et pourquoi.

Cependant, il y a un problème. Bien que SCMT_ACT_LOG pris en charge par le noyau Linux depuis fin 2017, il n’est entré dans l’écosystème Kubernetes que relativement récemment. Par conséquent, pour utiliser cette méthode, vous aurez besoin d'un noyau Linux 4.14 et d'une version runC au minimum. v1.0.0-rc9.

Astuce # 5: Un profil de mode audit pour les tests en production peut être créé en combinant des listes noires et blanches, et toutes les exceptions peuvent être enregistrées.

6. Utilisez des listes blanches

La mise sur liste blanche nécessite des efforts supplémentaires car vous devez identifier chaque appel dont l'application pourrait avoir besoin, mais cette approche améliore considérablement la sécurité :

Il est fortement recommandé d’utiliser l’approche liste blanche car elle est plus simple et plus fiable. La liste noire devra être mise à jour chaque fois qu'un appel système potentiellement dangereux (ou un indicateur/option dangereux s'il figure sur la liste noire) est ajouté. De plus, il est souvent possible de modifier la représentation d'un paramètre sans en changer l'essence et ainsi contourner les restrictions de la liste noire.

Pour les applications Go, j'ai développé un outil spécial qui accompagne l'application et collecte tous les appels effectués lors de l'exécution. Par exemple, pour l'application suivante :

package main

import "fmt"

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

... lançons gosystract comme suit:

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

... et on obtient le résultat suivant :

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

Pour l’instant, ce n’est qu’un exemple ; plus de détails sur les outils suivront.

Astuce # 6: autorisez uniquement les appels dont vous avez réellement besoin et bloquez tous les autres.

7. Posez les bonnes bases (ou préparez-vous à un comportement inattendu)

Le noyau appliquera le profil indépendamment de ce que vous y écrivez. Même si ce n'est pas exactement ce que tu voulais. 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 raccrochez-leo pour une durée indéterminée. En conséquence, vous obtiendrez une utilisation élevée du processeur dans le cluster :

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

Dans de tels cas, un utilitaire peut venir à la rescousse strace - cela montrera quel peut être le problème :

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

Assurez-vous que les profils contiennent tous les appels système dont l'application a besoin au moment de l'exécution.

Astuce # 7: Faites attention aux détails et assurez-vous que tous les appels système nécessaires sont sur la liste blanche.

Ceci conclut la première partie d'une série d'articles sur l'utilisation de seccomp dans Kubernetes dans l'esprit de SecDevOps. Dans les parties suivantes, nous expliquerons pourquoi cela est important et comment automatiser le processus.

PS du traducteur

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire