Rechercher
Contactez-nous Suivez-nous sur Twitter En francais English Language
 

De la Théorie à la pratique











Abonnez-vous gratuitement à notre NEWSLETTER

Newsletter FR

Newsletter EN

Vulnérabilités

Se désabonner

Sécurité accrue pour les conteneurs et les Kubernetes avec les filtres syscall

mars 2020 par Andreas Jaekel, Head of PaaS Development, IONOS

Il est rare aujourd’hui de rencontrer un développeur capable de se passer de conteneurs et de Kubernetes, car la technologie des conteneurs facilite grandement le travail avec les micro-services et dans des équipes agiles. Kubernetes est une réussite depuis cinq ans et s’est imposé comme l’outil standard pour l’orchestration de conteneurs.

Cependant, les technologies populaires attirent également davantage l’attention des pirates informatiques qui veulent diffuser des logiciels malveillants. Cela concerne également les conteneurs. Les conteneurs ne sont pas totalement sûrs, même ceux que nous créons nous-mêmes peuvent être piratés. Les logiciels malveillants peuvent être contenus dans des images ou téléchargés dans des conteneurs.

Les conteneurs génériques sont censés tenir les intrus à distance. Cependant, nous pouvons encore augmenter le niveau de sécurité. Une approche consiste à instrumentaliser une fonction bien connue de Linux et à indiquer aux conteneurs quels appels système (syscalls) ils sont autorisés à exécuter.

Que sont les appels système ?

Chaque processus Linux reçoit une part de mémoire lorsqu’il est lancé. Le code est alors libre d’opérer sur cette mémoire, par exemple pour effectuer des calculs. Pour tout le reste, il doit demander l’autorisation au noyau. Quelques exemples :
● Ecrire dans un fichier (write)
● Recevoir des paquets de réseau (lire)
● Créer un répertoire (mkdir)
● Démarrer un nouveau processus (fork)
● Obtenir l’heure (gettimeofday)

Le code est autorisé en envoyant la demande par syscall au noyau, qui vérifie ensuite les autorisations avant de répondre à la demande. Il y a environ 330 syscalls sous Linux, que vous pouvez trouver ici. Les appels système sont indépendants du langage de programmation. L’écriture reste écrite, qu’elle soit écrite en C ou en GoLang.

Pourquoi filtrer les appels système ?

Il existe trois principaux vecteurs d’attaque des conteneurs :
1) Les portes dérobées dans les images en amont de Docker
2) Les bugs d’application exploitables
3) Les appels système vulnérables dans le noyau Linux Filtrer les appels système peut empêcher les programmes et les conteneurs de faire ce que nous ne voulons pas qu’ils fassent. Par exemple, si vous utilisez Nginx, vous pouvez désactiver les appels système dont vous êtes sûr que Nginx n’aura jamais besoin, comme :

Ce faisant, vous rendez Nginx plus sûr sans réduire ses fonctionnalités.

Quels appels devrions-nous filtrer ?

L’exemple ci-dessus semble simple, mais comment savoir quels appels système une application utilise ? Si vous en filtrez un trop grand nombre, l’application ne fonctionnera pas. Mais si vous n’en filtrez pas assez, vous laissez la place aux attaques. Il est pratiquement impossible de créer une liste parfaite de filtres. Cependant, il existe cinq approches différentes que vous pouvez utiliser pour vous rapprocher d’une liste de filtres appropriée.
1) Lire la source – en entier, y compris les bibliothèques
C’est la seule façon d’être vraiment certain d’avoir exclu le code malveillant. Cependant, en raison du nombre presque infini de chaînes de dépendance, cette approche est irréalisable en pratique.
2) Essayer et échouer
Bien entendu, vous pouvez vous contenter de régler les filtres. Néanmoins, avec environ 330 syscalls et une infinité de combinaisons possibles, cette approche n’est pas non plus pratique.
3) Faire une déduction logique
Une meilleure option consiste à faire une déduction ciblée. Bien entendu, cela n’a de sens que si vous avez une connaissance approfondie de la conception de logiciels et des appels système. Même dans ce cas, le résultat probable est que le nombre de filtres définis est trop élevé ou trop faible. Faire une déduction logique est mieux que rien, mais on est encore loin d’un filtre qui fonctionne bien.
4) Analyser les binaires
Théoriquement, vous pourriez discerner les appels système utilisés en analysant les binaires. L’avantage est que les binaires sont toujours en langage machine. Cependant, les différents langages et compilateurs produisent un code machine différent. Il est donc pratiquement impossible de déterminer automatiquement quels appels système sont inclus et lesquels ne le sont pas.
5) Suivi des appels avec strace
Les appels système exécutés par une application peuvent être suivis à l’aide d’un outil dédié. Cela peut être fait pendant un test unitaire ou un pipeline d’IC, par exemple. Linux propose l’outil strace, qui est un excellent outil de débogage et de dépannage. strace affiche également les paramètres des appels. Voici un exemple en "Mode Compteur" :

