Aller au contenu
5 nuances de Network Policy

5 nuances de Network Policy

Romain Boulanger
Auteur
Romain Boulanger
Architecte Infra/Cloud avec une touche de DevSecOps
Sommaire

Le réseau dans Kubernetes, un rappel important
#

Lorsque l’on déploie dans Kubernetes, on a parfois tendance à oublier un concept extrêmement important basé sur le modèle réseau de Kubernetes : tous les Pods peuvent communiquer avec tous les autres Pods du cluster, quel que soit le namespace dans lequel ils se trouvent.

Et cette caractéristique est totalement voulue ! Kubernetes adopte un modèle de réseau plat où chaque Pod obtient sa propre adresse IP au niveau du cluster. Cela veut dire qu’un Pod dans le namespace frontend peut parfaitement communiquer avec un Pod dans le namespace database sans aucune restriction par défaut.

Cette simplicité facilite grandement la communication entre les services déployés, mais pose une question cruciale pour la sécurité : comment limiter efficacement les communications entre les Pods ?

Dans un environnement de production, cette perméabilité totale représente un risque considérable en matière de sécurité. Si un Pod compromis peut librement contacter n’importe quelle ressource du cluster, l’attaquant dispose d’un terrain de jeu pour se déplacer de manière latérale.

C’est exactement pour cette raison que l’ajout de politiques réseau avec l’objet NetworkPolicy devient un élément incontournable dans une stratégie de sécurisation d’un cluster Kubernetes.

Mais alors, comment mettre en place ce contrôle ? Et surtout, jusqu’où peut-on aller dans la granularité côté filtrage ?

Dans cet article, j’ai voulu faire un état de l’art des possibilités offertes par Kubernetes sur ce concept, que ce soit nativement mais aussi en étendant l’API avec des CustomResourceDefinitions (CRD).

La NetworkPolicy, le premier rempart
#

Ses avantages
#

La NetworkPolicy fait partie intégrante de l’API Kubernetes depuis bien des années. Elle permet de définir des règles de filtrage au niveau de la couche 3 et 4 du modèle OSI, en autorisant ou en bloquant le trafic en fonction des adresses IP, des ports et des protocoles.

Sa force réside dans son intégration native avec le système de labels de Kubernetes, ce qui rend sa prise en main relativement intuitive notamment pour un développeur sans connaissance réseau.

Imaginez un cas d’usage classique : vous disposez d’une application composée d’un frontend et d’un backend. Votre frontend doit pouvoir contacter le backend sur le port 8080, mais aucun autre Pod du cluster ne devrait avoir accès à ce service.

Voici comment il est possible de retranscrire cette règle avec une NetworkPolicy :

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

Exemple de NetworkPolicy

Cette politique s’applique à tous les Pods portant le label app: backend dans le namespace production. Elle autorise uniquement le trafic entrant en provenance des Pods étiquetés app: frontend, sur le port TCP 8080. Tout autre trafic sera implicitement refusé, car dès qu’une NetworkPolicy sélectionne un Pod, celui-ci adopte un comportement de type default deny pour les types de trafic spécifiés.

L’approche déclarative et la compatibilité avec n’importe quelle Container Network Interface (CNI) qui implémente les NetworkPolicy (Calico, Cilium, Weave, etc.) constituent des avantages indéniables.

Vous définissez vos règles au format YAML, et le CNI se charge de les traduire en configurations réseau au sein du cluster, facile non ?

Mais aussi ses limites…
#

Cependant, la NetworkPolicy montre rapidement ses limites lorsqu’on cherche à implémenter des politiques de sécurité plus sophistiquées. Elle opère exclusivement aux couches 3 et 4, ce qui signifie que vous ne pouvez filtrer que sur des critères comme les adresses IP, les ports et les protocoles. Impossible de filtrer sur la méthode HTTP utilisée ou sur le chemin d’une requête.

De plus, la NetworkPolicy est une ressource namespacée. Si vous souhaitez appliquer une règle de sécurité globale au niveau du cluster : par exemple, bloquer l’accès à serveur de métadonnées spécifique, comme c’est le cas dans le Cloud, il sera nécessaire de créer une politique identique dans chaque namespace du cluster. Vous l’aurez compris, cette approche devient vite fastidieuse dans des clusters comportant des dizaines ou des centaines de namespaces.

