Aller au contenu
Edouard Topin's Blog
Sécurité Kubernetes en production / Série 02/04

Network policies et Cilium : construire un default-deny défendable

L'API NetworkPolicy est livrée avec Kubernetes ; l'appliquer est le travail du CNI. Ce que Cilium ajoute, ce qui reste standard, et comment atteindre le default-deny sans casser la production.

Edouard Topin
23 min de lecture
Illustration éditoriale abstraite d'un maillage de flux réseau dont seuls quelques chemins restent ouverts.

Un auditeur ouvre le sujet réseau sur ton cluster de production et pose une seule question : la preuve de ce qui est refusé, pas la liste de ce qui est autorisé. Sur vks-fret-01, les trois namespaces webshop-web, webshop-app et webshop-db partagent le cluster avec quatre autres équipes, aucune NetworkPolicy n’existe, et la base répond à n’importe quel pod. Personne ne sait quels flux existent réellement — et ce n’est pas une négligence d’exploitation, c’est une non-capacité que le projet Kubernetes publie lui-même.

Cet article sépare trois plans que la moitié des contenus tiers mélange : l’API amont networking.k8s.io/v1 et la liste des dix choses qu’elle ne fait pas, ce que Cilium ajoute par-dessus en cilium.io/v2 avec ses limites publiées, et ce que VKS livre réellement. Il en tire un parcours de bascule en quatre phases dont la première est une phase d’observation. Le décor Nordwind Logistics est une hypothèse de travail : aucun lab n’a été exécuté pour cet article, et tout ce qui dépend d’une instance est signalé comme tel.

API amont networking.k8s.io/v1CRD Cilium cilium.io/v2Aucun lab exécuté

TL;DR

  • La décision : le default-deny s’obtient par sélection sans règle d’autorisation, jamais par un mot-clé deny — l’API amont publie qu’elle n’a pas de refus explicite. Et un objet appliqué sans greffon qui l’implémente ne filtre rien, sans erreur ni avertissement.
  • L’arbitrage qui coûte : changer de CNI pour obtenir le refus explicite et la journalisation est un choix qui se paie en portabilité — les politiques Cilium et Antrea sont des CRD hors arbre Kubernetes, et sur VKS le CNI se déclare à la création du cluster. Avant d’en arriver là, vérifie ce que le CNI déjà en place documente : sur vks-fret-01, Antrea couvre déjà ces deux points.
  • Lundi matin : un test négatif dans un namespace jetable. Applique un default-deny-all, vérifie qu’un flux est réellement coupé, supprime l’objet. Un kubectl apply réussi ne prouve rien.

Contrat de version

Cette série tourne sur un plancher Kubernetes 1.35 et une cible 1.36. Deux raisons pour le plancher : la branche 1.34 sort de support le 2026-10-27, avant la parution de cet article, et l’add-on Cilium de VKS exige publiquement « VKS 3.6 or later, and VKr 1.35 or later ». La branche de documentation Cilium citée ici est v1.20 ; l’ancrage Antrea est 2.6.1, version livrée dans VKr 1.36.1.

Une dérive à connaître : le dépôt officiel kubernetes/sig-release publie pour la 1.37 une date cible du 26 août 2026, donc antérieure à cette publication. Concrètement, les trois branches supportées au moment où tu lis ceci seront 1.35, 1.36 et 1.37 — mais aucune page de fonctionnalité 1.37 n’a été chargée pour cet article, et rien de ce qui suit ne décrit une capacité 1.37.

Mécanisme apiVersion État publié Version
NetworkPolicy networking.k8s.io/v1 stable — (la page ne porte pas de bandeau FEATURE STATE)
NetworkPolicy — champ endPort networking.k8s.io/v1 stable v1.25
AdminNetworkPolicy / BaselineAdminNetworkPolicy policy.networking.k8s.io/v1alpha1 alpha, hors arbre, classées « Previous » par leur projet fusionnées dans ClusterNetworkPolicy
ClusterNetworkPolicy policy.networking.k8s.io/v1alpha2 alpha, hors arbre, « Active (under development) »
CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy cilium.io/v2 CRD Cilium, hors arbre Kubernetes docs v1.20
ClusterNetworkPolicy / NetworkPolicy Antrea crd.antrea.io/v1beta1 CRD Antrea, hors arbre Kubernetes Antrea 2.6.1

