# CHANGELOG — Set-OPS ## 2026-07-03 ### Ajouté - **Rôle `serveur_oauth2_proxy` — passerelle SSO OIDC générique (Keycloak) + Icinga Web 2 au SSO.** oauth2-proxy (v7.15.3, binaire GitHub) place Keycloak **devant** n'importe quelle app sans OIDC natif : elle reçoit l'utilisateur authentifié via en-tête, en auth `external`. Rôle paramétrable (client, secret voûte, redirect, upstream, cookie voûte) — **réutilisable** pour toute app OIDC-less. Éprouvé sur `sup-01` **devant Icinga Web 2** : client Keycloak `icingaweb2`, oauth2-proxy `:4180` (exposé par l'edge) → upstream nginx local `:8080` → icingaweb2 `backend = external` (REMOTE_USER depuis `X-Forwarded-Preferred-Username`). **Prouvé** (flux authorization code headless) : `testmail` → oauth2-proxy → Keycloak → **icingaweb2 `/dashboard`, connecté** (« Se connecter avec Chezlepro », comme Grafana/Forgejo). Ferme le gap LDAP-direct d'icingaweb2. **Réglages appris** : `insecure_oidc_allow_unverified_email` (les users LDAP n'ont pas `email_verified` ; IdP interne de confiance) ; en reverse-proxy oauth2-proxy passe `X-Forwarded-*` (pas `X-Auth-Request-*`) ; **handler nginx en `restart` (pas `reload`)** car un changement d'adresse d'écoute n'est pas pris par un reload gracieux. - **Rôle `serveur_icingaweb2` — Icinga Web 2 (UI native) + module IcingaDB : éprouvé.** App PHP (php8.4-fpm) servie par un **nginx local**, exposée par l'edge (`icinga.lab.chezlepro.internal`, auto-dérivé : vhost + cert SAN + A PowerDNS + alias plancher). Config **par fichiers `.ini`** (config/resources/authentication/roles + module `icingadb`), pas d'assistant de setup. Base IcingaDB via `resoudre_base` (registre). **Auth LDAP direct** vers OpenLDAP (LDAPS, `client_pki` sur `sup-01`) — icingaweb2 n'a pas d'OIDC natif ; SSO-par-proxy = raffinement futur. **Prouvé** : `testmail` (LDAP) se connecte (`/dashboard`), et le **module IcingaDB affiche la supervision** (hôte `icinga`). Déploiement `failed=0` (le rôle est bon ; les frictions étaient dans le simulateur de login curl : contrôle de cookie `_checkCookie`, champs `uid`/`submit_login`, valeur CSRF avant `name`). - **Module BPM (Business Process) éprouvé + codifié — pile Icinga complète.** `serveur_icingaweb2` installe + active `icingaweb2-module-businessprocess` (`serveur_icingaweb2_modules`), crée le répertoire des processus (éditable via l'UI, groupe `icingaweb2`, setgid) et **sème des processus métier en IaC** (`serveur_icingaweb2_bpm_processes`, nom → contenu `.conf`). Format des feuilles `host;service` (éprouvé via les fixtures du module). **Prouvé** : un processus « Supervision Chezlepro » (agrège load/procs/swap/ping4/ssh du host `icinga` en logique ET) **rend un état** dans l'UI (`testmail` connecté), **avec le backend IcingaDB** (pas d'IDO). Rôle re-prouvé (reset → recrée le processus, idempotent). BPM n'est **pas remplaçable par Grafana** (roll-up d'impact métier). Pile Icinga = **moteur + Web 2 + BPM**, complète. - **Cœur Icinga éprouvé (supervision active).** `serveur_icinga` (cœur : `icinga2` + `icingadb` + `icingadb-redis`) déployé sur `sup-01` (🔧→⭐), base `icingadb` PostgreSQL via le registre. **Prouvé** : 3 services actifs, et le moteur **supervise** — IcingaDB peuplée (1 hôte, 12 services, résultats de checks persistés en base). Zéro bug de déploiement (rôle bien bâti). **Icinga Web 2 + module BPM restent différés** (phases dédiées : UI native + vues d'impact métier ; le BPM n'est pas remplaçable par Grafana). Confirme aussi le **DRY `resoudre_base`** sur icinga (les 3 rôles consommateurs validés). - **Forgejo branché au SSO OIDC (« Se connecter avec Chezlepro ») — 2e app SSO, prouvée.** `serveur_forgejo` (10.0.0) éprouvé sur `forge-01` (🔧→⭐), adossé à PostgreSQL (base `forgejo` auto-provisionnée depuis le registre), exposé par l'edge (vhost + cert SAN + A PowerDNS + alias plancher, tout auto-dérivé de `expose`). **Source OAuth2** vers Keycloak (`forgejo admin auth add-oauth`, idempotent, realm `chezlepro`), client OIDC `forgejo` enregistré via `serveur_keycloak_clients`. Auto-enregistrement OIDC (`[oauth2_client] ENABLE_AUTO_REGISTRATION` + `ALLOW_ONLY_EXTERNAL_REGISTRATION` : identités depuis l'annuaire seulement). **Prouvé** (flux authorization code headless) : `testmail` (LDAP) se connecte, **compte auto-créé** (`testmail@lab.chezlepro.internal`), atterrit sur le tableau de bord. **5 bugs de 1er déploiement corrigés** : dépendance périmée `serveur_sendmail`→`serveur_postfix` (le vrai MTA) ; `app.ini` doit appartenir au user `git` (Forgejo persiste des secrets générés) ; ordre admin/migrations (`flush_handlers` + `wait_for` avant `admin user create`) ; `HTTP_ADDR` `127.0.0.1`→`0.0.0.0` (l'edge nginx est sur un autre hôte, 502 sinon) ; auto-enregistrement OIDC. - **DRY : rôle utilitaire partagé `resoudre_base` (résolution BD depuis le registre).** Le bloc copié-collé dans `serveur_keycloak`, `serveur_forgejo` et `serveur_icinga` (charger le registre, filtrer par consommateur, déréférencer le secret via `lookup('vars', ...)`, résoudre hôte/port) est extrait dans `roles/resoudre_base` (facts `resoudre_base_entree/db_password/db_host/db_port`, `no_log`). Les 3 rôles l'incluent (`include_role`) et adoptent les facts. Le secret **ne quitte toujours pas le rôle** (déréférencé au déploiement). **Fait « sur la preuve »** : re-déploiement keycloak + forgejo `failed=0`, idempotent, `testmail` token Keycloak HTTP 200. Ferme le reste noté de la Phase 2 des bindings (cf. `docs/bindings-conception.md`). - **PowerDNS — A d'exposition auto-dérivés (le DNS de la Phase 3 des bindings).** `serveur_powerdns` génère désormais, dans la zone interne, un enregistrement A pour chaque FQDN d'exposition (champ `expose` des applications) vers l'**edge qui le sert** (`domaines.edge`) — via `expositions_des_applications` (même source que les vhosts nginx et les SANs du cert edge). Déclarer `expose` produit maintenant **vhost + SAN de cert + enregistrement DNS**, tout dérivé. **Prouvé** : `dig @infra-dns-01 grafana.lab.chezlepro.internal` et `keycloak.…` → `192.168.15.21` (edge). Option `serveur_powerdns_publier_expositions` (défaut true). **Limite / reste** : PowerDNS est **autoritatif, pas récursif** — pour que les nœuds *utilisent* ces A sans casser la résolution Internet, il faut un **récursif** (pdns-recursor : forward de la zone interne + récursion du reste) ou garder le plancher `/etc/hosts`. Ne PAS repointer naïvement `client_dns` vers l'autoritatif. - **`hosts_statiques` — alias d'exposition dans le plancher `/etc/hosts` (résolution client, sûre).** Le plancher pose désormais, sur **chaque nœud**, ` ` pour chaque `expose` (dérivé de `domaines.edge`, même source que nginx/PowerDNS). Indépendant du DNS, aucun risque de couper la résolution (choix retenu vs pdns-recursor). Chargement du plan **best-effort** (`stat` **`delegate_to: localhost`** + `become: false` — les registres vivent sur le nœud de contrôle ; ignoré si le plan est absent, ex. préparation du template). **Prouvé** : `/etc/hosts` d'obs-01 régénéré avec `keycloak`/`grafana` → edge (ligne manuelle éliminée), `getent` OK, et le **flux SSO Grafana fonctionne via la résolution du plancher** (`login: testmail`). Boucle Phase 3 fermée : déclarer `expose` → **vhost + SAN cert + A PowerDNS + alias plancher**, tout dérivé. Bugs corrigés en chemin : `serveur_loki` (groupe `loki` manquant), `stat` sur cible→contrôle, `become` inutile sur le contrôle. - **Rôle `client_unbound` — résolveur local (DNS dynamique) : éprouvé sur un nœud.** Unbound par nœud (`127.0.0.1`) avec **stub-zone** vers l'autoritatif interne (PowerDNS) + **récursion** Internet (ou forward via `client_unbound_transitaires`). Alternative *dynamique* au plancher `/etc/hosts` statique, sans casser Internet. Bascule de `/etc/resolv.conf` **protégée** (`client_unbound_apply` + `client_unbound_confirm`) **et validée AVANT** (Unbound doit résoudre interne + Internet, sinon pas de bascule → nœud jamais coupé). **Prouvé sur data-sql-01** (rayon d'impact minimal) : `dig @127.0.0.1 keycloak/id-sso-01.lab.chezlepro.internal` → PowerDNS, `deb.debian.org` → récursion, `apt` OK. Rôle sûr par défaut (`apply: false` : installe Unbound sans toucher au resolver). Rollout flotte = opt-in par nœud. **Note direction** : OPNsense embarque Unbound → à terme, l'Unbound *réseau* peut vivre sur l'appliance de bordure (nœud public, Étape B) ; le rôle par-nœud reste portable et complémentaire (cache local). - **Bindings — Phase 1 : résolveur de liens dans `instancier.py` (relations service→service déclaratives).** Une application déclare ses `liens: [{vers, role}]` dans `plan/applications.yml` ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (`setops_liens.accepte`, comme `meta/empreinte.yml`). `instancier` résout la cible (FQDN interne **dérivé de la nomenclature** + `domaine_interne`), substitue les gabarits (`{cible.fqdn}`, `{cible.hote}`, `{cible.ip}`) et **injecte les variables en host_vars du consommateur**. Validation : rôle accepteur, cible existante, genre attendu. **Migration prouvée** : les liens mail Postfix→Dovecot (`mailstore`) et Postfix→rspamd (`milter`) passent de group_vars codés en dur à des liens déclaratifs — `make instancier` donne **DIFF VIDE** (mêmes variables générées), puis les group_vars sont retirés. La topologie mail devient déclarative et portable. Cf. `docs/bindings-conception.md`. Suite : bases (Phase 2), exposition/domaines (Phase 3), GUI (Phase 4). - **Bindings — Phase 2 (bases) : constat + réconciliation de la note (`docs/bindings-conception.md` §5/§9).** Inspection du code réel : le binding app→base **existe déjà** — **côté base** (`consommateur`/`portee` dans `bases-donnees.yml`), résolu **dans le rôle** au déploiement (`include_vars` + filtre + `lookup('vars', secret)`), sur 4 rôles (postgresql, forgejo, keycloak, icinga). **Délibérément conservé** (le secret ne quitte jamais le rôle) — ne PAS dupliquer en app-side/instancier. Deux directions assumées : app→app côté app (instancier), app→base côté base (registre). Reste (reporté à l'épreuve de Keycloak) : factoriser le bloc de résolution copié-collé en include partagé (DRY). - **`docs/carte-set-ops.md` — carte d'orientation (index + mécanismes transverses).** Après audit du dépôt : point d'entrée « à lire d'abord » (index du corpus, ~22 docs), et **catalogue des mécanismes** dispersés dans le code (les 2 directions de binding, pont de cert, résolution BD par registre, socle-first, sûreté check-mode, voûte, dimensionnement) avec **où ils vivent**. But : ne plus re-découvrir l'existant. Constat : la **cruft était déjà inventoriée** dans `catalogue-services.md` (rôles-catégories inertes, échafaudages) — non dupliquée, référencée. `catalogue-services.md` « État d'implémentation » **rafraîchi** (rôles éprouvés sur VM réelles : socle, PKI, LDAP, DNS, nginx, pile courriel). Pointeur ajouté depuis `architecture-set-ops.md`. - **`serveur_postgresql` et `serveur_keycloak` éprouvés sur VM réelles (🔧→⭐).** PostgreSQL déployé (data-sql-01), écoute réseau + pg_hba VLAN, et **provisionne la base `keycloak` depuis le registre** (`bases-donnees.yml`) — **binding app→base prouvé en réel** (base + rôle créés, mot de passe = `vault_bd_keycloak`). Keycloak 26.0.7 déployé (id-sso-01), **mode prod**, connecté à PostgreSQL (**87 tables** du realm master écrites), **token admin obtenu** (auth adossée à la BD). Lacunes connues (documentées `catalogue-services.md`) : fédération LDAP et edge nginx pas encore câblés. Reste : DRY du bloc de résolution BD (keycloak/forgejo/icinga). - **`serveur_keycloak` — fédération LDAP (modèle d'identité A) : automatisée et prouvée.** Le rôle configure, via `kcadm` (idempotent), un realm applicatif (`serveur_keycloak_realm`, déf. `chezlepro`) et un **provider de stockage LDAP READ_ONLY** vers OpenLDAP (LDAPS, `uid`/`entryUUID`, `inetOrgPerson`). TLS LDAPS validé via le **truststore système** (`truststore-paths` → `/etc/ssl/certs/ca-certificates.crt`, racine step_ca posée par **client_pki**, désormais requis sur le nœud). Secrets par `environment` + `no_log`. **Éprouvé avant codification** puis prouvé par le rôle : un utilisateur LDAP (`testmail`) obtient un token via le realm (HTTP 200), et le redéploiement est **idempotent** (`changed=0`). Nouveau : `tasks/federation-ldap.yml`. Reste : edge nginx (accès HTTPS par nom), mappers d'attributs/groupes fins. - **Edge nginx + exposition (Phase 3 des bindings) — prouvés avec Keycloak.** Sans changement de code : la machinerie `serveur_nginx_publier_expositions` **existait déjà** (lit `expose` des applications + `edge` de `domaines.yml`, dérive `amont = http://:`, génère le vhost avec `X-Forwarded-*`). Le bac à sable déclare `keycloak.expose: [keycloak.lab.chezlepro.internal]` + le domaine interne `lab.chezlepro.internal` (edge `serveur_nginx`). **Prouvé** : le vhost s'auto-génère (`keycloak.lab.chezlepro.internal → http://192.168.15.81:8080`), et la découverte OIDC via l'edge renvoie `"issuer":"https://keycloak.lab.chezlepro.internal/..."` (les `X-Forwarded` passent, Keycloak se sait derrière HTTPS). **Limite connue** : le cert TLS de l'edge est encore le snakeoil auto-signé (avertissement navigateur). **Raffinement recommandé** (réutilise l'existant, pas de nouveau mécanisme) : ajouter les FQDN d'exposition aux `client_pki_sans` de l'edge (client_pki demande + renouvelle déjà le cert d'hôte), puis pointer `serveur_nginx_certificat` sur le cert client_pki (`/etc/step/certs/.crt`). - **Cert de l'edge : snakeoil → step_ca (HTTPS valide).** Appliqué le raffinement ci-dessus : `client_pki` ajouté à l'edge, ses `client_pki_sans` incluent le FQDN d'exposition (`keycloak.lab.chezlepro.internal`), et `serveur_nginx_certificat`/`_cle` pointent sur le cert client_pki. **Prouvé** : HTTPS `HTTP 200` avec `ssl_verify_result=0` (chaîne validée contre la racine step_ca, nom correct), émetteur `Set-OPS Internal CA`. Sans nouveau code (client_pki + group_var). **Gaps notés** : (1) recharger nginx au **renouvellement** du cert (le cert-renewer renouvelle en place, nginx ne recharge pas seul — hook à ajouter) ; (2) **auto-dériver** les SANs d'exposition de l'edge depuis le plan (au lieu de les lister dans le group_var). - **Grafana branché au SSO OIDC (« Se connecter avec Chezlepro ») — prouvé de bout en bout.** `serveur_grafana` : config OIDC via `GF_AUTH_GENERIC_OAUTH_*` (client confidentiel `grafana`, realm `chezlepro`, secret `vault_grafana_oidc`). `serveur_loki` + `serveur_prometheus` + `serveur_grafana` déployés sur `obs-01` (🔧→⭐). **Bug de rôle corrigé** : `serveur_loki` créait le répertoire en `group: loki` alors que le paquet crée l'utilisateur en `nogroup` sans groupe `loki` → ajout de la création du groupe. **Prouvé (flux authorization code headless, via l'edge HTTPS)** : `testmail` (user LDAP) se connecte à Grafana par le SSO — `/api/user` renvoie `login: testmail`, email et nom **fédérés depuis LDAP**. Chaîne complète LDAP → Keycloak → Grafana. **Gaps notés (pour rendre 100 % déclaratif)** : (1) l'**enregistrement du client OIDC** dans Keycloak a été fait via `kcadm` **à la main** (à codifier — rôle grafana ou liste de clients côté keycloak) ; (2) la **résolution** `keycloak.…internal → edge` sur obs-01 est un `/etc/hosts` manuel (**PowerDNS devrait porter les A d'exposition** — chaînon récurrent) ; (3) mapping de rôles Grafana (tous Viewer par défaut). - **`serveur_keycloak` — enregistrement des clients OIDC codifié (gap précédent fermé).** Le rôle gère une **liste déclarative** `serveur_keycloak_clients` (`clientId`, `redirect_uris`, `web_origins`, `secret`) et enregistre chaque client confidentiel via **kcadm idempotent** (`tasks/clients-oidc.yml`, create-si-absent, `no_log`). Décision : **côté Keycloak** (les creds admin restent dans le seul rôle Keycloak, pas répandus dans chaque rôle app) ; le `secret` référence la même variable de voûte que l'app. **Prouvé** : client `grafana` supprimé → rôle → recréé → `testmail` se connecte à Grafana (`login: testmail`) ; redéploiement **idempotent** (`changed=0`). Le déploiement de Grafana au SSO est désormais **autonome**. ## 2026-07-02 ### Décidé - **Bindings — conception des relations app/base/serveur/domaine (`docs/bindings-conception.md`).** Les relations service→service sont aujourd'hui codées en dur, éparpillées dans des group_vars (ex. Postfix→Dovecot/rspamd/LDAP), ce qui casse la portabilité multi-tenant. Direction retenue : **liens déclarés côté application** (`liens: [{vers, role}]`), résolus par `instancier.py` en variables Ansible ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (comme `meta/empreinte.yml`) ; FQDN cible **dérivé de la nomenclature** (jamais codé en dur). Domaines publics traités comme lien `exposition` (écrit sur l'edge). Réconcilie l'existant (bases `consommateur`, `domaines.edge`). Preuve de migration ciblée : les 3 liens mail. Implémentation à suivre (phasée). - **Licence : passage de CC BY-NC-SA 4.0 à AGPLv3.** Les licences Creative Commons ne sont pas faites pour du logiciel (position de CC elle-même) et la clause **NonCommercial contredisait le principe fondateur « tout est libre »** — en plus de bloquer les artisans/coopératives visés. `LICENSE` remplacé par le **texte officiel intégral de l'AGPLv3** (verbatim, non modifié). Attribution + modèle **libre + services/certification** documentés dans le `README` (méthode d'attribution recommandée, sans toucher au texte de la licence). L'AGPLv3 protège la souveraineté (anti-captation propriétaire en SaaS) **sans interdire l'usage commercial**. - **Architecture d'identité/SSO (`docs/identite-sso.md`).** Modèle A : **OpenLDAP source de vérité**, **Keycloak fédéré** (SSO web OIDC, MFA, self-service), **mail en bind LDAP direct**. Une identité, un mot de passe, deux chemins d'auth (web→Keycloak, mail→LDAP), tout adossé au même OpenLDAP. Sert la portabilité multi-tenant (chaque tenant = son LDAP + son Keycloak fédéré). Pas Keycloak-source (casse-tête mail + moins portable). - **Service courriel : pivot de Stalwart vers Postfix + Dovecot + rspamd.** La Phase 1 Stalwart (`serveur_stalwart`) avait été prototypée et déployée (v0.16.11, install + démarrage en mode récupération). Le prototypage a révélé un projet **trop jeune/volatil pour un pilier mail critique** : config cassée entre 0.15 et 0.16, outil IaC `stalwart config apply` **annoncé mais non livré** dans le binaire, API REST supprimée (JMAP), gros backlog. Pivot vers la stack **mature Postfix/Dovecot/rspamd**, en prime **100 % configurable par fichiers** (alignée au modèle déclaratif Set-OPS). Le rôle `serveur_stalwart` est **retiré** (git en garde la trace) ; `docs/courriel-conception.md` mis à jour. Réévaluer Stalwart ~2028. ### Ajouté - **`docs/pouvoirs-set-ops.md` — bilan des capacités du moteur.** Inventaire structuré (moteur/plan, plan de contrôle GUI, socle durci, piliers d'infrastructure, services outillés, patrons d'ingénierie), distinguant honnêtement « prouvé sur cluster réel » de « outillé ». - **Rôle `serveur_rspamd` (rspamd 3.x) — antispam + DKIM, en milter sur Postfix.** Installé sur le nœud edge-mta (avec Postfix), backend **Redis** local, worker proxy en **mode milter auto-scan** (`:11332`), **signature DKIM** sortante (clé générée par le rôle de façon idempotente ; enregistrement DNS public affiché pour l'Étape B). Config par surcharges `/etc/rspamd/local.d/`. Postfix branché via `smtpd_milters` (option `serveur_postfix_rspamd_milter`, `milter_default_action = accept` → tolérant si rspamd indisponible). **Prouvé** : un courriel traversant le milter ressort **scanné** (`rspamc stat` : 1) et **signé DKIM** (`DKIM-Signature: d=…`), puis livré et lu en IMAP. **Étape A (courriel interne) complète** : dovecot + postfix + rspamd. - **Flux courriel interne PROUVÉ de bout en bout (Étape A).** Envoi → Postfix (`edge-mta`, validation LDAP) → LMTP réseau → Dovecot (mail-store) → boîte Maildir → **lu en IMAP** (auth LDAP, TLS step_ca) : `status=sent`, message lu (sujet + corps). Réglages Dovecot 2.4 qui débloquent la remise LMTP : `userdb static { static_allow_all_users = yes }` (sinon NOTFOUND pour l'expéditeur/raw-mail-user externe), `mail_inbox_path =` vidé (le défaut mbox `/var/mail` root refusait l'autocréation de l'INBOX), Maildir explicite (`mail_home` + `mail_path = %{home}/Maildir`), chemin par nom d'utilisateur (home identique côté LMTP local-part et IMAP adresse complète ; mono-domaine, multi-domaine = raffinement Étape B). - **Rôle `serveur_postfix` (Postfix 3.x) — MTA du nœud edge-mta.** Réception `:25`, cartes **LDAP** (validation des boîtes via l'attribut `mail`), remise **LMTP réseau** vers le nœud mail-store Dovecot (`virtual_transport = lmtp:inet:[…]:24`), TLS via **step_ca** (pont de cert), aucune boîte locale. Config `main.cf` + carte `ldap-mailboxes.cf`, validée par `postfix check`. Secret de bind : `vault_openldap_admin`. Nécessite `serveur_postfix_mailstore_hote` (FQDN du mail-store). Validé statiquement ; déploiement réel à suivre. - **Rôle `serveur_dovecot` (Dovecot 2.4) — déployé et prouvé.** IMAP `:993`/`:143` + LMTP, **auth/annuaire LDAP** (vers `serveur_openldap`, filtre `mail`), stockage Maildir (user système `vmail`), **TLS via step_ca** (pont de cert + resync au renouvellement), neutralisation de l'auth système par défaut. Config en drop-in **syntaxe Dovecot 2.4** (`mail_driver`, `ssl_server_cert_file`, `passdb ldap`/`userdb static`, `%{user}`), **validée par `doveconf`** au déploiement. Sockets d'intégration Postfix **conditionnels** (rendus si l'utilisateur `postfix` est co-localisé). Prouvé : `doveadm auth test` — bon mot de passe accepté, mauvais refusé, sur cert step_ca. Secret de bind : `vault_openldap_admin`. - **`serveur_openldap` durci pour la prod : TLS via step_ca + organisation en intrant.** - **TLS (LDAPS + STARTTLS)** : le certificat d'hôte step_ca (déposé par `client_pki`, `root:root 600`) est synchronisé vers un emplacement lisible par `openldap` (`/etc/ldap/tls`) par un script + une unité `path` systemd qui **re-synchronise et recharge slapd à chaque renouvellement** ; `olcTLS*` configuré dans `cn=config`, `SLAPD_SERVICES` expose `ldaps://`. Dégrade proprement (slapd en clair local) si `client_pki` n'a pas encore posé le cert. - **Organisation** : nouvel intrant `chezlepro_organisation` (remplace le « Exemple Inc » codé). - *Écrit en code de prod, éprouvé statiquement ; validation par déploiement réel à suivre.* - **`docs/courriel-conception.md`** — cadrage du futur service de courriel souverain : full self-host, suite **Stalwart** (adoptée, enveloppée par un rôle mince), topologie MX primaire + MX secours, intégration identité (LDAP) / DNS / PKI (Let's Encrypt public vs step_ca interne), enregistrements DNS publics, décisions ouvertes (IP/PTR, secours, stockage) et phasage. **Conception seulement — aucun rôle livré.** ## 2026-07-01 ### Ajouté - **État RÉEL vs plan dans le GUI (sonde de vie + auto-actif).** Le badge `planifié`/ `actif` décrit l'*intention* du plan, pas l'existence de la VM — d'où la confusion « serveur planifié mais vivant ». Deux ajouts : - **Sonde de vie** : le GUI teste la joignabilité SSH de chaque hôte en arrière-plan (`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable** — sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan. - **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on **déploie** passe automatiquement `actif` dans le plan (matérialisé = actif), puis l'inventaire est **régénéré** pour que le changement se voie partout (en-tête inclus). - **Compteur « vivantes »** dans l'en-tête (depuis la sonde), à côté de actifs/planifiés. ### Corrigé - **nginx ne validait pas sur Debian 13 (`server_tokens` en double).** Debian 13 livre `server_tokens off;` **actif** dans `/etc/nginx/nginx.conf` (avant : commenté). Le drop-in `conf.d/99-setops.conf` du rôle le redéclarait → `nginx -t` échouait (« directive is duplicate ») et le déploiement plantait au handler de validation. Le rôle `serveur_nginx` neutralise désormais la ligne distro (le drop-in reste l'unique source). Trouvé en déployant nginx pour de vrai sur un hôte edge. - **Une voûte chiffrée cassait `instancier` / « Appliquer le plan ».** `ansible-inventory --list` (utilisé pour la comparaison sémantique du plan) tente de déchiffrer `group_vars/all/vault.yml` et échoue sans mot de passe (`exit 4`) — alors que l'opération est structurelle, sans secret. `instancier` utilise désormais automatiquement le fichier conventionnel `~/.config/setops-vault-pass` (si `ANSIBLE_VAULT_PASSWORD_FILE` n'est pas déjà défini). - **Secrets des rôles non câblés à la voûte (échafaudage manquant).** Les rôles à secrets déclaraient `serveur_X_password: ""` avec, en commentaire seulement, la variable de voûte attendue (`{{ vault_X }}`) — sans mapping réel. Résultat : remplir la voûte selon `vault.exemple.yml` ne suffisait pas, le secret restait vide et l'assertion « secrets requis » échouait. Les 12 secrets des 8 rôles (`serveur_step_ca`, `serveur_forgejo`, `serveur_grafana`, `serveur_keycloak`, `serveur_openldap`, `serveur_redis`, `client_ldap`, `client_pki`) pointent désormais vers leur variable de voûte : `serveur_X_password: "{{ vault_X | default('') }}"`. Chaque instance n'a plus qu'à remplir ses `vault_*` dans sa voûte chiffrée ; aucun mapping par instance. - **« Vérifier » (dry-run `--check`) échouait faussement sur un hôte frais.** Les tâches « démarrer service » et les handlers « redémarrer / recharger / valider » des rôles applicatifs touchent un paquet que `--check` n'installe pas réellement → le service (ou le fichier de zone/conf) n'existe pas encore → faux `fatal`, qui **bloquait le déploiement** (le dry-run doit réussir pour débloquer « Déployer »). Ajout de `when: not ansible_check_mode` sur ces tâches et handlers des 13 rôles `serveur_*` (29 gardes). En dry-run elles sont sautées ; en vrai déploiement, inchangées. - **Le bouton ⚙ « Appliquer le plan » du GUI refusait en silence** dès que le plan divergeait de l'inventaire (il appelait `instancier appliquer` **sans** `--force`). Résultat : après une édition (disque, état, auto-actif), l'inventaire n'était jamais régénéré et les compteurs restaient figés. Le clic « Appliquer » **est** l'intention explicite → le GUI force désormais (git reste le filet). - **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault + confirmation renforcée en prod) : - **Serveur** → `🖥 Pousser` clone la VM depuis le golden template (`make creer-vm`), disponible même sur un hôte planifié — comble le trou : le GUI ne clonait pas. - **Application** → `Pousser` déploie l'hôte porteur (`make deployer HOTE=`). - **Base** → `Pousser` déploie l'hôte du serveur de BD (crée la base). Nouveaux modes `creer` / `pousser` dans `executer_flux` + routes `/api/creer` et `/api/pousser`. Flux complet depuis l'interface : éditer → ⚙ Appliquer → 🖥 Pousser → Vérifier → Déployer. - **Premier déploiement RÉEL validé de bout en bout** (cluster asgard) : flux canonique plan-piloté clonant + durcissant + faisant tourner un PowerDNS qui résout. Voir les 3 correctifs ci-dessous. ## 2026-06-30 ### Ajouté - **Plancher de résolution `/etc/hosts` (indépendant du DNS).** Nouveau rôle de socle `hosts_statiques` (dans `serveur_debian`) : génère `/etc/hosts` sur **chaque** VM depuis l'inventaire (nom + FQDN interne → IP réelle). Tout l'écosystème se résout par nom **même serveur DNS éteint**, et le bootstrap ne dépend plus du DNS. PowerDNS devient une **commodité** (zone/externe/dynamique) ; `client_dns` est rendu tolérant (inerte si aucun DNS interne) et sa dépendance à `serveur_powerdns` passe **molle**. - **Adressage fédéré : index d'instance.** Le VMID n'est plus codé `9CSNN` en dur : il prend le préfixe d'un `index` déclaré en tête de `plan/nomenclature.yml` (`{index}{catégorie}{service}{séq}`). Convention : `supernet = 10.(10+index).0.0/16`, `VMID = index·CSNN`. Permet à N écosystèmes de **coexister/s'interconnecter** sans collision (Chezlepro=1 → `10.11`/`1xxxx`, Technolibre=2 → `10.12`/`2xxxx`). Sans index → `9CSNN` (rétro-compatible ; bacs à sable, plages ad-hoc `172.19.x`). Code mort retiré (`deriveServeur` JS). Voir `docs/multi-instances.md`. - **Doc `docs/multi-instances.md`** : cadrage « un moteur, N écosystèmes » — l'instance comme dépôt autonome, la bascule, l'isolation, et le socle multi-tenant. - **Bascule d'instance (`make instance-utiliser NOM=…`).** Repointe le symlink `instance` vers un autre dépôt d'instance (prod ↔ bac à sable) ; `make instance-courante` affiche l'instance montée. Permet d'exploiter plusieurs instances (séparation **par instance**) depuis un seul moteur. - **Identité des intrants relative à l'inventaire.** Le panneau « Intrants » lit/écrit l'identité dans `group_vars/all/10-intrants.yml` de l'inventaire monté (fichier réel pour une instance autonome, symlink vers une source partagée sinon — l'écriture suit le symlink). Fonctionne donc aussi bien pour une instance « par instance » que pour l'ancien partage lab/production. - **Inventaire d'instance neutre et configurable (`principal` / `SETOPS_INVENTAIRE`).** Le moteur (Makefile + `inventory_gui`, `instancier`, `config_proxmox`, `serveurs`, `applications`) ne code plus en dur `inventories/lab` / `inventories/production` : il vise **un inventaire par instance**, détecté de façon **rétro-compatible** (`principal` > `production` > `lab`) et surchargeable par `SETOPS_INVENTAIRE`. Les instances existantes (découpage lab/production) continuent de fonctionner à l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre de la séparation **par instance** (la 1re étant le drapeau `setops_production`). - **Garde-fou de prudence par instance (`setops_production`).** Le déploiement réel est désormais possible sur **toute instance** (un bac à sable déploie sur *son* infra lab — il est isolé **et** fonctionnel). Le drapeau `setops_production` dans `group_vars/all/` ne **bloque** plus rien : il marque la PRODUCTION pour exiger une **confirmation renforcée** au déploiement (bannière/badge rouge « PROD », bouton Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (`false`) affiche « bac à sable » et déploie sans cette étape. Rétro-compatible (à défaut de drapeau, ancien repère « inventaire production »). Le GUI affiche toujours quel type d'instance est monté. ### Modifié - **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous les secrets de l'instance (token Proxmox + 17 `vault_*` pour PKI, LDAP/SSO, bases, forge, observabilité) vivent dans **un seul fichier chiffré**, `inventories//group_vars/all/vault.yml`. Gabarit committé `exemples/vault.exemple.yml`. `make config` (`config_proxmox.py`) écrit/édite désormais cette voûte (semée depuis le gabarit si absente). `.gitignore` durci (`**/vault.yml`). Docs mises à jour (config-proxmox.md avec étapes de migration, intrants-communs.md §H, QUICKSTART). Le GUI ne stocke toujours aucun secret. ### Corrigé - **Trois bugs trouvés au premier déploiement réel (cluster asgard).** - **Clonage/Makefile codaient `inventories/lab` en dur** (config Proxmox + voûte) — vestige du modèle env qui cassait les instances `principal`. Le playbook de clonage et le Makefile détectent maintenant l'inventaire (`lab` > `principal` > `production`). - **Redimensionnement disque non idempotent** : quand le disque dérivé du plan est plus petit que le golden template, Proxmox refuse (`shrinking disks is not supported`) et le clone échouait. Le resize est désormais **grow-only** (tolère le cas, la VM garde le disque du template — le dérivé est un minimum). - **PowerDNS refusait de démarrer** (`multiple backends 'bind'`) : le rôle redéclarait `launch+=bind` que le paquet `pdns-backend-bind` pose déjà. Le rôle ne déclare plus `launch` (seulement `bind-config`). - **`chezlepro_timezone` n'était appliqué nulle part.** Cet intrant de base global était défini mais aucun rôle ne s'en servait. Le rôle `chrony` (appliqué à tout hôte via `serveur_debian`) règle désormais le fuseau horaire à partir de `chezlepro_timezone` (`chrony_timezone` par défaut, vide = ne pas toucher). Les autres défauts globaux (nœud / stockage / pont Proxmox) étaient déjà réutilisés comme valeurs par défaut, surchargeables par hôte. ### Modifié - **Détail GUI : section « Groupes (dérivés) » retirée (redondante).** Depuis la fusion en atelier maître-détail, elle ne répétait que le socle (universel), les rôles des applications (déjà dans « Applications ici ») et les intégrations (déjà cochées). Son seul signal unique — le prérequis bloquant — est désormais nommé dans le pied (« Prérequis manquant : … ») ; le panneau Dépendances reste pour la vue d'ensemble. Code mort retiré (`renduGroupe`, `detailGroupe`, `titreGroupe`, CSS `.groupe*`). - **Nettoyage CSS/HTML du GUI** après la refonte : retrait des règles et éléments morts (`.message`, `.chips-filtre`/`.chip-f`, `.onglet*`, `.base-ligne`, `.bases-liste`, `.ch-grp*`/`.ch-fleche`/`.ch-roles`, `.champ-val`, divs `#message` et `#chips`). - **GUI refondu en atelier maître-détail unifié.** Toutes les vues suivent le même motif : tuiles à gauche, **détail + saisie à droite**, le panneau droit reflétant la sélection de la vue courante (fin du panneau « figé » au changement de vue). Les vues **Applications** et **Bases** passent de tableaux pleine largeur à ce motif ; la saisie se fait dans le panneau droit, avec **liens cliquables** entre objets (serveur → application → base). Le panneau droit est élargi (~38 %). - **Fusion Serveur/Hôte.** Les vues « Inventaire » (hôtes, lecture seule) et « Serveurs » (plan) faisaient doublon : elles sont fusionnées en **une seule vue Serveurs**. Sa tuile porte le statut de réconciliation (réconcilié / divergent / non instancié) ; son détail réunit l'**identité éditable** (plan), les **dérivés** (VMID/IP/VLAN), les **groupes**, les **applications et bases hébergées**, et **Vérifier/Déployer**. La navigation clavier (`1-3`, `j/k`, `v/d`, `/`) et le filtre opèrent désormais sur les serveurs. Un bandeau « Comment lire ce parc » explicite le modèle serveur·hôte·groupe·application·base. Code mort retiré (`carte`, `renduChaine`, `ONGLETS`, `champLecture`, sélection d'hôte, etc.). ### Ajouté - **Fluidité d'exploitation du GUI** (sans dépendance, stdlib pure) : - **Notifications empilées (toasts)** auto-effaçables au lieu d'une bannière unique écrasée ; les erreurs restent plus longtemps, les opérations longues mettent à jour leur propre toast. - **Durée d'exécution** affichée en direct dans la console (Vérifier / Déployer) et suivi vivant de « Appliquer le plan ». - **Navigation clavier** : `1-5` changent de vue, `j/k` parcourent les hôtes, `v`/`d` vérifient/déploient l'hôte sélectionné, `/` cible le filtre, `Ctrl+S` sauvegarde la vue éditable courante (ignorés pendant la saisie). - **Validation inline** des champs du plan (vue Serveurs) : nom `fonction-NN`, mémoire, cœurs, disque — liseré rouge et blocage avant l'envoi. - **Garde-fou anti-perte** : confirmation `beforeunload` si des éditions de plan ne sont pas sauvegardées. - Le bouton **Sauvegarder** de l'en-tête devient contextuel (sauve la vue éditable, désactivé en lecture seule) — fin de l'impasse 409 ; suppression du code mort. ### Ajouté - **Vue Serveurs : champs alimentés par les paramètres globaux.** À `+ Serveur`, **Nœud** et **Stockage** deviennent des listes déroulantes (catalogues `proxmox_noeuds` / `proxmox_stockages`, vide = défaut global) et **Intégrations** une rangée de cases à cocher des rôles `client_*` disponibles (au lieu d'une saisie texte). Les catalogues Nœuds / Stockages / Ponts sont de nouveaux intrants (classe « Catalogue ») éditables dans le panneau « Intrants de base » et stockés dans `group_vars/proxmox.yml` ; l'API expose `integrations_disponibles` (scan de `roles/client_*`). Le type `liste` (chaîne virgulée → liste YAML dédoublonnée) est ajouté au schéma des intrants. ### Supprimé - **Vue « Chaîne » du bandeau (redondante).** Son contenu (`renduChaine`) était déjà rendu, par hôte, dans l'onglet « Chaîne » du détail de la vue Inventaire ; la vue ne faisait que le répéter pour tous les hôtes à la fois, sans réseau/proxmox ni boutons d'opération. Retirée (bouton, rendu `dessinerArbre`, CSS `.arbre-*`) ; la chaîne reste consultable hôte par hôte. Raccourcis clavier ramenés à 1-4. ### Modifié - **Vue Inventaire (cartes) : détail converti en inspection lecture seule.** La vue affichait « généré depuis le plan (lecture seule) » tout en exposant une surface d'édition (+ Hôte, ✕, bascule actif/planifié, ✨ Proposer, champs et cases de groupes éditables) qui ne pouvait pas être persistée (l'inventaire est généré ; `/api/inventaire` renvoie 409). Ces contrôles orphelins sont retirés : nom, champs Réseau/Proxmox et groupes en lecture seule, état affiché en badge. **Vérifier / Déployer** et les onglets d'inspection sont conservés. L'édition reste dans les vues **Serveurs / Applications / Bases**. Code mort supprimé (`ajouterHote`, `supprimerHote`, `definir`, `definirNom`, `definirEtat`, `basculerGroupe`, `proposer`, `autoProposer`, `prochainSeqLibre`, `marquerModifie`, état `modifie`). - **Référence des paramètres de `make config`** (`docs/config-proxmox.md`). Explique un par un les 16 paramètres Proxmox non sensibles + les secrets API demandés par l'assistant : sens, valeur par défaut, quoi saisir, et quels champs sont des constantes vs des défauts surchargeables par hôte. Renvois ajoutés depuis `make help` et `QUICKSTART.md`. Comble un trou : ces invites n'étaient expliquées nulle part de façon pérenne (impératif « exploitable sans IA »). - **Panneau « Intrants de base » dans le GUI.** Un bouton ⚙ Intrants ouvre une fenêtre unique pour saisir les valeurs communes à tout l'écosystème, avec une distinction visible entre **constantes** (valeur unique, non surchargeable) et **défauts** (valeurs proposées, surchargeables dans les instances). Les secrets ne sont **jamais** saisis ni affichés ici (garde-fou `INTRANTS_CLES_INTERDITES` + filtrage par schéma) : ils restent dans le Vault Ansible, édités en CLI ; le panneau les liste seulement à titre informatif. Un changement de `domaine_interne` (clé de voûte) demande une confirmation explicite ; un `domaine_interne` vide est refusé côté serveur. La nomenclature reste en lecture seule (modifiable dans le plan). Voir `docs/intrants-communs.md` et `docs/intrants-base-gui-conception.md`. - **Source unique d'identité partagée.** `domaine_interne` et `chezlepro_timezone` vivent désormais dans `inventories/partage/intrants-identite.yml`, référencé par symlink depuis chaque environnement (`group_vars/all/10-intrants.yml`) — fin de la duplication lab/production. - **Info-bulles d'aide sur les champs du GUI.** Survoler la description d'un champ à saisir affiche une bulle avec des instructions sommaires. ## 2026-06-28 ### Ajouté - **Document de présentation de l'écosystème Chezlepro** (`docs/ecosysteme-chezlepro.md`). Description vulgarisée à destination client/partenaire : les piliers de l'écosystème souverain, le modèle reproductible (plan → génération → clonage → conformité), et un inventaire des **mesures de renforcement** réellement en place (SSH durci, fail2ban, sysctl, nftables, AppArmor, auditd, mises à jour automatiques, gestion des secrets, garde-fous destructifs, sécurité par l'architecture). Honnête sur le statut « défini/validé vs déployé ». ### Ajouté - **Dimensionnement dérivé des ressources VM.** Les cœurs/RAM/disque d'une VM sont désormais **estimés depuis les logiciels hébergés + le socle SE**, au lieu d'hériter des specs du golden template (CPU/RAM identiques pour toutes les VM auparavant). Chaque rôle déclare son empreinte (`roles//meta/empreinte.yml`) ; le générateur somme par hôte (marge + arrondis) et écrit `proxmox_coeurs`/ `proxmox_memoire`/`proxmox_disque_taille` ; le clonage passe `cores`/`memory` à Proxmox (`omit` si absent → aucune régression). Override par hôte possible dans le plan (`serveurs.yml`). Voir `docs/dimensionnement-ressources.md`. ### Corrigé - **`make instancier-appliquer FORCE=1` n'honorait pas `--force`.** La recette Makefile lançait `instancier.py appliquer` sans relayer `FORCE` ; le message « Utilise FORCE=1 » était donc trompeur (un changement de plan intentionnel restait bloqué). La recette passe désormais `$(if $(FORCE),--force)`. Trouvé en dogfooding. ## 2026-06-24 — Première publication publique Première mise à disposition publique de **Set-OPS**, moteur Ansible d'écosystèmes numériques souverains sur Proxmox — offert à la communauté québécoise par l'**Alliance Boréale**, à la Saint-Jean-Baptiste 2026. - **Moteur générique, piloté par un plan déclaratif** : on édite le plan (`instance/plan/*.yml`), l'inventaire Ansible se génère, les VM se clonent depuis un golden template Debian 13, les rôles s'appliquent par groupes. Tout passe par `make`, le GUI local et la documentation. - **Piliers d'un écosystème souverain** : socle Debian durci, AC/PKI interne, DNS interne, identité (LDAP + SSO), relais courriel, bases de données, observabilité, forge. - **Catalogue de modèles prêts à déployer** (`exemples/modeles/`) : un hébergeur copie un modèle, le renseigne à ses couleurs, et instancie. - **Souveraineté jusqu'au bout** : Set-OPS s'exploite entièrement à la main, sans aucune IA. Pour démarrer : **`QUICKSTART.md`**.