Files
edubox/SUIVI_VPN_ONDEMAND.md
T

45 KiB
Raw Blame History

Suivi VPN on-demand studioE5 (client A)

Ce qui fonctionne

  1. Agent standalone (mode console / systray)

    • Exécutable : agent/studioE5-agent
    • Config lu depuis <data-dir>/studioE5-config.json
    • Mode console : -no-tray
  2. VPN on-demand dans l'agent

    • Lagent ne démarre plus Tailscale au boot.
    • Le VPN se lance automatiquement à la création/démarrage dune instance, ou sur commande serveur.
    • Implémentation basée sur les binaires tailscaled + tailscale up (pas tsnet, car tsnet ne loguait pas automatiquement avec une authkey sur un state vierge).
  3. Commandes serveur → agent

    • Endpoint de test : POST /api/internal/send-to-node
    • Actions supportées : start_vpn, stop_vpn, start, stop, reset, delete.
  4. Resolver/serveur dans le tailnet studioe5

    • Service resolver-vpn (conteneur Tailscale) partage le netns du resolver.
    • Le resolver peut joindre les IPs Tailscale des nodes (ping 100.64.0.x OK).
  5. Instance WordPress démarrée avec succès

    • Le resolver a renvoyé une 302 WordPress via http://resolver:2020/.
  6. Activation zéro-config de lagent (modèle commercialisable)

    • Lagent démarre sans headscale_url ni headscale_auth_key.
    • Lutilisateur entre seulement un code dactivation.
    • Le serveur envoie la config Headscale, lagent la sauvegarde et démarre le VPN automatiquement.

Blocage levé

Rate limit Lets Encrypt pour edudeploy.com est levé.
Le 2026-06-23 vers 09:35 UTC, Caddy a pu obtenir un certificat Lets Encrypt pour test-wp-001.studioe5.edudeploy.com :

tls.obtain: certificate obtained successfully identifier=test-wp-001.studioe5.edudeploy.com issuer=acme-v02.api.letsencrypt.org-directory

Le flux complet HTTPS public est désormais validé :

curl -sS -I -L https://test-wp-001.studioe5.edudeploy.com/
# => HTTP/2 302 -> HTTP/2 200 (WordPress install.php)

Le DNS wildcard *.studioe5.edudeploy.com est en place. Caddy utilise toujours tls { on_demand } et émet un certificat par sous-domaine dinstance.

🎯 Validation du flux HTTPS public

Le 2026-06-23 09:39 UTC, le flux complet a été validé :

Client (HTTPS) → Caddy (:443) → resolver (:2020) → Tailnet (100.64.0.8) → agent → WordPress (:8001)

Résultat :

$ curl -sS -I -L https://test-wp-001.studioe5.edudeploy.com/
HTTP/2 302
location: https://test-wp-001.studioe5.edudeploy.com/wp-admin/install.php
...
HTTP/2 200
  • Certificat Lets Encrypt obtenu automatiquement par Caddy (tls { on_demand }).
  • Le resolver réécrit les en-têtes Location et le contenu HTML pour passer de http:// à https://.

📝 Template WordPress prêt à lemploi

Un nouveau template wordpress-ready-wordpress-latest a été créé et validé le 2026-06-26. Il fournit un WordPress déjà initialisé en français, prêt à lusage en classe ou en examen.

Contenu du template

Élément Valeur / État
Langue Français (fr_FR)
Titre du site Mon site wordpress
Compte administrateur admin / admin
Thème actif Astra
Spectra installé et actif
Yoast SEO installé mais inactif
Mises à jour automatiques désactivées (core, plugins, thèmes)
DNS conteneur 8.8.8.8 + 1.1.1.1 pour permettre laccès à api.wordpress.org

Architecture technique

  • Le modèle Template de Prisma dispose dun nouveau champ initScript (TEXT?).
  • Le seed génère le template avec :
    • une section dns dans le service app du docker-compose.yml ;
    • un service sidecar wp-init (image wordpress:cli) exécutant le script dinitialisation.
  • Lagent écrit le script wp-init.sh dans le dossier de linstance au démarrage.
  • Le conteneur wp-init attend que WordPress soit prêt, puis exécute WP-CLI en tant que www-data.
  • Un fichier flag .studioe5-init-done évite de réinitialiser linstance à chaque redémarrage.

Fichiers modifiés / ajoutés

  • server/prisma/schema.prisma champ initScript sur Template.
  • server/prisma/seed.ts génération du template wordpress-ready-wordpress-latest.
  • server/templates/wordpress-ready/wp-init.sh script dinitialisation WP-CLI.
  • server/app/api/instances/route.ts envoi de initScript à lagent avec remplacement des placeholders.
  • agent/websocket.go réception et transmission de InitScript.
  • agent/docker.go écriture du script dans le dossier instance (writeInitScript).

Validation

Instance de test créée via lAPI (cmqv03a6v0001vg8zrpe8zqfy) :

$ curl -sS -I -L https://cmqv03a6v0001vg8zrpe8zqfy.studioe5.edudeploy.com/
HTTP/2 200
  • Page daccueil en français, titre « Mon site wordpress ».
  • Connexion admin /wp-login.php avec admin / admin fonctionnelle.
  • Tableau de bord en français.
  • Plugins : Spectra actif, Yoast SEO inactif.
  • wp-config.php contient les constantes de désactivation des mises à jour automatiques.

Les instances de test ont été nettoyées après validation.

Template versionné WordPress 7.0.0

Un second template wordpress-ready-wordpress-7.0.0-php8.3 a été ajouté pour figer WordPress sur la version 7.0.0 (PHP 8.3) au lieu de latest. Cela garantit que tous les postes déploient exactement la même version, sans dépendre du cache local de wordpress:latest.

