ajoute la visio de classe : Jitsi auto-hébergé, jetons du portail, prof seul modérateur
Jitsi (docker-jitsi-meet, version figée) derrière Nginx sur meet.syllabuslab.fr, accès par jeton JWT signé par le portail, modération du micro, mains levées synchronisées avec l'application. Pare-feu : UDP 10000 et pont interne br-visio (accords d'Adam). Jicofo n'authentifie plus lui-même : il donnait le rôle de propriétaire aux élèves. Capacité ramenée à 20 VM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd4f9c3c55
commit
ec4eae3744
36 changed files with 1406 additions and 18 deletions
54
docs/adr/0005-visio-jitsi.md
Normal file
54
docs/adr/0005-visio-jitsi.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# ADR 0005 — Visio de classe : Jitsi auto-hébergé, jetons du portail, prof seul modérateur
|
||||
|
||||
- **Date** : 2026-10-12
|
||||
- **Statut** : accepté
|
||||
|
||||
## Contexte
|
||||
|
||||
Étape 15 : une visio pour la classe, sur le même serveur, où seuls les élèves
|
||||
de la classe entrent (nom non modifiable), où tout le monde arrive en
|
||||
sourdine et où seul le prof donne la parole.
|
||||
|
||||
## Décisions
|
||||
|
||||
1. **Jitsi Meet en Docker** (docker-jitsi-meet, version figée `stable-11248`),
|
||||
rôle Ansible `visio`, derrière le Nginx de l'hôte sur `meet.syllabuslab.fr`.
|
||||
2. **JVB sur le réseau de l'hôte** : Docker tourne sans pont par défaut ni règles
|
||||
iptables (étape 5), donc la publication de l'UDP 10000 par NAT Docker est
|
||||
impossible. JVB écoute directement l'UDP 10000 (ouvert dans nftables, accord
|
||||
d'Adam) et joint Prosody par `127.0.0.1:5222`.
|
||||
3. **Pont interne `br-visio` autorisé** dans la chaîne `forward` (accord d'Adam) :
|
||||
avec `br_netfilter`, le trafic entre conteneurs d'un même réseau Docker passe
|
||||
par `forward`, dont la politique `drop` bloquait Jicofo et le conteneur web.
|
||||
Seul le trafic qui entre ET ressort par `br-visio` est accepté.
|
||||
4. **Accès par jeton JWT (HS256) signé par le portail**, un par personne, pour
|
||||
une salle au nom imprévisible ; pas d'invités (`ENABLE_GUESTS=0`). Le secret
|
||||
est généré sur le serveur et copié dans le `.env` du portail par Ansible.
|
||||
5. **Rôle de modérateur lu dans le jeton** (`context.user.moderator`, module
|
||||
natif `token_affiliation`) ; `ENABLE_AUTO_OWNER=0` et **`JICOFO_ENABLE_AUTH=0`** :
|
||||
sinon Jicofo authentifie chaque participant et donne le rôle de propriétaire à
|
||||
tous (constaté : un élève pouvait ouvrir son micro sans la parole).
|
||||
6. **Modération du micro** activée par la page du prof à son arrivée
|
||||
(`toggleModeration`), « Donner la parole » = `askToUnmute` (qui autorise le
|
||||
micro quand la modération est active, d'après la documentation de l'IFrame API).
|
||||
7. **État de la classe tenu par le portail** (cache Redis, verrou) : mains levées
|
||||
par ordre d'arrivée, correspondance élève ↔ participant Jitsi, salle d'attente.
|
||||
La main levée suit toujours l'événement Jitsi (`raiseHandUpdated`) : une seule source.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Publier l'UDP 10000 par Docker** : demande de rendre à Docker la main sur
|
||||
iptables, ce qu'on a refusé dès l'étape 5.
|
||||
- **Mettre tous les conteneurs sur le réseau de l'hôte** : conflits de ports avec
|
||||
Kolla (ex. 10.10.0.1:8000) et services internes exposés sur toutes les interfaces.
|
||||
- **Relais TURN sur 443** pour les réseaux qui bloquent l'UDP : impose de
|
||||
restructurer l'entrée HTTPS (aiguillage par nom) ; reporté (choix d'Adam).
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Réseaux scolaires qui bloquent l'UDP : pas de son ni d'image tant que le relais
|
||||
TURN sur 443 n'existe pas (`docs/dette.md`).
|
||||
- Capacité ramenée de 22 à 20 VM (`docs/capacite.md`).
|
||||
- `Permissions-Policy` du portail délègue caméra, micro et partage d'écran au seul
|
||||
`meet.syllabuslab.fr` ; le site `meet` n'est intégrable que dans le portail
|
||||
(`frame-ancestors`).
|
||||
|
|
@ -6,7 +6,14 @@ Kolla 2025.2 tout-en-un. Script : `openstack/tests/capacite.py` ; données
|
|||
brutes sur le serveur : `/root/capacite-20261009/` (`hote.csv` toutes les
|
||||
2 s, `demarrage.csv`, `resume.json`).
|
||||
|
||||
## Capacité retenue : 22 VM `eleve.petit` en même temps
|
||||
## Capacité retenue : 20 VM `eleve.petit` en même temps
|
||||
|
||||
Ramenée de 22 à 20 le 2026-10-12 pour la visio (Jitsi) : après une visio à
|
||||
trois, Jitsi occupe ≈ 710 Mo (JVB 459, Jicofo 206, Prosody 28, web 16) ; JVB est
|
||||
plafonné à 1 Go et Jicofo à 512 Mo, soit 1,5 Go réservés en plus
|
||||
(`reserved_host_memory_mb` 15000 → 16500 ; (31993 − 16500) / 768 = 20).
|
||||
|
||||
### Historique : 22 VM (étape 13)
|
||||
|
||||
Sessions d'élèves et stock chaud compris. Choisie par Adam d'après les
|
||||
mesures ci-dessous. Elle est appliquée à deux endroits, à garder alignés :
|
||||
|
|
@ -17,7 +24,7 @@ mesures ci-dessous. Elle est appliquée à deux endroits, à garder alignés :
|
|||
| Portail (plafond du stock chaud) | `LABS_CAPACITE_VM`, défaut 22 (`portail/config/labs.php`) |
|
||||
| Quota du projet (filet de sécurité, au-dessus) | 40 instances, 40 vCPU, 30 Go (rôle `openstack-projet`) |
|
||||
|
||||
Ce que ça permet : une classe de 22 sur un lab à 1 VM ; 7 élèves sur le
|
||||
Ce que ça permet (20 VM) : une classe de 20 sur un lab à 1 VM ; 6 élèves sur le
|
||||
lab 08 (3 VM chacun). Moins le stock en attente (1 stack par lab hors séance).
|
||||
|
||||
## Mesures
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
|
|||
| Correction séquentielle (~5 s pour le lab 08) | Acceptable ; à paralléliser si la charge l'exige. | Bloc B | Étape 11 |
|
||||
| Jeton de console : ≈ 1,1 s par appel Nova `remote-consoles` | Plus gros poste de l'ouverture d'un lab (2,8 s au total, critère < 5 s tenu). Préparer les jetons à l'attribution si besoin. | Bloc B | Étape 13 |
|
||||
| Tâches Ansible toujours « modifiées » (composer, npm, build, cache) | Brouille la lecture de l'idempotence. | Bloc B | Étape 8 |
|
||||
| `Permissions-Policy` ferme caméra et micro | À ouvrir pour la visio. | Étape 15 | Étape 12 |
|
||||
| Mentions légales à compléter | Article « Disponibilité et responsabilité » des CGU ; délégué à la protection des données s'il en est désigné (éditeur, SIREN et adresse complétés le 2026-10-09). | Avant le pilote | Étape 14 |
|
||||
| Slogans « du lycée au master » | Le catalogue va aujourd'hui du lycée au BUT. Positionnement laissé tel quel : décision éditoriale d'Adam. | À décider | Étape 14 |
|
||||
| Ancien `storage/logs/laravel.log` | Fichier unique d'avant la rotation quotidienne (étape 14) : jamais purgé, à supprimer ou archiver (accord d'Adam). | Dès que possible | Étape 14 |
|
||||
| Pages d'aperçu publiques (32) | Signalées comme aperçus avec données fictives ; à retirer ou remplacer au fur et à mesure que les fonctions réelles arrivent. | Au fil du bloc B | Étape 14 |
|
||||
| Invitation des élèves majeurs par e-mail (V2 du CLAUDE.md) | Demande un service d'envoi d'e-mails (compte ouvert par Adam, DNS SPF/DKIM). Jamais pour les mineurs. | V2 | 2026-10-09 |
|
||||
| Code de classe : caractères ambigus | Généré avec `Str::random` (0/O, 1/I/l possibles), contrairement aux codes élèves. Utiliser le même alphabet sans ambiguïté. | Bloc B | 2026-10-09 |
|
||||
| **Visio : pas de relais TURN sur 443** | Les réseaux scolaires qui bloquent l'UDP n'auront ni son ni image (CLAUDE.md : tout par 443). Restructurer l'entrée HTTPS (aiguillage Nginx par nom) + coturn ; demande un nom DNS (turn.syllabuslab.fr). | Avant le pilote | Étape 15 |
|
||||
| Visio : chat activé à chaud non vérifié | L'interrupteur « Chat des élèves » change la barre d'outils par `overwriteConfig` ; non vérifié en navigateur. Vue mobile de la maquette non reprise (la page élève est simplement adaptative). | Bloc B | Étape 15 |
|
||||
| Image Jitsi figée sans procédure de mise à jour | `stable-11248` : comme pour Kuma, prévoir la montée de version par Ansible (variable `visio_version`) et rejouer `visio-e2e.mjs`. | Bloc B | Étape 15 |
|
||||
|
|
|
|||
|
|
@ -625,3 +625,49 @@ Vérifié en production : `/ide` 1 script en ligne, 0 erreur de syntaxe, `sandbo
|
|||
- 6 tests Pest (41 au total) ; vérifié en production : `/rejoindre` 200 avec CSP stricte, code faux refusé avec message, table créée, nettoyage planifié.
|
||||
|
||||
**Clé des sauvegardes** : je ne peux ni ne dois voir la copie d'Adam. Procédure de preuve ajoutée au runbook des sauvegardes : un message chiffré sur le serveur avec la clé publique, que la copie doit déchiffrer sur le Mac. Fichier de test déposé sur le Bureau d'Adam. Corrigé aussi la commande de copie du runbook (un `scp` d'un fichier de root échouait).
|
||||
|
||||
## 2026-10-12 — Étape 15 : visio de classe
|
||||
|
||||
Démarrée après validation des étapes 1 à 11 par Adam. Décisions : [ADR 0005](adr/0005-visio-jitsi.md).
|
||||
|
||||
**Fait**
|
||||
|
||||
- **Jitsi Meet auto-hébergé** (docker-jitsi-meet `stable-11248`, version vérifiée sur le dépôt officiel, images figées), rôle Ansible `visio` : configuration d'après le `docker-compose.yml` officiel, JVB sur le réseau de l'hôte, secrets générés une fois sur le serveur, contrôle au déploiement que Jicofo voit un JVB opérationnel.
|
||||
- **Pare-feu** (deux accords explicites d'Adam, filet anti-coupure de 5 min, vérification SSH depuis le Mac, règles de NAT d'OpenStack intactes) : UDP 10000 ouvert ; pont `br-visio` autorisé en `forward` pour les échanges entre conteneurs.
|
||||
- **Accès par jeton JWT** signé par le portail (HS256, une salle imprévisible par séance, nom non modifiable : prénom et initiale pour les élèves) ; pas d'invités. **Prof seul modérateur** par le module natif `token_affiliation` (vérifié dans l'image : `context.user.moderator`).
|
||||
- **Pages prof et élève** reprises de `design/resources/views/visio.blade.php` : « En direct » et chrono, « Couper tous les micros », « Terminer la visio », mains levées (premier arrivé en haut, « Donner la parole »), salle d'attente (élèves présents tant que la visio est fermée, « Tout admettre » l'ouvre), chat des élèves désactivé par défaut avec interrupteur ; côté élève : statut, micro, caméra, « Lever la main », « Quitter ». La grille factice est remplacée par la vraie visio (IFrame API, `external_api.js` servi par `meet`).
|
||||
- **Main levée synchronisée** : l'événement Jitsi `raiseHandUpdated` met à jour l'état du portail (cache Redis, verrou) ; quand le prof donne la parole (`askToUnmute`, qui autorise le micro sous modération d'après la documentation officielle), le portail baisse la main.
|
||||
- `meet.syllabuslab.fr` ajouté au certificat ; site intégrable dans le seul portail (`frame-ancestors`) ; `Permissions-Policy` du portail : caméra, micro et partage d'écran délégués à `meet` seulement ; CSP stricte complétée (`frame-src` et `script-src` vers `meet`).
|
||||
- **Capacité ramenée à 20 VM** (Jitsi ≈ 710 Mo après une visio à trois, plafonds 1 Go + 512 Mo) : Nova et portail alignés.
|
||||
- 6 tests Pest de la visio (47 au total) ; test réel en navigateurs `portail/tests/Reels/visio-e2e.mjs` (Playwright 1.64, Chromium, caméra et micro simulés) ; politique de confidentialité complétée (visio sans enregistrement).
|
||||
|
||||
**Problèmes rencontrés et solution**
|
||||
|
||||
1. Conteneurs Jitsi en boucle : leurs volumes doivent appartenir à l'uid 1000 (ils tournent sous cet utilisateur).
|
||||
2. **Jicofo ne joignait pas Prosody** (« Connect timed out ») : le module `br_netfilter` fait passer le trafic entre conteneurs d'un même réseau Docker par la chaîne `forward`, dont la politique est `drop`. Jamais vu avant parce que tous les autres conteneurs ne sont joints que depuis l'hôte. Règle `br-visio` (accord d'Adam).
|
||||
3. DNS de `meet` absent au départ : test complet d'abord joué en résolvant `meet` vers le serveur dans Chromium (`--host-resolver-rules`, certificat du portail accepté), puis rejoué en conditions réelles une fois le DNS créé par Adam et le certificat étendu.
|
||||
4. Tinker resté ouvert en attente de saisie sur le serveur, bloquant la suite : lancé désormais avec `< /dev/null` (runbook).
|
||||
5. **Un élève pouvait ouvrir son micro sans la parole** — trouvé par le test en navigateurs. En observant les messages XMPP envoyés par chaque navigateur, aucun client n'approuvait les élèves : c'est **Jicofo**, avec son authentification active par défaut, qui donnait le rôle de propriétaire à tout utilisateur authentifié, élèves compris (ils échappaient donc aussi à la modération et pouvaient modérer les autres). `JICOFO_ENABLE_AUTH=0` : Prosody vérifie toujours chaque jeton, `token_affiliation` seul attribue les rôles. Vérifié dans les journaux : élèves `PARTICIPANT`, liste des autorisés = le prof, puis l'élève à qui il donne la parole.
|
||||
|
||||
**Critère vérifié (vrai DNS, vrai certificat, trois navigateurs + un intrus)**
|
||||
|
||||
```
|
||||
prof : visio ouverte, Jitsi chargé
|
||||
prof : · 3 en ligne (le prof et les deux élèves)
|
||||
prof : main levée reçue de « Lea M. »
|
||||
Yanis tente d'ouvrir son micro sans la parole → « En sourdine »
|
||||
Léa : sa main est baissée (parole donnée)
|
||||
Léa ouvre son micro → « Tu as la parole »
|
||||
intrus sans jeton : le prof voit toujours « · 3 en ligne » ; écran de l'intrus : « … Authentication required … »
|
||||
CRITÈRE DE L’ÉTAPE 15 : RÉUSSI
|
||||
```
|
||||
|
||||
Données et fichiers de test supprimés (accord d'Adam).
|
||||
|
||||
**Comprendre**
|
||||
|
||||
Dans une visio, l'image et le son ne sont que la partie visible : la sécurité tient dans *qui a quel rôle*. Ici, trois composants pouvaient en décider — Prosody (le serveur de messagerie), le module `token_affiliation` (qui lit le jeton signé par le portail) et Jicofo (l'organisateur des conférences) — et le moins attendu, Jicofo, accordait silencieusement le rôle de propriétaire à tout le monde. Aucune lecture de configuration ne l'aurait montré : il a fallu un test qui joue la situation réelle (un élève tente de parler sans y être autorisé) puis observer les messages échangés pour remonter à la cause. L'autre leçon est réseau : un pare-feu « tout refuser » protège aussi contre ses propres conteneurs, et une règle doit alors être la plus étroite possible (ici, un seul pont, dans les deux sens).
|
||||
|
||||
**Suite**
|
||||
|
||||
Étape 16 : nettoyage.
|
||||
|
|
|
|||
54
docs/runbooks/visio.md
Normal file
54
docs/runbooks/visio.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Runbook — Visio de classe (Jitsi, étape 15)
|
||||
|
||||
Décisions : [ADR 0005](../adr/0005-visio-jitsi.md).
|
||||
|
||||
## Où sont les choses
|
||||
|
||||
| Élément | Emplacement |
|
||||
|---|---|
|
||||
| Conteneurs (web, prosody, jicofo, jvb) | `/opt/visio/docker-compose.yml`, projet Docker `visio` (rôle `visio`) |
|
||||
| Configuration (sans secret) | `/etc/visio/visio.env` (modèle `ansible/roles/visio/templates/visio.env.j2`) |
|
||||
| Secrets (mots de passe internes, secret des jetons) | `/etc/visio/secrets.env`, `600 root:root`, générés une fois ; `VISIO_JWT_SECRET` copié dans le `.env` du portail |
|
||||
| Site public | `https://meet.syllabuslab.fr` (vhost du rôle `portail`, activé quand le DNS répond) |
|
||||
| Pare-feu | UDP 10000 ouvert ; pont `br-visio` autorisé en `forward` (rôle `durcissement`) |
|
||||
| Portail | `VisioController`, `JetonVisio`, vues `seances/visio-prof` et `seances/visio-eleve` |
|
||||
| Test réel | `portail/tests/Reels/preparer-visio.php` + `visio-e2e.mjs` (Playwright) |
|
||||
|
||||
## Déployer
|
||||
|
||||
```
|
||||
cd ansible && ansible-playbook visio.yml -i inventory.ini
|
||||
```
|
||||
|
||||
Le rôle vérifie que Jitsi web répond, que JVB écoute l'UDP 10000, et que
|
||||
**Jicofo voit un JVB opérationnel** (preuve que les services se parlent).
|
||||
|
||||
## Vérifier
|
||||
|
||||
```
|
||||
D="sudo docker compose -p visio -f /opt/visio/docker-compose.yml"
|
||||
$D ps
|
||||
$D exec -T jicofo curl -s http://127.0.0.1:8888/stats | python3 -m json.tool | grep -E "operational_bridge_count|conferences"
|
||||
$D logs --since 10m jicofo | grep -E "Member joined|Setting whitelist|Moderation for"
|
||||
```
|
||||
|
||||
Attendu dans les journaux de Jicofo : le prof `role=OWNER`, les élèves
|
||||
`role=PARTICIPANT` ; la liste des autorisés à parler ne contient que le prof,
|
||||
puis chaque élève au moment où il reçoit la parole. **Si un élève y apparaît dès
|
||||
son arrivée, Jicofo a repris l'authentification : vérifier `JICOFO_ENABLE_AUTH=0`.**
|
||||
|
||||
## Test de bout en bout (navigateurs)
|
||||
|
||||
1. Sur le serveur : `sudo -u www-data HOME=/tmp php8.3 artisan tinker /var/www/portail/tests/Reels/preparer-visio.php < /dev/null`
|
||||
(le `< /dev/null` évite que Tinker reste ouvert en attente d'une saisie).
|
||||
2. Sur le Mac : récupérer `/tmp/visio-identifiants.json` (fichier `600`), puis
|
||||
`node visio-e2e.mjs identifiants.json` (Playwright, Chromium).
|
||||
3. Supprimer les données et fichiers de test.
|
||||
|
||||
## Pièges rencontrés
|
||||
|
||||
- Conteneurs refusant de démarrer : leurs volumes doivent appartenir à l'uid 1000.
|
||||
- Conteneurs qui ne se joignent pas : `br_netfilter` + politique `forward drop` (règle `br-visio`).
|
||||
- Jicofo ne se reconnecte pas tout seul après une longue indisponibilité de Prosody : `$D restart jicofo`.
|
||||
- Élèves modérateurs malgré `token_affiliation` : `JICOFO_ENABLE_AUTH=0` obligatoire.
|
||||
- `Permissions-Policy` trop stricte (caméra interdite) ou `X-Frame-Options DENY` : la visio ne s'affiche pas sans erreur visible.
|
||||
Loading…
Add table
Add a link
Reference in a new issue