Comment j'ai hacké mon propre serveur - Kubernetes et la sécurité

Introduction

Malgré le titre un peu accrocheur, cet article est inspiré de faits réels qui me sont arrivés il y a quelques jours. Je me suis dit qu’il serait intéressant de vous partager cette petite mésaventure afin d’aborder le sujet de la sécurité des clusters Kubernetes.

Un peu de contexte

Je possède un RaspberryPI qui me sert de serveur et sur lequel j’ai installé Kubernetes pour déployer quelques applications. Si certains se demandent si “ce n’est pas un peu overkill ?”, la réponse est certainement oui, mais bon… après tout pourquoi pas ?

Afin de pouvoir accéder à mon serveur depuis n’importe où, j’ai décidé d’exposer mon port SSH. Cependant, soucieux de vouloir bien faire et de sécuriser tout cela au mieux, j’ai pris quelques précautions :

  • J’ai tout d’abord paramétré mon serveur SSH afin d’accepter seulement des connexions via une paire de clés et donc de refuser les connexions via un simple mot de passe
  • J’ai ensuite installé et configuré fail2ban, un outil permettant de bannir des adresses IP qui tenteraient des attaques par force brute
  • Enfin, au cas où cela ne suffirait pas, j’ai décidé de mettre en place du port knocking avec knockd, un outil permettant de modifier le comportement d'un firewall tel que iptable en temps réel pour provoquer l'ouverture d'un port suite au lancement préalable d'une suite de connexions sur des ports distincts et dans le bon ordre (tel un code qu’on frapperait à une porte)

Cette troisième action permet donc de filtrer le port SSH, et de ne l’exposer que pendant un court instant lorsque nécessaire. Ceci peut se faire avec les règles iptables suivantes :

Chain INPUT (policy ACCEPT)
target   prot opt source     destination
ACCEPT   tcp  --  anywhere   anywhere   tcp dpt:22 ctstate ESTABLISHED
DROP     tcp  --  anywhere   anywhere   tcp dpt:22

Le processus en charge d’exposer le port est alors en capacité de rajouter de manière temporaire une règle au début de la table INPUT :

ACCEPT   tcp  --  192.168.1.10  anywhere   tcp dpt:22

Seulement voilà, un beau jour, ce fameux processus s’est mystérieusement arrêté de fonctionner, m’empêchant d’accéder à mon serveur… Il ne me restait alors qu’un seul point d’entrée : le Kubernetes dashboard, me permettant notamment de lancer des pods.

Accéder à l'hôte depuis un conteneur

Un peu dépité, je me lance dans la recherche de cette solution, sans savoir si cela est effectivement possible, du moins de manière plutôt simple.

Je commence donc par créer un pod avec le manifest suivant :

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["/bin/bash"]
    stdin: true
    tty: true

Comme je ne spécifie pas d’utilisateur particulier, l’utilisateur par défaut de cette image est utilisé, à savoir l’utilisateur root. Cependant, j’ai beau avoir un accès root dans mon conteneur, je suis “bloqué” dans ce dernier, ou plutôt devrais-je dire restreint aux namespaces linux assignés à mon conteneur, et je n’ai donc pas (encore) accès à l’hôte.

Je me souviens alors qu’il y a un paramètre permettant de faire tourner mon conteneur en mode privilégié, me permettant donc d’avoir accès au noyau linux de mon hôte. Je le rajoute donc à mon manifest dans la partie securityContext, ce qui donne :

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["/bin/bash"]
    stdin: true
    tty: true
    securityContext:
      privileged: true

J’ai désormais un pod avec un conteneur privilégié pouvant accéder au noyau linux de mon hôte, mais… je bloque un peu.

Après plusieurs recherches, je découvre la commande linux nsenter, permettant d’exécuter un programme dans un namespace que l’on peut spécifier, notamment celui d’un autre programme. Je me dis alors que si j’arrive à accéder aux processus de mon hôte depuis mon conteneur, je peux peut-être m’en sortir.

