diff --git a/Dockerfile b/Dockerfile index 08cb76b..e6e1d23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY go.mod go.sum ./ RUN go mod download COPY cmd ./cmd COPY internal ./internal +COPY web ./web ARG VERSION=dev RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" \ -o /out/pve-confsync ./cmd/pve-confsync @@ -20,6 +21,9 @@ COPY --from=build /out/pve-confsync /usr/local/bin/pve-confsync USER confsync WORKDIR /data VOLUME /data -ENV CONFSYNC_CONFIG=/config/confsync.yaml +ENV CONFSYNC_CONFIG=/config/confsync.yaml DATA_DIR=/data LISTEN=:8080 +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1 ENTRYPOINT ["/usr/local/bin/pve-confsync"] -CMD ["check"] +# interface web ; la CLI reste disponible : docker compose run --rm confsync diff +CMD ["serve"] diff --git a/README.md b/README.md index 6d914a6..11b041d 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ 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 : phase 0 (CLI).** Export, diff et apply en YAML 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.** 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 @@ -71,12 +72,59 @@ 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 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. Le chiffrement au repos arrive avec - le stockage SQLite (phase 1). + 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) : @@ -99,7 +147,12 @@ un droit. ## Arborescence ``` -cmd/pve-confsync/ CLI (check, export, diff, apply) +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 @@ -111,18 +164,19 @@ 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). +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 -0. **CLI** export / diff / apply YAML (options datacenter, tags, SDN). ← ici +0. **CLI** export / diff / apply YAML (options datacenter, tags, SDN). 1. 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. -2. UI d'action : Dry-run / Synchroniser (par objet, par client, global), journal des exécutions. +2. UI d'action : Dry-run / Synchroniser (par objet, par client, global), journal des exécutions. ← ici 3. Planification, détection de dérive en lecture seule, notifications. -## Reste à faire avant la phase 1 +## 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`. diff --git a/docker-compose.yml b/docker-compose.yml index 71e39b4..e7848ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,31 +1,64 @@ -# Phase 0 : outil en ligne de commande, lancé à la demande : +# Interface web (phases 1-2) : +# docker compose up -d confsync-web # http://:8080 ; mot de passe admin dans les logs au 1er démarrage +# CLI de la phase 0, lancée à la demande avec confsync.yaml : # docker compose run --rm confsync check # docker compose run --rm confsync diff # docker compose run --rm confsync export -o /data/ref.yaml # docker compose run --rm confsync apply # dry-run # docker compose run --rm confsync apply -confirm # application (confirmation SDN interactive) services: - confsync: + confsync-web: build: . image: pve-confsync:dev - container_name: pve-confsync + container_name: pve-confsync-web + command: ["serve"] + restart: unless-stopped + ports: + - "8080:8080" # ou derrière Traefik (HTTPS : cookies Secure) environment: TZ: ${TZ:-Europe/Paris} - PVE_SITE_B_SECRET: ${PVE_SITE_B_SECRET:-} # secrets par variable (voir token_secret_env) + CONFSYNC_SECRET_KEY_FILE: /run/secrets/confsync_key # clé des secrets en base, hors volume /data + # CONFSYNC_ADMIN_PASSWORD_FILE: /run/secrets/confsync_admin + # CONFSYNC_IMPORT: /config/confsync.yaml # import initial de la config CLI (base vide) + # OIDC_ISSUER: https://auth.example.org/application/o/confsync/ + # OIDC_CLIENT_ID: confsync + # OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client + # OIDC_REDIRECT_URL: https://confsync.example.org/auth/callback + # OIDC_ADMIN_GROUP: confsync-admins + # OIDC_OPERATOR_GROUP: confsync-operators + env_file: + - path: .env + required: false + volumes: + - confsync-data:/data # SQLite, session.key + secrets: + - confsync_key + + confsync: + image: pve-confsync:dev + container_name: pve-confsync + profiles: ["cli"] # pas démarré par "up" ; "run" l'active + command: ["check"] + environment: + TZ: ${TZ:-Europe/Paris} + PVE_SITE_B_SECRET: ${PVE_SITE_B_SECRET:-} # secrets par variable (voir token_secret_env) env_file: - path: .env required: false volumes: - ./confsync.yaml:/config/confsync.yaml:ro - - confsync-data:/data # exports YAML, plus tard SQLite (phase 1) + - confsync-cli:/data # exports YAML secrets: - pve_ref - pve_site_a stdin_open: true tty: true -# Secrets des tokens API sous forme de fichiers (chmod 600), montés dans /run/secrets/ +# Fichiers chmod 600, propriétaire 10001, montés dans /run/secrets/. +# confsync_key : openssl rand -hex 32 > secrets/confsync_key secrets: + confsync_key: + file: ./secrets/confsync_key pve_ref: file: ./secrets/pve_ref pve_site_a: @@ -33,3 +66,4 @@ secrets: volumes: confsync-data: + confsync-cli: