# Runbooks d'exploitation — Set-OPS > **Pour qui :** l'exploitant, face à une situation. Pas le mainteneur, pas l'apprenant. > Le *pourquoi* vit dans les autres `docs/` ; la version pédagogique, dans le wiki. Ici, > uniquement le *comment faire*. > > Tu viens de reprendre l'écosystème et tu ne sais pas par où entrer ? > Wiki → **Reprendre l'écosystème**. --- ## 1. Un certificat a expiré / le login SSO est cassé **Symptôme** : connexion à une app via le SSO échoue *après* le login ; log Grafana/app : `tls: failed to verify certificate: x509: certificate has expired`. **Cause fréquente** : le cert step-ca a été **renouvelé sur disque** mais le service (nginx sur l'edge) **n'a pas été rechargé** → il sert l'ancien cert en mémoire. **Diagnostic-réflexe** — comparer le cert *servi* au cert *fichier* : ```bash # SERVI (en mémoire par nginx) echo | openssl s_client -connect infra-edge-01.chezlepro.internal:443 -servername keycloak.chezlepro.internal 2>/dev/null \ | openssl x509 -noout -enddate # FICHIER (sur disque) openssl x509 -in /etc/step/certs/infra-edge-01.chezlepro.internal.crt -noout -enddate ``` Dates différentes (servi < fichier) ⇒ nginx sert un cert périmé. **Fix immédiat** : `systemctl reload nginx` sur l'edge (idem `postfix`/`dovecot`/`slapd` selon le service touché). **Fix permanent (déjà en place)** : `client_pki_reload_services` recharge les vrais consommateurs après chaque renouvellement — edge→nginx, mail→postfix/dovecot, annuaire→slapd. Vérifier : `systemctl cat cert-renewer@$(hostname -f).service | grep ExecStartPost`. Voir aussi l'unité wiki *PKI & confiance* (⑤ le renouvellement). --- ## 2. Donner Explore/Editor à un opérateur dans Grafana (RBAC) Par défaut, un utilisateur SSO est **Viewer** (dashboards seulement, pas Explore). Pour l'élever : 1. Déclarer l'assignation dans l'inventaire de l'instance (group_vars `serveur_keycloak`) : ```yaml serveur_keycloak_role_assignments: - { user: , role: grafana-editor } # ou grafana-admin ``` 2. Redéployer Keycloak (`make deployer` sur le nœud SSO) — kcadm **à chaud, sans coupure**. 3. L'utilisateur doit **se déconnecter/reconnecter** (Grafana applique le rôle à la connexion). Mapping (défaut du rôle grafana) : `grafana-admin`→Admin, `grafana-editor`→Editor, sinon Viewer (`serveur_grafana_oidc_role_path`). > **Préférer le groupe à la personne — et c'est construit, pas un idéal.** Les groupes LDAP > sont projetés dans Keycloak et émis en claim (`tasks/groupes-ldap.yml`, `claim-groupes.yml`), > et `roles/serveur_grafana/meta/acces.yml` déclare déjà `sysadmin ⇒ Admin`, > `personnel ⇒ Viewer`. Ajouter quelqu'un au **groupe** lui ouvre Grafana, Forgejo, Icinga et > le courriel d'un seul geste — alors qu'une assignation nominative crée une dette qu'on > découvre le jour du départ, service par service. L'assignation explicite ci-dessus reste > le geste de dépannage, pas la façon normale d'accorder un accès. Voir `docs/autorisation.md` §5. --- ## 3. « Connection timed out during banner exchange » — le message qui accuse le réseau **Symptôme** : Ansible marque un ou plusieurs hôtes injoignables, avec ce message. On soupçonne le réseau, la route ou le pare-feu. **C'est presque toujours autre chose.** **Pourquoi le message trompe.** À travers la frontière, la poignée TCP aboutit *toujours* — le pare-feu y répond lui-même sans relayer (voir `frontiere-opnsense.md`). L'échec ne peut donc pas se présenter comme un « connection refused » franc : il se manifeste plus tard, au moment où le serveur devrait annoncer sa bannière SSH. D'où un message qui parle de délai réseau pour un hôte qui, souvent, n'a jamais rien reçu. **Les trois causes, par fréquence :** 1. **La VM n'est pas encore là.** `make creer-vm` rend la main dès que Proxmox a *démarré* la VM, pas quand elle répond. C'est le cas normal, quotidien, et aucun correctif ne l'abolira : les attentes actives du `Makefile` existent pour ça. 2. **`sshd` refuse une rafale.** `MaxStartups` / `MaxSessions` trop serrés coupent des connexions parfaitement légitimes — deux déploiements interrompus le 2026-08-09, sur des hôtes qui n'avaient ni redémarré ni perdu leur réseau. Réglés par `ssh_hardening_max_startups` (`10:30:60`) et `ssh_hardening_max_sessions` (`10`), avec l'arbitrage expliqué dans `roles/ssh_hardening/defaults/main.yml`. 3. **Le réseau, vraiment** — le cas le plus rare, et le dernier à examiner. **Ce qui tranche, dans l'ordre :** ``` make hote-afficher HOTE= # l'hôte existe-t-il au plan ? ssh -v ansible@ 2>&1 | tail -20 # où la négociation s'arrête exactement ``` Une bannière (`SSH-2.0-OpenSSH…`) prouve que le chemin est bon et que le problème est applicatif. **Aucun `connect()` ne prouvera quoi que ce soit ici** — il réussit vers le vide. --- ## 4. Brander une instance Forgejo (identité visuelle) Activer dans l'inventaire (group_vars `serveur_forgejo`) : ```yaml serveur_forgejo_branding: true serveur_forgejo_app_name: "Forge Chezlepro" serveur_forgejo_theme: "forgejo-dark" serveur_forgejo_meta_description: "…" ``` Puis redéployer. Le rôle déploie le dossier `custom/` officiel (logo/favicon aurore, accent CSS par variables, page d'accueil brandée) — léger, résistant aux MAJ (aucune classe interne touchée). Note : ne s'applique qu'aux Forgejo **gérées par Set-OPS**. ## 5. Restaurer — et d'abord : prouver qu'on peut ### 5.0 La reconstruction remet l'état (depuis le 2026-09-30) Jusqu'au 2026-09-30, **une reconstruction repartait d'un état neuf** : nouvelle racine d'AC, annuaire vierge (`sysadmin` au mot de passe d'amorçage), bases, Nextcloud et courriel vides. Les instantanés se restauraient pour *prouver* qu'ils s'ouvrent, jamais pour être remis en service. Désormais **chaque rôle propriétaire remet l'état de l'incarnation précédente**, au moment où il le créerait neuf — la bonne séquence vient des couches de déploiement : | Rôle | Quand | Condition « vierge » | |---|---|---| | `serveur_step_ca` | avant `step ca init` | pas de `ca.json` — et la voûte doit ouvrir les clés restaurées | | `serveur_postgresql` | juste après la création des bases | base sans table (hors `nextcloud`, `icinga`) | | `serveur_openldap` | en fin de rôle (schéma et `ppolicy` chargés) | aucune entrée hors la racine | | `serveur_dovecot` | après le répertoire des boîtes | répertoire vide | | `serveur_rspamd` | avant la génération DKIM | pas de clé DKIM | | `serveur_forgejo` | avant l'arborescence de données | répertoire vide | | `serveur_nextcloud` | **en fin de rôle** : base + fichiers + config ensemble, puis `occ upgrade` | pas de `config.php` au départ, base vide | | `serveur_web_dorsal` | après sa racine | racine vide | **L'instantané remis** est le dernier pris **avant la naissance de la machine** (date de sa clé d'hôte SSH). Un état en place n'est jamais écrasé d'office. Chaque jeu laisse un marqueur dans `/etc/setops/restauration/` ; ce qui a été remplacé est mis de côté dans `/var/backups/setops-avant-restauration/`. **La sauvegarde refuse de déposer** (code 3) tant qu'un état d'avant n'a été ni remis ni écarté : sinon `restic forget --keep-daily` chasserait l'instantané d'avant au profit de l'état neuf du même jour. **L'instantané d'avant rasage est étiqueté** `avant-raser` (depuis le 2026-10-07) et la rétention le garde **jusqu'à la reconstruction suivante**, qui lui retire l'étiquette. Sans elle, le premier dépôt de la machine reconstruite le chassait le jour même : après la reconstruction de Technolibre (M4), il ne restait rien à quoi comparer l'état remis. **Les témoins** comparent l'état vivant à cet instantané. Un écart, c'est une **perte** (fichier, courriel, entrée de l'annuaire, base, rôle, table, ligne d'historique, ou une base dont plus aucune ligne d'avant ne subsiste) ou une **identité changée** : clés et certificats de l'AC, AC d'Icinga et environnement d'Icinga DB, clé DKIM, `instanceid` de Nextcloud, mot de passe d'une entrée de l'annuaire. PostgreSQL se compare par clé (première colonne de chaque table), pas par nombre de lignes. Le reste (un courriel reçu depuis, une table de sessions, le bayes de rspamd, un cache) est listé sans être un écart. La reconstruction les lance à son étape `bilan`. Les gestes : ``` make sauvegarder-maintenant # JUSTE AVANT de raser : étiqueté « avant-raser » make restauration-etat [HOTE=...] # ce que chaque jeu est devenu, et si son instantané est encore au dépôt make temoins-etat [HOTE=...] # l'état vivant contre l'instantané d'avant rasage make temoins-etat SOURCE=candidat # faute d'étiquette : contre le dernier d'avant la naissance make restauration-renoncer HOTE=.. JEU=.. CONFIRMER=true # écarter un état, sans le remettre -e client_backup_restauration_instantane= # imposer un instantané, par hôte ``` Sur un nœud, sans Ansible : `setops-restaurer etat`, `setops-restaurer temoins`, et les répétitions qui ne touchent à rien — `setops-restaurer annuaire --essai `, `base --vers epreuve`, `fichiers --vers /var/tmp/epreuve `. > Éprouvé le 2026-08-12 sur Chezlepro. `make valider` rejoue la partie automatisable ; > la restauration d'une base reste manuelle, et porte un piège décrit plus bas. **Ce que `make valider` prouve tout seul**, pour chaque nœud détenteur d'état : le dernier instantané se restaure, il en sort des fichiers, et — pour l'annuaire — qu'il est **rejouable** (`slapadd -u`, essai à blanc, rien n'est écrit). Verdicts possibles : | Verdict | Sens | |---|---| | `OK` | restauré, et non vide | | `À CONFIRMER` | restauré, mais l'instantané n'emporte **aucun fichier** — légitime si ce nœud n'a pas encore de données, à trancher par un humain | | `SANS OBJET` | ce nœud ne détient rien de non régénérable | | `ÉCHEC` | la restauration elle-même a échoué | ### Restaurer les clés de l'autorité (`infra-pki-01`) ``` export RESTIC_REPOSITORY=$(grep -oP 'RESTIC_REPOSITORY="\K[^"]+' /usr/local/sbin/setops-sauvegarder.sh) export RESTIC_PASSWORD_FILE=/etc/setops/restic.pass t=$(mktemp -d); restic restore latest --target "$t" diff -r "$t/etc/step-ca" /etc/step-ca ``` Mesuré : **12 fichiers, 11 identiques octet pour octet**, `root_ca_key` et `intermediate_ca_key` compris. Le seul écart attendu est `db/000000.vlog` — le journal de la base badger de step-ca, qui avance à **chaque** émission de certificat. ### Rejouer une base depuis `pg_dumpall` — LE PIÈGE `pg_dumpall` écrit `CREATE DATABASE ` **avant** le `\connect` correspondant. Découper « du `\connect X` au `\connect` suivant » emporte donc un ordre qui vise une **autre** base. Couper aussi sur `CREATE DATABASE` et sur `DROP DATABASE` (avec `--clean`, la section de `nextcloud` finissait par `DROP DATABASE postgres;` — 2026-09-30), et **vérifier avant de rejouer** : ``` awk '/^\\connect forgejo$/{f=1;next} f && (/^\\connect /||/^CREATE DATABASE /||/^DROP DATABASE /){exit} f' \ toutes-bases.sql > section.sql grep -qE '^(DROP|CREATE|ALTER) DATABASE|^\\connect' section.sql \ && { echo "REFUS : ordre hors-perimetre"; exit 1; } runuser -u postgres -- createdb epreuve_restauration runuser -u postgres -- psql -q -d epreuve_restauration -f section.sql runuser -u postgres -- psql -tAd epreuve_restauration -c "select count(*) from information_schema.tables where table_schema='public'" runuser -u postgres -- dropdb epreuve_restauration ``` Mesuré sur `forgejo` : rejeu en **0 erreur**, **130 tables**, et les comptes réels (`forgejo-admin`, `sysadmin`). Production vérifiée intacte après coup. **Ne jamais rejouer un `pg_dumpall` entier sur un cluster vivant** : il contient les `DROP DATABASE` de toutes les bases. Une restauration réelle se fait sur un cluster neuf. ## 6. La bascule d'adressage de la fabric (D-77) — FAITE > **Cette section décrivait une transition en cours jusqu'au 2026-09-06.** Elle est > terminée : mesuré le 2026-08-22, **`10.0.0.0/24` n'existe plus** — ni `10.0.0.1`, ni > `10.0.0.41` ne répondent. Le plan d'administration est `10.17.0.0/24` : la frontière y > répond en `10.17.0.1` sur un port physique à elle, les commutateurs sont en `10.17.0.3` > et `.4` avec cette passerelle par défaut, le poste de l'exploitant en `10.17.0.17`. Le > renumérotage du tenant (`10.27` → `10.17`) est fait lui aussi. > > On garde la **méthode**, parce qu'elle est ce qui a permis de le faire sans coupure, et > qu'un autre site la rejouera : > > **Ajouter avant de retirer, jamais l'inverse.** Un point de routage qui change d'adresse > d'un coup coupe simultanément l'exploitant, les commutateurs qui l'ont en passerelle par > défaut, et l'outil qui devait faire la bascule. La seconde adresse a donc vécu à côté de > l'ancienne (`ipalias`), et l'ancienne n'est tombée qu'en **dernier**. > > **Distinguer une destination d'un chemin (D-78).** Un réseau qui n'est **jamais** une > destination — seulement un chemin — n'a aucune raison d'être unique entre deux hébergeurs : > il sort de l'espace dérivé, vers `192.168..0/24`. Seule la **gestion** doit rester > unique d'un site à l'autre, parce que le poste de l'exploitant, un VPN et demain un lien > inter-sites doivent l'atteindre. > > **Appliqué pour le transport VXLAN seulement, à ce jour** (mesuré le 2026-09-06) : > `underlay-vxlan` est bien en `192.168.50.0/24`. Le **transit** (`10.0.4.0/24`, VLAN 40) et > le **stockage** (`10.11.5-7.x`, VLAN 5/6/7) sont encore dans l'ancien espace. Ce n'est pas > une urgence — ces réseaux ne quittent jamais leur site — mais la carte doit dire ce qui est, > pas ce qui a été décidé. Un **site neuf** se monte directement au schéma final : il n'a > aucune transition à subir. > > **Un plan d'adressage ne doit pas dépendre de l'ordre d'une migration.** Le VLAN de > transport est passé de 11 à 50 — non parce que 11 était mauvais, mais parce que > `192.168.11.0/24` est occupé par le contrôle de la grappe. Faire dépendre un plan > d'adressage de l'**ordre** d'une migration est exactement la dette qui se paie un an > plus tard. ### Ce qui reste, et qui n'est PAS un reliquat de la bascule Les hyperviseurs gardent **deux** plans, et c'est voulu : | Plan | Réseau | Ce qu'il porte | |---|---|---| | administration | `10.17.0.0/24` (`vmbr3`, segment physique) | les **équipements** et l'exploitant ; **aucune VM ne peut y naître** (aucun pont ne le touche, et le validateur refuse qu'on y déclare une machine) | | contrôle de la grappe | `192.168.11.0/24` (`vmbr0`, carte dédiée) | l'**interface web Proxmox** et le dialogue entre nœuds — c'est par là qu'on atteint `ansible@192.168.11.4x` | > **Le `/24` de gestion vit à l'intérieur du `/16` du tenant, et ce n'est pas un conflit.** > Les zones d'un tenant commencent au 3ᵉ octet 16 ; la bande 0-15 est libre pour la fabric, > et la route connectée du `/24` est plus spécifique que celle du `/16` — la règle du > préfixe le plus long, pas une coïncidence. Il faut cependant le **déclarer** > (`bande_basse_de:` dans `underlay.yml`), sinon le validateur ne peut pas distinguer ce > chevauchement voulu d'un chevauchement accidentel. ## 7. Le nœud qui porte le gabarit tombe — la reproduction s'arrête **Symptôme.** Aucune VM nouvelle ne peut naître. `make creer-vm`, `make flotte-creer` et `make reconstruire` échouent au clonage. Les machines existantes, elles, ne bronchent pas. **Ce qui se passe.** Le gabarit doré vit sur un nœud nommé — `vishnu` chez l'hébergeur de référence — et sa configuration porte ce nom dans son chemin même : ``` /etc/pve/nodes/vishnu/qemu-server/9006.conf ``` Le clonage appelle `nodes/vishnu/qemu/9006/clone` : c'est l'API de **ce nœud-là** qui doit répondre. Nœud éteint, API muette, aucune naissance. **Ce qui ne se passe PAS, et qu'il faut savoir avant de paniquer.** Les données du gabarit ne sont pas perdues. Mesure du 2026-09-10 : ``` pool CephNVMe size=3 min_size=2 OSD sur les trois hôtes : asgard, gandalf, vishnu base-9006-disk-0, base-9006-disk-1 présentes dans le pool ``` L'image est répliquée trois fois et reste lisible avec un nœud en moins. Et les quatorze VM d'un écosystème tournent ailleurs, sur leurs propres disques Ceph : elles ne s'aperçoivent de rien. > **`vishnu` n'est pas un point unique de défaillance pour l'exploitation. Il l'est pour la > reproduction** — et la reproduction est ce que ce dépôt existe pour garantir. **La manœuvre.** Deux gestes, quelques minutes, aucun mouvement de données : ```bash # 1. re-héberger la configuration sur un nœud debout mv /etc/pve/nodes/vishnu/qemu-server/9006.conf \ /etc/pve/nodes/asgard/qemu-server/9006.conf # 2. le déclarer, sinon la garde refusera # SITE-Chezlepro/plan/10-intrants.yml : gabarit.noeud: asgard ``` Puis vérifier : ```bash make gabarit-etat # doit rendre « Conforme » ``` **Pourquoi c'est aussi court.** Parce que le disque du gabarit vit sur un stockage **partagé** depuis le 2026-09-10 : le déplacement ne bouge qu'un fichier de configuration. Sur un stockage local, il aurait fallu recopier 16 Go — ou refabriquer le gabarit. **L'ordre compte.** Déplacer sans déclarer laisse `gabarit_etat` en écart ; déclarer sans déplacer fait échouer le clonage sur un nœud qui ne détient pas le modèle. Faire les deux, dans cet ordre. **Ce que cette section ne fait pas.** Elle ne supprime pas la dépendance : elle la rend connue et courte. Deux remèdes de fond existent, aucun n'est appliqué : - **déplacer le gabarit là où vivent déjà les VM** — ça ne supprime pas le point unique, ça cesse d'en avoir *deux* (le nœud des VM et celui du modèle) ; - **une garde** qui refuse quand le nœud du gabarit n'héberge aucune machine de la flotte, c'est-à-dire quand la reproduction dépend d'un nœud qui ne porte rien d'autre. *Une dépendance qu'on documente sans la mesurer reste une dépendance qu'on découvrira au mauvais moment.* ## 8. Les contrôles à la demande — ce que `make prouver` ne peut pas voir `make prouver` est **statique** : il lit le dépôt, zéro appel réseau. C'est ce qui le rend rejouable partout, par n'importe qui, et présentable comme pièce justificative. Le prix de cette propriété : il ne peut rien dire de ce qui ne s'observe qu'en ouvrant une connexion. Quatre contrôles comblent ce creux. Aucun ne corrige quoi que ce soit — ils regardent, et rendent `0` si tout concorde. | Contrôle | Ce qu'il compare | Le piège qu'il attrape | | --- | --- | --- | | `make expositions-etat` | Expositions du plan ↔ **SAN du certificat servi** ↔ code du vhost | Un renommage déployé partout **sauf** dans le certificat | | `make gabarit-etat` | Gabarit déclaré ↔ VM modèle réelle | Le modèle a dérivé de ce que le plan promet | | `make routes-fabric-etat` | Zones déclarées ↔ routes déclarées ↔ routes vivantes | Une route **vivante mais non déclarée** — elle part au redémarrage | | `make frontiere-plan` | Registre des flux ↔ règles de la frontière | Une règle posée à la main, qu'aucune déclaration ne porte | Ajouter `SITE=1` à `expositions-etat` pour interroger l'écosystème du SITE plutôt que l'instance montée. ### Pourquoi `expositions-etat` existe Renommer une exposition touche cinq choses. Quatre suivent au déploiement ; la cinquième, non : ``` serveur_powerdns la zone publie le nouveau nom ✓ serveur_keycloak le client OIDC accepte le retour ✓ le service il fabrique ses URL avec le bon nom ✓ (P67) serveur_nginx le vhost répond sur le nouveau nom ✓ client_pki le SAN du certificat porte le nom ✗ il faut le rejouer ``` Le symptôme est trompeur : le site répond, la page s'affiche, et c'est le **navigateur** qui refuse — avec une erreur de certificat que personne ne relie à un renommage fait la veille. Mesuré deux fois le 2026-09-10, sur `grafana → observatoire` puis `icinga → vigie`. Le contrôle regarde **dans les deux sens**. Un nom resté dans le SAN après avoir quitté le plan est un nom que le certificat continue d'authentifier : c'est exactement ce qu'avait laissé le premier renommage, jusqu'au passage de `client_pki`. > Un `INJOIGNABLE` ne condamne pas le service : il dit que **ce poste** n'a pas pu ouvrir > la connexion. Les zones du SITE ne sont pas routées depuis le plan d'administration du > locataire — le mur est la frontière, pas le vhost. ## 9. Le premier jour d'un site — la séquence, et les dix-huit murs > **Écrit le 2026-09-12**, au sortir de la première reconstruction d'un site depuis zéro. > Avant elle, `SITE-Chezlepro` n'avait jamais été rasé : il avait été monté par ajouts > successifs, sur des semaines, avec un service déjà debout à chaque étape. > > La limite qu'on répétait — « l'infrastructure d'accueil n'a jamais été reconstruite > depuis zéro » — se lisait comme de la prudence. C'était **dix-huit défauts** que rien > d'autre n'aurait pu révéler. > > **Trois reconstructions complètes** ont été nécessaires : la première pour les trouver, > la deuxième pour vérifier les correctifs — elle en a révélé deux de plus, invisibles > tant que l'état n'était pas assez neuf — et la troisième pour prouver la séquence. > > | | Passages | Durée | Défauts trouvés | > |---|---|---|---| > | Tour 1 | 8 | ~2 h 30 | 16 | > | Tour 2 | 3 | 53 min | 2 | > | Tour 3 | **2** | **37 min 24** | **0** | > > La séquence ci-dessous est celle du troisième tour. Elle est mesurée, pas reconstituée. ### Ce qui rend un site différent d'un locataire Un locataire naît dans un monde déjà peuplé : le site lui fournit les paquets, les noms, le génome, l'heure et le dépôt de sauvegarde. **Un site n'a personne au-dessus de lui**, sauf sa frontière. Tout ce qu'un locataire reçoit, un site doit se le donner — et pendant qu'il se le donne, il ne l'a pas. C'est de là que viennent onze des quinze murs. ### La séquence, dans l'ordre ```bash # 0. AVANT TOUT — l'état sort du bâtiment make depot-hors-site VERS= # 1. Le résolveur d'amorçage, DÉCLARÉ dans le plan du site AVANT de créer quoi que ce soit # plan/10-intrants.yml : dns_amorcage: # Sans lui, les machines pointent sur le DNS du site — qui est l'une d'elles. # Et `nftables_admin_ssh: []`, sans quoi l'exploitant ne # pourra pas atteindre ce qu'il vient de construire. # 2. Les VM, depuis l'underlay ~9 min make site-creer CONFIRMER=true # 3. ATTENDRE QU'ELLES REPONDENT — `site-creer` ne le fait pas ~3 min until ansible -i scripts/site_inventaire.py all,'!' -m ping >/dev/null 2>&1 do sleep 10; done # 4. Passage 1 — va jusqu'à `serveur_ops`, s'arrête sur la forge vide ~20 min V="$(dirname "$(readlink -f underlay.yml)")/underlay.vault.yml" ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml -e "@$V" # 5. Amorcer la forge — elle tourne, elle est vide ~45 s export SETOPS_FORGE_MDP=… # jamais en argument de ligne de commande make forge-amorcer CONFIRMER=true # 6. Passage 2 — doit finir à 0 échec ~4 min ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml -e "@$V" ``` **Total mesuré : 37 min 24** pour sept machines, de rien du tout à un site complet. **La voûte se passe en `-e @`** — `make site-appliquer` la dérive du symlink `underlay.yml`, mais il n'existe aucune cible qui déploie le site EN ENTIER. Un `ansible-playbook` direct l'oublie, et l'échec parle d'une assertion, jamais d'un fichier manquant. **Deux passages, et le second ne sert qu'à la forge.** Le premier va jusqu'à la couche 45 sur 46 ; seul `serveur_ops` manque, faute de génome dans une forge neuve. > **Il en fallait TROIS avant le 2026-09-12.** `client_pki` posait les droits de la clé > d'hôte pour le groupe `git`, que le paquet de Forgejo crée dans une couche postérieure : > au premier passage la clé restait fermée, Forgejo ne démarrait pas — après **300 secondes > d'attente perdue** — et il fallait un passage entier pour qu'il démarre, un autre pour la > forge. Aucun ordre de couches ne dénoue ce cycle ; c'est la **propriété du geste** qui a > changé de main : `serveur_forgejo` revendique désormais la clé au moment où il crée le > groupe. `client_pki` garde la sienne et la repose à chaque passage, parce que `step` > réécrit la clé à chaque renouvellement. ### Les quinze murs, et ce que chacun enseigne | # | Le mur | Ce qu'il enseigne | |---|---|---| | 1 | Aucun moyen de raser le site | `make site-raser` — la limite était un trou d'outillage, pas une fatalité | | 2 | La forge naît vide, le runner y clone | `make forge-amorcer` — un amorçage vient de l'extérieur de ce qu'il amorce | | 3 | `dns_amorcage` pointe sur le DNS du site | le mécanisme existait, la **surcharge** n'avait jamais été posée | | 4 | La garde du résolveur teste l'adresse écrite | **une adresse écrite ne prouve pas qu'elle répond** | | 5 | Aucune cible « déployer tout le site » | la voûte n'était jointe nulle part à la séquence complète | | 6 | `client_pki` bloque sur un groupe absent | blocage **circulaire** : l'échec empêchait d'atteindre ce qui créait le groupe | | 7 | PostgreSQL du site sans TLS de l'AC | un écart de **sécurité**, révélé par le premier client exigeant `verify-full` | | 8 | `pg_hba` n'autorisait personne | le site ne déclarait aucun réseau client | | 9 | `/etc/setops` absent sur le dépôt | un rôle supposait qu'une couche ultérieure était déjà passée | | 10 | Forgejo attend 300 s une clé illisible | une attente devrait abandonner quand la cause est déjà au journal | | 11 | La clé SSH de l'exploitant inconnue de la forge | une forge neuve ne connaît personne | | 12 | L'accès de l'exploitant était **accidentel** | il tenait au chevauchement d'adressage que le renumérotage a supprimé | | 13 | Le devis reconnaît l'administration à son port | `"22" in ports` plutôt que `"admin" in pairs` | | 14 | Un flux à deux paires n'obtient qu'une branche | la chaîne de `elif` rangeait `[flotte, admin]` dans un seul cas | | 15 | Dépôts créés privés, runner anonyme | `could not read Username` — un message qui pointe ailleurs que sa cause | | 16 | Le wiki n'existe pas sur une forge neuve | Forgejo ne crée `.wiki.git` qu'à la **première page**, posée à la main dans l'interface | | 17 | `site-creer` rend la main avant que les machines répondent | mesuré à **3 min 20** — un enchaînement automatique échouerait sur `UNREACHABLE` | | 18 | Les clés d'hôte changent à chaque reconstruction | `accept-new` couvre la première rencontre, **jamais un changement** : `forge-amorcer` purge l'entrée périmée de l'hôte que `git` va contacter | ### Le motif Douze des dix-huit sont **du code juste en régime établi**, faux le premier jour : un résolveur qui se pointe sur lui-même, une clé dont le consommateur n'existe pas encore, un répertoire créé par une couche ultérieure, une forge vide qu'on croit remplie. Trois sont des **gardes qui vérifiaient la forme au lieu du résultat**. C'est la famille la plus coûteuse : elles donnent l'apparence d'une vérification. Un seul touchait la sécurité — et il était invisible tant qu'aucun client n'exigeait la vérification. **Le défaut n'a pas cassé la construction : la construction a révélé le défaut.** > **Pour le prochain site.** Poser `dns_amorcage` dès le départ, prévoir deux passages, > amorcer la forge entre les deux, déclarer `nftables_admin_ssh` — sans quoi l'exploitant ne > peut pas atteindre ce qu'il vient de construire — et créer la première page du wiki dans > l'interface avant `make wiki-publier`.