Deux mécanismes ne sont pas décrits ici parce qu’ils sont derrière nous. PodSecurityPolicy a été déprécié en v1.21 et retiré en v1.25 : il n’existe pas dans le plancher de cette série, et l’article RBAC le traite au passé. Et AdminNetworkPolicy n’est pas la direction du projet — voir plus bas.

Plan A — l’API amont, et les dix choses qu’elle publie ne pas faire

« Les network policies sont off par défaut » est faux dans les deux sens, et c’est le premier redressement à faire. La ressource NetworkPolicy appartient au groupe networking.k8s.io et existe dans tout cluster conforme : rien n’est à activer côté API. Ce qui manque, c’est l’exécutant, et la page amont l’écrit en prérequis :

« Network policies are implemented by the network plugin. To use network policies, you must be using a networking solution which supports NetworkPolicy. Creating a NetworkPolicy resource without a controller that implements it will have no effect. »

La traduction opérationnelle est brutale : un kubectl apply d’une politique de refus sur un cluster dont le CNI ne l’implémente pas retourne networkpolicy.networking.k8s.io/default-deny-all created. Aucune erreur, aucun avertissement, aucun paquet filtré. Le comportement par défaut d’un pod, lui, est bien tout ouvert — « By default, a pod is non-isolated for ingress; all inbound connections are allowed » — mais par absence de sélection, pas par configuration. Un pod devient isolé en entrée seulement dès qu’une politique le sélectionne et porte Ingress dans policyTypes.

Trois conditions indépendantes, donc trois modes d’échec distincts : l’objet existe toujours, le greffon peut ne pas être là, et le pod n’est isolé que s’il est sélectionné.

La liste que le projet publie sur lui-même

Section What you can’t do with network policies (at least, not yet), dix entrées, reproduites intégralement :

  • « Forcing internal cluster traffic to go through a common gateway (this might be best served with a service mesh or other proxy). »
  • « Anything TLS related (use a service mesh or ingress controller for this). »
  • « Node specific policies (you can use CIDR notation for these, but you cannot target nodes by their Kubernetes identities specifically). »
  • « Targeting of services by name (you can, however, target pods or namespaces by their labels, which is often a viable workaround). »
  • « Creation or management of “Policy requests” that are fulfilled by a third party. »
  • « Default policies which are applied to all namespaces or pods (there are some third party Kubernetes distributions and projects which can do this). »
  • « Advanced policy querying and reachability tooling. »
  • « The ability to log network security events (for example connections that are blocked or accepted). »
  • « The ability to explicitly deny policies (currently the model for NetworkPolicies are deny by default, with only the ability to add allow rules). »
  • « The ability to prevent loopback or incoming host traffic (Pods cannot currently block localhost access, nor do they have the ability to block access from their resident node). »

Trois de ces dix entrées ne sont pas des détails, ce sont des contraintes de projet qui décident du plan de bascule. Pas de refus explicite : on ne peut pas écrire « bloque webshop-db sauf webshop-app ». Pas de journalisation : la phase d’observation n’existe pas sur ce plan — on ne peut pas savoir quels flux on va casser avant de les casser. Pas de politique par défaut sur tous les namespaces : il faut un objet par namespace, appliqué à la main ou par GitOps.

Le lecteur venu chercher « comment activer le default-deny » repart donc avec l’information inverse et plus utile : l’API amont seule ne lui donne pas de quoi préparer la bascule. Ce n’est pas une opinion, c’est écrit dans sa documentation.

Le default-deny amont, tel qu’il est publié

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Trois choses à lire dans ces neuf lignes. podSelector: {} sélectionne tous les pods du namespace, pas du cluster — il n’existe pas de portée cluster dans l’API amont. Il n’y a aucune clause ingress ni egress : le refus vient de l’absence de règle, pas d’un mot-clé. Et policyTypes porte les deux directions ; en retirer Egress change complètement le comportement, c’est l’erreur la plus fréquente.

