Surveillance des comportements des containers

La surveillance du comportement des containers est essentielle pour la sécurité de votre infrastructure Kubernetes. En surveillant attentivement ces comportements, vous pouvez garantir le bon fonctionnement de vos images et prévenir les actions malveillantes, qu'elles soient volontaires ou involontaires.

Les containers peuvent avoir un impact significatif sur votre infrastructure, notamment en :

  • Accédant en lecture et écriture à des fichiers systèmes sensibles.
  • Exploitant des vulnérabilités pour obtenir des privilèges root.
  • Provoquant des fuites de données sensibles vers des destinations externes.
  • Initiants des processus supplémentaires non autorisés, compromettant ainsi la stabilité du système.

L’une des difficultés est que par défaut, Kubernetes ne propose pas d’outil et d’une configuration permettant de sécuriser la partie exécution d’un container. Il sera nécessaire préalablement de configurer les ressources afin d’incorporer des règles de sécurité (exemple PSS, Seccomp, AppArmor,...).

Présentations de Falco, des datasources et règles

Collaboration avec Sysdig et CNCF

Falco est un outil développé par Sysdig en tant que projet open source. Spécialement conçu pour détecter les actions malveillantes dans les environnements distribués, notamment dans les clusters Kubernetes, Falco surveille en temps réel les activités des conteneurs et déclenche des alertes en cas de comportements suspects ou non autorisés. En février 2024, Falco a été diplômé par la CNCF, ce qui confirme sa maturité et sa fiabilité en tant que projet open source. Étant pensé pour être une application Cloud Native, il incorpore de nombreux concepts tels que la compatibilité des différents systèmes, sa rapidité et sa scalabilité.

Falco offre la possibilité d'ajouter des plugins permettant de surveiller d'autres sources telles que les audits Kubernetes ou des fichiers JSON CloudTrail. Vous pouvez retrouver ces différents plugins sur le lien vers les plugins Falco

Si nous rentrons un peu plus en détail, Falco est en capacité d’intervenir sur les différentes couches d’une application tels que le kernel, le container, et le cluster.

Les différentes couches d'une application

Avant de parler des datasources permettant de collecter les événements des appels systèmes, laissez-moi vous expliquer en quelques lignes les espaces dans un noyau Linux.

Dans Linux, deux espaces principaux permettent de séparer les ressources système entre différents processus :

Différence entre les namespaces Kernel

Espace utilisateur (User space) :

  • Les processus s'exécutent normalement dans cet espace, avec des limites de droits sur les ressources système.
  • Ils doivent utiliser des appels système (syscall) pour accéder aux ressources système.

Espace noyau (Kernel space) :

  • Réservé à l'exécution du noyau Linux et des pilotes périphériques.
  • Le noyau a un accès direct aux ressources matérielles et est responsable de la gestion du système.

Les appels système sont collectés par les drivers (datasource) tels que le module noyau Falco (Falco Kernel Module), Falco Probe eBPF et Falco Modern eBPF dans l'espace noyau, puis ils sont stockés dans un ring buffer. Ce ring buffer permet de temporiser les événements avant qu'ils ne soient consommés depuis l'espace utilisateur. Ensuite, Falco filtre ces événements en fonction des règles utilisées.

Les datasources dans Falco (drivers)

Les trois principaux drivers utilisés dans Falco sont les suivants :

Le module noyau Falco est le pilote par défaut utilisé lors de l'installation. Il offre une grande polyvalence en pouvant être déployé dans n'importe quel environnement Linux, y compris les systèmes hébergés chez certains fournisseurs de services cloud. Cependant, ses inconvénients peuvent résider dans son lien étroit avec le noyau Linux et les possibles évolutions de celui-ci. Une mauvaise configuration du module pourrait avoir un impact direct sur le noyau Linux. De plus, le chargement d'un module de noyau n'est pas toujours autorisé dans les différents environnements, ce qui peut poser des problèmes de déploiement.

Quant au Probe eBPF, il sera possible de l'utiliser dans des environnements où le chargement de modules de noyau n'est pas autorisé, comme dans GKE par exemple. eBPF présente l'avantage de ne pas être intrinsèquement lié au noyau, ce qui réduit le risque de provoquer un plantage du système. Il est déjà compilé dans certains systèmes Linux et peut être activé facilement à l'aide d'une application eBPF. Cependant, il sera nécessaire de disposer d’une version récente du noyau Linux (égal ou supérieur à la version 4.14).

Et le dernier, Modern eBPF offre une plus grande flexibilité et des fonctionnalités et performances avancées par rapport au Probe eBPF classique.

Les règles dans le moteur Falco