La granularité des actions reste binaire : on autorise ou on refuse, il n’est pas possible d’auditer ou même d’avoir de la visibilité sur le trafic.

Enfin, cette dernière ne propose pas véritablement de deny un flux en particulier. En reprenant l’exemple du dessus avec la volonté de bloquer un serveur de métadonnées (169.254.169.254), il est nécessaire de tout autoriser sauf une IP, ce qui rend l’approche peu naturelle :

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-metadata-server
  namespace: production
spec:
  podSelector: {}  
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32

Exemple de NetworkPolicy bloquant le serveur de métadonnées sur le Cloud

Pour les environnements exigeant une approche Zero Trust avec des contrôles d’accès basés sur l’identité (ServiceAccount) et le contexte applicatif, la NetworkPolicy native s’avère aussi insuffisante.

C’est là qu’interviennent les alternatives proposées par différents acteurs de l’écosystème Kubernetes.

ClusterNetworkPolicy, me voilà !
#

Face aux limitations de la NetworkPolicy, plusieurs initiatives ont émergé pour enrichir les capacités de filtrage réseau dans Kubernetes. L’une d’entre elles est la ClusterNetworkPolicy, une extension développée dans le cadre du Network Policy API Working Group au sein de la communauté Kubernetes SIG Network.

La ClusterNetworkPolicy répond à un besoin précis : permettre aux administrateurs de cluster d’appliquer des règles de sécurité réseau au niveau global, sans qu’elles puissent être contournées ou annulées par des politiques au niveau des namespaces. Contrairement à la NetworkPolicy qui s’adresse principalement aux développeurs d’applications, la ClusterNetworkPolicy cible explicitement les administrateurs de cluster qui doivent imposer des garde-fous de sécurité globaux.

L’un des apports majeurs de cette ressource se trouve dans son système de tiers et d’actions. Plutôt que de se limiter à “autoriser” ou “refuser” le trafic, la ClusterNetworkPolicy introduit trois actions :

  • Allow : autorise le trafic et arrête l’évaluation des règles suivantes ;
  • Deny : refuse explicitement le trafic ;
  • Pass : délègue l’évaluation à la règle suivante ou au tier inférieur.

Ces actions sont évaluées dans un ordre précis défini par les tiers : les règles du tier Admin sont évaluées en premier, suivies des NetworkPolicy classiques au niveau des namespaces, puis du tier Baseline.

Cette hiérarchie garantit que les administrateurs du cluster peuvent définir des politiques de sécurité qui ne peuvent être contournées : par exemple, bloquer le trafic vers des sous-réseaux sensibles mais en laissant aux développeurs une certaine autonomie pour gérer les flux internes à leurs applications.

Voici un exemple de ClusterNetworkPolicy qui bloque tout trafic sortant vers un sous-réseau considéré comme malveillant :

apiVersion: networking.x-k8s.io/v1alpha1
kind: ClusterNetworkPolicy
metadata:
  name: block-malicious-network
spec:
  tier: Admin
  priority: 100
  policyTypes:
    - Egress
  egress:
    - action: Deny
      to:
        - ipBlock:
            cidr: 203.0.113.0/24

Cette politique s’applique à tous les Pods du cluster, dans tous les namespaces. Même si un développeur crée une NetworkPolicy autorisant le trafic vers cette plage d’adresses, la règle de tier Admin prévaudra et bloquera la communication.

L’inconvénient principal de la ClusterNetworkPolicy tient à son statut : il s’agit d’une ressource encore en phase alpha dans le cadre du projet Network Policy API. Son adoption n’est pas encore généralisée et dépend du support par les implémentations des CNI. Cependant, elle représente une direction prometteuse pour standardiser les contrôles réseau au niveau cluster au sein de Kubernetes.

À ce jour, il n’existe pas encore de version disponible, mais vous pouvez déployer la CustomResourceDefinition (CRD) pour expérimenter ce concept.