Je me rends donc sur la documentation de Kubernetes et découvre assez rapidement une option bien pratique (mais aussi bien dangereuse d’un point de vue sécurité) : le paramètre de pod hostPID. Cette option permet d’accéder aux processus de l’hôte depuis un conteneur, ce qui est exactement ce que je cherchais.

Arrêtons nous quelques instants sur ces notions de processus et de namespaces. Comme vous le savez peut-être déjà, les conteneurs fonctionnent grâce aux namespaces linux ainsi qu’aux groupes de contrôle cgroup, permettant d’apporter une couche d’isolation. Ainsi, lorsqu’on liste les processus actifs depuis un conteneur, on obtient un résultat similaire à :

root@ubuntu:/# ps aux
USER    PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      1  0.0  0.0   4624  3828 pts/0    Ss   Feb08   0:00 /bin/bash
root      9  0.0  0.0   7056  1588 pts/0    R+   00:36   0:00 ps aux

On peut noter la présence de seulement deux processus : le principal d’id 1, qui correspond à l’entrypoint de notre image, et le second d’id 9, qui correspond au processus que nous venons de lancer afin de lister les processus actifs. Le conteneur étant isolé, nous ne voyons pas les autres processus actifs de la machine hôte.

Pour bien comprendre ce qu’il se passe ainsi que le fonctionnement d’un conteneur, on peut lancer une commande depuis le conteneur (ici la commande sleep 3600), puis récupérer l’id de ce processus depuis ce même conteneur :

root@ubuntu:/# ps aux | grep sleep
root       10  0.0  0.0   2784  1112 pts/0   S+  00:41  0:00 sleep 3600

Si nous récupérons l’id de ce processus depuis la machine hôte, nous obtenons un résultat différent :

root@serveur:/# ps aux | grep sleep
root     5480  0.0  0.0   2784  1112 pts/0   S+  00:41  0:00 sleep 3600

Cette différence s’explique par le fait que le conteneur ne partage pas le même namespace de PID que l’hôte. Le processus n’a donc pas le même id selon les namespaces, bien qu’il s’agisse du même processus.

À présent, listons les namespaces de ce processus depuis l’hôte :

root@serveur:/# ls -al /proc/5480/ns
total 0
dr-x--x--x 2 root root 0 Feb 11 14:09 .
dr-xr-xr-x 9 root root 0 Feb 11 14:09 ..
lrwxrwxrwx 1 root root 0 Feb 11 14:09 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 ipc -> 'ipc:[4026532596]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 mnt -> 'mnt:[4026532707]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 net -> 'net:[4026532598]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 pid -> 'pid:[4026532709]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 pid_for_children -> 'pid:[4026532709]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 uts -> 'uts:[4026532708]'

Si nous comparons les identifiants des namespaces mnt (mount), net (network) et pid (process id) avec ceux du processus d'initialisation de notre machine d’id 1, nous constatons qu’aucun n’est identique :

root@serveur:/# ls -al /proc/1/ns
total 0
dr-x--x--x 2 root root 0 Feb 11 14:06 .
dr-xr-xr-x 9 root root 0 Feb 11 14:06 ..
lrwxrwxrwx 1 root root 0 Feb 11 14:09 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 ipc -> 'ipc:[4026532331]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 mnt -> 'mnt:[4026532329]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb 11 14:06 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 uts -> 'uts:[4026532330]'

Si maintenant nous comparons ces mêmes identifiants entre le processus d’initialisation et celui correspondant au shell courant, nous constatons qu’ils sont tous les trois identiques :

root@serveur:/# ls -al /proc/$$/ns
total 0
dr-x--x--x 2 root root 0 Feb 11 14:09 .
dr-xr-xr-x 9 root root 0 Feb 11 14:08 ..
lrwxrwxrwx 1 root root 0 Feb 11 14:09 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 ipc -> 'ipc:[4026532331]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 mnt -> 'mnt:[4026532329]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb 11 14:09 uts -> 'uts:[4026532330]'

Jusque là tout est normal : notre conteneur ainsi que tous les processus exécutés depuis ce dernier ne partagent pas les mêmes namespaces mnt, net et pid que notre machine hôte (et ce même en étant privilégié), tandis que deux processus exécutés depuis l’hôte si.