> strace -c -S name ./helloworld
Hello World ! _ % time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 4 brk
0.00 0.000000 0 1 execve
0.00 0.000000 0 1 uname
0.00 0.000000 0 1 write
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 8 total

Récapitulatif
● Il est difficile d’écrire une bonne liste de filtres
● Si vous filtrez trop, l’application cesse de fonctionner
● Si vous filtrez trop peu, la porte est ouverte aux attaques
● Commencez par utiliser strace pour suivre tous les appels système nécessaires
● Vous pouvez ensuite affiner votre liste avec des suppositions éclairées

Créer un eBPF avec Seccomp

Linux a la capacité d’exécuter de petites machines d’état avant tout appel système. Ces programmes doivent être fournis sous la forme de "filtres de paquets de Berkeley étendus", eBPF en abrégé, et peuvent être chargés dans le noyau via l’appel système bpf().

eBPF peut être utilisé pour toutes sortes de choses, par exemple la mesure des performances, le débogage, le traçage, etc. Cependant, l’écriture de programmes eBPF est complexe, alors que vous voulez juste filtrer les appels système, pas apprendre un nouveau langage de programmation.

Le Seccomp BPF peut vous aider dans ce domaine. Créé par Google en 2005, Seccomp cache à l’utilisateur la complexité de l’eBPF et peut filtrer les appels système individuels. Il offre également d’autres possibilités au-delà du filtrage et peut :
● prétendre qu’un appel système a été exécuté alors qu’il ne l’était pas
● renvoyer de faux résultats et numéros d’erreur
● faire apparaitre les points de rupture

Le Seccomp est donc un bon outil de test, d’injection d’erreurs et de débogage. Voici un exemple d’utilisation de Seccomp en C
int
main(int argc, char *argv[])

scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW) ;
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(getpid), 0) ;
seccomp_load(ctx) ;
pid_t pid = getpid() ;
/* never reached : process killed */
return 0 ;

