Créer un réseau privé avec Headscale sur ZCP
Ce tutoriel crée un réseau privé sur ZCP : un VPC avec un niveau réseau qui n’est pas exposé publiquement par défaut, ainsi qu’un serveur Headscale auto-hébergé qui vous donne un accès par réseau maillé WireGuard. Il sert de fondation au reste de cette série : le stockage privé et les postes de travail privés des employés résident dans le niveau créé ici et ne sont accessibles que par le réseau maillé, jamais par une adresse IP publique.
À la fin, vous disposerez de :
- Un VPC et un niveau réseau privés
- Une ACL réseau personnalisée qui limite ce niveau au trafic dont il a besoin
- Un serveur Headscale auto-hébergé, déployé depuis le Marketplace
- Un routeur de sous-réseau qui relie Internet au niveau privé
- Votre appareil connecté au réseau maillé et capable d’atteindre le niveau privé
Prévoyez environ 30 minutes.
Avant de commencer
Section intitulée « Avant de commencer »- Un compte ZSoftly Public Cloud. Créez-en un si vous n’en avez pas.
- Un terminal avec un client SSH.
jqinstallé. Les scripts de création et de suppression de ce tutoriel l’exigent (apt install jqoubrew install jq).- Le client Tailscale installé sur votre appareil pour vérifier la connectivité à la fin.
Étape 1 : Installer la CLI
Section intitulée « Étape 1 : Installer la CLI »# macOS and Linuxcurl -fsSL https://raw.githubusercontent.com/zsoftly/zcp-cli/main/scripts/install.sh | bash# Windows (PowerShell)irm https://raw.githubusercontent.com/zsoftly/zcp-cli/main/scripts/install.ps1 | iexConfirmez son fonctionnement avec zcp version.
Étape 2 : S’authentifier
Section intitulée « Étape 2 : S’authentifier »- Dans le portail, ouvrez Profil → Jetons d’API et créez un jeton. Copiez-le.
- Créez un profil CLI et répondez aux invites :
zcp profile add defaultLa CLI demande le jeton Bearer copié, puis une région par défaut et un projet par défaut
(consultez la note ci-dessous pour les trouver si vous ne les connaissez pas). Elle ne demande pas
l’URL de l’API. Elle utilise une valeur par défaut fixe, sauf remplacement avec
--api-url-override. Vérifiez ensuite :
zcp auth validateÉtape 3 : Trouver vos ressources
Section intitulée « Étape 3 : Trouver vos ressources »Trouvez le modèle Headplane (il regroupe le serveur de contrôle Headscale et une interface web) :
zcp template list | grep -i headplane
Notez le SLUG (par exemple zmi-headplane-070-ubuntu2404-100-1). Les slugs de modèles varient
selon la région et la version.
Vous avez aussi besoin d’un plan de calcul, d’un plan réseau, d’un plan de routeur VPC et d’une catégorie de stockage :
zcp plan vm # compute plans, e.g. ca2smzcp plan network # network plans, e.g. pnet-yulzcp plan router # VPC router plan, e.g. virtual-private-cloud-vpc-1zcp storage-category list # e.g. pro-nvme, ssd-storage, premium-ssdÉtape 4 : Ajouter votre clé SSH
Section intitulée « Étape 4 : Ajouter votre clé SSH »ssh-keygen -t ed25519 -C "you@example.com" # skip if you already have one
zcp ssh-key import --name my-key --key-file ~/.ssh/id_ed25519.pub
zcp ssh-key listExécuter le script
Section intitulée « Exécuter le script »Un seul script crée le VPC, le niveau privé, l’ACL, Headplane et le routeur de sous-réseau :
zcp/build-private-network.sh du dépôt zsoftly/tools. Il passe
en ordre par les mêmes phases expliquées dans la section suivante et affiche chaque ressource lors
de sa création.
bash <(curl -fsSL https://raw.githubusercontent.com/zsoftly/tools/main/zcp/build-private-network.sh) \ --ssh-key my-key --name my-workspaceCe tutoriel utilise my-workspace comme préfixe --name dans tous ses exemples de commandes. Les
captures d’écran ci-dessous viennent d’un essai antérieur avec un préfixe différent. Les noms de
ressources qui y figurent ne correspondent donc pas exactement aux exemples de commandes. Vos noms
de ressources différeront des deux, selon la valeur choisie pour --name.
Le script récupère ZCP_REGION et ZCP_PROJECT depuis votre shell si vous les avez exportées à
l’étape 2. Sinon, transmettez --region/--project.
| Option | Rôle | Valeur par défaut / exigence |
|---|---|---|
--ssh-key | Nom de clé de l’étape 4, pour les deux VM | Obligatoire |
--name | Préfixe de chaque ressource créée | workspace |
--region | Slug de région ZCP | Obligatoire (option ou var. d’env.) |
--project | Slug de projet ZCP | Obligatoire (option ou var. d’env.) |
-y/--yes | Ignore l’invite de confirmation ci-dessous | Désactivé |
Le modèle Headplane, les deux plans de calcul, le plan réseau, le plan du routeur VPC et les deux catégories de stockage sont détectés automatiquement dans votre compte. Le script affiche ses choix au début de sa sortie afin que vous puissiez les examiner avant toute création.
Le modèle du routeur de sous-réseau n’est pas recherché. Il utilise par défaut un slug fixe
(ubuntu-2404-lts-1). Transmettez --router-template si votre compte ne possède pas ce modèle.
Votre adresse IP publique (--my-ip), la base du réseau VPC (--network-address) et le cycle de
facturation (--billing-cycle) ne sont pas non plus recherchés dans le compte. Ils utilisent une
valeur détectée automatiquement ou fixe que vous pouvez remplacer.
Transmettez explicitement une option pour fixer une valeur précise. Exécutez le script avec --help
pour obtenir la liste complète.
==> Preflight checks[OK] zcp CLI authenticated, region=yul-1 project=default-9[INFO] Detecting your public IP...[INFO] Admin-port access scoped to: <your-ip>/32[INFO] Resolved resources: Headplane template : zmi-headplane-070-ubuntu2404-100-1 Headplane plan : ci2ls Router template : ubuntu-2404-lts-1 Router plan : ci2ls Network plan : pnet-yul VPC router plan : virtual-private-cloud-vpc-1 VPC storage category : pro-nvme VM storage category : pro-nvme
This creates a VPC, two VMs (plans above), and networking on your account now.Billing starts as soon as each resource is created.Type 'yes' to continue:À moins d’avoir passé -y/--yes, le script s’arrête ici et attend que vous saisissiez yes. Tout
ce qui précède est une recherche en lecture seule. Aucune ressource n’a encore été créée.
Le script prend ensuite plusieurs minutes. Il attend le démarrage des deux VM, la fin du provisionnement de premier démarrage de Headplane et la disponibilité de SSH avant toute configuration par ce canal.
Ce que le script crée
Section intitulée « Ce que le script crée »VPC et niveau privé
Section intitulée « VPC et niveau privé »Le niveau privé n’a lui-même aucune adresse IP publique. Seul Headplane reçoit un port d’application exposé sur Internet.
Le script crée un VPC (my-workspace) et un niveau réseau (my-workspace-tier) à l’intérieur, sans
IP publique sur le niveau lui-même. Les deux VM déployées reçoivent chacune une IP publique, dont le
script a besoin pour les configurer par SSH. Ensuite, chaque VM l’utilise pour une raison
différente. Headplane en a besoin car il est le serveur Headscale : le routeur et votre appareil
l’atteignent à cette adresse. Le routeur en a besoin seulement pendant sa configuration, pour
joindre Headplane avant son inscription au maillage. Headplane est le seul des deux avec un port
d’application exposé à Internet, ouvert délibérément par des règles de pare-feu et de redirection de
port.
L’ACL réseau personnalisée
Section intitulée « L’ACL réseau personnalisée »C’est l’ACL qui rend le niveau privé, pas le VPC à lui seul.
L’ACL par défaut du niveau autorise tout. Le script la remplace par une ACL qui n’autorise que ce dont le niveau a besoin, puis l’applique au niveau. Un VPC seul ne garantit pas l’isolation. L’ACL la garantit.
Vérifiez que les règles ont été appliquées (consultez zcp acl rules dans « Inspecter les
ressources créées » ci-dessous) :

Headplane
Section intitulée « Headplane »Headplane ouvre délibérément un seul port d’application sur Internet : le point de contrôle du réseau maillé. L’interface d’administration ne touche jamais Internet.
Le script déploie le modèle Marketplace Headplane (il regroupe le serveur de contrôle Headscale et une interface web) sur son propre réseau public.
Par défaut, le script de premier démarrage du modèle fait pointer la configuration Headscale vers l’adresse IP privée de la VM. Les appareils externes ont plutôt besoin de l’adresse IP publique. Le script se connecte donc par SSH, réécrit cette configuration et redémarre la pile.
Le script ouvre le port 8080 (le point de contrôle du réseau maillé de Headscale) dans le
pare-feu et crée la règle de redirection de port correspondante. Une règle de pare-feu seule
autorise le trafic au niveau réseau. Dans ce type de réseau, elle ne rend pas le service accessible
à elle seule. Une règle de redirection associe le port de l’adresse IP publique à l’adresse IP
privée de la VM. Les deux sont requises. Le port 8080 doit rester largement ouvert (0.0.0.0/0) :
tout appareil distant qui rejoint le réseau maillé doit pouvoir l’atteindre depuis son emplacement.
Le premier démarrage de la VM Headplane génère un secret de cookie unique, démarre la pile, crée un
utilisateur Headscale par défaut et génère une clé API, écrite dans /etc/headplane/credentials.txt
sur la VM. Le script la lit par SSH et l’affiche dans son résumé final. Cette lecture unique relève
d’une convention, pas d’une limite technique : si vous avez de nouveau besoin de la clé,
connectez-vous par SSH et lisez directement /etc/headplane/credentials.txt.
Lorsque le tunnel est ouvert, connectez-vous à http://localhost:3000/admin/login avec la clé API.

Le routeur de sous-réseau
Section intitulée « Le routeur de sous-réseau »Le routeur de sous-réseau est le seul chemin entre Internet et le niveau privé.
Le script déploie une petite VM avec deux interfaces réseau : son propre réseau public pour joindre Headscale lors de l’inscription, et le niveau privé attaché après sa création.
La plateforme ajoute la seconde interface à chaud, mais le système d’exploitation ne l’active pas automatiquement. Le script écrit et applique un fichier netplan pour cette nouvelle interface, puis lit l’adresse attribuée par DHCP dans le niveau.
Il installe le client Tailscale sur le routeur, le même client qu’utilise Headscale mais pointé vers
un serveur de contrôle personnalisé, puis active le transfert IP avant l’inscription.
tailscale up affiche un avertissement à ce sujet (« IP forwarding is disabled, subnet routing/exit
nodes will not work »). Il ne bloque pas l’opération. Sans cette étape, la route est approuvée mais
ne transfère jamais de trafic.
Le script génère une clé preauth sur le serveur Headplane et l’utilise pour inscrire le routeur en annonçant le CIDR du niveau comme route.
L’approbation d’une route annoncée côté Headscale n’est pas automatique. Le script s’en charge : il
recherche l’ID de nœud du routeur et approuve le CIDR du niveau pour celui-ci. Sans approbation,
list-routes indique Available, mais jamais Approved ni Serving. Après l’approbation,
Serving (Primary) peut prendre quelques secondes à apparaître. C’est normal.
Vous pouvez aussi le vérifier dans l’interface Headplane : le routeur affiche Connected avec le badge Subnets dès qu’il annonce la route.

Cette VM est la porte d’accès au niveau privé. Les bureaux et le stockage des tutoriels suivants se trouvent derrière elle, sans nécessiter leurs propres IP publiques.
Inspecter les ressources créées
Section intitulée « Inspecter les ressources créées »Tout ce que le script crée est une ressource ZCP ordinaire. Listez-les comme n’importe quelle autre ressource :
zcp vpc listzcp network listzcp instance listzcp acl rules my-workspace my-workspace-aclID NAME STATE PRIVATE IP PUBLIC IP REGIONa1b2c3d4-... my-workspace-headscale Running 10.0.0.214 198.51.100.10 YUL-1e5f6a7b8-... my-workspace-subnet-router Running 10.0.0.76 198.51.100.11 YUL-1acl rules prend le slug du VPC, pas son nom. Ils sont identiques dans un compte propre, mais un
nom peut recevoir automatiquement un suffixe (my-workspace-1) s’il a déjà été utilisé. Si la
commande ci-dessus ne se résout pas, récupérez d’abord le vrai slug avec zcp vpc list.
Vous connecter depuis votre appareil
Section intitulée « Vous connecter depuis votre appareil »Le résumé final du script de construction génère déjà une clé preauth neuve pour votre appareil. Il
affiche une commande prête à l’emploi qui installe Tailscale s’il n’est pas déjà présent et
l’inscrit auprès de votre serveur Headscale en une étape. C’est le même script vpn/install.sh
utilisé pour intégrer tout autre point de terminaison. Copiez cette ligne depuis votre terminal.
Elle ressemble à ceci (la clé est générique, la vôtre est réelle) :
HEADSCALE_URL="http://198.51.100.10:8080" \ bash <(curl -fsSL https://raw.githubusercontent.com/zsoftly/tools/main/vpn/install.sh) "your-name" --key "hskey-auth-EXAMPLE..."Collez-la dans un terminal sur votre machine et exécutez-la. Sous macOS, vous devrez peut-être
approuver une extension réseau dans Réglages Système → Confidentialité et sécurité avant la fin
de la connexion. Sous Windows sans WSL ni Git Bash, utilisez plutôt vpn/install.ps1 du même dépôt
zsoftly/tools. Son README donne la méthode d’appel PowerShell.
Vérifiez :
tailscale statusping <router-tier-ip>

Les connexions directes dans la même région répondent généralement en quelques millisecondes. Une connexion relayée par un serveur DERP, fréquente entre réseaux éloignés, peut prendre plusieurs centaines de millisecondes, surtout pour le premier paquet durant la négociation du chemin. Les deux cas sont normaux. Le chemin réseau a bien plus d’effet sur la latence que la taille de la VM.
Vérifier l’isolation
Section intitulée « Vérifier l’isolation »Atteindre l’IP de niveau du routeur de sous-réseau dans la section précédente prouve que le routeur lui-même n’est pas exposé publiquement. Cela ne prouve pas que le niveau entier est isolé. Ce trafic se termine directement au point de terminaison WireGuard du routeur. Atteindre une autre VM du niveau passe au contraire par le transfert du routeur, un chemin différent.
Pour une preuve réelle, déployez une seconde VM comme le script déploie le routeur de sous-réseau : sa propre IP publique pour la configuration, puis son attachement au niveau. Cette IP publique sert uniquement à la configurer et ne fait pas partie de ce qui est testé :
zcp instance create --name isolation-check \ --template ubuntu-2404-lts-1 --plan ci2ls --billing-cycle hourly \ --network-plan pnet-yul --storage-category pro-nvme --ssh-key my-key --wait
zcp instance add-network isolation-check --network my-workspace-tieradd-network prend aussi le slug du niveau, pas son nom, avec la même réserve que ci-dessus.
Récupérez-le avec zcp network list si le nom seul ne se résout pas.
Connectez-vous avec SSH par sa propre IP publique et activez l’interface réseau de niveau ajoutée à chaud comme le script le fait pour le routeur. Consultez « Le routeur de sous-réseau » ci-dessus pour savoir pourquoi cette étape manuelle est nécessaire :
ip -br link show # find the new interface, typically ens8
sudo tee /etc/netplan/60-tier-nic.yaml <<'EOF'network: version: 2 ethernets: ens8: dhcp4: trueEOFsudo netplan apply
ip -4 -br addr show ens8 # note the address it gets, e.g. 10.20.1.201Depuis votre appareil déjà connecté :
ping <isolation-check-tier-ip>Cette commande réussit à travers le maillage et le transfert du routeur, pas seulement vers l’adresse du routeur. Essayez maintenant d’atteindre cette même IP de niveau depuis un endroit qui n’a jamais rejoint le maillage : votre réseau domestique, une autre machine ou Internet. Elle échoue à chaque fois. Cette adresse est privée, sans IP publique ni règle de redirection dans cette architecture. Elle n’a jamais été joignable depuis Internet, maillage ou non. C’est la preuve réelle de l’isolation.
Comme chaque déploiement avec --network-plan de ce tutoriel, isolation-check a aussi créé son
propre réseau autonome et une IP source-NAT épinglée. instance delete ne les supprime pas et le
script de suppression ne gère que les ressources associées à --name my-workspace. Il ne touchera
donc pas celles-ci. Relevez l’ID de ce réseau avant de supprimer la VM, car l’association
disparaît lorsque la VM est supprimée :
zcp ip list -o json | jq -r '.[] | select(.vm=="isolation-check") | .network_id // empty' | head -1Supprimez ensuite la VM (zcp instance delete isolation-check). Elle ne fait pas partie de la
configuration de travail, elle sert seulement à le constater vous-même. Trouvez le réseau associé à
cet ID et supprimez-le du portail web CMP, comme l’indique l’avertissement Nettoyer ci-dessous.
Nettoyer les ressources
Section intitulée « Nettoyer les ressources »La facturation horaire continue tant que les ressources existent. Le script de suppression retire le
routeur de sous-réseau, la VM Headplane, le niveau privé et le VPC correspondant à un préfixe
--name donné.
bash <(curl -fsSL https://raw.githubusercontent.com/zsoftly/tools/main/zcp/destroy-private-network.sh) \ --name my-workspaceIl récupère ZCP_REGION/ZCP_PROJECT dans votre shell comme le script de construction. Passez
plutôt --region/--project si vous ne les avez pas exportés.
Récapitulatif
Section intitulée « Récapitulatif »- Installez la CLI, authentifiez-vous, trouvez les slugs des ressources de votre compte et importez une clé SSH (étapes 1 à 4).
- Exécutez
build-private-network.sh --ssh-key <name> --name my-workspace. Le script crée le VPC, le niveau privé, l’ACL limitée, Headplane et le routeur de sous-réseau, puis affiche une commande de connexion pour votre appareil. - Copiez cette commande depuis la sortie du script, exécutez-la sur votre machine, puis vérifiez
avec
tailscale statuset un ping vers le niveau. - Exécutez
destroy-private-network.sh --name <prefix>lorsque vous avez terminé afin de tout supprimer et d’arrêter la facturation.
Prochaines étapes
Section intitulée « Prochaines étapes »- Déployer un stockage partagé privé : un partage NFS dans le niveau que vous venez de créer, accessible depuis le niveau et le maillage
- Déployer des postes Ubuntu pour les employés : un bureau complet pour un employé dans le même niveau, accessible uniquement via le maillage
- Référence CLI : chaque commande et indicateur
- Vue d’ensemble des tutoriels : la liste complète des tutoriels