Template Image Docker
wordpress-ready-wordpress-latest wordpress:latest
wordpress-ready-wordpress-7.0.0-php8.3 wordpress:7.0.0-php8.3

📁 Fichiers modifiés (non exhaustif)

  • agent/tailscale.go lancement tailscaled + tailscale up, gestion start/stop/status.
  • agent/websocket.go handlers start_vpn / stop_vpn, ensureTailscale().
  • agent/docker.go remplacement des placeholders {PORT} et {INSTANCE_ID} dans les compose.
  • docker-compose.yml ajout du sidecar resolver-vpn, suppression des cap_add/ip route obsolètes sur server/resolver.
  • Caddyfile configuration on-demand TLS pour les instances.
  • .env clé pré-auth Headscale mise à jour (clé réutilisable).

🧪 Tests / environnement de test actuel

Agent de test lancé en arrière-plan :

  • data-dir : /tmp/studioe5-test-clienta
  • node-id : vps-8fc665eb
  • tailnet IP actuelle : 100.64.0.8
  • PID : voir /tmp/studioe5-test-clienta/agent.pid (relancé le 2026-06-26 11:53 UTC avec lagent v0.3.5 corrigé)

Instance de test créée :

  • ID : test-wp-001
  • Node : vps-8fc665eb
  • Port : 8001
  • Template : wordpress-wordpress-latest
  • État : WordPress répond sur http://127.0.0.1:8001 et en HTTPS public sur https://test-wp-001.studioe5.edudeploy.com/.

🪟 Fix agent Windows v0.3.1

Problème rencontré sur le PC de test (OMEGA-GAMER-dc166b1a) :

  • Le nœud apparaissait online dans le dashboard mais sans IP Tailscale.
  • tailscale.exe ip -4 retournait une erreur de connexion au socket local.

Cause racine :

  • Lagent lançait tailscaled avec --socket=<fichier>.sock, mais Tailscale sur Windows utilise des named pipes (\\.\pipe\...), pas des sockets Unix.
  • De plus, les commandes podman/docker/tailscale ouvraient une fenêtre console à chaque exécution.

Corrections apportées (agent/tailscale.go, agent/docker.go, agent/instance.go, agent/systray.go, agent/ui.go, agent/main.go) :

  • Sur Windows, utilisation de la named pipe \\.\pipe\studioe5-tailscaled.
  • Application de hideWindow à tous les processus enfants (Tailscale, Podman, Docker, ouverture navigateur, redémarrage agent).
  • Redirection des logs agent vers <data-dir>/agent.log et des logs tailscaled vers <data-dir>/tailscale/tailscaled.log.
  • Suppression de --operator=root sur Windows (non pertinent).
  • Ajout de --unattended au tailscale up sur Windows pour que le daemon reste connecté après la déconnexion du client CLI.
  • Correction du chemin dataDir passé à startTailscale (évitait un double dossier tailscale/tailscale).

Validation manuelle sur Windows :

.\tailscaled.exe --state="C:\...\data\tailscale.state" --socket="\\.\pipe\studioe5-tailscaled" --tun=userspace-networking
.\tailscale.exe --socket="\\.\pipe\studioe5-tailscaled" status  # => Logged out (NeedsLogin)

🪟 Agent v0.3.5 forwarding entrant Windows + UI locale + cycle de vie

Problème

Sur Windows, Tailscale en userspace-networking ne forwarde pas automatiquement les connexions entrantes du Tailnet vers localhost. Résultat : les URLs publiques retournaient une erreur 502/timeout, bien que lagent soit online.

Logs caractéristiques :

client -> backend close connection: close tcp 100.64.0.12:8080->100.64.0.11:xxxxx: endpoint not connected

Solution : tailscale serve automatique

Lagent configure automatiquement un proxy TCP pour chaque instance démarrée :

tailscale serve --bg --tcp=<port> tcp://localhost:<port>
Action agent Commande Tailscale
Démarrage dinstance serve --bg --tcp=<port> tcp://localhost:<port>
Arrêt dinstance serve --bg --tcp=<port> off
Suppression dinstance serve --bg --tcp=<port> off
Redémarrage de lagent reconfiguration pour les instances déjà running

Fichiers modifiés : agent/tailscale.go, agent/websocket.go, agent/main.go, agent/ui.go.

UI locale modernisée

  • Tableau de bord avec indicateurs de service.
  • Liste des applications avec badges de statut.
  • Boutons daction par instance : Démarrer, Arrêter, Redémarrer, Supprimer.
  • Panneau de logs et diagnostic intégré.
  • Panneau de configuration (URL serveur, Headscale, node ID).

Cycle de vie des instances

  • Arrêterdocker compose stop (volumes conservés).
  • Démarrerdocker compose start (ou up -d la première fois).
  • Redémarrerdocker compose down -v + recréation (données remises à zéro).
  • Supprimerdocker compose down -v + suppression des fichiers.
  • À la fermeture de lagent, les instances en cours sont arrêtées proprement (stop) et le serveur est notifié (instance_stopped).

Démarrage du VPN après activation

Lagent redémarre tailscaled automatiquement au lancement, même si la clé pré-auth a déjà été utilisée. Il se base sur l’état persistant tailscaled.state (tailscale up sans --authkey).

Téléchargement

  • Windows (archive) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.5-windows.zip
  • Windows (exe) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.5.exe
  • Linux : https://studioe5.edudeploy.com/studioE5-agent-v0.3.5

🪟 Agent v0.3.6 recover() dans les goroutines de démarrage dinstance

Problème

Lors de la création dune instance depuis le dashboard vers certains agents (notamment Windows), lagent sarrêtait brutalement. Le recover() présent dans handleMessage ne capturait pas le panic car celui-ci survenait dans les goroutines lancées par go handleStartInstance(...).

