KubeCon Barcelone 2019, Day 3 !

thumbernail Kubernetes

Troisième et dernière journée de la Kubecon Europe 2019. L'équipe d'Alter Way vous résume les conférences notables auxquelles nous avons assisté.

Migrating from Docker to containerd

Le container runtime est le composant en charge de faire fonctionner les conteneurs. Il s'agit du composant intéragissant directement avec le kernel pour créer les namespaces et les cgroups. Il existe néanmoins plusieurs "niveaux" de runtimes, certains se reposant sur d'autres pour effectuer des opérations. Depuis quelques années, Docker délègue à runc la création et le fonctionnement des conteneurs, il ne les crée pas lui même.

Le sujet des container runtimes a déjà été abordé dans un de nos articles, vous pouvez vous y reporter pour plus de précisions.

https://blog.alterway.fr/le-point-sur-les-container-runtimes.html

Docker et containerd reposent en réalité tous les deux sur runc pour faire le travail côté kernel. Containerd respectant les normes de l'OCI, les images Docker que vous pourriez avoir créées précédemment fonctionneront sans broncher avec containerd. Cette spécification est d'ailleurs suivie par l'ensemble de l'écosystème des container runtimes.

Plusieurs objectifs, notamment en sécurité, cherchent à être remplis en remplaçant Docker par containerd :

  • containerd présente une surface d'attaque plus faible que Docker
  • containerd publie l'intégralité de ses audits de sécurité
  • un attaquant ayant accès à Docker peut créer n'importe quelle image, ce que containerd ne permet pas

Au niveau des performances, la balance penche clairement en faveur de containerd :

perf

Les gains sont relativement faibles côté RAM mais deviennent réellement intéressants au niveau du CPU. La rapidité d'exécution est aussi meilleure chez containerd : les conteneurs sont plus rapidement up and running.

Le changement de container runtime se fait au niveau de la configuration du kubelet. Sur un cluster non managé, il est assez facile de changer la configuration du kubelet. En revanche, cela n'est pas toujours possible sur un cluster managé par votre cloud provider :

  • EKS (Amazon Web Services) ne supporte pas containerd
  • AKS (Azure) ne supporte pas containerd
  • GKE (Google) supporte containerd par l'ajout d'un node-pool avec l'image cos-containerd qui force l'utilisation de containerd comme container runtime
  • Magnum (OpenStack) permet d'utiliser containerd puisque n'importe quelle option peut être passée au kubelet

SIG-OpenStack Intro & Deep Dive

Cloud-Provider

Les Cloud providers dans le projet Kubernetes existent pour la plupart des providers de cloud public, mais également pour les technologies de cloud privé comme OpenStack. Ces projets peuvent être assimilés à des "drivers", ou interfaces, pour Kubernetes. Si vous l'installez sur AWS, vous aurez besoin du Cloud provider AWS pour que Kubernetes pilote les ressources du IaaS sous-jacent (EBS, ELB etc). C'est par ce "driver" que Kubernetes peut piloter le stockage, les load-balancers, ou toute autre ressource. Le SIG-OpenStack est responsable du bon fonctionnement du cloud provider OpenStack.

Pour le moment ces cloud providers sont in-tree, c'est-à-dire dans le cœur du code source de Kubernetes. Ces cloud providers sont aujourd'hui dépréciés et seront remplacés par des cloud providers out-of-tree (dans leurs propres repos).

En plus de cela, toute la partie stockage de Kubernetes doit être remplacée par des drivers CSI qui sont également out-of-tree

Le SIG-OpenStack est responsable de tous ces changements au niveau d'OpenStack. Les principaux axes de travail sont :

  • Le maintien du cloud provider OpenStack
  • Le support de l'autoscaling
  • Le support d'OpenStack avec les principaux outils de déploiement (ClusterAPI pour Ironic et OpenStack, Kops, etc.)
  • Le maintien du plugin CSI pour Cinder

Dans le cas du cloud provider OpenStack, le SIG travaille sur le support des services OpenStack suivants :

  • Plugins Cinder (block) / Manilla(file) pour CSI
  • Octavia Ingress Controller : Octavia en tant que LoadBalancer L4 est déjà supporté, il s'agit ici de supporter les ressources de type Ingress L7
  • Support de Barbican
  • Support de Keystone

Les cloud providers in-tree seront sûrement supprimés d'ici 2 releases pour la prochaine KubeCon à San Diego.

Sortir ces plugins de Kubernetes a potentiellement beaucoup d'impact sur les cluster existants. En effet, pour le moment la logique des cloud providers étant temporairement intégrée à Kubernetes, le code est présent dans les différents composants comme le Kubelet, l'API server et le Controller Manager. Lorsqu'il sera supprimé, le code fonctionnera dans un service externe : le cloud controller manager. Trouver un chemin de migration pour les clusters existants n'est pas facile, c'est un sujet sur lequel le SIG-cloud-provider se penche actuellement.

Cluster-API

