Aller au contenu
Le réseau : réseaux privés, groupes de sécurité, répartiteurs de charge

Le réseau : réseaux privés, groupes de sécurité, répartiteurs de charge

200 Pratiquer ⏱ 1 h 30 cloudscaleway

À la fin, vous saurez

  • Décrire les rôles d'un VPC, d'un réseau privé, d'une passerelle publique et d'un répartiteur de charge, et leurs portées (région, zone)
  • Rattacher des instances et une base managée à un réseau privé, puis supprimer leurs points d'accès publics
  • Donner une sortie Internet à des instances sans adresse publique par NAT, et les administrer par un bastion SSH
  • Configurer un répartiteur de charge HTTP avec une vérification de santé applicative
  • Choisir où filtrer le trafic : groupe de sécurité, liste de contrôle d'accès réseau, pare-feu de l'hôte
  • Diagnostiquer les pannes classiques : instance sans sortie, route par défaut qui coupe SSH, backend marqué hors service

Prérequis

Testé avec scaleway-cli 2.62.0 , vérifié le 5 octobre 2026

Pourquoi

À la fin de la leçon 4, Signalements tourne sur deux instances, sig-app-1 et sig-app-2, chacune avec une adresse IPv4 publique, et sa base sig-db répond sur un point d'accès public. Tout fonctionne, et tout est exposé : le port SSH de chaque instance reçoit des tentatives de connexion automatisées quelques minutes après sa création, le port 8000 de gunicorn est joignable par n'importe qui, sans TLS, et PostgreSQL n'est protégé que par son mot de passe. Les utilisateurs, eux, doivent choisir entre deux adresses, et si sig-app-1 tombe, la moitié d'entre eux voit une erreur.

L'architecture visée est celle de presque toutes les applications web dans le cloud :

  • une seule porte d'entrée publique, un répartiteur de charge, qui termine TLS et ne transmet qu'aux instances en bonne santé ;
  • des instances et une base sans adresse publique, qui ne se parlent que dans un réseau privé ;
  • une sortie contrôlée vers Internet (mises à jour, registre d'images), par une passerelle qui fait de la traduction d'adresses ;
  • un accès d'administration unique et filtré, un bastion SSH.

Sans cela, chaque nouvelle machine agrandit la surface d'attaque, l'ensemble des points par lesquels on peut tenter d'entrer. Avec cela, ajouter une troisième instance ne change rien à ce qui est visible depuis Internet.

Les concepts

VPC et réseau privé

Un VPC (Virtual Private Cloud) est un espace réseau isolé, découpé dans l'infrastructure partagée du fournisseur. Les adresses que vous y choisissez (par exemple 172.16.20.0/22) peuvent être utilisées par mille autres clients dans leur propre VPC sans conflit : l'isolation est assurée par le fournisseur, pas par l'unicité des adresses.

Chez Scaleway, le vocabulaire a deux niveaux :

  • le VPC est un domaine de niveau 3 (routage), régional ;
  • le réseau privé (Private Network) est un domaine de niveau 2 (Ethernet commuté) à l'intérieur d'un VPC, régional lui aussi : il s'étend sur toutes les zones de la région. Une instance de fr-par-1 et une instance de fr-par-2 rattachées au même réseau privé se voient comme sur un même commutateur.

Chaque réseau privé reçoit un bloc IPv4 (en /22 par défaut) et un bloc IPv6 en /64. Un DHCP géré attribue une adresse à chaque ressource rattachée, et cette adresse ne change pas tant que la ressource n'est pas détachée ou supprimée, même après un redémarrage ou un long arrêt. Le service IPAM (IP Address Manager) est la source de vérité de ces attributions, et permet aussi de réserver une adresse précise. Un DNS géré résout le nom de chaque ressource : sig-app-1.pn-signalements.internal. Réseaux privés et VPC sont gratuits chez Scaleway.

Deux précisions récentes de la documentation : les projets créés après le 13 mai 2025 n'ont plus de VPC par défaut (il faut en créer un, ou l'API en crée un à la première demande de réseau privé), et le routage entre réseaux privés d'un même VPC est activé par défaut sur les VPC récents.

La comparaison avec les autres fournisseurs montre que les mêmes mots recouvrent des portées différentes, ce qui compte au moment de concevoir la tolérance aux pannes de zone :

NotionScalewayAWSAzureGoogle CloudOVHcloud
Espace isoléVPC (régional)VPC (régional)Virtual Network, VNet (régional)VPC (global)vRack, réseau privé Public Cloud
Segment d'adressesPrivate Network (régional, niveau 2)Subnet (une seule zone)Subnet (régional)Subnet (régional)Sous-réseau du réseau privé
Filtrage par ressourceSecurity group (trafic public seulement)Security group (à état)Network Security Group, NSGRègles de pare-feu VPCSecurity groups (OpenStack)
Filtrage entre segmentsNetwork ACL (sans état)Network ACL (sans état)NSG sur le sous-réseauRègles de pare-feu
Sortie sans IP publiquePublic Gateway (NAT)NAT GatewayNAT GatewayCloud NATGateway
Répartiteur de chargeLoad BalancerElastic Load Balancing (ALB, NLB)Load Balancer (niveau 4), Application Gateway (niveau 7)Cloud Load BalancingLoad Balancer

Chez AWS, un sous-réseau vit dans une seule zone : une architecture sur deux zones demande au moins deux sous-réseaux. Chez Scaleway, un seul réseau privé couvre toute la région. Chez Google Cloud, le VPC lui-même est global, et ses sous-réseaux sont régionaux.