La page publie aussi l’avertissement qui vaut à lui seul un incident : « A default deny-all egress policy also blocks DNS traffic. If your workloads need DNS resolution, you must add a separate NetworkPolicy that allows egress to your cluster’s DNS service. » Le DNS est donc le premier flux à rouvrir, et il se rouvre avant d’appliquer le refus.

Pour le reste de la surface amont : le label immuable kubernetes.io/metadata.name que Kubernetes appose sur tout namespace est le contournement publié de l’entrée « Targeting of services by name » ; le champ endPort est stable depuis v1.25, avec endPort supérieur ou égal à port et les deux ports numériques ; et la note SCTP (« You must be using a CNI plugin that supports SCTP protocol NetworkPolicies ») ne porte aucun bandeau de version sur la page chargée, donc je ne lui en attribue pas.

L’API hors arbre : la piste que le projet a laissée derrière lui

Le sous-groupe network-policy-api.sigs.k8s.io de SIG Network classe ses propres API : ClusterNetworkPolicy en Active (« undergoing active development »), AdminNetworkPolicy et BaselineAdminNetworkPolicy en Previous (« have evolved to the next stage and are no longer actively developed »), DeveloperNetworkPolicy en Future. Un billet du sous-groupe daté du 9 octobre 2025, signé Nadia Pinaeva — billet de projet, pas documentation produit — publie la fusion : les deux API v1alpha1 deviennent une seule v1alpha2.ClusterNetworkPolicy avec un champ tier, et l’ordre d’évaluation publié est tier: Admin, puis l’API amont NetworkPolicy, puis tier: Baseline.

Un contenu de 2025 qui présente AdminNetworkPolicy comme l’avenir de la politique réseau décrit donc un état dépassé. Reste l’atterrissage plateforme : la page Implementations du même projet donne pour ClusterNetworkPolicy un « Standard support in Calico v3.32 », et VKr 1.36.1 livre Calico 3.31.5. Sur la plateforme de référence de cette série, cette API n’est donc pas atteignable par le Calico livré — décalage de version, pas défaut de produit.

Plan B — ce que Cilium ajoute, et ce qu’il ne réplique pas

Premier fait à poser, parce qu’il répond à « qu’est-ce que je perds » : Cilium supporte quatre ressources de politique, et la première de la liste est la ressource amont — NetworkPolicy, ClusterNetworkPolicy Kubernetes, CiliumNetworkPolicy, CiliumClusterwideNetworkPolicy. Le positionnement de la CRD est publié tel quel : « The CiliumNetworkPolicy is very similar to the standard NetworkPolicy. The purpose is to provide the functionality which is not yet supported in NetworkPolicy. » Le projet publie aussi son propre avertissement sur le cumul : mélanger plusieurs types de politiques rend l’ensemble du trafic autorisé difficile à raisonner.

L’identité change le raisonnement. Chez Cilium, « security is based on the identity of a pod, which is derived through labels. This identity can be shared between pods. » Les entités réservées sont l’apport le plus directement utile à une bascule d’egress : host (« includes the local host »), remote-node, cluster (« the logical group of all network endpoints inside of the local cluster »), world (« Allowing to world is identical to allowing to CIDR 0.0.0.0/0 ») et surtout kube-apiserver, qui « represents both deployments of the kube-apiserver: within the cluster and outside of the cluster ». Cette dernière répond à l’entrée amont « Targeting of services by name » pour ce cas précis et pour lui seul : sur l’API amont, autoriser l’egress vers l’API server oblige à écrire un ipBlock avec une IP de VIP, qui bouge.

La limite est publiée sur la même page et doit voyager avec : « The kube-apiserver entity may not work for ingress traffic in some Kubernetes distributions, such as Azure AKS and GCP GKE. » VKS n’est pas nommé dans cette phrase — je ne l’y ajoute pas et je n’en déduis rien ; c’est à vérifier sur ta plateforme.

