Contexte : projet de cyber range léger (laboratoire de cybersécurité isolé) conçu pour un client industriel, hébergé sur l'hôte Proxmox · Testé sur : Proxmox VE 8 (Debian 12), VM Debian 13, Python 3 / FastAPI, LXD · Dernière vérification : contenu de janvier 2026 non revérifié (état du lab mis à jour le 01/10/2026)
État au 01/10/2026 : lab démonté. La VM de gestion
debian-mgmt(VM 150) n'existe plus sur l'hôte et l'interface web publiée n'a plus de machine derrière elle. Seul le pont isolévmbr150subsiste, sans adresse IPv4. L'hôte est depuis passé en Proxmox VE 9. Cette page documente la conception et les étapes de construction, réutilisables pour remonter le lab.
vmbr150 et son rôle d'isolement.eno1.| Pont | Rôle |
|---|---|
vmbr0 |
Accès Internet, gestion de Proxmox VE, réseau local (porte l'adresse IP de l'hôte). |
vmbr1 |
Pont isolé (usages divers, tests internes). |
vmbr20 |
Pont isolé (usages divers, tests internes). |
vmbr150 |
Pont dédié au lab : réseau isolé (air gap virtuel), sans port physique (bridge-ports none). |
bridge-ports none, bridge-stp off (si le STP n'est pas requis), bridge-fd 0.Déclaration correspondante dans /etc/network/interfaces de l'hôte :
auto vmbr150
iface vmbr150 inet manual
bridge-ports none
bridge-stp off
bridge-fd 0
La VM debian-mgmt sert de bastion et de « cerveau » du cyber range : accès d'administration, outillage de déploiement, supervision. Elle est à double attachement (dual-homed) : net0 sur vmbr0 pour Internet et la gestion, net1 sur vmbr150 pour le réseau isolé du lab. Debian 13 (Trixie) a été choisie, plus récente que l'hôte Debian 12 de l'époque, pour bénéficier des derniers correctifs de sécurité.
| Élément | Valeur |
|---|---|
| ID / nom | 150 / debian-mgmt |
| Système | Debian 13 (Trixie) |
| CPU | 4 cœurs, type host (virtualisation imbriquée) |
| RAM | 8 Go (8 192 Mo) |
| Stockage | 40 Go sur le pool LVM-thin, avec discard=on et ssd=1 |
| Réseau de gestion | net0 : bridge=vmbr0 (accès Internet, mises à jour) |
| Réseau du cyber range | net1 : bridge=vmbr150 (isolé, air gap) |
Pourquoi --cpu host : ce type propage le modèle de processeur de l'hôte dans la VM et active la virtualisation imbriquée (VT-x). La VM peut ainsi héberger des hyperviseurs ou des appliances internes au lab sans perte de fonctionnalités processeur.
La VM a été supprimée puis recréée avec qm create pour garantir le contrôleur virtio-scsi-single et le type de CPU host :
vm-150-disk-0 de 40 Go provisionné sur le pool LVM-thin, avec discard=on et ssd=1 pour optimiser les entrées-sorties ;scsihw=virtio-scsi-single, pour des performances stables et une large compatibilité ;local:iso/Debian13.iso (lecteur ide2), dans l'ordre virtio0 puis ide2 pour l'installation initiale.# Création propre de la VM 150 (sur l'hôte Proxmox, en root)
qm create 150 \
--name debian-mgmt \
--memory 8192 --cores 4 --cpu host \
--scsihw virtio-scsi-single \
--virtio0 pool:40,discard=on,ssd=1 \
--net0 virtio,bridge=vmbr0 \
--net1 virtio,bridge=vmbr150 \
--cdrom local:iso/Debian13.iso \
--boot 'order=virtio0;ide2'
L'option
--bootdoit être entre guillemets : sans eux, le shell interprète le;comme une fin de commande.
Résultat : la VM démarre et l'installateur Debian est prêt.
Chaîne d'accès : poste Windows → hôte Proxmox (utilisateur mass) → VM debian-mgmt.
1. Côté poste Windows, générer une paire de clés ED25519 dans %USERPROFILE%\.ssh (emplacement utilisé automatiquement par le client SSH du profil) :
ssh-keygen -t ed25519 -C "bastion-access" -f "$env:USERPROFILE\.ssh\id_ed25519"
2. Sur l'hôte Proxmox (en root), créer l'utilisateur et déposer la clé publique :
adduser mass
# (facultatif, si sudo est installé) usermod -aG sudo mass
mkdir -p /home/mass/.ssh && chmod 700 /home/mass/.ssh
cat <<'EOF' >> /home/mass/.ssh/authorized_keys
# coller ici la clé publique ED25519
EOF
chmod 600 /home/mass/.ssh/authorized_keys
chown -R mass:mass /home/mass/.ssh
Les permissions doivent être strictes : .ssh en 700, authorized_keys en 600.
3. Variante : envoyer la clé depuis Windows vers l'hôte Proxmox, puis vers la VM :
# Vers l'hôte Proxmox
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh mass@<IP_PROXMOX> "umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys"
# Vers la VM debian-mgmt
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh mass@<IP_VM> "umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys"
4. Durcir SSH sur l'hôte et sur la VM :
Désactivez la connexion root et l'authentification par mot de passe pour imposer les clés. Vérifiez d'abord que la connexion par clé fonctionne, sous peine de perdre l'accès.
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
systemctl restart ssh
Sur Debian, sshd est un alias du service ssh : systemctl restart sshd fonctionne également.
Dépannage : si usermod ou sudo est introuvable dans une session SSH non interactive, vérifiez que /usr/sbin est dans le PATH, ou utilisez le chemin complet : sudo /usr/sbin/usermod -aG sudo mass.
L'interface de gestion de la VM, identifiée avec ip a, est ens18. Elle passe de DHCP à une adresse statique pour stabiliser l'adresse de gestion.
État initial (DHCP) :
# /etc/network/interfaces (extrait)
auto ens18
iface ens18 inet dhcp
Configuration cible (statique) :
# /etc/network/interfaces (extrait)
auto ens18
iface ens18 inet static
address 192.168.1.X
netmask 255.255.255.0
gateway 192.168.1.1
dns-nameservers 1.1.1.1 8.8.8.8
Application et vérification :
sudo systemctl restart networking
ip a show dev ens18 # vérifier l'adresse et l'état
Dépannage : si l'ancienne adresse reste affichée avec la mention secondary, c'est un reliquat du bail DHCP : la reconfiguration ajoute l'IP statique sans retirer l'ancienne. Deux corrections possibles :
sudo ip addr flush dev ens18, puis sudo systemctl restart networking ;sudo dhclient -r ens18 pour libérer l'ancien bail avant le redémarrage du réseau.Vérifiez ensuite avec ip a qu'il ne reste qu'une seule adresse active.
Objectif : valider manuellement, puis automatiser, l'interconnexion de deux hôtes virtuels (pc1, pc2) reliés à un commutateur virtuel sur 10.150.0.0/24.
br-lab joue le rôle de commutateur L2 virtuel.pc1 et pc2 sont des hôtes isolés.veth sont les câbles virtuels qui les relient au pont.pc1(ns) -- veth-pc1 <-> veth-pc1-br -- [ br-lab ] -- veth-pc2-br <-> veth-pc2 -- pc2(ns)

# Commutateur virtuel
ip link add br-lab type bridge
ip link set br-lab up
# Namespaces (piles réseau)
ip netns add pc1
ip netns add pc2
ip -n pc1 link set lo up
ip -n pc2 link set lo up
# Câbles veth vers le pont
ip link add veth-pc1 type veth peer name veth-pc1-br
ip link set veth-pc1 netns pc1
ip link set veth-pc1-br master br-lab
ip link set veth-pc1-br up
ip -n pc1 link set veth-pc1 up
ip link add veth-pc2 type veth peer name veth-pc2-br
ip link set veth-pc2 netns pc2
ip link set veth-pc2-br master br-lab
ip link set veth-pc2-br up
ip -n pc2 link set veth-pc2 up
# Adressage du lab en /24
ip -n pc1 addr add 10.150.0.11/24 dev veth-pc1
ip -n pc2 addr add 10.150.0.12/24 dev veth-pc2
Ping de pc1 vers pc2 (résultat attendu : 0 % de perte) :
ip netns exec pc1 ping -c 2 10.150.0.12
Capture ICMP sur le pont, dans deux terminaux :
# Terminal 1
tcpdump -ni br-lab icmp -vv
# Terminal 2
ip netns exec pc1 ping -c 2 10.150.0.12
tcpdump affiche les echo request de 10.150.0.11 vers 10.150.0.12 et les echo reply en retour.
setup_lab.sh crée le pont, les namespaces, les paires veth et les adresses IP. Il doit être idempotent : on peut le relancer sans rien casser.cleanup.sh supprime les namespaces, les paires veth et le pont pour repartir d'un état propre.setup_lab.sh reconstruit, cleanup.sh remet tout à zéro../setup_lab.sh
ip netns exec pc1 ping -c 2 10.150.0.12
tcpdump -ni br-lab icmp -c 4 & sleep 0.3; ip netns exec pc1 ping -c 2 10.150.0.12; wait
./cleanup.sh
Objectif : décrire la topologie en JSON et bloquer tout déploiement incohérent avant exécution. Format minimal (MVP) :
nodes : un nœud network (le pont) et des nœuds host (avec leur IP) ;edges : des liens host -> network qui déclarent les connexions.Exemple de topologie (mini-cyberlab/topologies/topo.json) :
{
"name": "mvp-2hosts-1net",
"nodes": [
{ "id": "net1", "type": "network", "bridge": "br-lab" },
{ "id": "pc1", "type": "host", "ip": "10.150.0.11/24" },
{ "id": "pc2", "type": "host", "ip": "10.150.0.12/24" }
],
"edges": [
{ "source": "pc1", "target": "net1" },
{ "source": "pc2", "target": "net1" }
]
}
Le validateur tools/validate_topology.py contrôle :
nodes et edges, et l'unicité des identifiants ;network et au moins deux host ;edges.S'il n'y a pas d'erreur, il affiche le pont, les hôtes et le sous-réseau déduit. Le JSON est lu avec l'encodage utf-8-sig, pour accepter les fichiers enregistrés avec un BOM sous Windows.
python3 tools/validate_topology.py topologies/topo.json

Objectif : automatiser le déploiement à partir d'une topologie validée. Trois briques :
scripts/setup_lab.sh et scripts/cleanup.sh (création et suppression du pont, des namespaces, des paires veth et des IP) ;tools/plan_topology.py, simulation (dry-run) lisible des actions prévues ;tools/deploy_topology.py, qui valide le JSON puis appelle le moteur réseau.Flux complet :
tcpdump, puis nettoyage.Le lab est éphémère : cleanup.sh supprime tout, deploy_topology.py recrée tout.
python3 tools/plan_topology.py topologies/topo.json
python3 tools/deploy_topology.py topologies/topo.json
ip netns exec pc1 ping -c 2 10.150.0.12
bash scripts/cleanup.sh br-lab pc1 pc2

Objectif : piloter l'orchestrateur par HTTP plutôt qu'en lançant les scripts à la main (mode « plateforme »). Composants : api/app.py (FastAPI), serveur uvicorn, réutilisation du validateur, du planificateur et du déployeur.
cd /home/mass/mini-cyberlab
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install fastapi uvicorn
source .venv/bin/activate
uvicorn api.app:app --host 0.0.0.0 --port 8000 --reload
curl -s http://127.0.0.1:8000/health doit renvoyer {"ok":true}.http://127.0.0.1:8000/docs.# Création de la topologie (JSON existant)
curl -s -X POST http://127.0.0.1:8000/topologies \
-H "Content-Type: application/json" \
--data-binary @topologies/topo.json
# Noter l'identifiant renvoyé, par exemple <ID>
# Plan puis déploiement
curl -s -X POST http://127.0.0.1:8000/topologies/<ID>/plan
curl -s -X POST http://127.0.0.1:8000/topologies/<ID>/deploy
# Vérifications
ip netns list
ip netns exec pc1 ping -c 2 10.150.0.12
# Nettoyage
curl -s -X POST http://127.0.0.1:8000/topologies/<ID>/cleanup
Par défaut, l'API écoute sur
0.0.0.0:8000, donc sur toutes les interfaces. Pour la garder confinée, liez-la à l'adresse de gestion de la VM ou à127.0.0.1et accédez-y par un tunnel SSH.
L'historique (GET /history) est réservé aux rôles admin et superadmin.

Une interface web statique pilote le plan de contrôle FastAPI : elle envoie une topologie JSON et déclenche le plan, le déploiement et le nettoyage. Les réponses de l'API s'affichent dans une zone de journaux, ce qui permet de valider rapidement chaque action. En pratique : on colle une topologie, on clique sur Create, Plan, Deploy puis Cleanup, et l'on voit ce que fait l'API. C'est un MVP crédible pour montrer le flux complet à petite échelle.
UI (MVP) -> API FastAPI -> Orchestrateur -> pont Linux + netns + veth + IP
Démonstration :
br-lab, les namespaces pc1 et pc2, les paires veth et les IP.
Après une première passe de mise en forme CSS :

admin et superadmin (l'API bloque aussi GET /history) ; l'onglet « Comptes & Droits » est réservé au superadmin.mini_logo_cyberlab.png, logo de la barre supérieure Logo_Page_carre.png, écran de connexion Logo_Page.png, pied de page logo_ssbiker.png (fichiers dans mini-cyberlab/api/custom).POST /topologies/{id}/deploy exécute le moteur réseau.br-lab, pc1, pc2, paires veth, IP).POST /topologies/{id}/cleanup revient à l'état initial.Objectif : isoler, dans la VM, le plan de gestion (SSH, API) du plan de données du lab.
| Interface | Rôle |
|---|---|
ens18 |
Gestion : IP de gestion, SSH, API. |
ens19 |
Lab : 10.150.0.1/24, sans passerelle. |
net.ipv4.ip_forward=0, complété si besoin par des règles de pare-feu).ip a show ens19 affiche 10.150.0.1/24 et l'API reste sur ens18.Le MVP crée ses hôtes avec des namespaces Linux (veth et ip netns) : c'est rapide et léger, mais ce ne sont pas de « vraies machines » (tous partagent le même système). Pour se rapprocher d'un cyber range réaliste tout en restant léger, le lab passe à des conteneurs LXC pilotés par LXD :
br-lab) ;Objectifs : installer LXD, activer les services, stocker les conteneurs hors de /var (pour ne pas remplir la VM) et garantir la persistance après redémarrage.
labpool en mode dir, situé dans /home/lxd/labpool.for_debian/01_install_lxd.sh. Il installe lxd, lxd-client, lxcfs et leurs dépendances, démarre et active lxd et lxcfs, initialise LXD par preseed, crée et valide le pool labpool (source /home/lxd/labpool) et, en option, ajoute l'utilisateur mass au groupe lxd.Vérifications :
lxc version # client et serveur OK
lxc storage list # labpool présent, source /home/lxd/labpool
Objectifs : raccorder automatiquement au lab les conteneurs créés par l'API, en réutilisant br-lab (relié à vmbr150 via ens19), avec un profil LXD stable.
br-lab porte 10.150.0.1/24 et ens19 est un port du pont (bridge_ports ens19).lab : interface eth0 en mode bridged, parent br-lab.for_debian/02_config_lab_network.sh. Il vérifie la présence d'ens19, vérifie ou crée br-lab, y attache ens19, applique 10.150.0.1/24 sur br-lab et crée le profil LXD lab.Vérifications :
ip -br addr show br-lab # 10.150.0.1/24
bridge link # ens19 master br-lab
lxc profile show lab # eth0 en bridged sur br-lab
Résultat : LXD est prêt et persistant, le stockage des conteneurs est dans /home (et non dans /var), le réseau du lab est persistant et le profil lab raccorde automatiquement tout conteneur à br-lab.
Le backend (API) est ensuite modifié pour que le déploiement et le nettoyage pilotent LXD, avec une image locale choisie selon la topologie :
10.150.0.11/24, 10.150.0.12/24) ;os_release (debian13 ou alpine-linux) et tools_profile (base, attack ou defense) ;L'interface web reste identique (interface, puis API, puis déploiement).
Objectif : préconstruire des images LXD locales pour les profils « attack » et « defense », afin de ne rien télécharger au moment du déploiement.
debian13-attack, debian13-def, alpine-attack, alpine-def.mass (membre de sudo, mot de passe <MOT_DE_PASSE_LAB_MASS>) et test (sans droits, mot de passe <MOT_DE_PASSE_LAB_TEST>)..tar.gz dans /home/mini-cyberlab-images.Les images de défense contiennent volontairement des identifiants faibles : elles ne doivent jamais être raccordées à un autre réseau que le pont isolé du lab.
cd /home/mass/mini-cyberlab
chmod +x scripts/*.sh
# Construction et export des 4 images (via l'interface Internet de la VM)
sudo BUILD_PARENT=ens18 ./scripts/build_lxd_tool_images.sh
# Vérification des alias et des exports
lxc image list | grep -E "debian13-(attack|def)|alpine-(attack|def)"
ls /home/mini-cyberlab-images
Adaptez BUILD_PARENT si l'interface Internet de la VM n'est pas ens18.
Chaque hôte déclare son système et son profil d'outils :
{
"id": "pc1",
"type": "host",
"ip": "10.150.0.11/24",
"os_release": "debian13",
"tools_profile": "attack"
}
Valeurs autorisées :
os_release : debian13 ou alpine-linux ;tools_profile : base, attack ou defense.L'export est fait automatiquement par scripts/build_lxd_tool_images.sh vers /home/mini-cyberlab-images. Pour un import manuel :
sudo ./scripts/import_lxd_images.sh
Dans son dernier état, l'interface (éditeur JSON, boutons Create, Plan, Deploy et Cleanup, journaux) était publiée via le reverse proxy sur un sous-domaine cyberlab de 2sinnovation.com, et appelait l'API FastAPI locale.
mini-cyberlab-ui.service, exécuté par l'utilisateur mass :# mini-cyberlab-ui.service (extrait)
[Service]
User=mass
WorkingDirectory=/home/mass/mini-cyberlab/ui
ExecStart=/usr/bin/python3 -m http.server 8088 --bind 0.0.0.0
find ui -type d -exec chmod 755 {} +
find ui -type f -exec chmod 644 {} +
debian13-attack, debian13-def, alpine-attack et alpine-def (comptes SSH mass et test, mots de passe de lab <MOT_DE_PASSE_LAB_MASS> et <MOT_DE_PASSE_LAB_TEST> ; ping validé). Dans cette version du dépôt, les scripts s'appellent scripts/creer_images_lxc_outils.sh (avec BUILD_PARENT=ens18), qui génère et exporte les archives dans /home/mini-cyberlab-images, puis importer_images_lxd.sh si besoin.network (pont br-lab) et des hôtes décrits par os_release (debian13 ou alpine-linux) et tools_profile (attack ou defense). Le pilote LXD impose une IP statique persistante et relance sshd et le ping.systemctl status mini-cyberlab-api.service mini-cyberlab-ui.service, puis, dans l'interface, Create, Plan, Deploy et Cleanup. Depuis le client debian-cli, lancer /home/nettoyer_known_hosts.sh, puis :ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password mass@10.150.0.11 # ou .12
Si le lab est remonté, les étapes suivantes étaient prévues :
vmbr150 (CIDR, DHCP statique, MAAS ou ISC, réservations) ;vmbr150 vers vmbr0 ;