Sommaire
À propos
Cet article est le deuxième d’une série qui consiste à plonger en profondeur dans Cilium, l’outil novateur de gestion de réseau d’un cluster Kubernetes. La dernière fois, nous avions introduit le sujet en analysant l’installation de Cilium et ce qui en fait sa spécificité. Aujourd’hui, nous allons pouvoir rentrer dans le vif du sujet en abordant concrètement et techniquement certains aspects techniques de Cilium, à commencer par :
- L’équilibrage de charge des services et la propagation des IP publiques (« Load Balancing and Announcements »)
- La configuration des politiques de trafic réseau, qui peuvent se faire aux niveaux 3, 4, voire 7 du modèle OSI
Le dernier article sera, lui, dédié à ces exigences non fonctionnelles fondamentales qui balisent notre métier : la Sécurité et l’Observabilité.
L’Équilibrage de charge
Cilium est capable de prendre en charge l’équilibrage de charge des services. Ce processus natif de l’API Kubernetes consiste en deux étapes :
- D’abord attribuer une adresse IP d’un pool déterminé afin de répartir de manière équitable le trafic d’un service exposé à l’extérieur du cluster. Nous allons traiter cette partie en configurant un pool d’adresses IP à partir duquel les services de type LoadBalancer pourront se voir attribuer une adresse IP pour une entrée de l'extérieur.
- D’annoncer cette adresse IP aux routeurs en amont afin qu’ils puissent router correctement le trafic de manière automatique (si l’adresse change, les routeurs doivent être mis à jour de manière appropriée et automatique). Cette section va nous permettre d’aborder un sujet beaucoup plus méconnu, à savoir la différence entre le routage sur la base des informations disponibles dans la couche 2 à l’aide du protocole ARP, et le routage en couche 3/4 l’aide du protocole BGP.
Configuration de Cilium pour faire de l’équilibrage de charge
Il n’y a qu’une seule condition pour « activer » la possibilité de faire de l’équilibrage de charge dans Cilium, c’est de créer une Custom Resource de type CiliumLoadBalancerIPPool qui va contenir, tout simplement, un CIDR.
Pour rappel : Un CIDR (Classless Inter-Domain Routing) est la représentation d’un sous-réseau par une adresse IP et son masque de sous réseau (par exemple "172.19.0.0/16") du pool d’adresses utilisables pour l’attribution à un service Kubernetes de type LoadBalancer.
Un exemple :
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
namespace: kube-system
name: "my-pool"
spec:
cidrs:
- cidr: "172.19.255.0/24"
Dès que la CiliumLoadBalancerIPPool (nom raccourci : ippool) est créée, les services de type LoadBalancer qui étaient en statut <Pending> (faute de configuration activant l’équilibrage de charge) se voient automatiquement attribuer une adresse IP.
Mais, avoir une adresse IP externe, c'est bien, faire en sorte que le reste du monde soit au courant, c’est mieux !
Annonces des IP externes, des services équilibrés
Une fois qu’un service dispose d’une adresse IP externe prête à être utilisée de manière publique (soit directement, soit au travers d’un nom de domaine) il faut pouvoir faire en sorte que les réseaux soient au courant de son existence. C’est un mécanisme dédié, appelé Announcement, qui remplit cette tâche.
Il y a deux grandes méthodes (entre autres) d’annonce qui peuvent s’appliquer ici :
- Une méthode simple, mais qui passe difficilement à l’échelle, se reposant sur ARP (Address Resolution Protocol: un protocole qui permet d’obtenir l’adresse physique d’une adresse IP).
- Une méthode plus complexe, mais qui a été conçue pour passer à l’échelle, se reposant sur BGP (Border Gateway Protocol: un protocole spécifiquement conçu pour que des Systèmes Automatisés (AS) s’échangent automatiquement des règles de routage).
La méthode layer2/ARP
Celles et ceux qui ont utilisé MetalLB dans sa configuration par défaut seront en terrain familier : c’est exactement de ça dont il s’agit.
En résumé rapide :
- Le nœud qui porte le service exposé est le seul à recevoir le trafic final (ce qui est logique).
Si un autre nœud reçoit une requête ARP concernant l’adresse IP du service exposé, il répond avec l’adresse du nœud qui porte le service exposé et une redirection sNAT (Source Network Address Translation, un mode de redirection de trafic qui change l’adresse IP de la source) est appliquée, ce qui permet de rediriger le trafic au sein du cluster :
Redirection layer2/ARP classique
Cela a donc toutes les caractéristiques d’un équilibrage de charge, même s’il y a la possibilité que le trafic arrive initialement sur un nœud occupé, voire hors service, avant d’être redirigé vers le nœud qui expose effectivement le service. Ceci étant dit, il y a de toute façon un problème de fond : on ne veut pas que les nœuds passent leur temps à se demander lequel d’entre eux porte le service exposé, on veut que le trafic soit correctement routé dès le départ.
C’est pour ça que Cilium ne se repose pas sur la méthode ARP, mais sur la seconde méthode, basée sur BGP.
La méthode BGP
La méthode BGP est extrêmement puissante, car c’est elle qui gère la quasi-totalité des annonces d’adresses IP sur Internet. Elle repose sur une logique finalement assez simple :
- Il existe des « Systèmes Automatisés » (AS, Autonomous Systems), donc des systèmes capables d’adapter automatiquement leurs tables de routage et de faire eux même des annonces.
- Ces systèmes communiquent entre eux au travers du protocole BGP deux à deux : un système avec son « voisin » (neighbor).
- Il est possible de quadriller un réseau entier en appairant deux à deux les différents systèmes
- BGP permet, contrairement à des protocoles plus simples comme ARP, d’utiliser des techniques de routage avancées comme ECMP (Equal Cost Multi Path, l’utilisation de plusieurs liens physiques pour le même chemin logique), ou BFD (Bidirectional Forwarding Detection, la détection rapide d’échecs de routage pour de la remédiation automatique dans les réseaux), etc.
Dans Cilium, la fonctionnalité pérenne pour gérer le BGP par Cilium se fait en mettant la valeur --set bgpControlPlane.enabled=true dans le Chart Helm, ou en installant Cilium par la CLI en y ajoutant l’option --enable-bgp-control-plane=true. Un ancien mode de fonctionnement de BGP est disponible dans Cilium mais déprécié en 1.13 donc inutile de s’y attarder ici.
Cilium permet de créer une Custom Resource de type CiliumBGPPeeringPolicy qui va indiquer :
- Les nœuds du cluster concernés par BGP
- Les routeurs BGP virtuels qu’on va créer dans le cluster
- Les « voisins » (neighbors) concernés par la connexion BGP
- Les services dont on va permettre l’annonce sur les réseaux surjacents
Par exemple, si on veut une politique qui :
- S’applique sur les nœuds disposant d’un label « a ».
- Crée un routeur virtuel ayant pour numéro d’AS 65000 (à partir de 65 000, c'est considéré comme privé)
- Exporte le CIDR de tous les pods
- Annonce tous les services de type LoadBalancer (par défaut aucun ne l’est !)
- Se couple avec un réseau surjacent dont le CIDR est 10.0.0.0/16.
Schématisé, ça donnerait ça :
Exemple de maillage entre des nœuds et des routeurs avec BGP
On peut alors écrire le manifeste suivant :
apiVersion: "cilium.io/v2alpha1"
kind: CiliumBGPPeeringPolicy
metadata:
name: 01-bgp-peering-policy
spec: # CiliumBGPPeeringPolicySpec
nodeSelector:
matchLabels:
bgp-policy: a
virtualRouters: # []CiliumBGPVirtualRouter
- localASN: 65000
exportPodCIDR: true
serviceSelector:
matchExpressions:
# Force broadcasting all services to upstream router
- {key: somekey, operator: NotIn, values: ['never-used-value']}
neighbors: # []CiliumBGPNeighbor
- peerAddress: 10.0.0.0/16
peerASN: 65000
Il suffit alors de correctement labelliser un nœud pour que la politique s’applique :
$ kubectl label nodes my-node bgp-policy=a
node/my-node labeled
Maintenant se pose la question : « Comment valider que la politique BGP s’applique correctement ? »
C’est là qu’intervient un outil comme BIRD (The « BIRD Internet Routing Daemon ») qui permet de gérer automatiquement le routage des systèmes UNIX-like en s’appuyant sur un certain nombre de règles et de protocoles dont BGP. On peut donc utiliser BIRD pour simuler le comportement qu’aurait un routeur qui gère nativement le BGP.
Sans rentrer dans la configuration fine de BIRDv2, la partie importante à configurer est celle-ci :
protocol bgp router1 {
local 172.18.0.213 as 65000;
neighbor 172.18.0.4 as 65000;
ipv4 {
import all;
export all;
};
}
Cette entrée permet, de manière bilatérale, d’échanger les informations de routage IPv4 entre deux AS (ici local et neighbor au sein d’un AS virtuel ayant pour ASN 65000). Naturellement, vous pouvez configurer BIRD pour ne faire en sorte que de recevoir, que de donner, etc. De plus, si vos AS nécessitent une authentification, une gestion sécurisée est possible pour éviter de donner ou de recevoir n’importe quoi à n’importe qui sur le même réseau.
Une fois la configuration effectuée, un birdc show protocols all permet de valider que la connexion est bien établie avec le(s) nœud(s) et que les adresses IP publiques sont bien annoncées :
vm1 BGP --- up 09:02:04.293 Established
BGP state: Established
Neighbor address: 10.17.31.50
Neighbor AS: 65000
Local AS: 65000
Neighbor ID: 198.20.0.4
Local capabilities
<...snip...>
Routes: 5 imported, 3 exported, 1 preferred
Route change stats: received rejected filtered ignored accepted
Import updates: 5 0 0 0 5
Import withdraws: 0 0 --- 0 0
Export updates: 10 7 0 --- 3
Export withdraws: 0 --- --- --- 0
BGP Next hop: 10.17.31.1
IGP IPv4 table: master4
Par exemple, ici, nous avons un service exposé en 172.18.255.107, et il apparaît comme tel dans un ip route (l’entrée mentionne d’ailleurs qu’elle provient de bird). Si on essaie d'accéder au service exposé à l’aide de curl, tout fonctionne comme prévu, car notre système a bien une route définie pour atteindre l’IP du service exposé :
$ ip route | grep 172.18.255.107
172.18.255.107 via 172.18.0.2 dev br-3b9d79dd2b15 proto bird metric 32
$ curl 172.18.255.107
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
Politiques Réseau
Tout part de la CiliumNetworkPolicy
Dans Cilium toutes les politiques de routage se font à l’aide d’une Custom Resource de type CiliumNetworkPolicy. Tous les détails d’application de la politique (et donc indirectement sa couche OSI de fonctionnement) se retrouvent dans la définition d’une ressource de ce type.
Ceci est naturellement dû au fait que l’eBPF permet à Cilium d’avoir un niveau de compréhension total du modèle, car étant présent dans le noyau Linux qui sous-tend tout le cluster.
La différence principale entre une CiliumNetworkPolicy et la NetworkPolicy du standard Kubernetes est qu’elle va utiliser les sélecteurs pour déterminer les entités au sens Cilium (des pods, services, le réseau hôte du cluster, le monde extérieur, etc.) sur lesquelles s’applique la politique réseau, là où la NetworkPolicy va uniquement chercher à déterminer les pods sur lesquels vont s’appliquer la politique réseau.
L’autre différence est la finesse de la politique : là où une NetworkPolicy traite des sujets sur les couches L3 et L4 du modèle OSI, une CiliumNetworkPolicy permet d’aborder les sujets de politique réseau sur la couche L7 en plus des deux autres. De plus, comme on va le voir, elle permet de mélanger les différentes caractéristiques des couches si c’est là notre besoin.
Politique de routage L3
La couche L3 du modèle OSI est celle qui traite de la couche « réseau » du modèle (soit tout ce qui a trait aux adresses IP, à IPSec, ICMP, etc.). Elle se situe entre la couche L2 (celle des adresses MAC, des paquets ARP, etc.) et la couche L4 (TCP, UDP, etc.) qu’on verra plus loin.
Créer une politique de routage sur la couche L3 consiste à définir des règles qui vont concerner plus ou moins directement les adresses IP. On y retrouve le désormais fameux CIDR, mais aussi tout ce qui peut trivialement se convertir en adresse IP, comme les noms de domaines ou même les services Kubernetes (qui ne sont finalement qu’une IP devant d’autres IP). Et comme en plus dans Kubernetes, on peut adresser plusieurs services avec des étiquettes (« Labels »), il est également possible de définir une politique L3 en n’utilisant que les labels.
De fait, les manifestes suivants sont des manifestes traitant de la couche L3 :
- Autoriser le trafic entrant (ingress) vers les services étiquetés backend depuis les services étiquetés frontend :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "front-to-backends"
spec:
endpointSelector:
matchLabels:
role: backend
ingress:
- fromEndpoints:
- matchLabels:
role: frontend
- Autoriser le trafic des services étiquetés dev vers le réseau hôte du cluster (notez le toEntities qui permet de lister des entités au sens Cilium du terme) :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "dev-to-host"
spec:
endpointSelector:
matchLabels:
env: dev
egress:
- toEntities:
- host
- Autoriser le trafic sortant des services étiquetés app=myService vers le CIDR 20.1.1.1/32 ainsi que le 10.0.0.0/8, mais pas son sous-réseau 10.96.0.0/12 :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "cidr-rule"
spec:
endpointSelector:
matchLabels:
app: myService
egress:
- toCIDR:
- 20.1.1.1/32
- toCIDRSet:
- cidr: 10.0.0.0/8
except:
- 10.96.0.0/12
- Autoriser le trafic sortant des services étiquetés app=test-app vers l’adresse IP qui est renvoyée par une requête DNS sur my-remote-service.com (en respectant le TTL —Time To Live— qui s’applique sur ce type de requête) :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "to-fqdn"
spec:
endpointSelector:
matchLabels:
app: test-app
egress:
- toFQDNs:
- matchName: "my-remote-service.com"
Attention : Il est à noter que tant qu’un endpoint (au sens de Cilium) n’est concerné par aucune politique de routage d’entrée (respectivement sortie) de trafic, il est en Default Allow pour l’entrée (resp. sortie) de trafic. Par contre, dès qu’un endpoint est concerné par une seule règle d’entrée (resp. sortie) de trafic, il passe automatiquement en Default Deny sur l’entrée (resp. la sortie) de trafic. Il faut alors s’assurer que la/les politiques qui l’incluent sont correctement configurées. Cela peut surprendre les plus novices, mais la commande cilium endpoint list permet de voir le mode de fonctionnement qui s’applique à chaque ingress sur l’entrée (resp. la sortie) de trafic :
ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY
ENFORCEMENT ENFORCEMENT
123 Enabled Disabled 34512
Comme on peut le constater, la définition d’une politique réseau L3 est très souple dans Cilium.
Politique de routage L4
La couche L4 du modèle OSI est celle qui traite de la couche « transport » du modèle (soit tout ce qui a trait aux protocoles qui s'appuient sur le réseau, comme TCP et UDP par exemple). Elle se situe entre la couche L3 (celle des adresses IP) et la couche L5 (SOCKS, NetBIOS, PPTP, RTP, etc.).
Créer une politique de routage sur la couche L4 permet d’aller un peu plus loin qu’avec la L3. En effet, la L4 va permettre d’adresser les spécificités de transport, à savoir les ports ainsi que les protocoles, là où une L3 va autoriser ou interdire le trafic vers une adresse IP dans sa globalité.
Ce sont donc les politiques réseaux d’une plus grande finesse qui sont gérées de cette manière :
- Autoriser le trafic sortant des services étiquetés app=myService vers n’importe quelle adresse IP, mais seulement sur le port 80 et uniquement pour le protocole TCP :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l4-rule"
spec:
endpointSelector:
matchLabels:
app: myService
egress:
- toPorts:
- ports:
- port: "80"
protocol: TCP
- Naturellement, on peut combiner un aspect L3 (comme un CIDR) et un aspect L4 (comme un port ou un protocole) dans une seule politique. Ici, on autorise une sortie de trafic d’un service crawler que vers le sous-réseau 192.0.2.0/24, et uniquement sur le port 80 et le protocole TCP :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "cidr-l4-rule"
spec:
endpointSelector:
matchLabels:
role: crawler
egress:
- toCIDR:
- 192.0.2.0/24
toPorts:
- ports:
- port: "80"
protocol: TCP
On voit là la souplesse et la granularité fine que permet l’inspection des données sur de multiples couches du modèle OSI.
L7
La couche L7 du modèle OSI, peut-être la plus connue, est celle qui traite de la couche « application » du modèle (soit tout ce qui a trait au plus haut niveau comme les protocoles HTTP, FTP, SMTP, etc.). Elle se situe au-dessus de la couche L6 (celle sur laquelle sont basés les protocoles de haut niveau, et qui comprend MIME, ASCII, et PGP).
Étant la plus haute des couches, c’est également celle qui va permettre de renvoyer des informations plus précises sur les violations de politiques réseau : typiquement, une violation de type HTTP va permettre de renvoyer une erreur 403 plutôt que de simplement empêcher le trafic de passer.
Voici une liste d’exemples de politiques réseaux concernant la couche L7 :
- N’autoriser que les requêtes entrantes HTTP utilisant la méthode GET et uniquement vers le chemin /public des services étiquetés app=service (on peut constater que la configuration L7 est une sous-partie de la L4 car on utilise ici une configuration avancée de toPorts déjà vu plus haut) :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "Allow HTTP GET /public from env=prod to app=service"
endpointSelector:
matchLabels:
app: service
ingress:
- fromEndpoints:
- matchLabels:
env: prod
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/public"
- N’autoriser que les requêtes entrantes HTTP utilisant la méthode PUT et uniquement vers le chemin /public des services étiquetés app=service, et il faut en plus que les requêtes aient un entête X-My-Header: true :
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l7-rule"
spec:
endpointSelector:
matchLabels:
app: service
ingress:
- toPorts:
- ports:
- port: '80'
protocol: TCP
rules:
http:
- method: PUT
path: "/public"
headers:
- 'X-My-Header: true'
Kafka (expérimental)
Cilium est d’ailleurs en train d’aller encore plus loin dans le traitement des politiques, car une implémentation expérimentale d’un méta-protocole Kafka est en cours d’implémentation. Si elle advient, elle pourra permettre de définir des règles de trafic en se basant sur des informations propres à Kafka, comme les roles et les topics:
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "enable empire-hq to produce to empire-announce and deathstar-plans"
endpointSelector:
matchLabels:
app: kafka
ingress:
- fromEndpoints:
- matchLabels:
app: empire-hq
toPorts:
- ports:
- port: "9092"
protocol: TCP
rules:
kafka:
- role: "produce"
topic: "deathstar-plans"
- role: "produce"
topic: "empire-announce"
Nul doute que d’autres méta-protocoles (qu’on pourrait considérer comme étant un hypothétique niveau L8 dans le modèle OSI) pourraient apparaître dans le futur !
En conclusion
Rentrer dans le cœur de Cilium nous a permis de constater à la fois la flexibilité et la puissance de sa configuration, pour une application à de multiples cas d’usage.
Le prochain article nous permettra d’en apprendre encore davantage sur Cilium, en passant des politiques réseaux au maillage de services, à l’inspection TLS, et à l’observabilité détaillée de Cilium lui-même !