Les AdminNetworkPolicy et BaselineAdminNetworkPolicy n’étant plus activement développées, je ne m’attarderais pas dessus. La ClusterNetworkPolicy étant la nouvelle version des deux précédentes.

NetworkPolicy et GlobalNetworkPolicy, des alternatives à la sauce Calico
#

Pendant que la communauté Kubernetes travaille sur la standardisation de la ClusterNetworkPolicy, Calico, l’un des CNI les plus populaires, propose depuis longtemps ses propres extensions pour pallier aux limites de la NetworkPolicy. Deux ressources se démarquent : la NetworkPolicy (Calico) et la GlobalNetworkPolicy.

La NetworkPolicy de Calico, bien que portant le même nom que la ressource Kubernetes standard, offre des fonctionnalités significativement plus riches. Elle conserve le caractère namespacé de son équivalent natif, mais introduit plusieurs mécanismes avancés.

Parmi eux, on trouve la notion d’ordre de priorité via le champ order, qui permet de contrôler l’évaluation des règles : les politiques avec une valeur d’ordre plus faible sont appliquées en premier. Cette capacité résout l’un des problèmes de la NetworkPolicy native, où l’absence de priorité rend difficile la composition de règles complexes.

Calico ajoute également un ensemble d’actions étendues : Allow, Deny, Log et Pass.

L’action Log permet d’enregistrer le trafic correspondant sans le bloquer ni l’autoriser, ce qui s’avère précieux pour l’audit et le debugging. L’action Pass fonctionne de manière similaire à celle de la ClusterNetworkPolicy : elle indique au moteur de politique de sauter les règles restantes et de passer au profil associé au endpoint.

Mais l’apport le plus important est dans le support des politiques au niveau applicatif (couche 7). Calico permet de filtrer le trafic HTTP en fonction de la méthode ou du chemin de la requête:

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: api-gateway-policy
  namespace: production
spec:
  order: 100
  selector: app == "api-gateway"
  types:
    - Ingress
  ingress:
    - action: Allow
      protocol: TCP
      http:
        methods: ["GET"]
        paths: ["/api/v1/users"]
      source:
        selector: app == "frontend"
    - action: Deny

Exemple de NetworkPolicy selon l’API de Calico

Cette politique autorise uniquement les requêtes HTTP GET vers le chemin /api/v1/users en provenance du frontend, et refuse tout autre trafic. On franchit ici un palier en termes de granularité.

Cette fonctionnalité s’appelle l’Application layer policy (ALP) et demande quelques éléments de configuration avec l’ajout de Sidecar tout en introduisant des limitations notamment l’impossibilité d’avoir un Service Mesh en parallèle ou de filtrer qu’en entrée.

La GlobalNetworkPolicy, quant à elle, répond au besoin de politiques à l’échelle d’un cluster. Contrairement à la NetworkPolicy (native ou Calico), elle n’est pas namespacée et s’applique à l’ensemble du cluster. Cette dernière permet de définir des règles de sécurité globales en un seul endroit, sans duplication.

Un cas d’usage typique consiste à bloquer systématiquement l’accès à un ensemble de ports sensibles pour tous les Pods du cluster :

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: deny-sensitive-ports
spec:
  order: 10
  types:
    - Ingress
  ingress:
    - action: Deny
      protocol: TCP
      destination:
        ports: [22, 3389, 5432]

Exemple de GlobalNetworkPolicy

Cette GlobalNetworkPolicy bloque l’accès SSH, RDP et PostgreSQL pour tous les Pods, quel que soit le namespace. Elle peut être combinée avec des NetworkPolicy plus permissives au niveau des namespaces, grâce au système d’ordre : en assignant une valeur d’ordre faible à cette règle globale, elle sera évaluée en priorité.

L’avantage de l’approche de Calico se répercute dans sa maturité et son adoption large au sein de la communauté. Ces ressources sont éprouvées en production depuis plusieurs années et bénéficient d’un écosystème d’outils de visualisation et de débogage (comme Calico Enterprise ou le projet open source calicoctl).

L’inconvénient, en revanche, tient à la dépendance au CNI : ces politiques ne fonctionnent qu’avec Calico, ce qui peut poser problème si vous envisagez de changer de couche réseau ou que cette dernière est imposée par votre distribution Kubernetes.