Seccomp peut filtrer en fonction des paramètres :
unsigned char buf[BUF_SIZE] ;
int fd = open(“data.raw", 0) ;
int rc = seccomp_rule_add(
ctx,
SCMP_ACT_ALLOW,
SCMP_SYS(read), 3,
SCMP_A0(SCMP_CMP_EQ, fd),
SCMP_A1(SCMP_CMP_EQ, (scmp_datum_t)buf),
SCMP_A2(SCMP_CMP_LE, BUF_SIZE)) ;

Cela filtre les appels read() qui ne remplissent pas les trois conditions suivantes :
1. Les données sont lues à partir du descripteur de fichier exact qui a été créé précédemment avec open()
2. Les données lues sont stockées dans la zone de mémoire désignée "buf"
3. Aucune donnée n’est lue au-delà de la zone "buf"

Le filtrage par paramètres peut être utile pour de nombreuses raisons, par exemple pour :
● forcer les appels de système en lecture seule
● limiter l’écriture et la lecture à STDOUT et STDIN
● limiter setuid() à des UID spécifiques
● interdire l’envoi de signaux autres que ceux de SIGHUP
● éviter de fixer des autorisations de fichiers trop généreuses

Cependant, le filtrage des paramètres a ses limites. Seuls les paramètres Pass by Value peuvent être évalués. Cela signifie que vous ne pouvez pas regarder dans les chaînes ou les structures. Par exemple, vous ne pouvez pas limiter open() à certains noms de fichiers.

Appliqué aux conteneurs et aux K8

La bonne nouvelle est que le support de Seccomp a été ajouté à Docker dans la version 1.10.
44 syscalls sont bloqués par défaut, y compris reboot(). Les appels système non désirés échouent, mais le programme n’est pas tué.
Lors de l’écriture d’un filtre personnalisé, il est recommandé de commencer avec le filtre par défaut et de l’ajuster si nécessaire. Les filtres personnalisés sont exprimés sous forme de fichiers JSON.

À quoi cela ressemble-t-il dans le Docker ?


"defaultAction" : "SCMP_ACT_ERRNO",
"syscalls" : [

"names" : [
"accept",
"access",

],
"action" : "SCMP_ACT_ALLOW",
"args" : [],
"comment" : "",
"includes" : ,
"excludes" :

]

Les appels système énumérés ci-dessus sont autorisés, quels que soient leurs paramètres.


"names" : [
"Ptrace"
],
"action" : "SCMP_ACT_ALLOW",
"args" : null,
"comment" : "",
"includes" :
"minKernel" : "4.8"
,
"excludes" :

Ici : autoriser ptrace(), mais seulement sur les noyaux plus récents ou égaux à linux-4.8.
Le fichier JSON du filtre (Docker l’appelle un "profil seccomp") peut être donné comme paramètre de ligne de commande :
# docker run -ti —rm —security-opt seccomp:custom_filter.json alpine /bin/sh Tout profil seccomp donné remplacera celui par défaut, sans l’étendre. Le filtre s’appliquera à l’ensemble du conteneur.

Filtres Syscall dans Kubernetes

Les filtres Seccomp syscall ont été ajoutés dans Kubernetes 1.3 et sont supportés par la plupart des runtimes, pas seulement par Docker. Les profils Seccomp s’appliquent à l’ensemble du pod, et pas seulement à des conteneurs individuels.
Pour créer des profils personnalisés via Seccomp, vous devez activer les politiques de sécurité du pod dans le cluster K8s, puis définir une politique de sécurité du pod qui permet d’utiliser les profils Seccomp. En créant un RoleBinding, vous permettez aux pods d’utiliser cette politique.
Pour activer les politiques de sécurité des pods, ajoutez au moins une politique permissive et créez au moins un rôle correspondant et un RoleBinding pour l’espace de nommage du système kube-system. Sinon, les K8 ne pourront démarrer aucun pod.

Ensuite, ajoutez PodSecurityPolicy à la liste des contrôleurs d’admission autorisés :
kube-apiserver \
—enable-admission-plugins= \
PodSecurityPolicy,LimitRanger ...

Puis, fournissez des profils Seccomp en écrivant des profils et en les plaçant sur les nœuds de travailleurs :

kubelet —seccomp-profile-root=
(Default : /var/lib/kubelet/seccomp).

Pour appliquer un filtre Seccomp à un pod, ajoutez des annotations au pod (modèle) :

[…]
metadata :
labels :
app : problemsolver
annotations :
kubernetes.io/psp : allowseccomp
seccomp.security.alpha.kubernetes.io/pod : localhost/custom-profile.json
[…]

Téléchargez le fichier d’exemple pour démarrer rapidement :
https://github.com/ionos-enterprise...

Résumé

Il n’est pas facile de créer un filtre adapté. S’il est trop généreux, il ne permettra pas de se défendre contre les logiciels malveillants. S’il est trop strict, l’application risque de ne pas être exécutable. En outre, les paramètres par défaut du Docker sont déjà assez sophistiqués.
Cependant, dans certains cas, il est judicieux d’investir du temps dans la création de son propre filtre. Si vous connaissez bien votre application, elle peut vite vous offrir beaucoup d’avantages. Si vous avez besoin d’un environnement Docker hautement sécurisé (par exemple, dans le secteur de la fintech), cela peut valoir la peine. Si vous êtes un hôte de conteneur, il vaut la peine de définir vous-même quels appels système sont utilisés et lesquels ne le sont pas.




Voir les articles précédents

    

Voir les articles suivants