Adresses publiques

Une instance Scaleway reçoit par défaut une IP flexible, une adresse publique réservée dans votre compte, que l'on peut détacher d'une instance et rattacher à une autre de la même zone. Elle est facturée à l'heure (0,004 € HT de l'heure pour une IPv4 d'instance selon la FAQ de Scaleway), qu'elle soit attachée ou non. Scaleway a migré ses instances vers le mode routed IP (mobilité d'adresse par routage plutôt que par NAT), qui permet notamment d'attacher des adresses IPv6 flexibles.

Une IP flexible sert surtout à garder une adresse stable quand la ressource derrière change : on recrée l'instance ou le répartiteur, on rattache la même adresse, et les enregistrements DNS restent valables. Chaque produit a ses propres IP flexibles : celles des instances, des passerelles et des répartiteurs ne sont pas interchangeables.

Groupes de sécurité, ACL, pare-feu de l'hôte

Trois mécanismes filtrent le trafic, à trois endroits différents.

Un groupe de sécurité (security group) est un pare-feu appliqué par l'hyperviseur, en dehors de la machine virtuelle. Il a une politique par défaut en entrée et en sortie (accept ou drop) et des règles par protocole, port et plage d'adresses. Il peut être à état (stateful) : si une connexion est autorisée dans un sens, les paquets de réponse sont autorisés automatiquement. Il est zonal : un groupe de fr-par-1 ne s'applique qu'aux instances de fr-par-1.

Important

Chez Scaleway, les groupes de sécurité ne filtrent que le trafic public des instances. Le trafic sur un réseau privé ne passe pas par eux. La documentation renvoie, pour le filtrage interne, au pare-feu de l'instance (nftables, iptables) et, entre réseaux privés d'un même VPC, aux Network ACL. C'est une différence importante avec AWS, où un groupe de sécurité s'applique à chaque interface, et avec Azure, dont les NSG filtrent aussi le trafic interne au réseau virtuel.

Deux comportements par défaut à connaître, documentés par Scaleway :

  • le groupe de sécurité créé automatiquement à la première instance d'une zone est sans état : une instance qui ouvre une connexion sortante ne reçoit pas la réponse si aucune règle entrante ne l'autorise. Les groupes que vous créez vous-même, par la console ou la CLI, sont à état par défaut ;
  • les ports SMTP sortants sont bloqués par défaut, pour lutter contre le spam. Le déblocage passe par le support.

Une liste de contrôle d'accès réseau (Network ACL) filtre le trafic entre réseaux privés d'un VPC, sans état. Vide par défaut, elle laisse tout passer.

Le pare-feu de l'hôte (nftables) est le seul des trois qui voit le trafic du réseau privé arrivant sur l'instance elle-même.

La passerelle publique : NAT et bastion

Une instance sans adresse publique ne peut pas joindre Internet : son adresse 172.16.x.x n'est pas routable. Une passerelle NAT (Network Address Translation) résout ce problème : elle remplace l'adresse source privée des paquets sortants par sa propre adresse publique, mémorise la correspondance, et renvoie les réponses à la bonne instance. Les connexions sortantes fonctionnent, les connexions entrantes non sollicitées n'ont nulle part où aller.

La Public Gateway de Scaleway joue ce rôle, et plus :

  • NAT dynamique (sortant), activé quand la passerelle annonce une route par défaut sur le réseau privé (propagée par DHCP) ;
  • NAT statique (entrant, port forwarding), en option, pour exposer un port précis d'une ressource privée ;
  • bastion SSH, en option.

La passerelle est une ressource zonale, attachée à un réseau privé régional : une passerelle en fr-par-1 sert aussi les instances de fr-par-2. Mais si la zone fr-par-1 tombe, les instances de fr-par-2 perdent leur sortie. La FAQ de Scaleway propose d'attacher plusieurs passerelles, dans plusieurs zones, au même réseau privé.

Un bastion est une machine dont le seul rôle est de servir de point d'entrée SSH vers les machines privées. On ne s'y connecte pas pour travailler : on le traverse (ssh -J, ProxyJump). Celui de la passerelle Scaleway importe les clés SSH du projet au moment de son activation, écoute par défaut sur le port 61000, et peut restreindre les adresses sources autorisées (Allowed IPs), qui valent 0.0.0.0/0 tant qu'on ne les a pas changées.

Le répartiteur de charge

Un répartiteur de charge (load balancer) reçoit les connexions des clients sur une adresse publique et les distribue entre plusieurs serveurs. Chez Scaleway, il se configure en trois objets :

  • un frontend écoute sur un port (80, 443) et porte éventuellement des certificats TLS ;
  • un backend décrit vers où transmettre : une liste d'adresses de serveurs, un port, un protocole (http ou tcp), un algorithme de répartition (tourniquet, moins de connexions, premier disponible) ;
  • une vérification de santé (health check), propriété du backend, interroge régulièrement chaque serveur. Après un nombre d'échecs consécutifs (3 par défaut), le serveur est marqué hors service et ne reçoit plus de trafic, jusqu'à ce qu'il réponde de nouveau.

En mode http (niveau 7), le répartiteur comprend les requêtes : il peut router selon le nom d'hôte ou le chemin, et ajoute l'en-tête X-Forwarded-For avec l'adresse du client. En mode tcp (niveau 4), il transmet le flux sans le lire. Les serveurs peuvent être dans n'importe quelle zone de la région, et joints par leur adresse privée si le répartiteur est rattaché au même réseau privé.

Le répartiteur est lui-même zonal. En cas de panne, Scaleway documente un mécanisme de bascule : une réplique est déployée et l'IP flexible du répartiteur y est reroutée automatiquement.

En pratique

L'objectif est l'architecture suivante. Les commandes supposent la CLI scw configurée sur le projet signalements, région fr-par, et les ressources des leçons précédentes : sig-app-1 (zone fr-par-1), sig-app-2 (zone fr-par-2), la base sig-db. Elles ne montrent pas de sortie : la leçon ne présente pas de sortie inventée, et les identifiants seront différents chez vous.

    flowchart LR
  U["Utilisateurs"] -->|"HTTPS 443"| LB["lb-signalements<br/>IP flexible publique"]
  A["Équipe"] -->|"SSH 61000<br/>adresses autorisées"| GW["gw-signalements<br/>NAT + bastion"]
  subgraph PN["pn-signalements (172.16.20.0/22, région fr-par)"]
    S1["sig-app-1<br/>fr-par-1, :8000"]
    S2["sig-app-2<br/>fr-par-2, :8000"]
    DB[("sig-db<br/>PostgreSQL :5432")]
  end
  LB -->|"HTTP :8000<br/>vérif. GET /sante"| S1
  LB --> S2
  GW -.->|"SSH"| S1
  GW -.-> S2
  S1 --> DB
  S2 --> DB
  S1 -.->|"sortie NAT"| GW
  GW -.-> I["Internet<br/>(registre, mises à jour)"]
  

1. Créer le VPC et le réseau privé

$ VPC_ID=$(scw vpc vpc create name=vpc-signalements region=fr-par -o json | jq -r .id)
$ PN_ID=$(scw vpc private-network create name=pn-signalements vpc-id="$VPC_ID" \
    subnets.0=172.16.20.0/22 region=fr-par -o json | jq -r .id)
  • Le bloc 172.16.20.0/22 (1 024 adresses) est choisi explicitement. Sans subnets.0, Scaleway en choisit un ; le fixer soi-même évite les chevauchements le jour où l'on reliera ce VPC à un autre, à un réseau d'entreprise par VPN, ou à un second environnement. Tenez un registre des plages utilisées.
  • Le nom du réseau privé entre dans les noms DNS internes (<ressource>.pn-signalements.internal) : choisissez-le court et stable.

2. Rattacher les instances

Chaque instance reçoit une interface privée (private NIC) dans le réseau, dans sa propre zone :

$ SRV1=$(scw instance server list name=sig-app-1 zone=fr-par-1 -o json | jq -r '.[0].id')
$ SRV2=$(scw instance server list name=sig-app-2 zone=fr-par-2 -o json | jq -r '.[0].id')
$ NIC1=$(scw instance private-nic create server-id="$SRV1" private-network-id="$PN_ID" \
    zone=fr-par-1 -o json | jq -r .id)
$ NIC2=$(scw instance private-nic create server-id="$SRV2" private-network-id="$PN_ID" \
    zone=fr-par-2 -o json | jq -r .id)

L'interface apparaît à chaud dans l'instance, sans redémarrage, et obtient son adresse par DHCP. L'adresse attribuée se lit dans IPAM, à partir de l'identifiant de l'interface :

$ IP1=$(scw ipam ip list resource-id="$NIC1" is-ipv6=false region=fr-par -o json \
    | jq -r '.[0].address | split("/")[0]')
$ IP2=$(scw ipam ip list resource-id="$NIC2" is-ipv6=false region=fr-par -o json \
    | jq -r '.[0].address | split("/")[0]')
$ echo "$IP1 $IP2"

IPAM renvoie l'adresse avec son préfixe (172.16.20.x/22) ; le filtre jq ne garde que l'adresse. Depuis sig-app-1, ping sig-app-2.pn-signalements.internal doit répondre : les deux zones sont sur le même segment.

2 bis. Publier l'application sur l'interface privée

À la leçon 4, l'unité systemd publiait le conteneur sur 127.0.0.1:8000, pour ne rien exposer avant d'avoir un réseau privé. Le répartiteur de charge ne pourrait pas joindre cette adresse. Sur chaque instance (encore joignable par son IP publique à ce stade), remplacez-la par l'adresse privée relevée ci-dessus, puis redémarrez le service :

# sed -i 's/-p 127.0.0.1:8000:8000/-p 172.16.20.x:8000:8000/' /etc/systemd/system/signalements.service
# systemctl daemon-reload && systemctl restart signalements
# curl -s http://172.16.20.x:8000/sante

Remplacez 172.16.20.x par l'adresse de l'instance ($IP1 pour sig-app-1, $IP2 pour sig-app-2). Le port n'écoute ainsi que sur le réseau privé, jamais sur l'interface publique, ce que l'encadré de l'étape 6 justifie. L'adresse reste stable tant que l'interface privée existe, parce qu'IPAM la réserve pour elle.

Note

Cette retouche à la main contredit le principe du bétail de la leçon 4 : une instance recréée depuis signalements.yaml repartirait sur 127.0.0.1. La version durable consiste à faire calculer l'adresse au démarrage (une ligne ExecStartPre qui lit l'adresse de l'interface privée), ou à écrire la configuration avec l'outil qui crée l'instance et connaît donc son adresse : c'est ce que fera le cours Terraform et OpenTofu : les fondamentaux.

3. Passer la base sur le réseau privé

On ajoute à sig-db un point d'accès privé, puis on retire le public :

$ DB_ID=$(scw rdb instance list name=sig-db region=fr-par -o json | jq -r '.[0].id')
$ scw rdb endpoint create "$DB_ID" private-network.private-network-id="$PN_ID" region=fr-par
$ scw rdb instance get "$DB_ID" region=fr-par -o json \
    | jq '.endpoints[] | {id, ip, port, prive: (.private_network != null)}'

La dernière commande liste les points d'accès : le nouveau, privé (prive: true), avec son adresse dans 172.16.20.0/22 et son port, et l'ancien, public. Avant de supprimer ce dernier, donnez aux deux instances une variable DATABASE_URL qui désigne l'adresse privée : ajoutez la ligne DATABASE_URL=postgresql://signalements:<mot de passe>@<ip privée>:<port>/rdb au fichier /etc/signalements/env créé à la leçon 4 (droits 0640, lisible par root seulement), sur l'instance et non dans les données utilisateur, qui ne doivent pas contenir de secret. Redémarrez le service, et vérifiez GET /sante, qui teste désormais la base. Puis :

$ scw rdb endpoint delete <id-du-point-d-acces-public> instance-id="$DB_ID" region=fr-par

C'est la procédure que décrit la documentation de Scaleway pour rendre une base injoignable depuis Internet. Remarquez l'ordre : on ajoute le nouveau chemin, on y bascule les clients, on vérifie, et seulement ensuite on retire l'ancien. Dans l'ordre inverse, l'application perd sa base.

4. Créer la passerelle : sortie NAT et bastion

$ GW_ID=$(scw vpc-gw gateway create name=gw-signalements type=VPC-GW-S \
    enable-bastion=true zone=fr-par-1 -w -o json | jq -r .id)
$ scw vpc-gw gateway-network create gateway-id="$GW_ID" private-network-id="$PN_ID" \
    push-default-route=true zone=fr-par-1
$ GW_IP=$(scw vpc-gw gateway get "$GW_ID" zone=fr-par-1 -o json | jq -r '.ipv4.address')
  • enable-bastion=true active le bastion sur le port par défaut, 61000. bastion-port= permet d'en choisir un autre, entre 1024 et 59999.
  • push-default-route=true fait annoncer par la passerelle une route par défaut sur le réseau privé, ce qui active aussi le NAT dynamique. Les instances l'apprennent par DHCP.
  • type=VPC-GW-S est la plus petite offre ; les offres VPC-GW-L et VPC-GW-XL montent à 3 et 10 Gbit/s.

Le bastion importe les clés SSH du projet au moment de son activation. Une clé ajoutée plus tard au projet n'y est pas : il faut relancer l'import (scw vpc-gw gateway refresh-ssh-keys). Restreignez ensuite les adresses sources autorisées, qui laissent passer 0.0.0.0/0 par défaut, à l'adresse publique de votre réseau d'entreprise, depuis la console (onglet de la passerelle, section SSH bastion) : la CLI 2.62 n'expose pas encore cette opération.

Pour se connecter :

$ ssh -J "bastion@$GW_IP:61000" root@sig-app-1.pn-signalements.internal

Utilisez root ou l'utilisateur créé par cloud-init en leçon 4. Pour ne pas répéter l'option -J, la documentation de Scaleway propose une entrée dans ~/.ssh/config :

Host *.pn-signalements.internal
  ProxyJump bastion@<IP de la passerelle>:61000

5. Retirer les adresses publiques des instances

D'abord vérifier que la sortie par la passerelle fonctionne, depuis l'instance :

$ ip route show default
$ curl -sI https://ghcr.io | head -1

La route par défaut doit désigner l'adresse de la passerelle sur le réseau privé, et le registre d'images doit répondre. Alors seulement :

$ scw instance server detach-ip "$SRV1" zone=fr-par-1
$ scw instance server detach-ip "$SRV2" zone=fr-par-2
$ scw instance ip list zone=fr-par-1
$ scw instance ip delete <adresse-ou-id> zone=fr-par-1

Détacher une IP flexible ne la supprime pas : elle reste dans votre compte, et reste facturée. Supprimez-la, sauf si vous comptez la réutiliser. Répétez pour fr-par-2.

6. Groupe de sécurité et pare-feu de l'hôte

Les instances n'ont plus d'interface publique : un groupe de sécurité n'a donc plus rien à filtrer. On en crée tout de même un, strict, comme filet de sécurité pour le jour où quelqu'un rattacherait une adresse publique « pour déboguer » :

$ SG1=$(scw instance security-group create name=sg-sig-app stateful=true \
    inbound-default-policy=drop outbound-default-policy=accept zone=fr-par-1 -o json \
    | jq -r '.security_group.id // .id')
$ scw instance server update "$SRV1" security-group-id="$SG1" zone=fr-par-1

Le groupe est zonal : créez le même en fr-par-2 pour sig-app-2. Aucune règle entrante : tout ce qui arriverait par une interface publique serait rejeté.

Le filtrage du trafic privé relève du pare-feu de l'instance. Un exemple nftables, à déposer par cloud-init dans /etc/nftables.conf, qui n'accepte SSH que depuis le réseau privé :

table inet filtre {
  chain entree {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iif "lo" accept
    meta l4proto { icmp, ipv6-icmp } accept
    udp dport 68 accept
    ip saddr 172.16.20.0/22 tcp dport 22 accept
  }
}

La règle udp dport 68 laisse passer les réponses DHCP, dont l'interface privée a besoin pour garder son adresse.

Warning

Le port 8000 n'apparaît pas dans cette chaîne, et ce n'est pas un oubli : la documentation de Docker rappelle que le trafic vers les ports publiés d'un conteneur est détourné dans la table nat avant d'atteindre la chaîne INPUT. Une règle d'entrée de l'hôte ne protège donc pas un port publié par docker run -p. Pour le restreindre, publiez le port sur l'adresse privée seulement (-p 172.16.20.x:8000:8000), ou filtrez dans la chaîne DOCKER-USER prévue à cet effet.

7. Créer le répartiteur de charge

$ LB_ID=$(scw lb lb create name=lb-signalements type=LB-S zone=fr-par-1 -w -o json | jq -r .id)
$ scw lb private-network attach lb-id="$LB_ID" private-network-id="$PN_ID" zone=fr-par-1

Le répartiteur reçoit une IP flexible publique (option assign-flexible-ip, vraie par défaut) et une adresse dans le réseau privé. Le backend vise les adresses privées des instances, port 8000, avec une vérification de santé applicative :

$ BE_ID=$(scw lb backend create lb-id="$LB_ID" name=sig-app \
    forward-protocol=http forward-port=8000 forward-port-algorithm=roundrobin \
    sticky-sessions=none \
    server-ip.0="$IP1" server-ip.1="$IP2" \
    health-check.port=8000 \
    health-check.http-config.method=GET \
    health-check.http-config.uri=/sante \
    health-check.http-config.code=200 \
    health-check.check-delay=5s health-check.check-timeout=2s \
    health-check.check-max-retries=3 \
    zone=fr-par-1 -o json | jq -r .id)
  • forward-protocol=http : le répartiteur parle HTTP aux instances, et peut donc ajouter X-Forwarded-For.
  • server-ip.N : les adresses privées relevées à l'étape 2. Le répartiteur en fr-par-1 joint sans difficulté sig-app-2 en fr-par-2, sur le même réseau privé.
  • health-check.http-config.uri=/sante et code=200 : un serveur n'est sain que si GET /sante répond 200, et cette route teste la base. Une instance dont le conteneur a planté, ou qui a perdu la base, sort du tourniquet.
  • check-delay, check-timeout, check-max-retries : un serveur est retiré au bout d'environ trois échecs espacés de cinq secondes. Plus court, on retire plus vite un serveur malade, mais un ralentissement passager suffit à le sortir.

Puis le frontend, d'abord en HTTP pour tester :

$ FE_ID=$(scw lb frontend create lb-id="$LB_ID" name=http inbound-port=80 \
    backend-id="$BE_ID" zone=fr-par-1 -o json | jq -r .id)
$ LB_IP=$(scw lb lb get "$LB_ID" zone=fr-par-1 -o json | jq -r '.ip[0].ip_address')
$ curl -s "http://$LB_IP/"

GET / renvoie l'identité et la version de l'application. Arrêtez le conteneur sur sig-app-1 : après quelques secondes, le backend le marque hors service (scw lb backend list-statistics lb-id="$LB_ID" zone=fr-par-1 ou l'onglet du répartiteur dans la console le montrent), et toutes les requêtes partent vers sig-app-2. Relancez-le : il revient de lui-même.

8. Passer en HTTPS

Pour un certificat Let's Encrypt, il faut un nom de domaine dont l'enregistrement A désigne l'adresse du répartiteur. Une fois l'enregistrement propagé :

$ CERT_ID=$(scw lb certificate create lb-id="$LB_ID" name=signalements \
    letsencrypt-common-name=signalements.exemple.fr zone=fr-par-1 -o json | jq -r .id)
$ scw lb frontend create lb-id="$LB_ID" name=https inbound-port=443 \
    backend-id="$BE_ID" certificate-ids.0="$CERT_ID" zone=fr-par-1

Le répartiteur termine TLS (SSL offloading) et parle HTTP en clair aux instances, dans le réseau privé. Le frontend 80 peut ensuite être réduit à une redirection vers HTTPS, avec une ACL de redirection. La gestion du DNS, des zones et des enregistrements fait l'objet du cours Réseau cloud : VPC, load balancers, DNS.

Sous le capot

Comment un réseau privé traverse plusieurs zones. Les machines de fr-par-1 et fr-par-2 sont dans des bâtiments différents, sur des réseaux physiques différents, et pourtant elles partagent un segment Ethernet. La technique générale est l'encapsulation : la trame Ethernet de la machine virtuelle est enveloppée dans un paquet IP du réseau physique du fournisseur, transportée jusqu'à l'hôte de destination, puis déballée et remise à la machine virtuelle. Le protocole standard le plus répandu est VXLAN (RFC 7348), qui encapsule les trames dans de l'UDP et identifie chaque réseau virtuel par un numéro sur 24 bits, soit environ 16 millions de réseaux possibles, contre 4 096 pour les VLAN classiques. Les fournisseurs ne documentent pas tous leur mécanisme exact ; Scaleway ne le fait pas pour ses réseaux privés. Ce qui compte pour vous : l'isolation ne repose pas sur le secret des adresses, mais sur l'étiquette que l'hyperviseur pose sur chaque trame, et qu'une machine virtuelle ne peut pas choisir.

Ce que veut dire « à état ». Un filtre sans état examine chaque paquet isolément : pour autoriser une connexion SSH entrante, il faut une règle pour les paquets qui entrent vers le port 22, et une autre pour les réponses qui sortent depuis le port 22 vers des ports clients imprévisibles. Un filtre à état tient une table de suivi de connexions (sous Linux, conntrack) : le premier paquet d'une connexion est confronté aux règles ; s'il est accepté, une entrée est créée (adresses, ports, protocole, état), et tous les paquets suivants de cette connexion, dans les deux sens, passent sans réexamen. C'est ce que fait la règle ct state established,related accept de l'exemple nftables, et ce que font les groupes de sécurité à état. Microsoft documente un effet secondaire utile à connaître, valable aussi ailleurs : supprimer une règle n'interrompt pas les connexions déjà établies, seulement les nouvelles.

La traduction d'adresses. Quand sig-app-1 (172.16.20.5) ouvre une connexion vers le registre, la passerelle réécrit l'adresse source en son adresse publique et choisit un port source libre, puis note : « port 40312 public correspond à 172.16.20.5, port 51022 ». La réponse arrive sur le port 40312, la passerelle consulte sa table et réécrit dans l'autre sens. Conséquence pratique : vu d'Internet, toutes vos instances ont la même adresse, celle de la passerelle. C'est l'adresse à déclarer dans les listes blanches d'un partenaire, et la raison pour laquelle on la garde stable avec une IP flexible.

Ce que voit l'application derrière le répartiteur. Un répartiteur de niveau 7 est un mandataire inverse (reverse proxy), comme Nginx ou HAProxy ; pour le PROXY protocol, la documentation de Scaleway renvoie d'ailleurs à la spécification publiée par HAProxy. En mode HTTP, chaque requête cliente aboutit sur le répartiteur, qui ouvre ou réutilise une connexion vers un serveur du backend : l'application voit donc arriver des connexions dont l'adresse source est celle du répartiteur, et trouve l'adresse du client dans X-Forwarded-For. En mode TCP, il n'y a pas d'en-tête HTTP à modifier : c'est le PROXY protocol qui transmet l'adresse d'origine, en tête du flux TCP, et le serveur doit savoir le lire.

Pièges courants

Retirer l'adresse publique avant d'avoir une sortie. L'instance tourne, mais apt update reste bloqué, et un redémarrage du conteneur échoue parce que l'image ne peut plus être tirée du registre. Une instance sans IP publique a besoin d'une passerelle NAT (ou d'un registre et de miroirs joignables dans le réseau privé). Vérifiez ip route show default avant de détacher l'IP.

La route par défaut qui coupe SSH. Si une instance a encore une adresse publique quand la passerelle commence à annoncer sa route par défaut, la documentation de Scaleway prévient que cette route prend la priorité sur celle de l'interface publique : les réponses aux connexions entrantes sur l'IP publique repartent par la passerelle, avec une autre adresse source, et sont jetées. Symptôme : SSH par l'IP publique se fige juste après l'attachement de la passerelle. Passez par le bastion, ou terminez la migration en retirant l'IP publique.

Ouvrir le port 22 à 0.0.0.0/0. Sur un groupe de sécurité, ou en laissant les adresses autorisées du bastion à leur valeur par défaut. Les robots qui balaient Internet trouvent un port SSH ouvert en quelques minutes. AWS le rappelle dans ses bonnes pratiques : n'autorisez SSH et RDP que depuis des plages précises.

Croire que le groupe de sécurité protège le réseau privé. Chez Scaleway, il ne voit que le trafic public. Une règle drop sur le port 8000 dans sg-sig-app ne change rien pour une machine du réseau privé qui joint 172.16.20.5:8000.

Le groupe sans état qui bloque les réponses. Une instance placée dans le groupe créé automatiquement pour sa zone, sans état par défaut, ne reçoit pas les réponses de ses propres connexions sortantes si aucune règle entrante ne les autorise : curl attend, puis échoue. Basculez le groupe en mode à état, ou utilisez un groupe créé par vous.

La vérification de santé qui ment. Une vérification TCP sur le port 8000 ou un GET / réussissent tant que gunicorn répond, même si la base est injoignable : le répartiteur continue d'envoyer des requêtes à une instance qui renvoie des erreurs 500. Faites vérifier une route qui teste les dépendances indispensables (/sante). À l'inverse, une route de santé qui teste trop de choses (un service externe facultatif, par exemple) peut faire sortir toutes les instances en même temps à cause d'une seule dépendance, et transformer une dégradation en panne totale. Notez aussi que le répartiteur ne suit pas les redirections : une route qui répond 301 est considérée en échec si l'on attend 200.

L'application qui ne voit plus l'adresse des clients. Derrière le répartiteur, tous les accès semblent venir de la même adresse privée : journaux inutilisables, limitation de débit par adresse qui bloque tout le monde à la fois. L'adresse réelle est dans X-Forwarded-For ; l'application doit la lire, mais seulement quand la requête vient du répartiteur, sinon n'importe quel client peut forger l'en-tête. Pour Flask, c'est le rôle de werkzeug.middleware.proxy_fix.ProxyFix, configuré avec le nombre exact de mandataires de confiance.

Les plages d'adresses qui se chevauchent. Deux réseaux privés en 172.16.0.0/22, créés avec les valeurs par défaut, ne pourront jamais être reliés sans renumérotation. Planifiez vos plages dès le premier environnement.

Sécurité

Défense en profondeur. Aucune de ces couches ne suffit seule, et c'est voulu. Si un attaquant obtient l'exécution de code dans un conteneur de sig-app-1, il est dans le réseau privé : le pare-feu de l'hôte limite ce qu'il peut joindre, la base exige toujours un mot de passe, et il ne peut pas ouvrir de port vers Internet. Si une règle du pare-feu de l'hôte est mal écrite, l'absence d'adresse publique empêche quand même tout accès direct depuis Internet. On empile des protections indépendantes pour qu'une erreur ne suffise pas.

Réduire la surface d'attaque, et la mesurer. Après cette leçon, ce qui est visible depuis Internet tient en deux lignes : les ports 80 et 443 du répartiteur, et le port 61000 du bastion, filtré par adresse. Notez-le, et vérifiez-le régulièrement de l'extérieur (un balayage nmap de vos adresses publiques depuis une machine tierce, par exemple) : c'est la seule façon de savoir ce qui est réellement exposé, par opposition à ce que vous croyez avoir configuré.

Le bastion est une cible de choix. Il donne accès à toutes les machines du réseau privé. Restreignez ses adresses sources, retirez du projet les clés SSH des personnes qui partent (puis relancez l'import des clés sur le bastion, sinon elles y restent), et préférez des clés matérielles ou des certificats SSH à durée courte quand l'équipe grandit. Certains fournisseurs proposent des accès sans port SSH exposé (Session Manager chez AWS, IAP chez Google Cloud, Azure Bastion), qui passent par l'IAM du fournisseur et laissent une trace d'audit.

TLS se termine sur le répartiteur. Entre le répartiteur et les instances, le trafic circule en clair dans le réseau privé. C'est un choix courant et défendable, puisque ce réseau est isolé, mais c'est un choix : pour des données de santé, un client qui exige le chiffrement de bout en bout ou une exigence réglementaire, configurez le backend en HTTPS (SSL bridging) avec un certificat sur les instances.

Les flux sortants comptent aussi. Une instance compromise exfiltre des données ou télécharge des outils par sa sortie NAT. Une politique sortante accept est le défaut presque partout. Pour des environnements sensibles, restreignez la sortie aux destinations nécessaires (registre, miroirs de paquets, API utilisées) : c'est plus contraignant, et c'est souvent ce qui fait la différence lors d'une intrusion. Le blocage de SMTP par défaut chez Scaleway relève de la même logique.

En production

  • Réservez les IP flexibles des points d'entrée. L'adresse du répartiteur et celle de la passerelle sont inscrites dans le DNS et dans les listes blanches de partenaires. En supprimant un répartiteur, gardez son IP flexible pour la rattacher au suivant (scw lb ip create en réserve une à l'avance, ip-ids.0= la rattache à la création).
  • Une passerelle par zone. Une seule passerelle en fr-par-1 est un point de défaillance unique pour la sortie de toute la région. Attachez une seconde passerelle, en fr-par-2, au même réseau privé.
  • Le répartiteur aussi est zonal. Sa bascule automatique couvre la panne d'un répartiteur, pas celle de sa zone. Pour survivre à la perte d'une zone entière, il faut des répartiteurs dans plusieurs zones et un DNS qui sait basculer entre eux. La leçon 3 a posé l'arbitrage : quelle panne voulez-vous survivre, et à quel coût ?
  • Dimensionnez le répartiteur. Les offres diffèrent par le nombre de connexions simultanées (20 000 pour LB-S, 50 000 pour LB-M selon la documentation de Scaleway) et par la bande passante. Surveillez ces métriques : un répartiteur saturé ressemble, vu des utilisateurs, à une application lente.
  • Sur Kubernetes, le répartiteur se crée tout seul. Sur Kapsule, un Service de type LoadBalancer fait créer un Load Balancer Scaleway par le cloud controller manager du cluster, et ses réglages passent par des annotations : on ne le modifie pas depuis la console. C'est ainsi que le contrôleur d'entrée Traefik du cluster de Lyneko est exposé.
  • Décrivez tout cela en code. Huit étapes, une vingtaine de commandes, autant d'identifiants à reporter à la main : c'est exactement ce que le cours Terraform et OpenTofu : les fondamentaux automatisera, avec un état qui sait ce qui existe et un plan qui montre ce qui va changer.

Exercices

1. Lire une architecture (niveau 100). Pour chacun des flux suivants, indiquez quel composant de l'architecture de la leçon il traverse, et lequel l'autorise ou le bloque : (a) un utilisateur ouvre https://signalements.exemple.fr ; (b) sig-app-2 télécharge une mise à jour de sécurité ; (c) un robot tente ssh root@<ancienne IP publique de sig-app-1> ; (d) un administrateur se connecte à sig-app-2 ; (e) sig-app-1 interroge sig-db.

Solution

(a) Frontend 443 du répartiteur (certificat Let's Encrypt), backend sig-app vers l'adresse privée d'une instance saine, port 8000. (b) Route par défaut annoncée par gw-signalements, NAT dynamique : la connexion sort avec l'adresse publique de la passerelle. (c) L'adresse a été détachée et supprimée : elle n'appartient plus à l'instance (elle peut même avoir été réattribuée à un autre client). Rien n'arrive à sig-app-1. (d) Bastion de la passerelle, port 61000, si l'adresse source est dans les adresses autorisées et la clé dans le projet au moment de l'import, puis SSH dans le réseau privé, autorisé par la règle nftables sur 172.16.20.0/22. (e) Réseau privé directement, vers le point d'accès privé de la base ; aucun groupe de sécurité n'intervient, l'authentification PostgreSQL fait le reste.

2. Diagnostiquer une instance muette (niveau 200). Après l'étape 5, sig-app-2 ne peut plus tirer d'image : docker pull échoue par dépassement de délai. sig-app-1 n'a aucun problème. Donnez trois hypothèses, la commande qui vérifie chacune, et l'ordre dans lequel vous les testeriez.

Solution

Hypothèse 1, la plus fréquente : sig-app-2 n'a pas de route par défaut par la passerelle, par exemple parce que son interface privée n'a pas renouvelé son bail DHCP. Vérifier avec ip route show default et ip -br addr sur l'instance. Hypothèse 2 : résolution DNS en échec. Vérifier avec getent hosts ghcr.io ou resolvectl query ghcr.io. Hypothèse 3 : un pare-feu local qui bloque les réponses (règle ct state established,related accept absente, ou politique sortante restrictive). Vérifier avec sudo nft list ruleset. On teste dans cet ordre, du plus probable et du plus simple au plus rare : route, puis DNS, puis pare-feu. Un curl -sv https://ghcr.io aide à distinguer une résolution DNS échouée d'une connexion qui n'aboutit pas.

3. Concevoir le filtrage (niveau 200). L'équipe ajoute un second réseau privé, pn-outils, dans le même VPC, pour une instance de supervision qui doit interroger sig-app-1 et sig-app-2 sur le port 9100 (métriques), et rien d'autre. Décrivez le filtrage à mettre en place, à quel niveau, et pourquoi les groupes de sécurité n'y suffisent pas.

Solution

Le trafic entre deux réseaux privés d'un VPC est routé par le VPC et ne passe pas par les groupes de sécurité, qui ne filtrent que le trafic public. Deux niveaux se complètent : une Network ACL du VPC, qui n'autorise de pn-outils vers pn-signalements que le TCP 9100 depuis l'adresse de l'instance de supervision (en pensant que l'ACL est sans état, donc qu'il faut aussi autoriser les réponses dans l'autre sens), et le pare-feu de chaque instance, qui n'accepte le port 9100 que depuis cette même adresse. L'ACL protège si une instance est mal configurée ; le pare-feu local protège si l'ACL est modifiée par erreur.

4. Vérification de santé (niveau 200). La route /sante de Signalements teste la connexion à la base. Un matin, une maintenance de sig-db la rend injoignable pendant quarante secondes. Décrivez ce que voient les utilisateurs avec la vérification configurée dans la leçon (délai 5 s, trois échecs), puis avec une vérification TCP sur le port 8000. Laquelle préférez-vous, et que changeriez-vous ?

Solution

Avec /sante : après environ quinze secondes (trois échecs), les deux instances sont marquées hors service, puisque toutes deux perdent la base. Le répartiteur n'a plus de serveur sain et renvoie 503 Service Unavailable (ou la page de secours configurée), jusqu'à ce que les vérifications réussissent de nouveau. Avec une vérification TCP : les instances restent en service, et les utilisateurs reçoivent des erreurs 500 de l'application pendant quarante secondes. Le résultat est comparable pour l'utilisateur ; la différence apparaît quand une seule instance a un problème (conteneur planté, connexion à la base cassée sur une seule machine) : là, /sante la retire et l'autre absorbe la charge, alors que la vérification TCP laisse passer la moitié des requêtes vers une instance malade. On garde donc /sante, en configurant une page de secours (customized error page, servie depuis un bucket) pour le cas où tous les serveurs sont hors service, et en veillant à ce que /sante ne teste que les dépendances sans lesquelles l'instance ne peut vraiment rien servir.

Récapitulatif

  • Un VPC isole un espace réseau ; chez Scaleway, le réseau privé est un segment de niveau 2 régional, avec DHCP, IPAM et DNS internes (<ressource>.<réseau>.internal), là où un sous-réseau AWS est limité à une zone.
  • Les instances et la base n'ont pas d'adresse publique ; on ajoute le chemin privé, on bascule, on vérifie, puis on retire le public.
  • La passerelle publique donne la sortie NAT (route par défaut annoncée par DHCP) et le bastion SSH (port 61000, adresses autorisées à restreindre, clés importées à l'activation). Elle est zonale.
  • Le répartiteur de charge est la seule porte d'entrée : frontends, backends sur les adresses privées, vérification de santé applicative (GET /sante), TLS terminé avec Let's Encrypt, adresse du client dans X-Forwarded-For.
  • Chez Scaleway, le groupe de sécurité ne filtre que le trafic public ; le trafic privé se filtre par le pare-feu de l'hôte et, entre réseaux privés, par les Network ACL. Les ports publiés par Docker contournent la chaîne INPUT.
  • La défense en profondeur empile des protections indépendantes ; la surface d'attaque se mesure de l'extérieur.

Pour aller plus loin

  • La page Understanding routing de la documentation VPC de Scaleway, pour les tables de routage, les routes personnalisées et la propagation des routes par défaut entre réseaux privés.
  • La FAQ Public Gateways, sur la haute disponibilité de la sortie avec plusieurs passerelles.
  • La spécification du PROXY protocol, publiée par HAProxy, pour comprendre comment l'adresse du client traverse un répartiteur de niveau 4.
  • La leçon suivante, Identité et accès, qui limite ce que chaque personne et chaque automate peut faire sur toutes ces ressources.
Voir ma constellation →

Sources