Les trois modes d’application

Les définitions publiées, mot pour mot : default — « endpoints have unrestricted network access until selected by policy. Upon being selected by a policy, the endpoint permits only allowed traffic » ; always — « policy enforcement is enabled on all endpoints even if no rules select specific endpoints » ; never — « policy enforcement is disabled on all endpoints, even if rules do select specific endpoints ». Le réglage passe par la valeur Helm policyEnforcementMode ou le drapeau enable-policy.

Le mode always est la seule réponse propre à l’entrée amont « Default policies which are applied to all namespaces or pods ». C’est aussi le plus dangereux à activer sur un cluster de production : il isole tout, y compris ce qui n’a jamais été sélectionné, donc jamais observé.

Les politiques de refus : la précédence et sa limite

apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "external-lockdown"
spec:
  endpointSelector: {}
  ingressDeny:
  - fromEntities:
    - "world"
  ingress:
  - fromEntities:
    - "all"

La précédence est publiée : « Deny policies take precedence over allow policies, regardless of whether they are a Cilium Network Policy, a Clusterwide Cilium Network Policy or even a Kubernetes Network Policy. » Et la limite, publiée sur la même page, ne s’en sépare jamais : « Deny policies do not support: policy enforcement at L7, i.e., specifically denying an URL and toFQDNs, i.e., specifically denying traffic to a specific domain name. » Autrement dit : on ne peut pas écrire une règle de refus vers un nom de domaine.

Le L7 ne coupe pas — il répond

La structure L7Rules porte deux protocoles sur la page chargée, HTTP et DNS. Le comportement est le fait le plus contre-intuitif du plan B : « violation of layer 7 rules does not result in packet drops. Instead, if possible, an application protocol specific access denied message is crafted and returned, e.g. an HTTP 403 access denied ». Le coût architectural est publié aussi : « Layer 7 policies will proxy traffic through a node-local Envoy instance, which will either be deployed as a DaemonSet or embedded in the agent pod. » Une politique L7 n’est donc pas un filtre réseau, c’est un contrôle d’accès applicatif rendu par un proxy — un composant de plus dans le chemin de données, avec son cycle de vie.

Côté noms de domaine, « By default, Cilium uses an in-agent DNS proxy for DNS policy enforcement », le proxy autonome étant annoncé alpha. Le manifeste publié combine deux clauses d’egress : un toEndpoints vers le service DNS portant rules: dns, et un toFQDNs par matchName ou matchPattern. La dépendance mérite d’être énoncée : toFQDNs s’appuie sur les réponses vues par le proxy. Autoriser le port 53 en L4 sans la clause rules: dns laisse résoudre mais n’alimente pas la table des IP autorisées — la page ne publie pas cet enchaînement comme tel, c’est une reconstitution à vérifier sur ta plateforme.

Host Policies, et l’avertissement de verrouillage

« Host policies take the form of a CiliumClusterwideNetworkPolicy with a Node Selector instead of an Endpoint Selector », activées par --set hostFirewall.enabled=true. C’est le rattrapage de l’entrée amont « The ability to prevent loopback or incoming host traffic », et il se paie : les politiques DNS L7 d’hôte sont publiées comme beta, et ajouter des règles DNS L7 à une politique d’hôte fait passer toutes les requêtes DNS du nœud par le proxy — indisponible pendant un redémarrage de l’agent, ce qui peut rendre le nœud inutilisable, kubelet compris.

Deux réserves que j’assume à voix haute

La page Caveats de Cilium v1.20 écrit : « Cilium Network Policies do not perfectly replicate the functionality of Kubernetes Network Policies. » Elle renvoie vers une table de comparaison dont la page cible, chargée deux fois, ne contient ni la section annoncée ni la table. Je publie donc l’existence de la réserve et pas son détail : la liste des écarts ne s’invente pas.