CiliumNetworkPolicy, avec eBPF le champ des possibles augmente !
#

Si Calico a ouvert la voie aux politiques réseau avancées, Cilium repousse encore plus loin les limites grâce à sa fondation sur eBPF (Extended Berkeley Packet Filter). eBPF permet d’exécuter du code directement dans le noyau Linux de manière sécurisée et performante, offrant ainsi des capacités de filtrage et d’observabilité impossibles à atteindre avec des solutions traditionnelles basées sur iptables.

Cilium propose deux ressources principales pour la gestion des politiques réseau : CiliumNetworkPolicy et CiliumClusterwideNetworkPolicy. La première est namespacée, tandis que la seconde opère au niveau cluster. Toutes deux supportent des règles s’étendant de la couche 3 à la couche 7, avec une expressivité qui dépasse largement celle de la NetworkPolicy classique.

Au niveau de la couche 7, Cilium offre un éventail important de fonctionnalités : il supporte nativement de nombreux protocoles applicatifs : HTTP, gRPC, Kafka, DNS, et bien d’autres.

Vous pouvez ainsi filtrer les requêtes HTTP en fonction de la méthode, du chemin, des headers, ou encore des paramètres de query string. Pour Kafka, il est possible de contrôler l’accès à des topics spécifiques. Pour DNS, on peut autoriser ou bloquer les résolutions vers certains domaines.

Voici un exemple de CiliumNetworkPolicy qui autorise uniquement les requêtes HTTP GET vers un chemin spécifique, tout en bloquant les requêtes POST :

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: frontend-api-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/api/v1/.*"

Exemple de CiliumNetworkPolicy

Cette politique s’applique aux Pods portant le label app: api et autorise uniquement les requêtes HTTP de type GET vers les chemins correspondant à l’expression régulière /api/v1/.*, en provenance des Pods possédant le label app: frontend. Toute autre méthode HTTP ou tout autre chemin sera refusé.

La CiliumClusterwideNetworkPolicy ajoute une dimension supplémentaire en permettant de cibler non seulement les Pods, mais aussi les nœuds du cluster grâce au Node Selector. Cette capacité s’avère intéressante pour appliquer des règles de sécurité spécifiques à certains nœuds : par exemple, des nœuds hébergeant des charges de travail sensibles ou situées dans une zone réseau à risque.

L’avantage de Cilium ne s’arrête pas à la richesse fonctionnelle. Grâce à eBPF, les performances de traitement des paquets sont nettement supérieures à celles des solutions reposant sur iptables. Les règles sont compilées en bytecode eBPF et exécutées directement dans le kernel, ce qui réduit la latence et améliore le débit, même sous charge de travail élevée.

De plus, Cilium intègre nativement des outils d’observabilité comme Hubble, qui permettent de visualiser en temps réel les flux réseau et de déboguer les politiques.

L’inconvénient, ici encore, réside dans la dépendance au CNI. Si vous utilisez Cilium, vous bénéficiez de ces capacités avancées, si vous changez de solution réseau, vous devrez les migrer vers une autre solution.

Par ailleurs, la courbe d’apprentissage de Cilium est plus raide que celle de solutions plus traditionnelles, en raison de la richesse des options et des concepts à maîtriser.

AuthorizationPolicy, le Zero Trust à l’état pur
#

Jusqu’ici, on a exploré des solutions de filtrage réseau opérant principalement aux couches 3, 4 et 7 du modèle OSI. Ces approches reposent sur des critères comme les adresses IP, les ports, les labels Kubernetes, ou encore les méthodes HTTP. Mais qu’en est-il de l’identité des charges de travail ? C’est précisément là qu’intervient Istio, et plus particulièrement sa ressource AuthorizationPolicy.

Istio est un Service Mesh dont je vous ai déjà parlé qui permet soit en mode Sidecar ou Ambient d’intercepter le trafic grâce à Envoy. Ce proxy intercepte tout le trafic entrant et sortant, permettant ainsi de mettre en œuvre des fonctionnalités avancées de sécurité, d’observabilité et de gestion du trafic. L’AuthorizationPolicy exploite cette architecture pour offrir un contrôle d’accès basé sur l’identité des charges de travail au format SPIFFE, établie via mTLS (mutual TLS).