Falco à fait en sorte de disposer d’une syntaxe suffisamment compréhensible et compacte afin d’être facilement utilisable. Il dispose déjà de nombreuses règles permettant la surveillance de vos clusters kubernetes.

Un exemple concret est de surveiller l’accès à un fichier (comme ci-dessous):

- rule: Unauthorized access to /etc/shadow
  desc: Detects unauthorized access to the /etc/shadow file.
  condition: >
    open and fd.name = "/etc/shadow" and container.id != host
  output: >
    Unauthorized access to /etc/shadow (container_id=%container.id container_name=%container.name user=%user.name command=%proc.cmdline)
  priority: WARNING
  tags: [filesystem, mitre_privilege_escalation, NIST_800-53_AC-3]

Nous en parlerons un peu plus loin dans l’article sur sa mise en place.

Installation et configuration de Falco

Pour installer Falco, nous disposons de deux options. La première consiste à déployer directement le binaire Falco sur les nœuds Kubernetes, ce qui est particulièrement adapté aux clusters non managés. Cette approche offre l'avantage d'isoler le composant Falco du reste du cluster Kubernetes.

La seconde option est d'utiliser un Helm chart ou des manifests Kubernetes. Dans nos exemples, nous privilégierons la méthode Helm en raison de sa simplicité de déploiement, de configuration et de mise à jour. Cependant, contrairement à la première méthode, Falco ne sera pas isolé du cluster dans cette approche.

Dans notre exemple, nous utiliserons un Helm chart pour faciliter son installation sur l’ensemble des nœuds du cluster Kubernetes.

Commencer par ajouter le dépôt falco à Helm :