Parmi les autres nouveautés, beaucoup d'efforts sont fournis dans l'implémentation de cluster API pour OpenStack.

Alors qu'est-ce que [cluster API] (https://github.com/kubernetes-sigs/cluster-api) ? C'est un vaste sujet dont on entend parler de plus en plus souvent.

Pour faire simple, le projet a pour but de standardiser la partie infrastructure de Kubernetes en ajoutant des notions d'infrastructure (sous forme d'objets API) dans Kubernetes :

  • Cluster : la configuration d'un cluster Kubernetes
  • Machine : un nœud Kubernetes
  • MachineSet : un ensemble de nœuds
  • MachineDeployment : comment déployer Kubernetes sur les machines

L'architecture est relativement complexe et mériterait un article dédié :

https://github.com/kubernetes-sigs/cluster-api/blob/master/docs/book/common_code/architecture.svg

Declarative API to deploy Kubernetes cluster

Ce projet vise à fournir une interface standard et laisser ensuite la liberté d'implémentation aux différentes solutions de déploiement. L'objectif n'est pas de remplacer Kops ou Magnum, mais plutôt que ces outils s'appuient sur Cluster API. Il existe des implémentations de référence de Cluster API :

Une liste plus exhaustive est disponible ici.

Pour fermer cette parenthèse, le SIG-OpenStack travaille donc sur l'implémentation pour OpenStack et est encore en early development et loin d'être production ready. Cluster API est également en v1alpha1 et aucune décision n'a encore été statuée.

Cluster AutoScaler for Magnum

Enfin, un dernier sujet lors de cette session est l'implémentation de l'auto-scaling des workers dans un cluster Kubernetes hébergé sur un IaaS OpenStack. Cette fonctionnalité a été développée et présentée par le CERN.

Elle permet de scaler horizontalement les workers Kubernetes en fonction du nombre de Pods en exécution. Si des ods sont provisionnés alors que la puissance est insuffisante, le cluster démarre automatiquement de nouveaux workers pour accueillir les Pods. A l'inverse, le "scale-down" est également possible, bien que difficile puisqu'il faut à la fois interagir avec Magnum et Heat, qui doivent absolument rester synchronisés.

D'un point de vue pratique, la configuration se fait comme une configuration d'auto-scaling classique, avec un MIN et un MAX lors de la déclaration du cluster ([MIN:MAX]).

Cette fonctionnalité est déjà présente dans la version Stein d'OpenStack.

Latest Kubernetes Scalability Improvements

Depuis de nombreuses années déjà, Kubernetes supporte des clusters de large taille (~2K nœuds) et a gagné une certaine popularité auprès des entreprises. Toutefois des problèmes de performance sur le control plane sont générés par ce type de déploiement. Cette session est l'occasion pour le SIG scalability de présenter les problèmes récurrents qui ont été détectés ainsi que les solutions qui ont été implémentées au fil des releases de Kubernetes.

Kubelet pulling configs

La majorité des applications se servent des secrets/configmaps pour s’exécuter. Ces objets sont constamment consommés par les applications ce qui engendre de nombreux appels API sur l'API Server. Sur un cluster de taille importante, ces transactions peuvent allonger et alourdir la queue des requêtes que l'API Server doit traiter, notamment sur des environnements ayant plus de 250 secrets/configmaps.

Solution :

  • modifier le [kubelet] pour écouter individuellement chaque secret/configmap sur un node . (par défaut depuis la 1.14.2)

Iptables performance

Iptables maintient une liste linéaire des règles sur le node. La mise à jour de ces règles peut prendre du temps, ralentissant le routage de paquets sur le node.

Solution :

  • utiliser IPVS comme alternative à kube-proxy

Cet article explique plus en détail les raisons du choix de IPVS.

La liste des problèmes est longue mais le SIG garde une constance à résoudre ces problèmes, concrétisée à sortie d'une version de Kubernetes par une optimisation du code source et une amélioration des performances.

DIY Pen-Testing for Your Kubernetes Cluster

Le penetration testing, alias pentesting, est une discipline qui consiste en la recherche et l'analyse des vulnérabilités d'un système en vue de les exploiter ou de les corriger. Dans cet excellent talk, Liz Rice (Aqua Security) nous introduit les vecteurs d'attaque les plus courants pour un cluster Kubernetes.

Quelle est la première étape lorsque l'on veut tester un système ? Généralement, commencer par chercher les ports ouverts. Un outil très connu pour vérifier l'état des ports est nmap.

matrix

Voici un exemple de scan de ports d'un cluster Kubernetes :

