Réseau privé (VPC) : relier ses VM hors Internet

Une machine app avec une adresse publique, une machine base qui n'en a aucune, PostgreSQL entre les deux par le réseau privé. Nous avons vérifié que la base est injoignable depuis Internet, mesuré 0,1 ms de plus par requête et 20 Gbit/s de débit, et administré la base par rebond SSH.

Deux machines reliées par un lien privé ; seule celle de gauche a une porte vers Internet, la base à droite reste fermée

Une application et sa base de données sur deux machines, c'est le montage le plus courant dès qu'un projet grandit un peu. La question qui suit tout de suite : comment la base reçoit-elle les requêtes de l'application sans être exposée au reste d'Internet ? Un pare-feu bien réglé est une réponse. Une base qui n'a tout simplement pas d'adresse publique en est une meilleure.

C'est ce que permet un réseau privé (VPC) chez FFxF. Nous avons monté le cas réel le 3 novembre 2026 : un réseau, une machine « app » avec une adresse publique, une machine « base » sans aucune adresse publique, PostgreSQL installé à la main sur la seconde. Toutes les sorties ci-dessous viennent de ce test.

1. Le montage

  • Un réseau privé 10.20.0.0/24 à Montréal.
  • app : un Nano (1 vCPU, 2 Go, 20 Go) avec IPv4 et IPv6 publiques, plus une carte dans le réseau privé. C'est la seule porte d'entrée.
  • base : un Nano dans le réseau privé seulement, avec PostgreSQL 17. Aucune adresse IPv4 ni IPv6 publique.

Les deux machines sont commandées à l'heure. Le réseau privé lui-même ne se paie pas.

La machine privée coûte moins cher : elle est facturée comme une machine sans IPv4. Au mois, un Nano revient à 8,50 CAD.

2. Créer le réseau

Dans la console, sous Réseaux privés, ou par l'API avec une clé qui a la portée network.write :

bash
curl -s -X POST https://api.ffxf.net/v1/vpcs \
     -H "Authorization: Bearer $FFXF_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"name": "blog-01-vpc-reseau", "cidr": "10.20.0.0/24", "region": "montreal"}'

{"data":{"id":40,"name":"blog-01-vpc-reseau","cidr":"10.20.0.0\/24","region":"montreal",
 "status":"active","gateway":"10.20.0.1","internet_gateway":false,"members":[]}}

Le réseau est actif au retour de l'appel. L'adresse .1 est réservée à la passerelle, les machines prennent les suivantes dans l'ordre. Vous choisissez la plage, de /16 à /29, dans les plages privées habituelles ; un autre client peut utiliser la même sans que vos réseaux se voient. Un compte a droit à trois réseaux.

3. Commander les deux machines

La machine « app » reçoit simplement le champ vpc. La machine « base » y ajoute "private_only": true.

bash
curl -s -X POST https://api.ffxf.net/v1/vms \
     -H "Authorization: Bearer $FFXF_TOKEN" \
     -H "Idempotency-Key: $(uuidgen)" \
     -H "Content-Type: application/json" \
     -d '{"plan": "nano", "region": "montreal", "image": "debian-13",
          "hostname": "blog-01-vpc-app", "billing": "hourly",
          "ssh_keys": ["34"], "password_delivery": "none", "vpc": 40}'

curl -s -X POST https://api.ffxf.net/v1/vms \
     -H "Authorization: Bearer $FFXF_TOKEN" \
     -H "Idempotency-Key: $(uuidgen)" \
     -H "Content-Type: application/json" \
     -d '{"plan": "nano", "region": "montreal", "image": "debian-13",
          "hostname": "blog-01-vpc-base", "billing": "hourly",
          "ssh_keys": ["34"], "password_delivery": "none",
          "vpc": 40, "private_only": true}'

Premier piège : ssh_keys attend des chaînes. Un identifiant passé comme nombre ([34]) est refusé avec validation_failed ; il faut ["34"], ou l'empreinte SHA256:… de la clé.

Les deux machines étaient en marche une centaine de secondes après la commande. GET /vms/{vm}/network montre la différence :

bash
# app (310)
"ipv4":[{"address":"203.0.113.80", ...}],
"ipv6":[{"address":"2001:db8:100::4c", ...}],
"private":[{"vpc":40,"address":"10.20.0.2","prefix_length":24,"configuration":"automatic"}]

# base (311)
"ipv4":[], "ipv6":[],
"private":[{"vpc":40,"address":"10.20.0.3","prefix_length":24,"configuration":"automatic"}]

Dans le système, l'adresse privée est déjà posée au premier démarrage. Sur la base, la route par défaut passe par la passerelle du réseau :

bash
debian@blog-01-vpc-base:~$ ip -br a; ip route
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             10.20.0.3/24 fe80::60dd:1dff:fe0a:42ff/64
default via 10.20.0.1 dev eth0 proto static
10.20.0.0/24 dev eth0 proto kernel scope link src 10.20.0.3