Second trou, une divergence entre deux pages officielles chargées le même jour. La doc Cilium v1.20 écrit que le support de ClusterNetworkPolicy est « available starting with Cilium 1.20 », drapeau --enable-k8s-cluster-network-policy=true. La page Implementations de network-policy-api.sigs.k8s.io, elle, ne liste pas Cilium pour cette API — elle le range sous AdminNetworkPolicy avec la mention « tracking issue ». Je cite les deux et je ne tranche pas : une page de projet CNI documente ce que le CNI implémente, une page de conformité documente ce qui a été vérifié en conformité ; les deux peuvent être vraies en même temps.

Un écart, lui, est publié et purement opérationnel : « By default, ipBlock rules in NetworkPolicy do not match intra-cluster IPs (such as Pod or Node IPs) », comportement modifiable par --policy-cidr-match-mode (pods ou nodes). Conséquence directe pour une migration de CNI : une NetworkPolicy amont écrite sur un CNI qui fait correspondre les ipBlock aux IP de pods, puis rejouée telle quelle sur Cilium, change de sens sans changer de texte.

Plan C — ce que VKS livre réellement

C’est la distinction que cet article doit poser explicitement, parce qu’elle décide de l’ordre des travaux : sur VKS, Cilium et Antrea ne sont pas au même rang.

La page VKS Components de l’arbre 9.1 écrit : « VKS clusters support the following Container Network Interface (CNI) options: Antrea (default) and Calico », avec la précision d’implémentation « Antrea uses Open vSwitch. Calico uses Linux IP tables. » La page VKS Cluster Networking de l’arbre 9.0 dit la même chose. Cilium n’est nommé sur aucune de ces deux pages, vérifié sur les deux arbres.

Cilium existe pourtant : les notes de version VKS 3.6 le donnent comme « validated Standard Package », et une page de procédure dédiée publie ses prérequis — « Verify that your environment is running VKS 3.6 or later, and VKr 1.35 or later », plus un dépôt d’add-ons « v3.6.0+20260320 or later » installé sur le Supervisor. Version de paquet relevée : 1.19.1+vmware.1-vks.1, avec quatre capacités listées par Broadcom, dont « Kubernetes NetworkPolicy (L3/L4 Support) » et « CiliumNetworkPolicy (L7 Support) ». Hubble Relay et Hubble UI sont des options de déploiement du paquet, l’UI exigeant Relay. Ports à ouvrir en entrée : 2379-2380/TCP, 8472/UDP, 4240/TCP, 4244/TCP.

Le choix se fait à la création du cluster :

apiVersion: addons.kubernetes.vmware.com/v1alpha1
kind: AddonInstall
metadata:
  name: REPLACE_WITH_CLUSTER_NAME-cilium
  namespace: REPLACE_WITH_CLUSTER_NAMESPACE
spec:
  addonRef:
    name: cilium
  clusters:
  - constraints:
      expression: "cluster.cniRefName() == 'cilium' && version_in_range(cluster.spec.topology.version, '>=1.35.0')"
    selector:
      matchLabels:
        cluster.x-k8s.io/cluster-name: REPLACE_WITH_CLUSTER_NAME
  stopMatchingBehavior: Retain

Le CNI se déclare ensuite dans spec.topology.variables de la ressource Cluster, via bootstrapAddons.cniRef. Aucune page chargée ne décrit une bascule d’Antrea vers Cilium sur un cluster existant. C’est pour cette raison que le fil rouge introduit un second cluster, vks-fret-lab-01, plutôt qu’une migration en place — et non une coexistence de deux CNI dans le même cluster, qui n’est décrite nulle part.

Deux contraintes de plus, publiées. Un problème connu : « The VKS Istio add-on is currently incompatible with clusters provisioned using the Cilium CNI; installation will fail as a result of a known limitation in the current Cilium add-on version. » Et un décalage de version qui commande toute la lecture : le paquet VKS livre Cilium 1.19.1 quand la branche de documentation citable est v1.20. Toute capacité explicitement datée de 1.20 — le support de ClusterNetworkPolicy, précisément — est donc hors de portée du paquet livré. La règle : sur VKS, lis la doc Cilium de la version du paquet, pas celle de la branche stable.

Antrea répond déjà à deux non-capacités amont