Voyons maintenant l’effet de l’option hostPID en modifiant notre manifest :

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["/bin/bash"]
    stdin: true
    tty: true
    securityContext:
      privileged: true
  hostPID: true

Si nous listons les processus actifs depuis le conteneur avec la commande ps aux, nous obtenons désormais le même résultat que lorsqu’on exécute cette même commande depuis l’hôte : nous avons accès aux processus de la machine hôte.

Suivons la même démarche et lançons la commande sleep 3600 depuis notre conteneur. Si nous listons les processus actifs depuis notre conteneur, nous obtenons :

root@ubuntu:/# ps aux | grep sleep
root    215013  0.0  0.0   2784  1016 pts/0  S+  00:50  0:00 sleep 3600

Et depuis notre machine hôte, nous obtenons cette fois-ci le même identifiant :

root@serveur:/# ps aux | grep sleep
root    215013  0.0  0.0   2784  1016 pts/0  S+  00:50  0:00 sleep 3600

Ce comportement est dû au fait que notre conteneur partage désormais le même namespace pid que notre hôte. Comme précédemment, nous pouvons lister les namespaces de ce processus :

root@serveur:/# ls -al /proc/215013/ns
total 0
dr-x--x--x 2 root root 0 Feb  9 00:52 .
dr-xr-xr-x 9 root root 0 Feb  9 00:50 ..
lrwxrwxrwx 1 root root 0 Feb  9 00:52 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 ipc -> 'ipc:[4026532716]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 mnt -> 'mnt:[4026532713]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 net -> 'net:[4026532717]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb  9 00:52 uts -> 'uts:[4026532826]'

Et les comparer aux namespaces du processus d’initialisation de l’hôte :

root@serveur:/# ls -al /proc/1/ns
total 0
dr-x--x--x 2 root root 0 Feb  8 20:35 .
dr-xr-xr-x 9 root root 0 Feb  8 20:35 ..
lrwxrwxrwx 1 root root 0 Feb  8 23:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 ipc -> 'ipc:[4026532331]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 mnt -> 'mnt:[4026532329]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb  8 20:35 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 uts -> 'uts:[4026532330]'

Cette fois, nous constatons que l’identifiant du namespace pid est identique, à savoir 4026532332. Cependant, ce n’est pas le cas des namespaces mnt et net. Essayons alors d’ajouter l’option hostNetwork à notre manifest :

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["/bin/bash"]
    stdin: true
    tty: true
    securityContext:
      privileged: true
  hostPID: true
  hostNetwork: true

Procédons de la même manière et comparons les namespaces du processus sleep 3600 :

root@serveur:/# ls -al /proc/222277/ns
total 0
dr-x--x--x 2 root root 0 Feb  9 00:58 .
dr-xr-xr-x 9 root root 0 Feb  9 00:58 ..
lrwxrwxrwx 1 root root 0 Feb  9 00:58 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 ipc -> 'ipc:[4026532830]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 mnt -> 'mnt:[4026532831]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb  9 00:58 uts -> 'uts:[4026532330]'

Avec ceux du processus d’initialisation :

root@serveur:/# ls -al /proc/1/ns
total 0
dr-x--x--x 2 root root 0 Feb  8 20:35 .
dr-xr-xr-x 9 root root 0 Feb  8 20:35 ..
lrwxrwxrwx 1 root root 0 Feb  8 23:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 ipc -> 'ipc:[4026532331]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 mnt -> 'mnt:[4026532329]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb  8 20:35 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 uts -> 'uts:[4026532330]'

Comme on pouvait s’y attendre, les namespaces net sont eux aussi identiques, ce qui permet donc aux processus s’exécutant dans le conteneur de partager les mêmes interfaces réseau que l’hôte.

Maintenant, essayons de modifier l’entrypoint de notre image ubuntu en utilisant la commande nsenter :

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - name: ubuntu
    image: ubuntu:latest
    command: ["nsenter", "--mount=/proc/1/ns/mnt", "--", "/bin/bash"]
    stdin: true
    tty: true
    securityContext:
      privileged: true
  hostPID: true
  hostNetwork: true

