Full web : fichiers restants

This commit is contained in:
cedric committed 2026-10-23 17:43:20 +02:00
1 parent ec3b7c9a01
commit 4826fa5b82
26 files changed
+2537 -793

No files matched your search

+92 -106
View File
@@ -1,27 +1,35 @@
# pve-confsync
Synchronisation d'une partie de la configuration d'un cluster Proxmox VE 9.x
« référence » vers N clusters « clients », via l'API PVE et des tokens API.
Sens unique référence → clients. Pas de VM, pas de disques.
Réplication d'une partie de la configuration d'un cluster Proxmox VE 9.x **primaire**
vers une ou plusieurs **cibles**. Sens unique primaire → cibles. Pas de VM, pas de disques.
Tout se fait depuis l'interface web : plus de CLI ni de fichier YAML.
**État : phases 0 à 2.** CLI (export, diff et apply en YAML) et interface web
(écarts par client, dry-run et synchronisation par objet, par client ou global, journal,
audit) pour les options datacenter et le SDN (zones VLAN/VXLAN, VNets, subnets).
Testé contre un faux serveur PVE ; **pas encore testé sur les clusters de labo.**
**État : phases 0 à 2, version « full web ».** Ajout des clusters (un par un ou en lot),
choix du primaire, éléments répliqués et cibles choisis dans des menus, écarts par cible,
dry-run et réplication par objet, par cible ou global, journal, audit.
Testé contre un faux serveur PVE (API + SSH) ; **pas encore testé sur un vrai cluster.**
## Périmètre
| Type (`kind`) | API | Politique par défaut |
| Type (`kind`) | Accès | Politique par défaut |
|---|---|---|
| `datacenter-options` | `/cluster/options` | `tag-style`, `registered-tags`, `user-tag-access`, `next-id` en `sync`, le reste ignoré ; `webauthn`, `u2f`, `http_proxy` jamais synchronisés |
| `sdn-zone` (vlan, vxlan) | `/cluster/sdn/zones` | `nodes`, `bridge`, `peers`, `fabric`, `dns`, `reversedns`, `dnszone` en `override` (mapping client) ; le reste en `sync` |
| `sdn-vnet` | `/cluster/sdn/vnets` | `sync` |
| `sdn-subnet` | `/cluster/sdn/vnets/{vnet}/subnets` | `sync` |
| `datacenter-options` | API `/cluster/options` | `tag-style`, `registered-tags`, `user-tag-access`, `next-id` en `sync`, le reste ignoré ; `webauthn`, `u2f`, `http_proxy` jamais répliqués |
| `pve-group` | API `/access/groups` | `sync` |
| `pve-role` | API `/access/roles` | `sync` (rôles intégrés ignorés) |
| `pve-user` | API `/access/users` | `sync`, comptes du royaume `pve` uniquement |
| `pve-password` | SSH `/etc/pve/priv/shadow.cfg` | `sync` (haché recopié tel quel, jamais affiché) |
| `sdn-zone` (vlan, vxlan) | API `/cluster/sdn/zones` | `nodes`, `bridge`, `peers`, `fabric`, `dns`, `reversedns`, `dnszone` en `override` (mapping de la cible) ; le reste en `sync` |
| `sdn-vnet` | API `/cluster/sdn/vnets` | `sync` |
| `sdn-subnet` | API `/cluster/sdn/vnets/{vnet}/subnets` | `sync` |
Hors périmètre v1 : EVPN (et les zones simple, QinQ, faucet, avec leurs VNets, ignorées
avec un avertissement), firewall, storage, pools/ACL, jobs, secrets.
Dépendances : un mot de passe exige son compte, un compte exige ses groupes ; le menu
« Éléments répliqués » les coche ensemble. Un parent absent de la cible et non répliqué
est une erreur bloquante.
Détail des routes, schémas et droits, avec les sources : [docs/api-pve.md](docs/api-pve.md).
Hors périmètre : EVPN (et les zones simple, QinQ, faucet, avec leurs VNets, ignorées
avec un avertissement), firewall, storage, pools, ACL, jobs, comptes `@pam` et autres royaumes.
Détail des routes, schémas et accès, avec les sources : [docs/api-pve.md](docs/api-pve.md).
## Fonctionnement
@@ -29,77 +37,65 @@ Détail des routes, schémas et droits, avec les sources : [docs/api-pve.md](doc
lecture source -> normalisation -> diff -> plan -> apply -> vérification
```
- **Lecture** : la référence est lue en configuration SDN *appliquée* (`running=1`) ; les
clients en configuration courante. Chaque cluster est refusé si un nœud en ligne n'est
- **Lecture** : le primaire est lu en configuration SDN *appliquée* (`running=1`) ; les
cibles en configuration courante. Chaque cluster est refusé si un nœud en ligne n'est
pas en PVE 9.x (`/nodes/{node}/version`).
- **Normalisation** : objets JSON génériques → champs canoniques (chaînes, listes triées,
property strings à clés triées). Les champs calculés par PVE sont retirés.
- **Politiques** : `sync` (copie de la référence), `ignore`, `override` (valeur fournie
uniquement par le `mapping` du client). Le mapping d'un client est prioritaire sur la
référence. Résolution : politique du client → politique globale → défaut de l'adaptateur.
- **Politiques** : `sync` (copie du primaire), `ignore`, `override` (valeur fournie
uniquement par le `mapping` de la cible). Le mapping d'une cible est prioritaire sur le
primaire. Résolution : politique de la cible → politique globale → défaut de l'adaptateur.
- **Plan** : création / mise à jour (champs modifiés, champs à supprimer) ; suppression
d'objets uniquement avec `prune` (option explicite), dépendants d'abord. Erreurs bloquantes :
champ obligatoire absent à la création (ex. `bridge` d'une zone VLAN sans mapping),
champ immuable modifié (type de zone, vnet d'un subnet).
- **Apply SDN** : `POST /cluster/sdn/lock` → écritures avec `lock-token` → `PUT /cluster/sdn`
(commit + rechargement réseau, suivi de la tâche). Toute erreur avant le commit déclenche
`POST /cluster/sdn/rollback`. Le verrou est refusé si le client a déjà des changements SDN
`POST /cluster/sdn/rollback`. Le verrou est refusé si la cible a déjà des changements SDN
en attente : on ne mélange pas nos modifications avec celles de quelqu'un d'autre.
- **Vérification** : relecture du client, nouveau plan qui doit être vide, et aucune zone en
- **Apply comptes** : API pour groupes, rôles et comptes ; mots de passe par SSH (script
perl sous `lock_shadow_config`, comme `pveum passwd`).
- **Vérification** : relecture de la cible, nouveau plan qui doit être vide, et aucune zone en
`error` sur les nœuds (`/nodes/{node}/sdn/zones`).
## Utilisation
## Démarrage
```sh
go build -o pve-confsync ./cmd/pve-confsync
cp examples/confsync.example.yaml confsync.yaml # puis adapter
./pve-confsync check # accès, version 9.x, droits des tokens
./pve-confsync export -o ref.yaml # état normalisé de la référence (versionnable dans Git)
./pve-confsync diff # écarts par client ; code retour 2 s'il y en a
./pve-confsync diff -format json
./pve-confsync apply # dry-run (défaut) : affiche le plan, n'écrit rien
./pve-confsync apply -client site-a -confirm # applique ; SDN : saisir le nom du client
./pve-confsync apply -source ref.yaml -confirm -yes # depuis un export Git, sans question
go run ./cmd/pve-confsync -demo # démo : faux clusters PVE, http://localhost:8080
docker compose -f docker-compose.demo.yml up -d --build # même démo en conteneur
docker compose up -d # production : données dans le volume /data
```
Avec docker compose (images et utilisateur non privilégié alignés sur Logstream) :
```sh
mkdir -p secrets && printf '%s' 'xxxxxxxx-...' > secrets/pve_ref
chmod 600 secrets/* && sudo chown 10001 secrets/* # lisibles par l'utilisateur du conteneur uniquement
docker compose run --rm confsync check
docker compose run --rm confsync apply -confirm
```
## Interface web
```sh
go run ./cmd/pve-confsync serve -demo # démo : faux clusters PVE, http://localhost:8080
docker compose -f docker-compose.demo.yml up -d --build # même démo en conteneur, sans secret à créer
docker compose up -d confsync-web # production : données dans le volume /data
```
Le mode démo démarre une référence PVE 9.1, deux clients avec des écarts (dont un mapping
Le mode démo démarre un primaire PVE 9.1, deux cibles avec des écarts (dont un mapping
manquant, bloquant) et un cluster PVE 8 refusé. Comptes `admin`, `operateur` et `lecteur`,
mot de passe `confsync-demo`.
- **Écarts** : une carte par cluster (version, compteurs à créer / modifier / erreurs /
hors référence), puis les objets du client choisi ; le détail d'un objet montre pour
chaque champ la politique, la valeur de référence, le mapping, la valeur actuelle et
la valeur après synchro.
- **Dry-run / Synchroniser** par objet (avec ses dépendances : zone et vnet parents à
créer), par client ou global. La synchronisation passe toujours par un dry-run : le
serveur recalcule le plan et refuse s'il a changé depuis (empreinte du plan). Avec des
changements SDN, il faut taper le nom du client (celui de la référence en global).
Une seule synchronisation à la fois, suivie étape par étape.
- **Journal** des dry-runs et synchronisations (plans, étapes, résultats) et **Audit**
(connexions, actions, modifications des paramètres ; admin).
- **Paramètres** : clusters (test d'accès et des droits, TLS, mapping et politiques par
client), types synchronisés, sélection, politiques, suppression (désactivée par défaut),
utilisateurs locaux, import / export YAML (format de la CLI), compte, apparence.
**Base vide** : la page de connexion propose de créer le premier administrateur
(12 caractères minimum). Tant que ce n'est pas fait, **le premier qui atteint l'instance
devient administrateur** : ne pas l'exposer avant. Alternative : `CONFSYNC_ADMIN_USER` et
`CONFSYNC_ADMIN_PASSWORD` / `_FILE` créent ce compte au démarrage.
Rôles : `admin` (tout), `operator` (dry-run et synchronisation), `viewer` (lecture).
## Interface web
- **Clusters** : ajout d'un cluster (URL `https://…:8006`, mot de passe root) ou d'une
liste (« nom URL » par ligne, même mot de passe), test (API, version 9.x, SSH), rôle
primaire / cible / désactivé. Un seul primaire, jamais écrit.
- **Éléments répliqués** et **Cibles** : menus de la vue Écarts ; les cibles décochées
sont exclues du global (choix mémorisé dans le navigateur).
- **Écarts** : une carte par cluster (version, compteurs à créer / modifier / erreurs /
hors primaire), puis les objets de la cible choisie ; le détail d'un objet montre pour
chaque champ la politique, la valeur du primaire, le mapping, la valeur actuelle et
la valeur après réplication. Les hachés sont masqués (`<masqué xxxxxx>`).
- **Dry-run / Répliquer** par objet (avec ses parents à créer), par cible ou global.
La réplication passe toujours par un dry-run : le serveur recalcule le plan et refuse
s'il a changé depuis (empreinte du plan). Avec des changements SDN, il faut taper le
nom de la cible (celui du primaire en global). Une seule réplication à la fois.
- **Journal** des dry-runs et réplications et **Audit** (connexions, actions,
modifications ; admin).
- **Paramètres** : sélection, politiques, mapping par cible, suppression (désactivée par
défaut), utilisateurs locaux, compte, apparence.
Rôles : `admin` (tout), `operator` (dry-run et réplication), `viewer` (lecture).
Les comptes locaux restent disponibles même avec OIDC. Toutes les variables sont reprises,
commentées, dans [`.env.example`](.env.example) (`cp .env.example .env`).
@@ -107,9 +103,8 @@ commentées, dans [`.env.example`](.env.example) (`cp .env.example .env`).
|---|---|---|
| `LISTEN` | `:8080` | adresse d'écoute |
| `DATA_DIR` | `/data` | base SQLite, `secret.key`, `session.key` |
| `CONFSYNC_SECRET_KEY` / `_FILE` | `DATA_DIR/secret.key` (créée) | clé AES-256 (hex ou base64) des secrets en base |
| `CONFSYNC_ADMIN_USER`, `CONFSYNC_ADMIN_PASSWORD` / `_FILE` | `admin`, aléatoire (journalisé une fois) | premier administrateur, base vide |
| `CONFSYNC_IMPORT` | | YAML de la CLI importé si la base n'a aucun cluster |
| `CONFSYNC_SECRET_KEY` / `_FILE` | `DATA_DIR/secret.key` (créée) | clé AES-256 (hex ou base64) des mots de passe en base |
| `CONFSYNC_ADMIN_USER`, `CONFSYNC_ADMIN_PASSWORD` / `_FILE` | `admin`, vide | premier administrateur sans passer par la page web |
| `AUTH_LOCAL` | `true` | comptes locaux |
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` / `_FILE`, `OIDC_REDIRECT_URL` | | OIDC (code + PKCE) ; `OIDC_REDIRECT_URL` complète, par ex. `https://confsync.example.org/auth/callback` |
| `OIDC_SCOPES`, `OIDC_GROUPS_CLAIM` | `openid profile email`, `groups` | ajouter le scope qui fournit les groupes chez l'IdP |
@@ -119,68 +114,59 @@ commentées, dans [`.env.example`](.env.example) (`cp .env.example .env`).
## Sécurité
- Secrets des tokens : jamais dans le YAML (`token_secret` en clair est refusé) ; lus depuis
une variable d'environnement (`token_secret_env`) ou un fichier (`token_secret_file`,
secret docker). Ils ne sont ni affichés ni journalisés. Dans l'interface, ils sont chiffrés en base
(AES-256-GCM, clé hors base) et jamais renvoyés au navigateur ; l'export YAML les remplace
par `token_secret_env`.
- **Accès PVE en `root@pam`** (API par ticket, SSH avec le même mot de passe) : nécessaire
pour les mots de passe du royaume `pve`, absents de l'API. Le compte root ne doit pas
avoir de TFA (refusé). Le primaire n'est jamais écrit.
- **Épinglage au premier test** : empreinte SHA-256 du certificat TLS (mode par défaut,
adapté aux certificats auto-signés PVE) et clé d'hôte SSH. Une clé SSH différente
ensuite bloque l'accès ; l'accepter demande une action explicite (« faire confiance à la
nouvelle clé »). Changer l'URL ou le nœud SSH oublie les empreintes. Autres modes TLS :
CA (PEM), CA système, `insecure` (labo).
- **Mots de passe root** chiffrés en base (AES-256-GCM, clé hors base), jamais renvoyés au
navigateur ni journalisés. Les hachés du shadow sont masqués dans l'UI, le journal et
les réponses JSON.
- Interface : mots de passe PBKDF2-SHA256 (600 000 itérations), cookie de session signé
(HttpOnly, SameSite=Lax, Secure derrière HTTPS) invalidé au changement de mot de passe,
en-tête `X-Confsync` exigé sur les écritures (CSRF), CSP sans script ni style en ligne.
- TLS : `fingerprint` (pinning SHA-256 du certificat, adapté aux certificats auto-signés PVE),
`ca_file`, ou `insecure: true` réservé au labo.
- Tokens (privilege separation activée, l'ACL doit être posée sur l'utilisateur et le token) :
```sh
# référence : lecture seule
pveum role add ConfSyncRO --privs "Sys.Audit SDN.Audit"
pveum user add confsync@pve
pveum user token add confsync@pve ro --privsep 1
pveum acl modify / --users confsync@pve --roles ConfSyncRO
pveum acl modify / --tokens 'confsync@pve!ro' --roles ConfSyncRO
# clients : Sys.Modify uniquement si les options datacenter sont synchronisées
pveum role add ConfSyncRW --privs "Sys.Audit Sys.Modify SDN.Audit SDN.Allocate"
```
`check` signale un token de référence capable d'écrire et un token client à qui il manque
un droit.
## Arborescence
```
cmd/pve-confsync/ CLI (check, export, diff, apply, serve)
cmd/pve-confsync/ serveur web (seul point d'entrée)
internal/service/ analyse pour l'UI : lecture parallèle, plans, lignes de diff par champ
internal/store/ SQLite : clusters (secrets chiffrés), paramètres, utilisateurs, journal, audit
internal/store/ SQLite : clusters (mots de passe chiffrés, rôles, empreintes), utilisateurs, journal, audit
internal/server/ HTTP : auth locale + OIDC, rôles, API, jobs de synchronisation
internal/demo/ faux clusters du mode démo
web/ UI embarquée (index.html, app.js, style.css, login.html)
internal/pve/ client API : token, TLS, form-encoding, version, tâches
internal/pve/pvetest/ faux serveur PVE 9.x pour les tests
internal/model/ objets génériques, snapshot YAML, valeurs canoniques
internal/pve/ client API : ticket root@pam, TLS épinglé, form-encoding, version, tâches
internal/sshx/ SSH root, clé d'hôte épinglée
internal/pve/pvetest/ faux serveur PVE 9.x (API + SSH) pour les tests
internal/model/ objets génériques, valeurs canoniques
internal/adapter/ un adaptateur par type : routes, champs calculés, locaux, immuables
internal/config/ configuration YAML : clusters, politiques, mappings, sélection
internal/config/ configuration en mémoire : clusters, politiques, mappings, sélection
internal/engine/ lecture, plan, apply (verrou SDN, rollback), vérification
docs/api-pve.md routes et schémas vérifiés dans les sources PVE
```
Tests : `go test ./...` (bout en bout contre le faux serveur : synchro complète, mapping
manquant, rollback sur erreur, refus de PVE 8, prune et ordre de suppression, forme
canonique des options datacenter, CLI dry-run/apply/export ; sélection d'objets et dépendances ; chiffrement, utilisateurs ;
serveur de bout en bout : rôles, CSRF, dry-run, empreinte périmée, confirmation SDN, apply).
Tests : `go test ./...` (bout en bout contre le faux serveur : réplication SDN et comptes
avec mots de passe par SSH, mapping manquant, parent absent, rollback sur erreur, refus de
PVE 8, prune et ordre de suppression ; sélection et dépendances ; chiffrement, migration
de schéma, premier administrateur ; serveur depuis une base vide : création de l'admin,
ajout des clusters, épinglage, primaire, réplication globale, primaire jamais écrit).
## Phasage
0. **CLI** export / diff / apply YAML (options datacenter, tags, SDN).
1. UI lecture seule : clusters, diff visuel par client. Ergonomie reprise de Logstream
0. Moteur export / diff / apply (options datacenter, tags, SDN) ; la CLI a été retirée.
1. UI lecture seule : clusters, diff visuel par cible. Ergonomie reprise de Logstream
(`web/index.html`, `style.css`, `app.js`, `login.html`, auth locale + OIDC, audit) ;
SQLite (clusters, profils, mappings, historique, audit), secrets chiffrés.
2. UI d'action : Dry-run / Synchroniser (par objet, par client, global), journal des exécutions. ← ici
2. UI d'action : Dry-run / Répliquer (par objet, par cible, global), journal ; gestion
des clusters en web, primaire / cibles, comptes et mots de passe du royaume pve. ← ici
3. Planification, détection de dérive en lecture seule, notifications.
## Reste à faire
- Valider sur les deux clusters de labo : forme réelle des réponses (`nodes`, `dhcp-range`,
- Valider sur un vrai cluster : écriture du shadow par SSH, forme de `/access/users?full=1`, forme réelle des réponses (`nodes`, `dhcp-range`,
booléens), comportement de `PUT /cluster/options` avec `delete`.
- Exploiter `GET /cluster/sdn/dry-run` (diff frr / interfaces, PVE ≥ 9.2 environ) pour
enrichir le dry-run SDN.