Corrections apportées

  • Ajout dun defer recover() dans handleStartInstance ; en cas de panic, linstance passe en statut error et un message instance_error est envoyé au serveur.
  • Ajout dun defer recover() dans toutes les goroutines critiques du WebSocket :
    • start_vpn
    • stop_vpn
    • start
    • reset
    • startTailscaleAndReport
    • cleanup au shutdown
  • Ajout de logs de traçage au début de handleStartInstance (instance, type, port, dataDir, initScriptLen).

Téléchargement

  • Windows (archive) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.6-windows.zip
  • Windows (exe) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.6.exe
  • Linux : https://studioe5.edudeploy.com/studioE5-agent-v0.3.6

Redeploiement

  • Agent rebuildé en v0.3.6 pour Windows et Linux.
  • Binaires versionnés copiés dans server/public/.
  • Page /dashboard/download mise à jour vers la v0.3.6.
  • Serveur rebuildé et redémarré.

🪟 Agent v0.3.7 recover() dans les notifications UI

Problème

Lagent continuait de sarrêter brutalement lors de la création dune instance depuis le dashboard. Le crash survenait juste après les logs Start instance ... et notifyUI: broadcasting to ..., sans laisser de trace de panic. Cela pointait vers une panique dans les goroutines de notification UI ou dans l’écriture des logs vers les clients UI locaux.

Corrections apportées

  • Ajout dun defer recover() dans notifyUI pour chaque goroutine de notification.
  • Ajout dun defer recover() dans sendUILog (logs diffusés aux clients UI).
  • Ajout dun defer recover() dans broadcastUI (messages diffusés aux clients UI).

Téléchargement

  • Windows (archive) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.7-windows.zip
  • Windows (exe) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.7.exe
  • Linux : https://studioe5.edudeploy.com/studioE5-agent-v0.3.7

🪟 Agent v0.3.8 DNS automatique pour Podman machine (Windows/macOS)

Problème

Après correction du crash, lagent Windows avec Podman échouait au docker compose up avec :

lookup registry-1.docker.io: Temporary failure in name resolution

La VM Podman machine navait pas de DNS fonctionnel, ce qui empêchait le téléchargement des images Docker. Le DNS des conteneurs (dns: 8.8.8.8 dans le compose) résout le problème à lintérieur des conteneurs, mais pas pour le pull dimages par Podman machine.

Solution

Lagent configure automatiquement le DNS des machines Podman en cours dexécution au démarrage :

  • Détection de Podman sur Windows/macOS.
  • Liste des machines Podman (podman machine list --format json).
  • Pour chaque machine running, exécution de :
    podman machine ssh <name> sudo sh -c 'echo nameserver 8.8.8.8 > /etc/resolv.conf && echo nameserver 1.1.1.1 >> /etc/resolv.conf'
    

Fichier ajouté : agent/podman.go. Appel depuis agent/main.go au démarrage.

Téléchargement

  • Windows (archive) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.8-windows.zip
  • Windows (exe) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.8.exe
  • Linux : https://studioe5.edudeploy.com/studioE5-agent-v0.3.8

🐛 Fix synchronisation agent / dashboard

Problème

Le statut affiché dans le dashboard pouvait diverger de l’état réel de lagent :

  • Après un Arrêter lancé depuis le dashboard, linstance restait affichée comme elle l’était avant, ou disparaissait avec perte des données.
  • Après une Suppression, linstance n’était pas retirée de la liste.

Causes racines

  1. Action stop du dashboard envoyée comme delete à lagent (server/app/api/instances/route.ts).
    Lagent exécutait alors docker compose down -v + suppression des fichiers, cest-à-dire une suppression réelle, tout en marquant linstance stopped en base.
  2. Lagent ne confirmait pas les actions serveur (agent/websocket.go).
    Les handlers stop et delete ne renvoyaient jamais les messages instance_stopped / instance_deleted au serveur ; seule lUI locale le faisait.
  3. Le handler stop de lagent utilisait dockerComposeDown au lieu de dockerComposeStop, ne respectant pas le cycle de vie documenté (arrêt = conteneurs et volumes conservés).

Corrections apportées

Fichier Changement
server/app/api/instances/route.ts Laction dashboard stop envoie désormais action: "stop" à lagent (et non plus "delete").
agent/websocket.go Le cas stop utilise dockerComposeStop, puis envoie instance_stopped au serveur. Le cas delete envoie instance_deleted au serveur.
server/lib/websocket.ts Utilisation de updateMany/deleteMany pour ignorer silencieusement les messages dinstances déjà absentes/supprimées (évite les erreurs Prisma en double suppression).

Résultat

Le dashboard reflète désormais l’état réel après une action serveur-initiée, dès le rechargement de la page. Le cycle de vie respecte la sémantique attendue :

  • Arrêter : docker compose stop → statut stopped.
  • Démarrer : docker compose up -d → statut running.
  • Redémarrer : docker compose down -v + recréation.
  • Supprimer : docker compose down -v + suppression fichiers.

Redeploiement effectué le 2026-06-26

  • Agent rebuildé en v0.3.5 (agent/studioE5-agent, .exe, .zip et server/public/ mis à jour).
  • Serveur rebuildé et redémarré (docker compose up -d --build server) pour intégrer les corrections TypeScript.
  • Page /dashboard/download mise à jour : passage à la version 0.3.5 et ajout des liens Windows (.exe, .zip) et Linux.
  • Corrections défensives agent après signalement darrêt brutal lors dactions dashboard :
    • sendMessage exécuté de manière asynchrone (go) dans les handlers stop, delete, stop_vpn et cleanup, pour ne pas bloquer la boucle de lecture WebSocket.
    • Ajout dun recover dans handleMessage pour capturer d’éventuels panics sans tuer lagent.
    • Correction du cleanup main.go : modification de inst[id].Status (et non de la copie locale info).
  • Agent de test Linux relancé (PID dans /tmp/studioe5-test-clienta/agent.pid).
  • Agents clients : il faut redémarrer lagent sur chaque poste, ou télécharger à nouveau le binaire v0.3.5 depuis le dashboard pour Windows.