Lorsqu’un Pod A souhaite communiquer avec un Pod B, les proxies Envoy établissent une connexion mTLS, chacun pouvant vérifier l’identité de l’autre. L’AuthorizationPolicy peut alors filtrer le trafic sur un compte de service (ServiceAccount), un namespace, ou même des attributs extraits de tokens JWT.

L’AuthorizationPolicy supporte trois actions principales : ALLOW, DENY, et AUDIT. L’action ALLOW autorise le trafic correspondant aux règles spécifiées. L’action DENY le refuse explicitement, et prend priorité sur les règles ALLOW (un modèle de sécurité “deny by default” peut ainsi être facilement mis en œuvre). L’action AUDIT enregistre les requêtes sans affecter le flux, ce qui permet de tester des politiques avant de les appliquer.

La force de ce type de politique est sa capacité à filtrer en fonction de critères applicatifs diversifiés : méthodes HTTP, chemins, headers, claims JWT, et même des attributs personnalisés issus de métadonnées Istio.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: backend-admin
  namespace: production
spec:
  selector:
    matchLabels:
      app: backend
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/frontend
      to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/admin*"]
      when:
        - key: request.auth.claims[role]
          values: ["admin"]

Exemple d’AuthorizationPolicy

Cette politique s’applique aux Pods portant le label app: backend dans le namespace production. Elle autorise uniquement les requêtes provenant d’un workload identifié par le service account frontend dans le même namespace, et seulement si la méthode HTTP est GET ou POST, que le chemin correspond à /api/admin*, et enfin que le claim JWT dispose d’un champ role a la valeur admin. Tout autre trafic sera refusé.

À noter que l’AuthorizationPolicy ne permet de filtrer qu’en entrée, pas possible de contrôler les flux sortants. Néanmoins vous pouvez la combiner avec une NetworkPolicy native pour combler ce manque.

Cette approche incarne la philosophie Zero Trust et du principe du moindre privilège. Plutôt que de se fier à des critères réseau volatils ou des labels qui peuvent être modifiés, on s’appuie sur un mécanisme d’identité en associant cela pourquoi pas à un contexte applicatif pour prendre des décisions d’autorisation. L’AuthorizationPolicy peut même déléguer l’autorisation à un système externe via l’action CUSTOM, permettant ainsi d’intégrer des plateformes de gestion des accès tierces ce qui permet là aussi, d’étendre les possibilités.

Istio ne permet pas à proprement parler de faire des politiques étendues à l’ensemble du cluster sous forme d’objet, mais vous pouvez utiliser le namespace racine istio-system pour l’appliquer à l’ensemble du Mesh.

Pour résumer, la caractéristique principale d’Istio et de l’AuthorizationPolicy se trouve dans cette profondeur de contrôle et dans les nombreux mécanismes de sécurité qu’offre l’outil.

En revanche, l’adoption d’un Service Mesh implique une complexité opérationnelle non négligeable : il vous faudra déployer, que ce soit sous forme Sidecar ou Ambient, une couche d’infrastructure supplémentaire au sein de votre cluster, même si celle-ci est compatible quel que soit la CNI utilisée.

Quelques mots en guise de conclusion
#

Vous l’aurez compris, les possibilités offertes par Kubernetes sont grandes, notamment grâce à la possibilité d’étendre l’API avec des CustomResourceDefinition (CRD), et, il y en a clairement pour tous les goûts.

Une chose est sûre, la NetworkPolicy de base, directement inclue dans Kubernetes se retrouve limitante en termes de fonctionnalités surtout quand on a besoin de filtrer avec une granularité très fine les flux réseau.

Est-ce que la ClusterNetworkPolicy viendra compléter la NetworkPolicy dans un futur proche ? Personnellement je l’espère, avec pourquoi pas quelques caractéristiques additionnelles comme le filtrage couche 7 ou le fait de se baser sur des identités que l’on retrouve chez Cilium et Istio.

Articles connexes