Files

11 KiB

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.

É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.

Périmètre

Type (kind) API 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

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étail des routes, schémas et droits, avec les sources : docs/api-pve.md.

Fonctionnement

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 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.
  • 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 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 error sur les nœuds (/nodes/{node}/sdn/zones).

Utilisation

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

Avec docker compose (images et utilisateur non privilégié alignés sur Logstream) :

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

go run ./cmd/pve-confsync serve -demo        # démo : faux clusters PVE, http://localhost:8080
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 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.

Rôles : admin (tout), operator (dry-run et synchronisation), viewer (lecture). Les comptes locaux restent disponibles même avec OIDC.

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 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
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é

  • 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.
  • 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) :
# 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)
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/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/adapter/      un adaptateur par type : routes, champs calculés, locaux, immuables
internal/config/       configuration YAML : 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).

Phasage

  1. CLI export / diff / apply YAML (options datacenter, tags, SDN).
  2. UI lecture seule : clusters, diff visuel par client. 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.
  3. UI d'action : Dry-run / Synchroniser (par objet, par client, global), journal des exécutions. ← ici
  4. 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, 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.
  • Rechargement en échec après commit : aujourd'hui signalé, pas d'annulation automatique (la configuration appliquée est déjà la nouvelle).