helm repo add falcosecurity [https://falcosecurity.github.io/charts](https://falcosecurity.github.io/charts)

Récupérer ensuite les détails du dépôt précédemment ajouter:

helm repo update

Nous utiliserons un fichier values.yaml pour alimenter notre chart:

# values.yaml

# Active la collecte des métadonnées k8s afin d'enréchir les évènements Falco.
collectors:
  enabled: true 

# Configure le driver à utiliser pour la collecte des données
driver:
  kind: modern_ebpf

# Configure le service qui sera utilisé par le plugin k8saudit
# services:
#  - name: k8saudit-webhook
#    type: NodePort
#    ports:
#      - port: 9765 # See plugin open_params
#        nodePort: 30007
#        protocol: TCP

falco:
  # Indique les chemins où se trouvent les fichiers de configuration des règles
  rules_file:
    - /etc/falco/falco_rules.yaml
    - /etc/falco/k8s_audit_rules.yaml
    - /etc/falco/rules.d

  # Configuration du plugin k8s audit 
  # plugins:
  #  - name: k8saudit
  #    library_path: libk8saudit.so
  #    init_config:
  #      ""
  #      # maxEventBytes: 1048576
  #      # sslCertificate: /etc/falco/falco.pem
  #    open_params: "http://:9765/k8s-audit"
  #  - name: json
  #    library_path: libjson.so
  #    init_config: ""
  load_plugins: [k8saudit, json]

La configuration de k8saudit nécessite d’avoir accès à l’Audit log k8s. Il est courant que les fournisseurs de cloud ne proposent pas de l’activer sur un control plan mutualisé. Actuellement, uniquement EKS (AWS) supporte le plugin (voir plugin k8s-audit-eks). Dans notre exemple, nous utilisons un cluster déployé à partir de minikube sans la configuration du k8saudit et json.

Vous pouvez choisir un namespace particulier pour l’installation de Falco, dans notre exemple nous installerons la chart dans un namespace “falco”:

helm install falco falcosecurity/falco --create-namespace -n falco --version 4.2.5

Une fois Falco installé, vous pouvez vérifier le bon fonctionnement de Falco en regardant l’état des ressources du namespace choisie.

kubectl get pods -n falco 
kubectl events -n falco

Vous disposez maintenant de l’outil Falco installé sur votre cluster Kubernetes ! La configuration par défaut vous permet de disposer de plusieurs règles par défaut, des plugins déjà installés et configurés. On retrouve notamment le plugin Cloudtrail (nécessitant une configuration supplémentaire) et l'audit k8s.

Création et déploiement d’une règle

Fonctionnement des conditions

Dans une règle Falco, vous devez spécifier une condition qui retourne une valeur booléenne. Celle-ci se base sur les champs obtenus à partir des appels systèmes (syscalls).

Nous retrouvons les opérateurs les plus couramment utilisés tels que "=","<=", "contains", "icontains", “and”, etc. Pour plus de détails sur les opérateurs disponibles, je vous invite à consulter cette page.

Voici un exemple de condition permettant de filtrer :

syscall.type=connect and evt.dir=< and (fd.typechar=4 or fd.typechar=6)

Dans cet exemple, nous précisons plusieurs conditions :

  • Le champ syscall.type doit être égal à connect (pour filtrer les types de syscalls).
  • Le champ evt.dir doit être < (pour filtrer les événements de direction).
  • Le champ fd.typechar doit être égal à 4 ou 6 (pour filtrer les descripteurs de fichiers de type IPv4 ou IPv6).

Une fois que ces différentes conditions sont réunies, Falco déclenche une alerte.

Fonctionnement des champs (Fields)

Pour construire vos conditions, vous aurez besoin de différents champs. Ces derniers sont obtenus à partir de diverses sources d'événements, telles que les appels systèmes (syscall), les plugins, les méta-événements de Falco, ainsi que les événements de points de trace (Trace point Events) présents dans le noyau Linux.

Pour vérifier si un fichier est ouvert, nous utiliserons le champ "open", appartenant à la catégorie d'événement syscall. Ce champ nous permettra de filtrer nos événements relatifs à l'ouverture de fichiers. Vous pouvez retrouver la liste des champs disponibles sur cette page.

Chaque champ possède un format spécifique, comprenant des types allant du numérique au caractère en passant par l'ID de processus, etc. Bien que la liste soit exhaustive, nous utiliserons principalement les types les plus simples, tels que CHARBUF, FD, INT.

Fonctionnement des macros / lists

Afin d'améliorer la lisibilité, Falco nous offre la possibilité de réutiliser des conditions et des listes de valeurs, ce qui réduit la taille de nos règles et améliore la clarté de nos configurations.

Le concept de macro nous permet de définir une condition pouvant être réutilisée ultérieurement dans nos règles. Cette macro est définie avec la syntaxe suivante :

- macro: spawned_process
  condition: evt.type = execve and evt.dir = <

Nous pouvons ensuite utiliser cette condition dans d'autres règles en utilisant simplement son nom :

- rule: shell_in_container
  desc: notifier une activité shell
  condition: >
    spawned_process
  output: >
    activité shell detecté
  priority: WARNING

Le fonctionnement des listes, quant à lui, nous permet de spécifier plusieurs valeurs (par exemple des noms de répertoires), afin de les utiliser dans d'autres règles ou macros.

Message de sortie de la règle

Lorsque la condition d'une règle est remplie, Falco génère un message vers la sortie configurée. Ce message fournit des détails supplémentaires sur la détection de l'alerte. Vous pouvez inclure des valeurs de champs, telles que le nom d'un conteneur, l’ID de l'événement, etc.. dans ce message.

Voici un exemple de sortie que l'on peut retrouver dans Falco:

output: "File below a known binary directory opened for writing (user=%user.name command=%proc.cmdline file=%fd.name)"

Cet output utilise des champs avec un préfixe "%" pour leur appel. Nous y indiquons le nom de l'utilisateur, la commande utilisée ainsi que le fichier. Le résultat de cette sortie serait :

"File below a known binary directory opened for writing (user=john command=/usr/bin/cat file=/usr/bin/test)"

Création de notre première règle

Dans cet exemple, nous allons créer notre première règle qui nous alertera lors de l'exécution d'un shell à partir d'un conteneur.

La condition suivante permet de différencier l'hôte du conteneur en utilisant container.id != host. Elle déclenchera un événement si le processus est nommé "bash", initié par "execve", et vérifiera si le processus parent existe et n'est ni "bash" ni "docker" :

condition: container.id != host and proc.name = bash and evt.type = execve and evt.dir=< and proc.pname exists and not proc.pname in (bash, docker)

Pour améliorer la lisibilité et la taille de la condition, nous utilisons des macros et une liste de binaires pour identifier les différentes variantes de shells :

   - macro: container
      condition: container.id != host

    - macro: spawned_process
      condition: evt.type = execve and evt.dir=<

    - macro: shell_procs
      condition: proc.name in (shell_binaries)

    - list: shell_binaries
      items: [bash, csh, ksh, sh, tcsh, zsh, dash]

Notre règle finale est ainsi optimisée en utilisant ces champs macros et la liste de binaires déjà configurés dans les règles par défaut de Falco, réduisant ainsi la taille totale de notre règle :

    - rule: shell_in_container
      desc: notice shell activity within a container
      condition: >
        spawned_process and
        container and
        shell_procs
      output: >
        shell in a container
        (user=%user.name container_id=%container.id container_name=%container.name
        shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)
      priority: WARNING

Cette règle est intégrée dans un bloc 'rule', incluant les champs suivants :

  • 'desc' pour décrire l'événement déclencheur
  • 'condition' pour spécifier la surveillance requise,
  • 'output' pour le message généré lors de la détection
  • 'priority' pour indiquer la criticité de l'événement.

Déploiement d’une règle dans Falco

Maintenant que nous avons défini une règle, nous pouvons la déployer à partir de notre chart. Pour ce faire, créez un fichier values.yaml et ajoutez-y les lignes suivantes :

# values.yaml 
...
customRules:
  rule-run-shell-container.yaml: |- 
   - list: shell_binaries
      items: [bash, csh, ksh, sh, tcsh, zsh, dash]

    - macro: shell_procs
      condition: proc.name in (shell_binaries)

    - rule: shell_in_container
      desc: notice shell activity within a container
      condition: >
        spawned_process and
        container and
        shell_procs
      output: >
        shell in a container
        (user=%user.name container_id=%container.id container_name=%container.name
        shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)
      priority: WARNING
...

Le paramètre tty permet à Falco d'envoyer immédiatement les événements sans les mettre en attente dans un buffer. Nous le conservons dans le cadre de nos tests.

# values.yaml
...
tty: true
...

Il ne reste plus qu’à lancer notre chart avec le fichier values.yaml :

helm upgrade falco -f values.yaml falcosecurity/falco 

Nous disposons maintenant d’une règle nous alertant en cas de création d’un processus bash à l’intérieur d’un conteneur.

Vous pouvez tester cette règle à partir d’un Pod en exécutant un bash à l'intérieur :

kubectl run evil-event --image=ubuntu -- sh

# bash

Vous trouverez dans les logs du Pod Falco l’événement suivant :

08:52:48.515727809: Warning shell in a container (user=root container_id=87bf980d4d3d container_name=evil-event shell=bash parent=sh cmdline=bash) container_id=87bf980d4d3d container_image=docker.io/library/ubuntu container_image_tag=latest container_name=evil-event k8s_ns=default k8s_pod_name=evil-event

Suivi des détections et intégration aux outils d'alerting

Maintenant que Falco est déployé sur notre cluster, nous pouvons le connecter à des solutions de messagerie, d’alerting ou de monitoring.

Pour activer Sidekick, il suffit d'ajouter les valeurs suivantes dans le fichier values.yaml de la chart :

# values.yaml
...
falcosidekick:
  enabled: true
...

Cela permettra d'activer le composant Sidekick en provisionnant un déploiement dans le namespace Falco.

Configuration d’un canal de messagerie

Pour connecter Sidekick à Slack, il est nécessaire de créer une application Slack simple et d'associer un webhook à un canal spécifique. Vous recevrez alors une URL que vous devrez intégrer dans votre fichier values.yaml (Lien vers la documentation Slack)

En fonction de l'emplacement et du type de votre cluster Kubernetes, il est envisageable d'ajouter des champs supplémentaires pour fournir des informations complémentaires, notamment si vous disposez de plusieurs clusters avec Falco installé.

# values.yaml
...
falcosidekick:
  enabled: true
  config:
    slack:
      webhookurl: < Votre url webhook >
  config:
    customfields: "environment:dev,datacenter:paris"
...

Une fois cette étape achevée, exécutez la commande helm upgrade pour finaliser la configuration de Sidekick.

Après avoir effectué un second test avec un Pod, vous recevrez une alerte comprenant toutes les informations nécessaires.

Message Slack par le plugin Sidekick

Par la suite, vous pourrez configurer Sidekick pour qu'il communique avec Alert Manager ou d'autres outils similaires (consultez la liste des intégrations disponibles).

Conclusion

Il est crucial de prendre en compte les risques de sécurité liés à l'utilisation des solutions de containerisation. Bien que l'intégration d'outils tels que Falco ne puisse pas résoudre tous les défis sécuritaires, elle offre une visibilité détaillée sur le comportement des conteneurs spécifiques, permettant ainsi de mettre en œuvre des correctifs ciblés.

En adoptant des solutions comme Falco, les organisations renforcent leur posture de sécurité et se préparent efficacement à faire face aux menaces émergentes dans l'écosystème Kubernetes.

Pour conclure, je vous recommande vivement d'instaurer un rituel régulier dédié à la sécurité et à l'optimisation des solutions déjà en place. Cela inclut non seulement l'analyse du code à l'aide d'outils SAST/DAST, mais également la mise en place de bonnes pratiques pour la configuration de vos clusters Kubernetes. En intégrant ces actions dans vos processus habituels, vous renforcez la robustesse et la fiabilité de votre environnement.