C’est le point le plus contre-intuitif de ce plan. Antrea publie deux CRD en apiVersion: crd.antrea.io/v1beta1kind: ClusterNetworkPolicy (portée cluster) et kind: NetworkPolicy (portée namespace) — avec quatre actions de règle : "Allow", "Drop", "Reject", "Pass". Et une journalisation : lorsque enableLogging vaut true, « the first packet of any traffic flow that matches this rule will be logged to a file (/var/log/antrea/networkpolicy/np.log) on the Node on which the rule is enforced ». Six tiers sont publiés avec leurs priorités — Emergency 50, SecurityOps 100, NetworkOps 150, Platform 200, Application 250, Baseline 253 — et les politiques du tier baseline ont une précédence inférieure aux NetworkPolicy amont écrites par les développeurs.

Donc : sur vks-fret-01, avec le CNI par défaut et sans rien installer, deux des dix non-capacités publiées de l’API amont ont déjà une réponse documentée — le refus explicite et la journalisation. Un exploitant VKS qui croit devoir passer à Cilium pour obtenir une phase d’observation résout un problème qu’il n’a pas.

Qu’est-ce que je perds si je change de CNI

Aucune page ne publie ce découpage : c’est un assemblage de six pages officielles, à lire avec ses trois précautions.

Capacité API amont networking.k8s.io/v1 Antrea crd.antrea.io/v1beta1 (2.6.1) Cilium cilium.io/v2 (docs v1.20)
refus explicite publié comme absent actions Drop / Reject ingressDeny / egressDeny, précédence publiée
journalisation des flux publié comme absent enableLoggingnp.log sur le nœud Hubble + Policy Audit Mode
portée cluster publié comme absent kind: ClusterNetworkPolicy CiliumClusterwideNetworkPolicy
priorités / tiers non six tiers, priorités 50→253 absent de la page chargée ; la précédence des refus tient lieu d’ordre
ciblage par nom de service publié comme absent absent de la page chargée partiellement — entité kube-apiserver
ciblage par nom de domaine non absent de la page chargée toFQDNs (pas en refus)
L7 HTTP publié comme absent absent de la page chargée proxy Envoy, réponse 403 et non drop
politique d’hôte / nœud publié comme absent absent de la page chargée Host Policies, hostFirewall.enabled
plages de ports endPort, stable v1.25 oui oui, sauf règles DNS
ClusterNetworkPolicy v1alpha2 sans objet (hors arbre) listé sur ANP v1alpha1 divergence non tranchée

Les cases « absent de la page chargée » sont des absences de la page consultée, pas des absences de produit. Les colonnes Antrea et Cilium décrivent des CRD hors arbre : une politique écrite dans l’une n’est pas portable dans l’autre, et la seule ligne portable est celle de l’API amont. Enfin, la colonne Cilium décrit la branche de documentation v1.20 quand le paquet VKS livre 1.19.1.

Le parcours en quatre phases

Le principe qui les ordonne tient en une ligne : on n’écrit un refus qu’après avoir observé ce qu’il va couper. Aucune de ces phases n’a été exécutée ; ce qui suit est un plan de travail, pas un retour d’exploitation.

Phase 0 — vérifier que le greffon applique quoi que ce soit. Sur VKS, le CNI se lit côté ressource Cluster, pas côté pods :

kubectl get cluster REPLACE_WITH_CLUSTER_NAME \
  -n REPLACE_WITH_CLUSTER_NAMESPACE \
  -o jsonpath='{.spec.topology.variables}'

kubectl get daemonset -A

Ce qu’on s’attend à y trouver — un DaemonSet Antrea sur vks-fret-01, un DaemonSet Cilium et un objet AddonInstall sur vks-fret-lab-01 — reste une attente, pas un relevé. Le seul contrôle qui prouve quelque chose est le test négatif : un default-deny-all dans un namespace jetable, un flux qui devait être coupé et qui l’est, puis suppression de l’objet.