$ nmap -T4 -p100-10000 kubernetes-master.local
Starting Nmap 7.70 ( https://nmap.org ) at 2019-05-23 14:59 CEST
Nmap scan report for kubernetes-master.local (127.0.0.1)
Host is up (0.00022s latency).
rDNS record for 127.0.0.1: kubernetes-master.local
Not shown: 9893 closed ports
PORT     STATE SERVICE
111/tcp  open  rpcbind
139/tcp  open  netbios-ssn
443/tcp  open  https
445/tcp  open  microsoft-ds
2379/tcp open  etcd-client
5355/tcp open  llmnr
6443/tcp open  sun-sr-https
8080/tcp open  http-proxy
9099/tcp open  unknown

On constate l'existence d'un certain nombre de ports en écoute (open), dont certains propres à un cluster Kubernetes - bon là c'est facile on est en local. Kubernetes étant principalement composé d'API accessibles via des GET et POST HTTP, on peut essayer d'écrire des requêtes API en passant par curl. En fonction de la configuration en place, la prise d'information est plus ou moins simple :

  • en HTTP (typiquement port 80), peu de barrières à l'entrée. Un simple curl http://<IP> peut déjà vous fournir des informations intéressantes
  • en HTTPS (typiquement ports 443, 6443...), le trafic est chiffré via un échange de certificats entre le client et le serveur. Le certificat du serveur peut être valide ou non (--insecure ou -k peut être utile pour curl). En outre, les certificats peuvent également être utilisés pour la mise en place d'un mécanisme d'autorisation, ce qui peut empêcher l'accès sans le certificat client adapté.
  • en fonction de l'API ou du service sur un port répondant aux protocoles HTTP/HTTPS, une identification côté applicatif peut ajouter une couche de sécurité supplémentaire (Tokens d'authentification...)
  • même authentifié avec un token valide, un contrôle de l'identité de l'utilisateur (de son rôle) peu restreindre l'accès

Afin de nous introduire la recherche de vulnérabilité, la présentatrice nous a fait une démo sur un cluster... très ouvert.

Le mieux est de commencer par le haut de la liste précédente et de continuer vers le bas. Testons l'accès par http, ou le port non-chiffré :

$ curl http://<ip>

Pas trop de réponse... C'est cohérent avec le résultat de notre commande nmap. On peut également tester le port 8080.

$ curl http://<ip>:8080

Cette fois, une API répond. Il est alors possible de naviguer à la recherche de routes valides, par exemple dans le cas de l'API Kubernetes, dans /api/v1 qui est une route standardisée :

{
  "kind": "APIResourceList",
  "groupVersion": "v1",
  "resources": [
    {
      [...],

      {
      "name": "pods",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": [
        "create",
        "delete",
        "deletecollection",
        "get",
        "list",
        "patch",
        "update",
        "watch"
      ],
      "shortNames": [
        "po"
      ],
      "categories": [
        "all"
      ]
    },

    [...]

...puis de tenter de découvrir les pods existants dans le namespace par défaut dans /api/v1/namespaces/default/pods.

{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "pods is forbidden: User \"system:anonymous\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403

Résultat attendu dans une configuration normale où il n'est pas possible de lire les pods d'un namespace sans authentification. Pendant la démo de Liz, l'ensemble des pods étaient visibles.

Et maintenant, comment avoir accès à encore plus d'informations, ou peut-être même comment avoir accès en écriture ?

On a besoin des privilèges d'un utilisateur. Un utilisateur simple à tester, généralement présent sur les cluster Kubernetes (entre autres pour effectuer les healthchecks et autres requêtes anonymes), est l'utilisateur system: anonymous. Il faut faire particulièrement attention aux droits accordés à cet utilisateur et en particulier les restreindre avec des RBAC plus fins, au risque de donner accès à trop d'informations à un attaquant.

Mais il existe une ressource encore plus intéressante à exploiter et qui possède potentiellement encore plus de droits. C'est le serviceaccount. A la création du pod, un serviceaccount par défaut (default) est créé. Si de l'extérieur on parvient à s'introduire dans un pod et à obtenir un shell, il suffit ensuite de récupérer le token associé au serviceaccount:

export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

En ayant accès aux secrets des pods, on obtient un moyen d’escalader nos privilèges et d'accéder à plus de ressources de l'API Kubernetes dans le cadre des serviceaccounts.

Par ailleurs, en tant qu'auditeur ou attaquant on peut aussi vérifier si d'autres APIs du master sont accessibles:

  • etcd : le key/value store contenant toute la configuration du cluster Kubernetes, généralement sur le port 2379 (http(s)://kubernetes-master:2379/version)
  • API Kubelet : généralement sur le port 10250 (https), sur les routes /metrics ou /pods avec le token obtenu précédemment

Jusqu'ici, nous n'avons vu que des techniques dites de "chasse passive", c'est-à-dire que le chasseur ne fait qu'observer le cluster. Mais il est aussi possible de passer en mode "actif" et d'apporter des modifications, en créant des roles ou des pods.

Kube-hunter permet d'automatiser tous ces tests (actifs et passifs). Il peut être lancé depuis :

  • son laptop, pour connaître le point de vue d'un attaquant externe
  • une machine du cluster, pour sonder toutes les interfaces du cluster
  • un pod, pour savoir à quel point les applications sont exposées dans le cas où un pod serait compromis

Découvrez les technologies d'alter way