🛠️ Commandes utiles pour reprendre

Voir lagent de test

pgrep -a studioe5-agent

Relancer lagent de test (si besoin)

mkdir -p /tmp/studioe5-test-clienta
cat > /tmp/studioe5-test-clienta/studioE5-config.json <<EOF
{
  "server": "wss://studioe5.edudeploy.com/api/websocket",
  "headscale_url": "https://headscale.studioe5.edudeploy.com",
  "headscale_auth_key": "$(grep HEADSCALE_AUTH_KEY /opt/studioe5-client-a/.env | cut -d= -f2)",
  "node_id": "vps-8fc665eb",
  "data_dir": "/tmp/studioe5-test-clienta"
}
EOF
cd /opt/studioe5-client-a/agent
./studioE5-agent -no-tray -data-dir /tmp/studioe5-test-clienta

Démarrer le VPN manuellement

curl -sS -X POST https://studioe5.edudeploy.com/api/internal/send-to-node \
  -H "Content-Type: application/json" \
  -d '{"nodeId":"vps-8fc665eb","message":{"action":"start_vpn"}}'

Voir les nodes Headscale

cd /opt/studioe5-client-a
docker compose exec -T headscale headscale nodes list studioe5

Tester le resolver (depuis Caddy)

cd /opt/studioe5-client-a
docker exec studioe5-caddy curl -sS -I -H "Host: test-wp-001.studioe5.edudeploy.com" http://resolver:2020/

Tester en HTTPS public (dès que la limite sera levée)

curl -sS -I -L https://test-wp-001.studioe5.edudeploy.com/

🌐 Flux complet testé via lAPI web

Test réalisé le 2026-06-23 en utilisant le compte superadmin :

  1. Authentification NextAuth sur /api/auth/callback/credentials.
  2. Création dinstance via POST /api/instances :
    {
      "nodeId": "vps-8fc665eb",
      "templateId": "wordpress-wordpress-latest",
      "port": 8002
    }
    
    → Instance créée : cmqqgrur20001lw67t2bdgzkg.
  3. Le serveur a automatiquement envoyé laction start au node via WebSocket.
  4. Lagent a démarré le VPN (si besoin), écrit le compose et a lancé les conteneurs WordPress.
  5. Caddy a obtenu un certificat Lets Encrypt pour cmqqgrur20001lw67t2bdgzkg.studioe5.edudeploy.com.
  6. Validation HTTPS :
    curl -sS -I -L https://cmqqgrur20001lw67t2bdgzkg.studioe5.edudeploy.com/
    # => HTTP/2 302 -> HTTP/2 200 (WordPress install.php)
    

Le flux UI → API → WebSocket → agent → Docker → VPN → Caddy → HTTPS public est fonctionnel.

💻 Téléchargement de lagent

Lagent est servi par Caddy depuis le dossier agent/ monté dans le conteneur Caddy (./agent:/usr/share/caddy/agent).

Binaires disponibles

  • Windows (archive complète) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.5-windows.zip
    • Contient studioE5-agent.exe + tailscale-bin/windows/ (tailscale.exe, tailscaled.exe, wintun.dll) + README-Windows.txt.
  • Windows (exécutable seul) : https://studioe5.edudeploy.com/studioE5-agent-v0.3.5.exe
    • Nécessite davoir installé Tailscale Windows séparément ou davoir les binaires dans tailscale-bin/windows/.
  • Linux : https://studioe5.edudeploy.com/studioE5-agent-v0.3.5

Builder / préparer les binaires

cd /opt/studioe5-client-a/agent

# 1. Télécharger les binaires Tailscale Windows (nécessite msitools)
./download-tailscale-bins.sh 1.98.4

# 2. Builder lagent pour Windows et Linux (macOS nécessite CGO)
./build.sh

Le build.sh génère automatiquement studioE5-agent-v0.3.5-windows.zip et copie les binaires versionnés dans server/public/.

Flow dactivation zéro-config (modèle commercialisable)