Phase 1 — observer, avant tout refus. C’est la phase que l’API amont ne permet pas, et l’outil dépend du plan. Sur Cilium, le Policy Audit Mode « configures Cilium to allow all traffic while logging all connections that would otherwise be dropped by network policies » et « supports auditing network policies implemented at networks layers 3 and 4 » — avec l’avertissement publié qu’il n’est « not recommended for production deployment », d’où son cantonnement au cluster de lab. Il s’active par --policy-audit-mode=true en portée daemon, ou plus étroitement par cilium-dbg endpoint config "$ENDPOINT" PolicyAuditMode=Enabled, et les verdicts se lisent ainsi :

hubble observe flows -t policy-verdict --last 1

Les verdicts publiés portent trois formes — policy-verdict:none AUDITED, policy-verdict:L3-L4 ALLOWED, policy-verdict:none DENIED — dont se déduisent labels source et destination, direction et port, donc la règle à écrire. Sur Antrea, une politique large en action Allow avec enableLogging: true écrit le premier paquet de chaque flux dans np.log sur le nœud ; l’agrégation de ce fichier vers un puits central n’est décrite par aucune page chargée. Et sur l’API amont seule, il n’y a pas de phase 1 : c’est un résultat, pas un manque à combler par un bricolage.

Phase 2 — autoriser, DNS d’abord. L’ordre est inverse de l’intuition : les autorisations s’écrivent et s’appliquent avant le refus, puisqu’une politique d’autorisation seule ne change rien tant qu’aucune politique ne sélectionne le pod dans la direction correspondante.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-dns
  namespace: webshop-app
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: REPLACE_WITH_DNS_POD_LABEL
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

REPLACE_WITH_DNS_POD_LABEL n’est pas une valeur à deviner : le label des pods du service DNS dépend de la distribution, aucune page Broadcom chargée ne le publie, et il se relève sur ton cluster. Le label de namespace kubernetes.io/metadata.name, lui, est garanti par Kubernetes. Viennent ensuite les flux applicatifs, un par relation effectivement vue en phase 1.

Phase 3 — refuser, par sélection, un namespace à la fois. Le manifeste est celui de la page amont, appliqué namespace par namespace. Ordre proposé : webshop-db d’abord — le moins de dépendances sortantes, le plus de valeur — puis webshop-app, puis webshop-web, qui porte l’entrée depuis l’ingress et casse le plus visiblement. Entre chaque namespace, trois contrôles : les sondes restent vertes, la résolution DNS répond depuis un pod du namespace, et le flux applicatif nominal passe de bout en bout.

Phase 4 — durcir, et uniquement là où c’est réversible. Trois durcissements, du moins risqué au plus risqué : le refus explicite d’un flux précis (Cilium ingressDeny/egressDeny, Antrea Drop), la politique de portée cluster (une erreur touche tout le cluster), et le mode always de Cilium (isole tout endpoint, y compris ceux jamais sélectionnés). Dans le fil rouge, aucun des trois ne s’applique à la production : phases 0 à 3 sur vks-fret-01 avec Antrea, phase 4 sur vks-fret-lab-01 avec Cilium, et une décision écrite avant toute généralisation.

Le retour arrière est asymétrique, et c’est ce qui justifie cet ordre. Supprimer un objet de test ou une autorisation ne coûte rien ; un kubectl delete networkpolicy default-deny-all -n <ns> est immédiat et granulaire ; défaire une politique de portée cluster ou le mode always est global. Et le changement de CNI n’a pas de retour arrière documenté : le CNI est figé dans la topologie à la création, donc le rollback du plan Cilium n’est pas une bascule inverse, c’est l’abandon du cluster de lab et le maintien de la production sur Antrea.

Pièges & points de vigilance

Compter les objets n’est pas auditer. Un audit qui compte les NetworkPolicy mesure une intention, pas un filtrage — « Creating a NetworkPolicy resource without a controller that implements it will have no effect ». C’est la première phrase à citer dans une réponse d’audit, avant tout tableau de couverture.

