Au sein d’un cluster Kubernetes, il vient rapidement un moment où l’on souhaite rajouter une couche de sécurité, notamment en ce qui concerne les flux réseaux.
Dans une optique de réduction des coûts, on peut se retrouver à vouloir mutualiser sur un même cluster plusieurs ensembles (contrats, clients, etc.) qui ne doivent en aucun cas pouvoir communiquer entre eux. Toutefois, chacun de ces ensembles doit pouvoir communiquer librement avec les autres composants communs du cluster (Kube DNS, Kubernetes API, etc.) et être atteignable par des composants définis (Ingress controller, stack de monitoring, etc.)
Pour résoudre cette problématique, différentes solutions peuvent être mises en place. Nous verrons les avantages et inconvénients de chacune et le choix vers lequel nous nous sommes dirigés au final.
Après une rapide analyse, deux principales solutions ont été posées sur la table : l’utilisation d’une solution de service mesh a l’instar d’istio ou linkerd ; ou l’utilisation des Network Policies (objet natif apparu en 2017 dans kubernetes 1.8).
Les Network Policies sont un objet natif de Kubernetes qui permet de contrôler les flux réseaux entre les pods.
Après avoir étudié les différentes solutions, nous avons décidé d'utiliser les Network Policies pour la ségrégation des flux réseaux au sein de notre cluster Kubernetes.
Les raisons de notre choix:
Dans un premier temps, nous avons voulu utiliser les Network Policies en mode paranoïaque : c'est-à-dire refuser tout trafic entrant et sortant d’un namespace donné, puis autoriser au cas par cas les flux en fonction des besoins.
Cette approche, bien que beaucoup plus sécurisée, avait deux défauts majeurs :
In fine, il a été décidé de laisser le trafic sortant des pods de chaque namespace libre d’aller où bon lui semble (ce qui était déjà le comportement avant mise en place des Network Policies). Seul le trafic entrant doit être régulé. En effet, si on souhaite ne pas autoriser de trafic entre un namespace A et un namespace B, on peut le laisser quitter A ; il suffit de ne pas le laisser entrer dans B.
Ainsi, une liste de namespaces autorisés à communiquer les uns avec les autres a été dressée. Les règles de network policies on pu être appliquées en se basant soit sur un système de labels apposés sur les namespaces soit sur un nom de namespace en particulier.
Voici un exemple :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-namespace
namespace: monitoring-test01
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
client: test01
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress
Dans cet exemple, le trafic du namespace monitoring-test01 est autorisé pour tous les pods provenant des namespaces :
La validation d’un POC nous a amené à considérer une industrialisation de cette solution. En effet, le déploiement à la main des Network Policies serait une tâche fastidieuse et répétitive. Il est donc indispensable d’automatiser cette tâche.
Afin de simplifier la labellisation du namespace et la mise en place de la Network Policy, nous avons mis en place un chart Helm dédié que nous avons publié sur Docker hub. Ce dernier se compose :
Au niveau des values du chart, on notera :
Dans notre cas d’utilisation, les namespaces à protéger contenaient déjà un chart Helm. Afin d’appliquer la solution de manière automatisée, nous l’avons intégrée en temps que dépendance (suivant un pattern d’umbrella chart).
Dans un second temps, la mise en place de l’industrialisation se fait via l’utilisation du gestionnaire de policies Kyverno. Ce dernier permet de définir des “cluster policies” qui s’occuperont de labelliser les namespaces et générer les network policies.
Le regroupement des namespaces pouvant communiquer entre eux se fait par l’application d’un label commun sur ces derniers. Avec Kyverno, nous pouvons utiliser une cluster policy de type mutation pour ajouter ce labels sur nos namespaces. Dans notre cas, les labels à regrouper sont tous suffixés par l’identifiant du client (test02 dans notre exemple). La policy suivante ajoute le label client=test02 sur les namespaces concernés :
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-client-label-namespaces
annotations:
policies.kyverno.io/title: Add client label to namespaces
policies.kyverno.io/category: Client isolation
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Namespace
kyverno.io/kyverno-version: 1.8.0
policies.kyverno.io/minversion: 1.7.0
kyverno.io/kubernetes-version: "1.24"
policies.kyverno.io/description: >-
Add a client label on client namespaces
spec:
mutateExistingOnPolicyUpdate: true
rules:
- name: label-client-namespaces
match:
any:
- resources:
kinds:
- Namespace
mutate:
targets:
- apiVersion: v1
kind: Namespace
patchStrategicMerge:
metadata:
<(name): "*-test02"
labels:
contract: test02
Ainsi, tout namespace existant ou futur étant suffixé par le nom du client se verra appliquer le label désiré.
Une fois nos namespaces labellisés, il devient possible d’appliquer les Network Policies (car elles se basent sur une sélection de namespaces par labels).
Pour cela, nous allons utiliser une cluster policy de type génération. Dans l’exemple ci-dessous, nous nous occuperons encore de notre client test02 :
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: generate-client-namespace-network-policy
annotations:
policies.kyverno.io/title: Add client Network Policy
policies.kyverno.io/category: Client isolation
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: NetworkPolicy
kyverno.io/kyverno-version: 1.8.0
policies.kyverno.io/minversion: 1.7.0
kyverno.io/kubernetes-version: "1.24"
policies.kyverno.io/description: >-
Network Policies are used to control the traffic flow between Pods in a cluster.
This policy allow communication between client namespaces.
spec:
generateExisting: true
rules:
- name: generate-client-namespace-network-policy
match:
any:
- resources:
kinds:
- Namespace
selector:
matchLabels:
client: test02
generate:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: allow-ingress-namespace
namespace: ""
synchronize: true
data:
spec:
podSelector: { }
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
client: ""
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress
...
Cette cluster policy génèrera dans tous les namespaces ayant le label client=test02 une network policy qui autorise le trafic depuis les namespaces labellisés client=test02 et depuis le namespace qui porte le nom “ingress”.
Les Network Policies sont une solution native, simple et efficace pour la ségrégation des flux réseaux au sein d'un cluster Kubernetes. Elles sont une bonne alternative aux services Mesh pour les environnements où la simplicité, les performances et le coût sont des facteurs importants.
Cependant, il est important de connaître les limitations des Network Policies et de mettre en place des mesures pour les compenser.
Pour pallier ces limitations, il est possible de mettre en place des mécanismes de surveillance afin de détecter les anomalies ainsi que des politiques de sécurité pour limiter les risques.