Le service Managed Kubernetes proposé par OVHcloud est une solution pratique pour déléguer la gestion du plan de contrôle Kubernetes, tout en conservant la souplesse d’accès aux nœuds de calcul. Mais cette abstraction ne doit pas compromettre la sécurité réseau – et c’est justement là que le bât blesse.
Sommare
Ce que nous voulons
En tant que clients de ce service, notre attente est simple :
Un accès sécurisé au cluster Kubernetes, à la fois depuis le réseau public et depuis le réseau privé OVH, via des Security Groups bien définis.
C’est un prérequis évident pour tout environnement de production qui prend au sérieux la sécurité réseau et la gestion des flux autorisés.
Ce que fait OVHcloud aujourd’hui
Lors de la création d’un cluster Managed Kubernetes sur OVHcloud, les nœuds de vos pools sont automatiquement associés au Security Group “default” du projet Public Cloud.
Et ce groupe “default” est… inquiétant :
Il autorise par défaut tout le trafic, dans toutes les directions, depuis n’importe quelle adresse IP (
0.0.0.0/0
), sur tous les ports et tous les protocoles.
Autrement dit, vos nœuds sont exposés à tout l’Internet, sauf si vous prenez vous-même l’initiative de restreindre les flux a posteriori. Ce comportement par défaut est très loin des bonnes pratiques attendues dans un service managé.
Un Issue Github a été créé il y a maintenant 3 ans sur le sujet, et pourtant rien de neuf : https://github.com/ovh/public-cloud-roadmap/issues/221. J’y ai d’ailleurs apporté ma pierre.
Un autre constat peut être fait. OVH popule les instances Public Cloud avec une adresse IP publique, flottante. Dans la grande majorité des cas, ce n’est pas nécessaire. La surface d’attaque est encore agrandie à cause de cela. Est-ce que cela indiquerait qu’OVH ne sait pas faire autrement pour faire communiquer le Control Plane avec le Data Plane ?
Une documentation qui ne rassure pas
Après avoir consulté la page officielle Managed Kubernetes – Known Limits # Ports, j’espérais trouver une configuration réseau plus solide.
Mais non : cette page se contente de lister les ports nécessaires au bon fonctionnement d’un cluster (API server, kubelet, etcd…), sans aucune mention d’un groupe de sécurité dédié ou d’un mécanisme de durcissement automatique.
Pire : certains ports importants semblent oubliés – notamment le port TCP 10256, pourtant clairement listé dans la documentation Kubernetes officielle : Kubernetes ports and protocols.
Ma solution avec OpenTofu
Pour pallier cette faille, j’ai créé un ensemble de règles via OpenTofu permettant de restreindre les flux entrants uniquement aux ports nécessaires. Voici les grandes lignes :
- Ingress autorisé uniquement sur les ports obligatoires :
- TCP 22 (SSH, OVH Management)
- UDP 8472 (flannel)
- UDP 4789 (kube-dns)
- TCP 10250 (kubelet)
- TCP 10256 (kube-proxy)
- TCP 443 (API server via le load balancer)
- Tout le TCP sur 10.2.0.0/16
- Tout le TCP sur 10.3.0.0/16
- Tout le UDP sur 10.2.0.0/16
- Tout le UDP sur 10.3.0.0/16
- Egress autorisé en totalité (peut être restreint selon vos besoins)
- NodePort (TCP 30000-32767) non inclus (non recommandé en production, nous préfèrerons l’utilisation d’un Load Balancer)
- NFS (TCP 111) non inclus plus bas car non utilisé dans mon contexte
Voici donc un template OpenTofu qui vous aidera à combler ce trou de sécurité :
resource "openstack_networking_secgroup_rule_v2" "rules" {
for_each = {
for index, rule in local.rules: index => rule
}
security_group_id = <id>
description = each.value.description
ethertype = each.value.ethertype
direction = each.value.direction
port_range_min = each.value.port_range_min
port_range_max = each.value.port_range_max
remote_ip_prefix = each.value.remote_ip_prefix
protocol = each.value.protocol
}
locals {
rules = [
# Outbound
{
description = "Allow TCP traffic to outside (v4)"
protocol = "tcp"
direction = "egress"
ethertype = "IPv4"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "0.0.0.0/0"
},
{
description = "Allow TCP traffic to outside (v6)"
protocol = "tcp"
direction = "egress"
ethertype = "IPv6"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "::/0"
},
{
description = "Allow UDP traffic to outside (v4)"
protocol = "udp"
direction = "egress"
ethertype = "IPv4"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "0.0.0.0/0"
},
{
description = "Allow UDP traffic to outside (v6)"
protocol = "udp"
direction = "egress"
ethertype = "IPv6"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "::/0"
},
# Internal Kubernetes and OVH services
{
description = "Allow TCP traffic from pods"
protocol = "tcp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "10.2.0.0/16"
},
{
description = "Allow TCP traffic from services"
protocol = "tcp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "10.3.0.0/16"
},
{
description = "Allow UDP traffic from pods"
protocol = "udp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "10.2.0.0/16"
},
{
description = "Allow UDP traffic from services"
protocol = "udp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 0
port_range_max = 0
remote_ip_prefix = "10.3.0.0/16"
},
# OVH and Kubernetes Data Plane
{
description = "Allow SSH for OVH Management (v4)"
protocol = "tcp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
},
{
description = "Allow communication between pods (flannel)"
protocol = "udp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 8472
port_range_max = 8472
remote_ip_prefix = "0.0.0.0/0"
},
{
description = "Allow DNS resolution between nodes (kube-dns internal usage)"
protocol = "udp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 4789
port_range_max = 4789
remote_ip_prefix = "0.0.0.0/0"
},
{
description = "Allow communication between apiserver and worker nodes (kubelet)"
protocol = "tcp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 10250
port_range_max = 10250
remote_ip_prefix = "0.0.0.0/0"
},
{
description = "Allow communication for Load Balancers (kube-proxy)"
protocol = "tcp"
direction = "ingress"
ethertype = "IPv4"
port_range_min = 10256
port_range_max = 10256
remote_ip_prefix = "0.0.0.0/0"
}
]
}
BashCe que OVHcloud devrait faire
Plusieurs solutions simples pourraient renforcer la sécurité par défaut :
- Permettre de choisir le Security Group à associer aux nœuds lors de la création du pool.
- Créer automatiquement un Security Group dédié, avec les bonnes règles, lors du provisionnement du cluster.
- (re)Documenter précisément les flux attendus, y compris les ports manquants (ex : TCP 10256).
Ce sont des pratiques courantes chez d’autres fournisseurs de Kubernetes managé, et elles évitent bien des erreurs de configuration de la part des utilisateurs.
Conclusion
Un service managé doit inspirer confiance. Si la plateforme laisse des ports grands ouverts sur tous vos nœuds, c’est à vous – client – de colmater les brèches. Ce n’est pas acceptable.
Je reste convaincu qu’OVHcloud peut et doit faire mieux, notamment en intégrant cette problématique dans le design de son service. D’autres utilisateurs ont déjà demandé cette évolution sur les forums, et j’espère qu’elle verra rapidement le jour.
En attendant, restez vigilants sur vos configurations réseau. Un cluster bien configuré commence par… un Security Group bien pensé.