Les sondes : le piège inverse de celui qu’on attend. L’API publie que « the only allowed connections into the pod are those from the pod’s node and those allowed by the ingress list », et que les pods ne peuvent pas bloquer l’accès depuis leur nœud de résidence. Les sondes kubelet venant du nœud, le piège n’est pas qu’elles cassent : c’est qu’on croit qu’elles vont casser, qu’on écrit une règle inutile, et qu’on attribue ensuite un autre incident à la mauvaise cause. Le comportement exact dépend du greffon et du mode de sonde — à vérifier sur ta plateforme, l’article ne garantit rien ici.

L’egress oublié dans policyTypes. Un default-deny écrit avec policyTypes: [Ingress] seul donne un sentiment de couverture et laisse toute la sortie ouverte, c’est-à-dire exactement le chemin d’une exfiltration. Inversement, ajouter Egress sans avoir rouvert le DNS coupe l’application immédiatement. Les deux erreurs viennent de la même ligne.

Chercher un connection refused là où il y a un 403. Une violation de règle L7 ne produit pas de drop mais une réponse applicative. Un exploitant qui fouille les traces d’incident avec le mauvais symptôme ne trouvera rien.

Le service mesh et TLS sont hors périmètre — et c’est publié. Deux entrées de la liste renvoient explicitement ailleurs (« Anything TLS related (use a service mesh or ingress controller for this) »). Sur VKS, la combinaison a un coût : l’add-on Istio est publié comme incompatible avec les clusters provisionnés en Cilium. Un lecteur qui prévoit mesh et Cilium a une décision à prendre avant d’écrire sa première politique.

Un dernier mot sur ce que cet article ne contient pas : aucun chiffre. Ni volume de flux, ni latence, ni consommation, ni nombre de règles. Aucun n’a été trouvé sur une page de documentation chargée, et le champ des mesures eBPF est saturé de chiffres promotionnels. L’absence est un résultat, pas une lacune.

Conclusion

Le default-deny défendable ne tient pas à un produit, il tient à un ordre d’opérations et à des phrases publiées — y compris celles qui décrivent ce que le mécanisme ne fait pas. La suite logique est le plan d’admission : chaque flux refusé ici est un flux que l’article suivant, Supply chain : Sigstore, SBOM, admission control, n’a plus à qualifier. Et pour le socle réseau de la plateforme sous le cluster, le détour par Réseau NSX en VCF 9 : du VDS au pod reste le prérequis.

Trois conditions, trois échecs

L’API existe toujours, le greffon peut ne pas être là, et le pod n’est isolé que s’il est sélectionné. Un apply réussi ne couvre que la première.

Regarde ton CNI avant de le changer

Antrea, le défaut VKS, documente déjà le refus explicite et la journalisation. Cilium est un paquet du catalogue, choisi à la création du cluster, sans retour arrière documenté.

RBAC et réseau sont deux plans

Les identités posées par l’article RBAC deviennent ici des sélecteurs. RBAC autorise des appels d’API, NetworkPolicy autorise des paquets.

Reçois le prochain par email

Les nouveaux articles et séries, envoyés à leur publication. Aucun autre courrier.

Désabonnement en un clic, à tout moment.

Retour au blog
Partager

Articles similaires

  1. 18 min de lecture

    RBAC Kubernetes : les fondations, et les pièges qui survivent à l'audit

    Chacun de ces pièges est publié sur kubernetes.io. Ce qui manque, c'est leur mise en ordre — et le chemin qui mène d'un Namespace vSphere jusqu'à cluster-admin.

  2. 18 min de lecture

    Sécurité runtime : Falco et Tetragon, sur quel critère trancher

    Falco et Tetragon collectent tous deux en eBPF. Ce qui les sépare est ailleurs : périmètre d’événements, modèle de règles, et surtout ce que chacun peut réellement empêcher.

  3. 18 min de lecture

    Supply chain : Sigstore, SBOM, admission control

    Kubernetes ne vérifie aucune signature d'image par lui-même. Signer avec Sigstore, inventorier avec un SBOM, refuser à l'admission — et ce que chacun de ces verbes recouvre réellement.

Suivre le blog

Nouveaux articles, réflexions et mises à jour.