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.
| Machine | Adresses | Par heure | Équivalent mensuel |
|---|---|---|---|
| app (Nano) | IPv4, IPv6, 10.20.0.2 | 0,018 CAD | 13,14 CAD |
| base (Nano, privée) | 10.20.0.3 seulement | 0,014 CAD | 10,22 CAD |
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 :
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.
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 :
# 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 :
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 :
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 :
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
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 :
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é :
$ 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 :
$ 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.
| Mesure | Commande | Résultat |
|---|---|---|
| Ping, 100 paquets espacés de 200 ms | ping -c 100 -i 0.2 | min 0,247 ms, moyenne 0,400 ms, max 0,648 ms |
| Ping en rafale, 5 000 paquets | sudo ping -f -c 5000 | moyenne 0,064 ms, 0 perte |
Requête SELECT 1, 2 000 à la suite | psql sur 10.20.0.3 | 187 à 206 µs par requête |
| Même chose en local sur la base | psql sur 127.0.0.1 | 86 à 87 µs par requête |
| Débit, un flux, 20 s | iperf3 -c 10.20.0.3 -t 20 | 20,6 Gbit/s |
| Débit, sens inverse | iperf3 … -R | 18,6 Gbit/s |
| Débit, quatre flux | iperf3 … -P 4 | 17,5 Gbit/s |
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 :
Host vpc-app
HostName 203.0.113.80
User debian
Host vpc-base
HostName 10.20.0.3
User debian
ProxyJump vpc-app
$ 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 :
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, pasroot: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 dopasse, 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/311renvoie « 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 :
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.