L’élève/employé na aucune configuration technique à saisir :

  1. Télécharger lagent Windows (studioE5-agent-v0.3.4-windows.zip).
  2. Extraire et lancer studioE5-agent.exe.
  3. Entrer le code dactivation à 6 caractères fourni par l’établissement (affiché dans lUI locale http://localhost:7070).
  4. Lagent contacte le serveur, le serveur vérifie le code et renvoie automatiquement :
    • lidentité de l’élève (studentName)
    • lURL Headscale
    • la clé pré-auth Headscale
  5. Lagent sauvegarde ces informations localement et démarre automatiquement le VPN.
  6. Lagent est alors visible dans le dashboard et peut recevoir des instances.

Configuration manuelle (mode debug / admin)

Si besoin, on peut toujours forcer une config via data/studioE5-config.json :

{
  "server": "wss://studioe5.edudeploy.com/api/websocket",
  "headscale_url": "https://headscale.studioe5.edudeploy.com",
  "headscale_auth_key": "CLE_PREAUTH_ICI",
  "node_id": "IDENTIFIANT_DU_POSTE",
  "data_dir": "C:\\studioE5-agent\\data"
}

⚠️ headscale_auth_key doit être une clé pré-auth réutilisable valide pour le tailnet studioe5. Ne jamais commiter cette clé.

Lancement :

.\studioE5-agent.exe -no-tray -data-dir C:\studioE5-agent\data

🔒 Durcissement du code dactivation

Génération

  • Les codes sont générés avec crypto.randomBytes (au lieu de Math.random).
  • Longueur conservée à 6 caractères, alphabet sans ambiguïté (ABCDEFGHJKLMNPQRSTUVWXYZ23456789).
  • Un champ activationCodeExpiresAt a été ajouté au modèle Student ; les codes expirent après 60 minutes.

Rate-limiting

  • Maximum de 5 tentatives dactivation par code sur une fenêtre de 15 minutes.
  • Maximum de 5 tentatives par nodeId sur la même fenêtre.
  • Au-delà, le serveur répond activation_failed avec Too many attempts.

Cycle de vie

  • Le code est invalide après une activation réussie (activationCode et activationCodeExpiresAt mis à null).
  • Un code expiré renvoie Code expired.
  • Un code déjà utilisé renvoie Invalid code.

Tests validés

  • Activation valide → activated + token node reçu.
  • Code expiré → Code expired.
  • Code déjà utilisé → Invalid code.
  • 5+ tentatives invalides → Too many attempts.

🔒 ACL Headscale (isolation du tailnet)

Objectif

Par défaut, tous les nœuds du tailnet peuvent communiquer entre eux. Les ACL restreignent la connectivité au strict nécessaire :

  • les agents élèves ne peuvent pas se parler entre eux ;
  • le resolver peut atteindre les agents sur leurs ports dinstance ;
  • les agents peuvent joindre le resolver sur son port HTTP interne.

Mise en œuvre

  • Fichier de politique : headscale/acl_policy.hujson.
  • headscale/config.yaml pointe vers ce fichier via policy.path.
  • Le resolver a été déplacé dans un utilisateur Headscale dédié resolver (clé HEADSCALE_RESOLVER_AUTH_KEY).
  • Les agents utilisent lutilisateur studioe5 et sont tagués tag:student-agent.
  • Les nouveaux agents recevront automatiquement le tag via la nouvelle clé pré-auth HEADSCALE_AUTH_KEY (créée avec --tags tag:student-agent).

Contenu de la politique

{
  "groups": {
    "group:agents": ["studioe5@studioe5.local"],
    "group:resolvers": ["resolver@studioe5.local"]
  },
  "tagOwners": {
    "tag:student-agent": ["studioe5@studioe5.local"],
    "tag:resolver": ["resolver@studioe5.local"]
  },
  "acls": [
    { "action": "accept", "src": ["tag:resolver"], "dst": ["tag:student-agent:*"] },
    { "action": "accept", "src": ["tag:student-agent"], "dst": ["tag:resolver:2020"] }
  ]
}

Tests validés

Test Résultat
resolver ping agent OK
Agent → agent (port instance) bloqué (timeout)
Agent → resolver:2020 OK
Flux HTTPS public HTTP 200

🔒 Authentification du canal serveur → agent

Token dauthentification par nœud

  • Le modèle Node dispose dun champ token unique.
  • Lagent envoie son token dans len-tête Authorization: Bearer <token> lors de la connexion WebSocket.
  • Le serveur rejette toute connexion/register dont le token ne correspond pas au nodeId (fermeture 1008).
  • Lors de lactivation, le serveur génère un token aléatoire (32 octets hex) et le renvoie dans le message activated ; lagent le sauvegarde dans <data-dir>/node.token (permissions 0600).
  • Pour les nœuds existants sans token, le serveur en génère un à la première connexion et lenvoie via set_token.

Endpoint /api/internal/send-to-node

  • Protégé par la variable denvironnement INTERNAL_API_KEY.
  • Requiert len-tête Authorization: Bearer <INTERNAL_API_KEY>.
  • Appel sans clé → 401 Unauthorized.

Routes API métier

  • Les routes de gestion des instances (/api/instances) requièrent une session NextAuth valide.
  • Un administrateur ne peut agir que sur les ressources de son établissement ; le superadmin peut tout voir/tout faire.

Endpoint /api/resolve

  • Protégé par la même clé INTERNAL_API_KEY.
  • Requiert len-tête Authorization: Bearer <INTERNAL_API_KEY>.
  • Le resolver (resolver:2020) ne lutilise pas ; il interroge directement PostgreSQL. Cette route est donc réservée aux outils/scripts internes authentifiés.

Exemples de commandes avec la clé interne

KEY=$(grep INTERNAL_API_KEY /opt/studioe5-client-a/.env | cut -d= -f2)

curl -sS -X POST https://studioe5.edudeploy.com/api/internal/send-to-node \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $KEY" \
  -d '{"nodeId":"vps-8fc665eb","message":{"action":"start_vpn"}}'

curl -sS -H "Authorization: Bearer $KEY" \
  "https://studioe5.edudeploy.com/api/resolve?subdomain=test-wp-001"

🔒 Clés pré-auth Headscale éphémères

Principe

À lactivation zero-config, le serveur génère désormais une clé pré-auth unique et à usage unique pour chaque agent, au lieu denvoyer la clé réutilisable HEADSCALE_AUTH_KEY.

Avantages :

  • une clé compromise ne permet pas denregistrer dautres nœuds ;
  • traçabilité directe entre une activation et une clé Headscale ;
  • expiration courte (15 min) ;
  • la clé nest pas persistée dans studioE5-config.json côté agent.

Implémentation

Composant Changement
server/lib/headscale.ts Nouveau helper : getHeadscaleUserId() + createEphemeralPreAuthKey() appelant POST /api/v1/preauthkey.
server/lib/websocket.ts Sur activate, génère une clé éphémère taguée tag:student-agent pour lutilisateur studioe5. Fallback sur HEADSCALE_AUTH_KEY si HEADSCALE_API_KEY nest pas configurée.
agent/websocket.go La clé reçue est utilisée immédiatement mais nest plus écrite dans studioE5-config.json.
agent/tailscale.go tailscale up fonctionne sans --authkey quand le state Tailscale existe déjà (reconnexion).
.env.example / docker-compose.yml Ajout de HEADSCALE_API_KEY pour le service server.

Configuration requise

Générer une clé API Headscale (depuis le conteneur ou la CLI) :

cd /opt/studioe5-client-a
# Clé valable 10 ans (87600h) pour éviter un renouvellement fréquent.
docker compose exec headscale headscale apikeys create -e 87600h

Puis lajouter dans .env :

HEADSCALE_API_KEY=hskey-api-...

⚠️ La clé API Headscale a une expiration par défaut de 90 jours. La clé de production a été créée avec une expiration de 10 ans (-e 87600h). Les anciennes clés ont été révoquées.

Rotation / renouvellement

Si la clé doit être changée :

  1. Créer une nouvelle clé API :
    docker compose exec headscale headscale apikeys create -e 87600h
    
  2. Mettre à jour .env :
    HEADSCALE_API_KEY=<nouvelle_clé>
    
  3. Redémarrer le serveur :
    docker compose up -d server
    
  4. Révoquer lancienne clé :
    docker compose exec headscale headscale apikeys expire --id <id_ancienne>
    

Déploiement effectué

  • Clé API créée et ajoutée au .env de production.
  • Image serveur rebuildée et redémarrée.
  • Agents Linux/Windows rebuildés en v0.3.4 et copiés dans server/public/.

🔒 Sécurité — points restants à traiter

Le certificat wildcard *.studioe5.edudeploy.com est désormais du ressort du deployeur (voir docs/ONBOARDING_CLIENT.md). Les points ci-dessous concernent lapplication studioE5 proprement dite.

Gestion et rotation des secrets

Secret Où ? Action
INTERNAL_API_KEY .env serveur Prévoir une procédure de rotation régulière.
HEADSCALE_API_KEY .env serveur Rotation tous les 10 ans max, stockage sécurisé.
NEXTAUTH_SECRET .env serveur Génération robuste, rotation si suspicion de fuite.
DATABASE_URL .env serveur Utilisateur DB dédié, mot de passe fort.
node.token <data-dir>/node.token Vérifier permissions 0600 sur tous les OS.

Durcissement des conteneurs

  • Limiter les cap_add au strict minimum.
  • Faire tourner les services avec un utilisateur non-root quand possible.
  • Mettre à jour régulièrement les images de base (Caddy, Node, Postgres, Headscale).
  • Scanner les images Docker pour les CVE.

Mises à jour de sécurité

  • Mise à jour des binaires Tailscale (Windows et Linux).
  • Mise à jour des images Docker (server, resolver, caddy, postgres, headscale).
  • Mise à jour de lOS des VPS et des postes agents.
  • Mécanisme de mise à jour automatique ou notification de lagent.

Logs daudit

  • Tracer la création / suppression dinstances.
  • Tracer la génération et lusage des codes dactivation.
  • Tracer les actions admin (connexion, création d’élève, démarrage/arrêt VPN).
  • Conservation et consultation des logs daudit.

Backups et reprise dactivité

  • Backup régulier de la base PostgreSQL.
  • Backup du state Headscale.
  • Backup des states Tailscale côté agents.
  • Procédure de restauration documentée et testée.

Sécurité du build et distribution de lagent

  • Vérifier lintégrité des binaires Tailscale téléchargés (checksum / signature).
  • Signer lexécutable Windows studioE5-agent.exe pour éviter les alertes Defender.
  • Fournir un hash SHA256 des archives dagent.

RGPD et données personnelles

  • Justifier la conservation des noms/prénoms des élèves.
  • Gérer les droits daccès, la suppression de compte et lexport de données.
  • Définir la durée de conservation des logs et historiques.

Sécurité réseau complémentaire

  • Restreindre laccès à /api/internal/send-to-node par IP source si possible.
  • Vérifier lexposition publique du dashboard Headscale et la durcir si nécessaire.
  • Évaluer si headscale.studioe5.edudeploy.com doit rester public.

Rate limiting et quotas

  • Rate-limiting global sur les routes publiques (/api/auth/*, activation, création dinstance).
  • Limitation du nombre dinstances par élève et par établissement.
  • Protection contre les abus sur la génération de codes dactivation.

Tests de sécurité

  • Tests dintrusion légers (agent → agent, accès aux endpoints internes sans clé, accès à une instance dun autre élève).
  • Tests automatisés du flux complet avant chaque release.

🖥️ Installateur agent professionnel

Objectif

Créer un package dinstallation unique et professionnel par OS, incluant lagent studioE5, Tailscale et Podman CLI, afin de ne plus dépendre dinstallations manuelles préalables par lutilisateur.

Choix des outils

OS Outil Format Justification
Windows Inno Setup .exe Gratuit, open source, très répandu, personnalisable, exécution de scripts PowerShell/silencieux.
macOS pkgbuild .pkg Outil natif Apple, gratuit, format professionnel pour la distribution macOS.
Linux Script shell (+ .deb/.rpm optionnels) .sh Universel, détecte le package manager, simple à maintenir.

Contenu du package par OS

  • Windows (Inno Setup) :

    • Installer lagent dans C:\Program Files\studioE5-agent\.
    • Extraire Tailscale dans C:\Program Files\studioE5-agent\tailscale-bin\windows\.
    • Installer Podman CLI via le MSI officiel en mode silencieux.
    • Exécuter podman machine init puis podman machine start.
    • Créer un raccourci de démarrage et/ou un service Windows.
  • macOS (pkgbuild) :

    • Installer lagent dans /Applications/studioE5-agent/.
    • Installer Podman CLI.
    • Exécuter podman machine init puis podman machine start.
    • Optionnellement créer un LaunchAgent pour démarrer lagent au login.
  • Linux (script shell) :

    • Détecter le package manager (apt, dnf, pacman, etc.).
    • Installer Podman et Podman Compose.
    • Copier lagent dans /opt/studioe5-agent/.
    • Créer le service systemd studioe5-agent.service.
    • Activer et démarrer le service.

Adaptations nécessaires dans lagent

  • Détecter si Podman est utilisé et si une machine est requise (Windows/macOS).
  • Vérifier au démarrage que la machine Podman est démarrée, et lancer podman machine start si besoin.
  • Gérer proprement larrêt de la machine à la fermeture de lagent (optionnel).

📋 Prochaines étapes à faire

Terminé

  • Rate limit Lets Encrypt levé.
  • Flux HTTPS public validé (test-wp-001.studioe5.edudeploy.com).
  • Branche feat/studioe5-vpn-ondemand créée, commit 124543d.
  • Flux complet UI → API → WebSocket → agent → Docker → VPN → Caddy validé.
  • Packager les binaires Tailscale pour Windows (studioE5-agent-v0.3.5-windows.zip).
  • Sécurité authentification du canal serveur → agent (token par nœud, clé API interne, sessions NextAuth sur les routes API).
  • Sécurité durcissement du code dactivation (crypto.randomBytes, expiration 60 min, rate-limiting, invalidation après usage).
  • Sécurité ACL Headscale (isolation agent ↔ agent, resolver → agent autorisé).
  • Sécurité clés pré-auth Headscale éphémères (génération côté serveur via HEADSCALE_API_KEY, non persistées côté agent).
  • Agent v0.3.5 forwarding entrant Windows (tailscale serve automatique au démarrage de chaque instance).
  • Agent v0.3.5 UI locale moderne (dashboard, logs, progression, actions dinstance).
  • Agent v0.3.5 cycle de vie des instances (stop/start préservent les volumes, reset/delete effacent).
  • Agent v0.3.5 cleanup au shutdown (arrêt propre de Tailscale et des instances, notification serveur).
  • Synchronisation dashboard (messages instance_stopped / instance_deleted traités côté serveur, et agent renvoyant correctement ces messages après un ordre serveur stop/delete).
  • Template WordPress prêt à lemploi (wordpress-ready-wordpress-latest) : WordPress en français, titre « Mon site wordpress », compte admin/admin, thème Astra, Spectra actif, Yoast SEO inactif, mises à jour automatiques désactivées, DNS 8.8.8.8/1.1.1.1 pour api.wordpress.org.

Reste à faire

  • Certificat wildcard : transféré au deployeur (docs/ONBOARDING_CLIENT.md). L’étude technique reste disponible ci-dessous pour référence.
  • Nettoyer les instances/agent de test une fois le push effectué.
  • Nettoyer les anciens nodes/volumes Headscale de test (nœuds edubox, prof, invalid-* hors ligne à supprimer).
  • Pousser la branche vers Gitea dès que le remote sera accessible.
  • Documenter la procédure de mise en production pour le client A (config agent, clés Headscale, ports, ACL, etc.).
  • Installateur agent professionnel (Windows / macOS / Linux) : créer un package dinstallation unique incluant lagent studioE5, Tailscale et Podman CLI. Voir la section « 🖥️ Installateur agent professionnel » ci-dessous pour le détail des outils (Inno Setup, pkgbuild, script shell) et du contenu par OS.
  • Template WordPress prêt à lemploi (usage examen/classe) :
    • Forcer le DNS (8.8.8.8, 1.1.1.1) dans le docker-compose.yml pour permettre laccès à la bibliothèque de plugins/mises à jour depuis le conteneur.
    • Pré-installer WordPress en français via WP-CLI avec le titre “Mon site wordpress” et le compte admin / admin.
    • Désactiver les mises à jour automatiques (core, plugins, thèmes) pour figer lenvironnement.
    • Installer et activer le thème Astra.
    • Installer Yoast SEO (inactif) et Spectra (actif).
  • Barre de progression basée sur les logs dinstallation : enrichir la barre de progression agent/dashboard en lisant les logs Podman/Docker (podman logs -f) pendant le premier démarrage dune instance. Définir des patterns de logs par template (ex. Installation successful pour PrestaShop) et relayer les étapes réelles au dashboard via WebSocket.
  • Étude interface de déploiement multi-clients : outil de provisionning dun nouveau serveur client + agent générique (option A : URL serveur déterminée à lactivation).
  • Sécurité gestion et rotation des secrets (INTERNAL_API_KEY, HEADSCALE_API_KEY, NEXTAUTH_SECRET, DATABASE_URL).
  • Sécurité durcissement des conteneurs (cap_add, utilisateurs non-root, scans CVE).
  • Sécurité mises à jour de sécurité (Tailscale, images Docker, OS agents).
  • Sécurité logs daudit (instances, codes dactivation, actions admin).
  • Sécurité backups et reprise dactivité (DB, state Headscale, states agents).
  • Sécurité intégrité et signature de lagent (checksum Tailscale, signature Windows, hash SHA256).
  • Sécurité conformité RGPD (données élèves, suppression de compte, export).
  • Sécurité restriction réseau (endpoint interne, dashboard Headscale).
  • Sécurité rate limiting et quotas (routes publiques, instances par élève/établissement).
  • Sécurité tests de sécurité (intrusion légère, tests automatisés avant release).

💡 Améliorations UI

Console / log intégrée dans lagent (v0.3.5)

Les logs de lagent sont redirigés vers <data-dir>/agent.log et diffusés en temps réel dans lUI locale (http://localhost:7070) via le WebSocket existant.

Barre de progression (v0.3.5)

Lagent envoie des messages progress au frontend pendant le démarrage dune instance :

Étape Poids
Préparation de lapplication 10 %
Configuration de lapplication 30 %
Application en cours de démarrage 60 %
Connexion sécurisée active 80 %
Finalisation de linstallation 90 %
Application prête 100 %

Boutons daction par instance (v0.3.5)

LUI locale affiche désormais des boutons Démarrer, Arrêter, Redémarrer et Supprimer pour chaque instance.

🚀 Scalabilité commerciale — déploiement multi-clients

Objectif

Permettre de déployer facilement une stack studioE5 complète pour un nouvel établissement/client sur un VPS dédié, sans intervention technique lourde.

Architecture cible

  • Un serveur = un client : chaque établissement a sa propre stack Docker Compose, sa base PostgreSQL, son Headscale et son Caddy.
  • Agent générique (option A) : un seul binaire agent pour tous les clients. LURL du serveur cible est déterminée au moment de lactivation, pas hardcodée dans lagent.
    • Pistes : code dactivation résolu par un hub central, code structuré contenant lidentifiant du serveur, ou champ URL serveur saisi dans lUI locale.
  • Interface de déploiement : dashboard superadmin (hub) permettant de créer un client, provisionner le VPS, générer les secrets et retourner les informations de connexion.

Prérequis techniques à préparer

Avant de pouvoir déployer un nouveau client en quelques clics, il faut encore préparer les éléments suivants :

# Élément État Détail
1 Agent générique À faire defaultServerURL est hardcodé (wss://studioe5.edudeploy.com/api/websocket). Lagent doit pouvoir déterminer lURL serveur cible à lactivation (option A : champ URL, hub de résolution, ou code structuré).
2 Script de provisionning À faire Aucun outil automatisé pour provisionner un VPS vierge : installation Docker, déploiement de la stack, génération des secrets, création des clés Headscale, configuration DNS wildcard.
3 Registry dimages À faire Les images server et resolver sont buildées sur le serveur cible. Il faut un registry privé pour builder une fois et déployer partout.
4 Hub central À faire Dashboard superadmin listant les clients, état des serveurs, versions déployées, logs distants et mises à jour.
5 Mises à jour à distance À faire Mécanisme pour pousser une nouvelle version du serveur et de lagent sur tous les déploiements clients.
6 Monitoring / support À faire Collecte centralisée de logs, alertes (serveur down, certificat expiré, agent hors ligne).
7 Branding / personnalisation À faire Logo, nom de l’établissement, couleurs configurables par client.
8 Tests automatisés À faire Tests du flux activation → VPN → instance → HTTPS public pour valider chaque nouveau déploiement.
9 Documentation procédure prod À faire Procédure complète de mise en production pour un nouveau client.

Statut

  • À étudier et planifier plus tard. Larchitecture actuelle (un serveur par client + agent zero-config) est déjà compatible avec cette vision, mais le code nest pas encore industrialisé pour un déploiement à grande échelle.

🔒 Étude certificat wildcard *.studioe5.edudeploy.com

Pourquoi passer en wildcard ?

Avec tls { on_demand }, Caddy émet un certificat Lets Encrypt par sous-domaine dinstance. Cela expose au rate limit de 50 certificats par domaine principal (edudeploy.com) sur 7 jours. Un certificat wildcard unique (*.studioe5.edudeploy.com) couvre tous les sous-domaines dinstances et évite ce problème.

Contrainte technique

Un certificat wildcard nécessite le challenge DNS-01 (le challenge HTTP-01 ne permet pas de valider *.domain.tld). Caddy doit donc pouvoir créer un enregistrement TXT automatiquement chez le registrar DNS.

Infomaniak (registrar actuel)

Le DNS de edudeploy.com est chez Infomaniak :

dig NS edudeploy.com +short
# nsany1.infomaniak.com.
# nsany2.infomaniak.com.

Il existe un module Caddy DNS pour Infomaniak :

  • Repository : github.com/caddy-dns/infomaniak
  • Nécessite un token API Infomaniak avec droits DNS.

Implémentation à envisager

  1. Générer un token API Infomaniak (compte client A ou compte dédié avec accès au domaine).
  2. Builder une image Caddy custom avec le module :
    FROM caddy:2-builder AS builder
    RUN xcaddy build --with github.com/caddy-dns/infomaniak
    
    FROM caddy:2-alpine
    COPY --from=builder /usr/bin/caddy /usr/bin/caddy
    
  3. Modifier le Caddyfile pour gérer le wildcard :
    *.studioe5.edudeploy.com {
        tls {
            dns infomaniak {env.INFOMANIAK_API_TOKEN}
        }
        reverse_proxy resolver:2020 {
            header_up Host {host}
        }
    }
    
  4. Ajouter le token dans .env et le passer au conteneur Caddy.
  5. Supprimer ou ajuster le bloc :443 actuel qui utilise on_demand pour les instances.

Alternative sans module DNS

Obtenir le certificat wildcard manuellement (Certbot DNS-01, acheté, etc.) et le charger dans Caddy :

*.studioe5.edudeploy.com {
    tls /data/certs/wildcard.crt /data/certs/wildcard.key
    reverse_proxy resolver:2020 {
        header_up Host {host}
    }
}

Inconvénient : renouvellement manuel.

🔧 Notes techniques

  • Le conteneur resolver-vpn utilise network_mode: service:resolver pour partager le netns avec le resolver.
  • Lagent utilise tailscaled --tun=userspace-networking ; le resolver-vpn utilise un vrai TUN (tailscale0).
  • Le Caddyfile actuel utilise tls { on_demand } pour les instances. En cas de nouvelle rate limit, on peut temporairement remettre tls internal dans le bloc :443 pour valider le flux sans certificat public.