Dès qu'une machine privée rejoint le réseau, celui-ci affiche "internet_gateway": true : la passerelle laisse la base sortir sur Internet avec une adresse partagée, pour les mises à jour et les téléchargements. Le port 25 reste fermé en sortie.

4. Installer PostgreSQL sur la base

La base se joint en SSH par rebond via la machine app (détails à la section 8). L'installation passe par la passerelle :

bash
sudo cloud-init status --wait
sudo apt-get update
sudo apt-get install -y postgresql

Le cloud-init status --wait n'est pas décoratif : juste après le démarrage, cloud-init fait encore tourner apt, et notre premier essai a échoué sur Could not get lock /var/lib/apt/lists/lock. Une fois cloud-init terminé, l'installation de PostgreSQL 17.11 a pris 1 min 14 s.

Par défaut, PostgreSQL n'écoute que sur localhost. On l'ouvre sur l'adresse privée, et seulement elle, puis on autorise le réseau privé dans pg_hba.conf :

bash
echo "listen_addresses = 'localhost,10.20.0.3'" \
  | sudo tee /etc/postgresql/17/main/conf.d/vpc.conf
echo "host  appdb  app  10.20.0.0/24  scram-sha-256" \
  | sudo tee -a /etc/postgresql/17/main/pg_hba.conf
sudo -u postgres psql -c "CREATE ROLE app LOGIN PASSWORD 'un-vrai-mot-de-passe';"
sudo -u postgres psql -c "CREATE DATABASE appdb OWNER app;"
sudo systemctl restart postgresql
sudo ss -ltnp | grep 5432
bash
LISTEN 0  200   127.0.0.1:5432   0.0.0.0:*   users:(("postgres",pid=4434,fd=7))
LISTEN 0  200   10.20.0.3:5432   0.0.0.0:*   users:(("postgres",pid=4434,fd=8))
LISTEN 0  200       [::1]:5432      [::]:*   users:(("postgres",pid=4434,fd=6))

Écrire l'adresse privée plutôt que * est une habitude à garder, même ici : le jour où la même configuration est copiée sur une machine qui a une adresse publique, la base ne s'y retrouve pas exposée par accident.

5. L'application joint la base

Sur la machine app, seul le client est nécessaire (apt-get install -y postgresql-client). La connexion vise l'adresse privée :

bash
debian@blog-01-vpc-app:~$ psql "host=10.20.0.3 dbname=appdb user=app" \
  -c "SELECT id, client, total, inet_server_addr() AS serveur,
             inet_client_addr() AS client_ip FROM commandes;"

 id |  client  | total |  serveur  | client_ip
----+----------+-------+-----------+-----------
  1 | Tremblay | 42.50 | 10.20.0.3 | 10.20.0.2
(1 row)

PostgreSQL voit la connexion arriver de 10.20.0.2, l'adresse privée de l'application. Dans votre code, la chaîne de connexion pointe simplement vers 10.20.0.3:5432.

6. La base est-elle vraiment injoignable ?

Pendant le test, la base faisait aussi tourner un serveur iperf3 qui écoutait sur toutes ses interfaces (*:5201) : s'il existait un chemin depuis l'extérieur, il serait visible. Premier essai depuis un autre de nos serveurs, voisin direct dans la même plage publique, mais hors du réseau privé :

bash
$ ip route get 10.20.0.3
10.20.0.3 via 23.159.52.1 dev eno1 src 23.159.52.5
$ timeout 5 bash -c '</dev/tcp/10.20.0.3/5432'; echo $?
124

L'adresse 10.20.0.3 n'existe pas hors du réseau privé : le paquet part vers la route par défaut et n'arrive nulle part (délai dépassé, code 124). Restait l'adresse partagée par laquelle la base sort sur Internet, ici 23.159.52.25. Essai depuis une machine chez un autre hébergeur :

bash
$ timeout 6 bash -c '</dev/tcp/23.159.52.25/5432'
bash: connect: Connection refused
$ timeout 6 bash -c '</dev/tcp/23.159.52.25/5201'
bash: connect: Connection refused

Ni PostgreSQL ni iperf3 n'y répondent. Cette adresse répond bien en SSH, mais ce n'est pas notre machine : sa clé d'hôte (SHA256:a977NC5N…) diffère de celle de la base (SHA256:Liteob51…). C'est la passerelle, partagée, qui ne transmet rien vers l'intérieur.

Côté app, le port 5432 de l'adresse publique est fermé lui aussi : PostgreSQL n'y tourne pas. Pour cette machine, un pare-feu cloud qui ne laisse passer que 22, 80 et 443 reste conseillé. Il ne filtre pas le réseau privé, donc il ne gênera pas le trafic vers la base.

7. Latence et débit mesurés

Toutes les mesures ont été faites depuis app vers base, sur l'adresse privée, sans aucun réglage du système.

Le réseau ajoute donc environ 0,1 ms par aller-retour à une requête SQL. C'est négligeable pour une page qui fait quelques requêtes, et cela devient visible pour une page qui en enchaîne des centaines : dans ce cas, regroupez les requêtes plutôt que de chercher du côté du réseau.