Comme nous l’avons vu plus tôt, cette commande permet d’exécuter un programme dans un namespace que l’on peut spécifier, qui peut notamment être celui d’un autre processus. Ici, nous essayons donc d’exécuter le programme /bin/bash dans le même namespace mnt que le processus d’initialisation de notre machine hôte, étant donné que nous partageons également le même namespace pid grâce à l’option hostPID, nous permettant de voir les processus de l’hôte.

Procédons une nouvelle fois de la même manière et comparons les namespaces du processus sleep 3600 :

root@serveur:/# ls -la /proc/224396/ns
total 0
dr-x--x--x 2 root root 0 Feb  9 01:00 .
dr-xr-xr-x 9 root root 0 Feb  9 01:00 ..
lrwxrwxrwx 1 root root 0 Feb  9 01:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 ipc -> 'ipc:[4026532596]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 mnt -> 'mnt:[4026532329]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb  9 01:00 uts -> 'uts:[4026532330]'

Avec ceux du processus d’initialisation :

root@serveur:/# ls -la /proc/1/ns
total 0
dr-x--x--x 2 root root 0 Feb  8 20:35 .
dr-xr-xr-x 9 root root 0 Feb  8 20:35 ..
lrwxrwxrwx 1 root root 0 Feb  8 23:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 ipc -> 'ipc:[4026532331]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 mnt -> 'mnt:[4026532329]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Feb  8 20:35 pid -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 pid_for_children -> 'pid:[4026532332]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb  8 23:12 uts -> 'uts:[4026532330]'

Cette fois-ci nous y sommes : les deux processus partagent les mêmes namespaces mnt, net et pid, comme c’est le cas de deux processus s’exécutant sur la machine hôte ou dans un même conteneur. L’utilisation du même namespace mnt permet aux processus s’exécutant dans le conteneur de partager le même système de fichier que l’hôte.

Il ne me reste donc plus qu’à exécuter la commande suivante pour m’attacher au pod créé et ainsi obtenir un shell en tant que root sur mon serveur :

$ kubectl attach -i ubuntu
If you don't see a command prompt, try pressing enter.
root@serveur:/#

La suite vous la devinez, j’ai redémarré le service knockd et tout est rentré dans l’ordre… enfin presque, puisque j’ai pris quelques mesures afin que cela ne se reproduise plus.

J’ai tout d’abord activé le service knockd afin qu’il redémarre automatiquement au démarrage de linux à l’aide de la commande suivante :

systemctl enable knockd.service

Dans un second temps, et pour éviter de tout miser sur ce fameux service, j’ai ajouté une règle iptables afin d’autoriser le trafic venant de mon réseau local, ce qui donne la configuration finale suivante :

Chain INPUT (policy ACCEPT)
target   prot opt source         destination
ACCEPT   tcp  --  anywhere       anywhere   tcp dpt:22 ctstate ESTABLISHED
ACCEPT   tcp  --  192.168.1.0/24 anywhere   tcp dpt:22
DROP     tcp  --  anywhere       anywhere   tcp dpt:22

Cependant, je ne voulais pas m’arrêter là, et je me suis renseigné sur les moyens à mettre en place pour sécuriser mon cluster Kubernetes.

Comment sécuriser son cluster Kubernetes

Kubernetes met à disposition des normes de sécurité des pods (Pod Security Standards) qui définissent trois politiques qui sont cumulatives et qui vont de très permissives à très restrictives :

  • Privileged : une politique sans restrictions offrant le niveau d’autorisations le plus large possible, autorisant de fait les élévations de privilèges connues
  • Baseline : une politique avec quelques restrictions empêchant les élévations de privilèges connues
  • Restricted : une politique avec de nombreuses restrictions suivant les meilleures pratiques actuelles concernant la sécurité des pods et des conteneurs

Il est important de noter que par défaut, un cluster Kubernetes ne dispose d’aucune restriction de sécurité des pods, ce qui équivaut à appliquer la politique privileged.

