11 KiB
pve-confsync
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, 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) |
Accès | Politique par défaut |
|---|---|---|
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 |
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.
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.
Fonctionnement
lecture source -> normalisation -> diff -> plan -> apply -> vérification
- 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 du primaire),ignore,override(valeur fournie uniquement par lemappingde 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.bridged'une zone VLAN sans mapping), champ immuable modifié (type de zone, vnet d'un subnet). - Apply SDN :
POST /cluster/sdn/lock→ écritures aveclock-token→PUT /cluster/sdn(commit + rechargement réseau, suivi de la tâche). Toute erreur avant le commit déclenchePOST /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. - Apply comptes : API pour groupes, rôles et comptes ; mots de passe par SSH (script
perl sous
lock_shadow_config, commepveum passwd). - Vérification : relecture de la cible, nouveau plan qui doit être vide, et aucune zone en
errorsur les nœuds (/nodes/{node}/sdn/zones).
Démarrage
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
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.
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.
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 (cp .env.example .env).
| Variable | Défaut | Rôle |
|---|---|---|
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 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 |
OIDC_ADMIN_GROUP, OIDC_OPERATOR_GROUP, OIDC_DEFAULT_ROLE |
viewer |
rôle d'après les groupes |
SESSION_TTL |
12h |
durée des sessions |
CONFSYNC_DEMO |
false |
mode démo |
Sécurité
- 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 royaumepve, 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-Confsyncexigé sur les écritures (CSRF), CSP sans script ni style en ligne.
Arborescence
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 (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 : 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 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 : 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
- Moteur export / diff / apply (options datacenter, tags, SDN) ; la CLI a été retirée.
- 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. - 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
- Planification, détection de dérive en lecture seule, notifications.
Reste à faire
- 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 dePUT /cluster/optionsavecdelete. - Exploiter
GET /cluster/sdn/dry-run(diff frr / interfaces, PVE ≥ 9.2 environ) pour enrichir le dry-run SDN. - Rechargement en échec après commit : aujourd'hui signalé, pas d'annulation automatique (la configuration appliquée est déjà la nouvelle).