Le ping espacé paraît plus lent que la rafale parce qu'une machine au repos met un peu de temps à se réveiller pour chaque paquet. Les débits sont ceux de deux machines à 1 vCPU sur notre infrastructure ce jour-là ; c'est un ordre de grandeur, pas un débit garanti.

Un point se vérifie dans l'API : environ 140 Go sont passés par le réseau privé pendant ces tests, et le compteur de trafic de la machine app indiquait 0,01 Go ensuite. Le trafic privé n'est pas compté dans le forfait de 1 To. Ce qui sort par la passerelle l'est : la base affichait 0,1 Go, soit à peu près ses téléchargements apt.

8. Administrer la machine privée

Sans adresse publique, la base se joint en passant par app. OpenSSH le fait seul avec ProxyJump. Dans ~/.ssh/config, sur votre poste :

bash
Host vpc-app
    HostName 203.0.113.80
    User debian

Host vpc-base
    HostName 10.20.0.3
    User debian
    ProxyJump vpc-app
bash
$ ssh vpc-base 'hostname; uptime'
blog-01-vpc-base
 15:34:44 up 5 min,  2 users,  load average: 0.30, 0.37, 0.18

La même clé SSH, posée à la commande, sert pour les deux machines. Aucune clé privée n'est copiée sur app : le rebond transporte la connexion chiffrée jusqu'à la base. Sur une seule ligne, sans fichier de configuration : ssh -J debian@203.0.113.80 debian@10.20.0.3.

Pour ouvrir la base dans un outil graphique depuis votre poste, un tunnel suffit :

bash
ssh -f -N -o ExitOnForwardFailure=yes -L 15432:10.20.0.3:5432 vpc-app
psql "host=127.0.0.1 port=15432 dbname=appdb user=app" \
  -Atc "select client, total, inet_client_addr() from commandes"

Tremblay|42.50|10.20.0.2

PostgreSQL voit la connexion venir de 10.20.0.2 : la règle pg_hba.conf du réseau privé suffit, rien à ouvrir de plus.

9. Pièges et limites

  • L'image Debian 13 se connecte avec l'utilisateur debian, pas root : ssh root@… renvoie un message qui le rappelle.
  • La carte privée a une MTU de 1450 octets, posée d'elle-même. Un ping de 1 422 octets avec -M do passe, un de 1 423 est refusé : rien à régler, mais à savoir si vous montez un tunnel par-dessus.
  • Une machine sans adresse publique ne peut pas quitter son seul réseau : DELETE /vpcs/40/members/311 renvoie « This machine has no public address: its private network is its only access ».
  • Un réseau qui contient encore des machines ne se supprime pas : réponse 409, « Delete the machines on this network first ».
  • Le réseau vit dans une région. Une machine ne rejoint qu'un réseau de sa région.
  • Nous n'avons pas activé TLS sur PostgreSQL pour ce test. Le réseau est isolé des autres clients ; si votre politique exige le chiffrement de bout en bout, ajoutez-le côté base comme ailleurs.

10. Tout supprimer

Les machines d'abord, le réseau ensuite. La suppression d'une machine demande son nom d'hôte dans confirm, une garde contre un identifiant mal tapé dans un script :

bash
curl -s -X DELETE -H "Authorization: Bearer $FFXF_TOKEN" \
     "https://api.ffxf.net/v1/vms/311?confirm=blog-01-vpc-base"
curl -s -X DELETE -H "Authorization: Bearer $FFXF_TOKEN" \
     "https://api.ffxf.net/v1/vms/310?confirm=blog-01-vpc-app"
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE \
     -H "Authorization: Bearer $FFXF_TOKEN" https://api.ffxf.net/v1/vpcs/40
204

Une quinzaine de secondes après la suppression des machines, le réseau était vide et sa suppression a répondu 204. Le test complet, de la création du réseau à sa suppression, a duré neuf minutes et coûté 0,032 CAD : une heure entamée par machine, à 0,018 et 0,014 CAD.

Un flux entrant partagé par un répartiteur entre trois machines, dont une drainée

Répartiteur de charge : une adresse, plusieurs VM

Un répartiteur prend le trafic sur sa propre adresse et le partage entre vos machines. Pools et contrôles de santé, port HTTPS et certificat automatique, routage par chemin, maintenance sans coupure : un montage complet, à 0,010 CAD l'heure.

Lire l'article 8 min de lecture

Une VM IPv6 reliée par une passerelle de traduction à l'Internet IPv4

VPS IPv6 seulement : 2 CAD de moins, l'IPv4 à portée

Pas d'IPv4 publique, 2 CAD de moins par mois, et GitHub reste à portée : démonstration d'un VPS IPv6 seulement, et de la passerelle qui le relie à l'Internet IPv4.

Lire l'article 6 min de lecture

Support & discussions

Questions techniques, retours d'incidents ou discussions d'infrastructure, l'équipe est présente sur Discord, Telegram, X, Instagram, Reddit et IRC.