Pour faire le lien avec notre exemple, la politique baseline ne nous permet donc pas d’utiliser les namespaces de l’hôte via les options hostPID et hostNetwork, ou encore de faire tourner notre conteneur en mode privilégié. La politique restricted ne nous le permet pas non plus, et nous empêche de surcroît de lancer notre conteneur avec l’utilisateur root. Vous trouverez le détail de ces politiques et de leurs effets sur la documentation de Kubernetes.

En plus de ces Pod Security Standards, Kubernetes met à disposition des Admission Controllers, et notamment le Pod Security Admission qui est présent par défaut depuis la version 1.23 et qui remplace les Pod Security Policies qui ont disparues avec la version 1.25. C’est ce contrôleur qui permet d’appliquer et de faire respecter les Pod Security Standards en se basant sur des labels de namespaces.

Le Pod Security Admission controller possède trois modes :

  • enforce : le non-respect de la politique entraînera le rejet et la non-création du pod
  • audit : le non-respect de la politique entraînera l'ajout d'une annotation d'audit à l'événement enregistré dans le journal d'audit (audit log) mais autorisera la création du pod
  • warn : le non-respect de la politique entraînera l’affichage d’un avertissement destiné à l’utilisateur mais autorisera la création du pod

Pour bien comprendre comment tout cela fonctionne, rien de tel qu’un bon exemple :

apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.26
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.26
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.26

Ici, nous avons ajouté au namespace my-namespace des labels afin de configurer le comportement du Pod Security Admission controller.

Si nous essayons à présent de créer notre fameux pod, nous obtenons l’erreur suivante :

Error from server (Forbidden): error when creating "pod.yaml": pods "ubuntu" is forbidden: violates PodSecurity "baseline:v1.26": host namespaces (hostNetwork=true, hostPID=true), privileged (container "ubuntu" must not set securityContext.privileged=true)

Cela est dû au fait que notre manifest ne respecte pas la norme de sécurité baseline, qui est la norme associée au mode enforce du Pod Security Admission controller.

L’erreur étant plutôt claire, il nous suffit de supprimer les options hostNetwork, hostPID et privileged pour que l’erreur disparaisse. En revanche, si nous réessayons de créer notre pod, nous obtenons l’avertissement suivant :

Warning: would violate PodSecurity "restricted:v1.26": allowPrivilegeEscalation != false (container "ubuntu" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "ubuntu" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "ubuntu" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "ubuntu" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

Cette fois-ci, cet avertissement apparaît car notre manifest ne respecte pas la norme de sécurité restricted, qui est la norme associée au mode warn.

Si nous modifions le label de notre namespace correspondant au mode enforce et que nous remplaçons baseline par restricted, l’avertissement se transforme en erreur :

Error from server (Forbidden): error when creating "pod.yaml": pods "ubuntu" is forbidden: violates PodSecurity "restricted:v1.26": allowPrivilegeEscalation != false (container "ubuntu" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "ubuntu" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "ubuntu" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "ubuntu" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

Pour satisfaire la politique restricted, il faudrait alors ajouter à notre manifest les options allowPrivilegeEscalation=false, runAsNonRoot=true, capabilities.drop=["ALL"] et seccompProfile.type=RuntimeDefault.

On ne rentrera pas dans le détail de chacune de ces options dans cet article, mais il s’agit des bonnes pratiques de sécurité à appliquer à vos pods si vous voulez sécuriser au mieux vos clusters Kubernetes. Je ne peux donc que vous recommander de respecter ces bonnes pratiques et de mettre en place ces restrictions au niveau de vos namespaces en ajoutant simplement les six labels que nous venons de voir. Si la norme restricted vous paraît trop contraignante, vous pouvez toujours restreindre un minimum vos utilisateurs en utilisant la norme baseline, tout en affichant des avertissements quant aux violations de la norme restricted.

Conclusion

Au travers de cette petite mésaventure, j’ai souhaité aborder et peut-être même vous faire découvrir certaines options de sécurité présentes dans Kubernetes et qui me semblent importantes à connaître et à maîtriser. Cela nous a également permis de voir comment sécuriser ses clusters Kubernetes et ce de manière extrêmement simpliste et efficace.

Sources