le site reconstruit depuis zero : 7/7, 0 echec, et seize corrections
SITE-Chezlepro n avait jamais ete rase. La limite qu on repetait partout — l infrastructure d accueil n a jamais ete reconstruite depuis zero — se lisait comme de la prudence. C etait seize defauts que rien d autre n aurait pu reveler. Un locataire naît dans un monde deja peuple : le site lui fournit paquets, noms, genome, heure et depot. Un site n a personne au-dessus, sauf sa frontiere. Onze des seize murs viennent de la. DEUX CAPACITES QUI N EXISTAIENT PAS. make site-raser — rien ne detruisait les machines du site, donc la limite etait un trou d outillage. make forge-amorcer — la forge naît vide et le runner y clone ; l amorcage part du poste, seul endroit qui detienne alors le genome. TROIS GARDES QUI VERIFIAIENT LA FORME. La plus couteuse des familles : elles donnent l apparence d une verification. Le resolveur comparait des adresses au lieu de mesurer si la resolution aboutit, et protegeait ainsi l etat casse. L administration etait reconnue a son port. Un flux a deux paires n obtenait qu une branche. UN ECART DE SECURITE. Le PostgreSQL du site servait le certificat auto-signe de Debian, sans reseaux autorises ni hostssl — invisible tant qu aucun client n exigeait la verification. Le defaut n a pas casse la construction : la construction a revele le defaut. UN ACCES ACCIDENTEL. Celui de l exploitant tenait au chevauchement d adressage que le renumerotage a supprime. Separer les index n a pas cause le probleme, il a retire le hasard qui le masquait. La sequence du premier jour est ecrite : runbooks §9, avec les seize murs et ce que chacun enseigne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
0c55aafc40
commit
bef0655112
13 changed files with 597 additions and 13 deletions
85
CHANGELOG.md
85
CHANGELOG.md
|
|
@ -1,5 +1,90 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-12 (2) — Le site est reconstruit depuis zero, et il a fallu seize corrections
|
||||
|
||||
`SITE-Chezlepro` n'avait jamais ete rase. Il avait ete monte par ajouts successifs, sur des
|
||||
semaines, avec un service deja debout a chaque etape. La limite qu'on repetait partout —
|
||||
« l'infrastructure d'accueil n'a jamais ete reconstruite depuis zero » — se lisait comme de
|
||||
la prudence.
|
||||
|
||||
7 machines 0 echec 0 injoignable 46 couches
|
||||
16:31 -> 19:00 creation 12 min 08 s, puis HUIT passages de deploiement
|
||||
|
||||
### Ce qui rend un site different d'un locataire
|
||||
|
||||
Un locataire naît dans un monde deja peuple : le site lui fournit les paquets, les noms, le
|
||||
genome, l'heure et le depot de sauvegarde. **Un site n'a personne au-dessus de lui**, sauf
|
||||
sa frontiere. Tout ce qu'un locataire recoit, un site doit se le donner — et pendant qu'il
|
||||
se le donne, il ne l'a pas.
|
||||
|
||||
Onze des quinze murs viennent de la.
|
||||
|
||||
### Deux capacites qui n'existaient pas
|
||||
|
||||
**`make site-raser`.** `raser.py` ne visait que l'instance active — un locataire. Rien ne
|
||||
detruisait les machines du site. Ce n'est donc pas que personne n'avait essaye de le
|
||||
reconstruire : **l'outil n'en offrait pas le moyen**. La limite etait un trou d'outillage,
|
||||
pas une fatalite. Les quatre verrous de `raser.py` sont repris tels quels.
|
||||
|
||||
**`make forge-amorcer`.** Le runner clone le genome depuis SA PROPRE forge, et
|
||||
`serveur_forgejo` ne cree ni organisation ni depot. A froid la forge naît vide. Le geste
|
||||
part du POSTE, et c'est structurel : a cet instant il est le seul endroit qui detienne le
|
||||
genome. Un amorcage vient toujours de l'exterieur de ce qu'il amorce.
|
||||
|
||||
### Trois gardes qui verifiaient la forme au lieu du resultat
|
||||
|
||||
C'est la famille la plus couteuse : elles donnent l'apparence d'une verification.
|
||||
|
||||
**Le resolveur.** « Le `resolv.conf` nomme-t-il deja le resolveur declare ? » — juste en
|
||||
regime etabli. A froid, les sept machines naissent avec `nameserver <site-dns-01>`, qui est
|
||||
l'une des sept et n'a aucun resolveur. La garde concluait « elle pointe deja au bon
|
||||
endroit » et **protegeait l'etat casse**. Elle mesure desormais si la resolution ABOUTIT.
|
||||
|
||||
**L'administration reconnue a son port.** `elif "22" in _ports(fl)` — juste tant que le seul
|
||||
flux administratif etait SSH. L'exploitant declare `admin` sur le 443 de sa forge, la machine
|
||||
accepte, la frontiere refuse, et les deux couches se croient d'accord.
|
||||
|
||||
**Un flux a deux paires n'obtient qu'une branche.** La forge declare `pair: [flotte, admin]`;
|
||||
la chaine de `elif` rangeait le flux dans le premier cas et la part administrative
|
||||
disparaissait en silence. La portee administrative s'AJOUTE desormais.
|
||||
|
||||
### Un ecart de securite, que seule la construction pouvait montrer
|
||||
|
||||
Le PostgreSQL du SITE servait le certificat AUTO-SIGNE du paquet Debian, pas celui de son
|
||||
AC — `serveur_postgresql_tls_actif` vaut `false` par defaut et le site ne le declarait
|
||||
nulle part, quand le locataire le met a `true`. Ni `reseaux_autorises`, ni `tls_force`.
|
||||
|
||||
Personne ne l'avait vu parce qu'aucun client n'exigeait la verification. Le premier a
|
||||
l'exiger — l'import du schema Icinga DB — a echoue sur `certificate verify failed`.
|
||||
**Le defaut n'a pas casse la construction : la construction a revele le defaut.**
|
||||
|
||||
### L'acces de l'exploitant etait accidentel
|
||||
|
||||
L'ancien plan d'administration `10.17.0.0/24` vivait DANS `10.17.0.0/16`, que les
|
||||
expositions du site acceptent deja pour les locataires. Sortir le site de ce supernet a
|
||||
revele qu'**aucune declaration n'avait jamais accorde cet acces**.
|
||||
|
||||
C'est l'explication complete d'une mesure du matin meme — « le site a une console deployee
|
||||
et sans chemin d'acces ». Ce n'etait pas Grafana : c'etait tout le site. La decision de
|
||||
separer les index n'a pas cause le probleme, elle a **retire le hasard qui le masquait**.
|
||||
|
||||
### Deux passages, au minimum
|
||||
|
||||
`client_pki` pose les droits d'une cle pour un consommateur qu'une couche ULTERIEURE cree.
|
||||
Au premier passage le groupe `git` n'existe pas, la cle reste `root:root` — donc FERMEE,
|
||||
jamais plus ouverte — et Forgejo ne demarre qu'au second.
|
||||
|
||||
C'etait d'abord un blocage CIRCULAIRE : l'echec arretait le play avant la couche qui aurait
|
||||
cree le groupe. Rejouer n'y changeait rien. La tache constate desormais l'absence et
|
||||
reporte, au lieu d'echouer.
|
||||
|
||||
### La sequence est ecrite
|
||||
|
||||
`docs/runbooks-exploitation.md` §9 — « Le premier jour d'un site », avec les seize murs et
|
||||
ce que chacun enseigne. Le seizieme s'est montre en ECRIVANT cette entree : `make
|
||||
wiki-publier` refuse, parce que Forgejo ne cree `<depot>.wiki.git` qu'a la premiere page.
|
||||
Sans cette section, le prochain site les rencontrerait tous.
|
||||
|
||||
## 2026-09-12 (1) — Un site prend son propre index, et la garde les compte enfin
|
||||
|
||||
Decision de l'exploitant : **sites et locataires se partagent la classe A**, chacun avec
|
||||
|
|
|
|||
16
Makefile
16
Makefile
|
|
@ -1109,6 +1109,22 @@ genome-pousser: ## Le runner du site pousse le genome sur la forge du site — D
|
|||
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
|
||||
|
||||
.PHONY: site-creer
|
||||
forge-amorcer: ## Amorce la forge d'un SITE neuf avec le genome — CONFIRMER=true
|
||||
@# LE MAILLON QUI N'EXISTAIT PAS (2026-09-12, premiere reconstruction du site depuis
|
||||
@# zero). Le runner clone le genome depuis SA PROPRE forge, et `serveur_forgejo` ne
|
||||
@# cree ni organisation ni depot : a froid la forge naît vide et le clone echoue.
|
||||
@#
|
||||
@# Le deploiement n'avait jamais rencontre ce cas — la forge existait depuis le premier
|
||||
@# jour, remplie a la main, a une date que personne n'a notee.
|
||||
@#
|
||||
@# DEPUIS LE POSTE, et c'est structurel : a cet instant il est le SEUL endroit qui
|
||||
@# detienne le genome. Un amorcage vient toujours de l'exterieur de ce qu'il amorce.
|
||||
@#
|
||||
@# export SETOPS_FORGE_MDP=… puis make forge-amorcer CONFIRMER=true
|
||||
python3 scripts/forge_amorcer.py $(if $(filter true,$(CONFIRMER)),--confirmer) \
|
||||
$(if $(FORGE),--forge "$(FORGE)")
|
||||
|
||||
.PHONY: forge-amorcer
|
||||
site-raser: ansible-runtime ## DESTRUCTIF : detruit les VM du SITE — CONFIRMER=true ET SITE=<depot>
|
||||
@# CETTE CIBLE MANQUAIT, ET SON ABSENCE AVAIT UN NOM (2026-09-12). `raser` ne vise que
|
||||
@# l'instance ACTIVE — un locataire. Rien ne detruisait les machines du site : ce n'est
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
|
||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 107 flux, schéma + matrice OK. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 109 flux, schéma + matrice OK. |
|
||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 61 scripts expliques et atteignables, 118 cibles make documentees, 68 roles avec README. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 62 scripts expliques et atteignables, 119 cibles make documentees, 68 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (138 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (38 genere(s) exempte(s)). |
|
||||
|
|
@ -53,16 +53,16 @@
|
|||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 41 role(s) serveur/client tous nommes, 41 groupe(s) cite(s) en table existent tous. |
|
||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. |
|
||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 57 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 58 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 151 regle(s) du site. |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 153 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (125 lignes). |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 219 regles `pass`), tous non consignes et tous motives. |
|
||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (127 lignes). |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 239 regles `pass`), tous non consignes et tous motives. |
|
||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
| `serveur_debian` | ingress | 22 | tcp | flotte, externe | ssh | Plan de gestion : administration et déploiement Ansible par SSH (inter-nœud ; l'accès depuis l'extérieur est filtré à l'OPNsense). |
|
||||
| `serveur_debian` | ingress | echo-request | icmp | serveur_icinga | n-a | La supervision verifie que ce noeud repond (hostalive). Sans lui, elle le tient pour mort et supprime ses notifications. |
|
||||
| `serveur_debian` | ingress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » entrant : sans lui, un distant ne peut pas nous demander de réduire nos paquets — les transferts se figent. |
|
||||
| `serveur_debian` | egress | 53 | udp | frontiere | n-a | Résolution de noms à l'amorçage, avant que le résolveur de l'écosystème n'existe. Sans elle, les machines d'un site neuf ne peuvent pas résoudre leurs dépôts de paquets — et rien ne peut donc s'installer, y compris le résolveur lui-même. |
|
||||
| `serveur_debian` | egress | 53 | tcp | frontiere | n-a | Réponses longues et bascule TCP, obligatoires en DNS. Déclarer l'UDP sans le TCP donne une résolution qui marche jusqu'à la première réponse tronquée. |
|
||||
| `serveur_debian` | egress | 80 | tcp | externe | clair | Dépôts apt en clair et redirections HTTP des miroirs (l'intégrité vient de la signature des paquets, pas du transport). |
|
||||
| `serveur_debian` | egress | 123 | udp | frontiere | n-a | Synchronisation d'horloge (NTP) contre la frontière, autorité de temps de l'écosystème. Une dérive fait échouer la validation des certificats step-ca et l'authentification SSO. |
|
||||
| `serveur_debian` | egress | 443 | tcp | externe | tls-requis | Dépôts apt en HTTPS (Debian, Smallstep, Grafana, Icinga, Forgejo, Nextcloud) — sans quoi aucun correctif de sécurité n'entre. |
|
||||
|
|
@ -39,7 +41,7 @@
|
|||
| `serveur_dovecot` | egress | 636 | tcp | serveur_openldap | tls-requis | userdb/passdb : Dovecot résout et authentifie les comptes sur l'annuaire (LDAPS). |
|
||||
| `serveur_forge_site` | ingress | 443 | tcp | voisins_site | tls-requis | Servir le génome aux écosystèmes de ce site : c'est de cette forge qu'ils clonent leur moteur, leurs modèles et la carte de la fabric (D-81). Sans ce flux, un écosystème neuf ne peut pas se reproduire. |
|
||||
| `serveur_forgejo` | ingress | 3000 | tcp | edge | clair | Interface web + Git HTTP derrière un edge : le nginx termine le TLS et parle en clair à la forge. C'est le cas de tout tenant. |
|
||||
| `serveur_forgejo` | ingress | derive | tcp | flotte | tls | Sans edge devant elle — la forge du SITE — elle sert son propre TLS sur le port du schéma (443), avec le certificat de la machine. `derive` parce que le port vient de `serveur_forgejo_http_port` : écrire 3000 en dur ici serait faux pour elle, et rien ne le signalerait puisque ce flux ne traverse pas la frontière. |
|
||||
| `serveur_forgejo` | ingress | derive | tcp | flotte, admin | tls | Sans edge devant elle — la forge du SITE — elle sert son propre TLS sur le port du schéma (443), avec le certificat de la machine. `derive` parce que le port vient de `serveur_forgejo_http_port` : écrire 3000 en dur ici serait faux pour elle, et rien ne le signalerait puisque ce flux ne traverse pas la frontière. |
|
||||
| `serveur_forgejo` | egress | 25 | tcp | serveur_postfix | starttls | Notifications courriel (relais via le MTA Postfix). |
|
||||
| `serveur_forgejo` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC et jetons auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||
| `serveur_forgejo` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Forgejo (verify-full). |
|
||||
|
|
@ -117,7 +119,7 @@
|
|||
## Synthèse chiffrement
|
||||
|
||||
- **clair** : 39 flux
|
||||
- **n-a** : 6 flux
|
||||
- **n-a** : 8 flux
|
||||
- **ssh** : 8 flux
|
||||
- **starttls** : 6 flux
|
||||
- **tls** : 11 flux
|
||||
|
|
|
|||
|
|
@ -326,3 +326,93 @@ 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 seize 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 **seize défauts** que rien
|
||||
> d'autre n'aurait pu révéler.
|
||||
|
||||
### 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=<support hors site>
|
||||
|
||||
# 1. Les VM, depuis l'underlay
|
||||
make site-creer CONFIRMER=true # ~12 min pour 7 machines
|
||||
|
||||
# 2. Le résolveur d'amorçage, DÉCLARÉ dans le plan du site
|
||||
# plan/10-intrants.yml : dns_amorcage: <patte de la frontière>
|
||||
# Sans lui, les machines pointent sur le DNS du site — qui est l'une d'elles.
|
||||
|
||||
# 3. Le déploiement, DEUX FOIS au minimum
|
||||
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
|
||||
-e "@$(dirname "$(readlink -f underlay.yml)")/underlay.vault.yml"
|
||||
|
||||
# 4. Entre les deux passages : amorcer la forge
|
||||
export SETOPS_FORGE_MDP=… # jamais en argument de ligne de commande
|
||||
make forge-amorcer CONFIRMER=true
|
||||
|
||||
# 5. Rejouer jusqu'à 0 échec
|
||||
```
|
||||
|
||||
**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, au minimum, et ce n'est pas un contournement.** `client_pki` pose les
|
||||
droits d'une clé pour un consommateur qu'une couche ultérieure crée : au premier passage le
|
||||
groupe `git` n'existe pas, la clé reste fermée, et Forgejo ne démarre qu'au second.
|
||||
|
||||
### 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 `<dépôt>.wiki.git` qu'à la **première page**, posée à la main dans l'interface |
|
||||
|
||||
### Le motif
|
||||
|
||||
Onze des seize 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`.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,29 @@
|
|||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
# UNE ADRESSE ECRITE NE PROUVE PAS QU'ELLE REPOND (2026-09-12).
|
||||
#
|
||||
# La garde ci-dessous se contentait de comparer des ADRESSES : « le resolv.conf
|
||||
# nomme-t-il deja le resolveur declare ? ». En regime etabli c'est juste — on ne
|
||||
# clobbere pas un resolveur qui marche. A froid c'est exactement a l'envers.
|
||||
#
|
||||
# Mesure a la premiere reconstruction du site depuis zero : les sept machines
|
||||
# naissent avec `nameserver 10.37.34.11`, l'adresse de `site-dns-01` — qui est
|
||||
# l'une des sept et n'a encore aucun resolveur. La garde concluait « elle pointe
|
||||
# deja au bon endroit », n'ecrivait rien, et `apt update` echouait sur les sept.
|
||||
# La garde protegeait l'etat casse.
|
||||
#
|
||||
# ON MESURE DONC LE RESULTAT, pas la forme : le resolveur en place resout-il ?
|
||||
# C'est la meme lecon que `client_resolveur` enonce a cote — « un resolv.conf qui
|
||||
# pointe vers un service muet » — appliquee un cran plus tot.
|
||||
- name: Le résolveur en place répond-il ?
|
||||
ansible.builtin.command:
|
||||
argv: ["getent", "ahostsv4", "deb.debian.org"]
|
||||
register: serveur_debian_resolution
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
timeout: 15
|
||||
|
||||
- name: Poser le résolveur d'amorçage (avant tout apt)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/resolv.conf
|
||||
|
|
@ -144,8 +167,8 @@
|
|||
- dns_amorcage is defined
|
||||
- dns_amorcage | string | length > 0
|
||||
- "'127.0.0.1' not in serveur_debian_resolv_texte"
|
||||
- serveur_debian_resolveurs_declares
|
||||
| select('in', serveur_debian_resolv_texte) | list | length == 0
|
||||
# Le resolveur en place ne resout pas : peu importe l'adresse qu'il porte.
|
||||
- serveur_debian_resolution.rc | default(1) != 0
|
||||
|
||||
roles:
|
||||
# LE PLANCHER AVANT LE PREMIER `apt` (2026-08-25).
|
||||
|
|
|
|||
|
|
@ -313,15 +313,55 @@
|
|||
# automatique aussi. Ne régler les droits qu'au moment où le certificat change les
|
||||
# perdrait au premier renouvellement — une panne qui surviendrait des semaines plus tard,
|
||||
# sans rapport visible avec cette tâche.
|
||||
# LE CONSOMMATEUR N'EXISTE PAS ENCORE AU PREMIER JOUR (2026-09-12).
|
||||
#
|
||||
# `client_pki` (couche `pki_client`) precede `serveur_forgejo` (couche `apps`), et c'est
|
||||
# le paquet de Forgejo qui cree le groupe `git`. Sur une machine deja construite le
|
||||
# groupe est la, herite d'un passage precedent ; a froid il n'existe pas :
|
||||
#
|
||||
# chgrp failed: failed to look up group git
|
||||
#
|
||||
# ET C'ETAIT UN BLOCAGE CIRCULAIRE, pas un ordre qui converge : l'echec arretait le play
|
||||
# pour cet hote AVANT la couche qui aurait cree le groupe. Rejouer n'y changeait rien —
|
||||
# mesure deux fois, le 2026-09-12.
|
||||
#
|
||||
# ON NE FABRIQUE PAS LE GROUPE ICI : le paquet du service le creera avec le GID qu'il
|
||||
# veut, et un groupe pose d'avance l'obligerait a composer avec le notre. On constate
|
||||
# son absence, on laisse la cle en `root:root` — donc FERMEE, jamais plus ouverte — et
|
||||
# le passage suivant, une fois le service installe, donne l'acces.
|
||||
- name: Le groupe qui doit lire la clé existe-t-il ?
|
||||
ansible.builtin.getent:
|
||||
database: group
|
||||
key: "{{ client_pki_cle_groupe }}"
|
||||
fail_key: false
|
||||
register: client_pki_groupe_lu
|
||||
when: client_pki_cle_groupe != 'root'
|
||||
changed_when: false
|
||||
|
||||
- name: Donner accès à la clé privée au service qui doit la lire
|
||||
ansible.builtin.file:
|
||||
path: "{{ client_pki_cle }}"
|
||||
owner: root
|
||||
group: "{{ client_pki_cle_groupe }}"
|
||||
mode: "{{ client_pki_cle_mode }}"
|
||||
when: client_pki_cle_groupe != 'root' or client_pki_cle_mode != '0600'
|
||||
when:
|
||||
- client_pki_cle_groupe != 'root' or client_pki_cle_mode != '0600'
|
||||
- client_pki_cle_groupe == 'root'
|
||||
or (client_pki_groupe_lu.ansible_facts.getent_group | default({}))
|
||||
.get(client_pki_cle_groupe) is not none
|
||||
notify: Recharger les consommateurs du cert
|
||||
|
||||
- name: Dire que l'accès à la clé attend son consommateur
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Le groupe « {{ client_pki_cle_groupe }} » n'existe pas encore : la clé reste en
|
||||
root:root, donc FERMEE. Le service qui doit la lire n'est pas installé — le
|
||||
passage suivant, après sa couche, lui donnera l'accès.
|
||||
when:
|
||||
- client_pki_cle_groupe != 'root'
|
||||
- (client_pki_groupe_lu.ansible_facts.getent_group | default({}))
|
||||
.get(client_pki_cle_groupe) is none
|
||||
|
||||
# Le certificat est public. `step` l'ecrit en 0600 comme la cle ; on le rend lisible, sans
|
||||
# quoi un service non-root echoue sur le CERT apres avoir obtenu la CLE — et le message
|
||||
# parle de permission sur un fichier que rien ne justifie de proteger.
|
||||
|
|
|
|||
|
|
@ -55,6 +55,42 @@ flux:
|
|||
partage: true
|
||||
raison: "La supervision verifie que ce noeud repond (hostalive). Sans lui, elle le tient pour mort et supprime ses notifications."
|
||||
|
||||
# LA FRONTIÈRE EST AUSSI L'AUTORITÉ DE NOMS D'AMORÇAGE (2026-09-12).
|
||||
#
|
||||
# TROUVÉ À LA PREMIÈRE RECONSTRUCTION DU SITE DEPUIS ZÉRO. Les sept machines du site
|
||||
# naissent avec `nameserver 10.37.34.11` — `site-dns-01`, qui est elle-même une des
|
||||
# sept et n'a pas encore de résolveur. `site-dns-01` pointe donc sur elle-même, et
|
||||
# `apt update` échoue sur les sept :
|
||||
#
|
||||
# Erreur temporaire de résolution de « deb.debian.org »
|
||||
#
|
||||
# Le locataire n'a jamais ce problème : son résolveur est le SITE, qui existe déjà.
|
||||
# Le site, lui, n'a personne au-dessus — sauf la frontière, dont l'Unbound tourne et
|
||||
# écoute sur `*:53`. Il ne manquait que le passage.
|
||||
#
|
||||
# CE QUE CETTE RÈGLE NE DIT PAS. « Toute la flotte résout ici, et nulle part ailleurs »
|
||||
# (`serveur_resolveur/meta/flux.yml`) reste la règle en régime établi : le résolveur
|
||||
# de l'écosystème est le seul que les machines INTERROGENT, parce que c'est lui que
|
||||
# leur `/etc/resolv.conf` nomme. Cette règle n'ouvre qu'une possibilité, celle qui
|
||||
# rend l'amorçage possible — et sans elle, un écosystème neuf ne peut pas naître.
|
||||
- sens: egress
|
||||
port: 53
|
||||
protocole: udp
|
||||
pair: frontiere
|
||||
chiffrement: n-a
|
||||
raison: >-
|
||||
Résolution de noms à l'amorçage, avant que le résolveur de l'écosystème n'existe.
|
||||
Sans elle, les machines d'un site neuf ne peuvent pas résoudre leurs dépôts de
|
||||
paquets — et rien ne peut donc s'installer, y compris le résolveur lui-même.
|
||||
- sens: egress
|
||||
port: 53
|
||||
protocole: tcp
|
||||
pair: frontiere
|
||||
chiffrement: n-a
|
||||
raison: >-
|
||||
Réponses longues et bascule TCP, obligatoires en DNS. Déclarer l'UDP sans le TCP
|
||||
donne une résolution qui marche jusqu'à la première réponse tronquée.
|
||||
|
||||
# L'HEURE VIENT DE LA FRONTIÈRE, PAS D'INTERNET (2026-09-11).
|
||||
#
|
||||
# C'était `pair: externe` : les quatorze machines interrogeaient `2.debian.pool.ntp.org`,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,16 @@ flux:
|
|||
- sens: ingress
|
||||
port: derive
|
||||
protocole: tcp
|
||||
pair: flotte
|
||||
# `admin` EN PLUS DE `flotte` (2026-09-12). Sans edge devant elle, la forge du SITE
|
||||
# n'etait joignable que par les machines de l'ecosysteme — jamais par l'exploitant.
|
||||
# Ca passait inapercu tant que son plan d'administration vivait dans le supernet d'un
|
||||
# locataire, que les expositions acceptent deja. Une fois le site sorti de ce
|
||||
# supernet, l'exploitant ne pouvait plus ni amorcer sa forge ni y pousser le genome.
|
||||
#
|
||||
# `admin` se resout depuis `nftables_admin_ssh` : un ecosysteme qui ne declare aucun
|
||||
# administrateur n'emet aucune regle, et c'est le comportement voulu — une source
|
||||
# vide ouvrirait le port a tous.
|
||||
pair: [flotte, admin]
|
||||
chiffrement: tls
|
||||
raison: >-
|
||||
Sans edge devant elle — la forge du SITE — elle sert son propre TLS sur le port du
|
||||
|
|
|
|||
|
|
@ -378,6 +378,25 @@
|
|||
src: "{{ serveur_icinga_ca }}"
|
||||
register: serveur_icinga_ca_contenu
|
||||
|
||||
# LE REPERTOIRE N'EXISTE PAS AU PREMIER JOUR (2026-09-12). `/etc/setops` est cree par
|
||||
# `client_sante`, qui vit dans une couche POSTERIEURE. Sur une machine deja construite il
|
||||
# est la ; a froid, non — et `copy` ne cree pas ses parents :
|
||||
#
|
||||
# Destination directory /etc/setops does not exist
|
||||
#
|
||||
# On le pose donc ici, avec les memes droits que `client_sante` lui donnerait. Deux roles
|
||||
# qui creent le meme repertoire ne se genent pas ; un role qui suppose qu'un autre est
|
||||
# deja passe, si.
|
||||
- name: Assurer le repertoire d'accueil sur le depot de sauvegarde
|
||||
when: groups['serveur_backup'] | default([]) | length > 0
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_icinga_ca_destination_depot | dirname }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
delegate_to: "{{ groups['serveur_backup'] | first }}"
|
||||
|
||||
- name: Deposer l'AC sur le depot de sauvegarde
|
||||
when: groups['serveur_backup'] | default([]) | length > 0
|
||||
ansible.builtin.copy:
|
||||
|
|
|
|||
|
|
@ -796,6 +796,17 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# jamais la zone entiere.
|
||||
_portees = [("SETOPS_SITE", z) for z in
|
||||
sorted({_if_de(m.get("reseau")) for m in _machines_site})]
|
||||
# UN FLUX A DEUX PAIRES N'OBTIENT QU'UNE BRANCHE (2026-09-12).
|
||||
#
|
||||
# La chaine de `elif` range chaque flux dans UN cas. La forge du site
|
||||
# declare `pair: [flotte, admin]` : `flotte` capte la premiere, et la
|
||||
# part administrative disparait sans que rien ne le dise. Mesure a la
|
||||
# premiere reconstruction — la machine acceptait l'exploitant, la
|
||||
# frontiere le refusait, et les deux couches se croyaient d'accord.
|
||||
#
|
||||
# On AJOUTE la portee administrative au lieu d'en faire un cas exclusif.
|
||||
if "admin" in _pairs_fl and _admin_site:
|
||||
_portees += [("SETOPS_ADMIN_SITE", if_gestion)]
|
||||
elif _entrant and "fabric" in _pairs_fl:
|
||||
# LA FABRIC PARLE AUSSI AU SITE (2026-09-10).
|
||||
#
|
||||
|
|
@ -828,7 +839,18 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
_portees = [("SETOPS_FABRIC", _if_de(r)) for r in _res_fabric]
|
||||
if not _portees:
|
||||
continue
|
||||
elif _entrant and "22" in _ports(_fl) and _admin_site:
|
||||
elif _entrant and ("admin" in _pairs_fl or "22" in _ports(_fl)) and _admin_site:
|
||||
# `admin` N'EST PAS QUE LE SSH (2026-09-12). Cette branche testait le
|
||||
# PORT — « si c'est du 22, ça vient de l'administration ». Juste tant
|
||||
# que le seul flux administratif etait SSH.
|
||||
#
|
||||
# Mesure a la premiere reconstruction du site : l'exploitant declare
|
||||
# `admin` sur le 443 de sa forge pour pouvoir l'amorcer, la machine
|
||||
# accepte, et la frontiere continue de refuser — parce que le devis ne
|
||||
# reconnaissait l'administration qu'a son port. Deux couches, une seule
|
||||
# au courant : c'est la panne la plus silencieuse de cette pile.
|
||||
#
|
||||
# On teste desormais la PAIRE DECLAREE, et le port reste ce qu'il est.
|
||||
# Le SSH de gestion arrive par la patte d'ADMINISTRATION de la
|
||||
# frontiere, pas par celle du site : c'est la que le paquet penetre
|
||||
# le pare-feu. Une regle posee ailleurs ne correspondrait jamais.
|
||||
|
|
|
|||
212
scripts/forge_amorcer.py
Normal file
212
scripts/forge_amorcer.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Amorce la forge d'un SITE neuf avec le genome. Le maillon qui n'existait pas.
|
||||
|
||||
POURQUOI CE SCRIPT EXISTE (2026-09-12, premiere reconstruction du site depuis zero).
|
||||
|
||||
Le runner du site clone le genome depuis SA PROPRE forge — `serveur_ops_forge_hote`
|
||||
derive de `groups['serveur_forgejo']`. L'ordre des couches est juste : `serveur_forgejo`
|
||||
precede `serveur_ops`. Mais `roles/serveur_forgejo` ne cree NI organisation NI depot : a
|
||||
froid, la forge naît vide, et le `git clone` du runner echoue apres trois essais.
|
||||
|
||||
LE DEPLOIEMENT N'AVAIT JAMAIS RENCONTRE CE CAS parce que la forge existait depuis le
|
||||
premier jour, remplie a la main, a une date que personne n'a notee. C'est exactement ce
|
||||
qu'une limite « jamais reconstruit depuis zero » cache : pas une fragilite, un MAILLON
|
||||
QUI N'AVAIT JAMAIS ETE FABRIQUE PAR LE CODE.
|
||||
|
||||
POURQUOI DEPUIS LE POSTE, ET PAS DEPUIS UN ROLE. A cet instant precis, le poste est le
|
||||
SEUL endroit qui detienne le genome : la forge est vide et le runner n'existe pas encore.
|
||||
Un role qui tenterait de se servir lui-meme tournerait en rond. C'est un geste d'amorcage,
|
||||
et un amorcage vient toujours de l'exterieur de ce qu'il amorce.
|
||||
|
||||
LA LISTE VIENT DE LA MEME DECLARATION QUE LE RUNNER LIT — `serveur_ops_depots` dans le
|
||||
plan du site. Ecrire une seconde liste ici la ferait diverger de celle qui compte, et ce
|
||||
depot passe son temps a supprimer ce genre de paire.
|
||||
|
||||
Usage :
|
||||
python3 scripts/forge_amorcer.py # montre ce qui serait pousse
|
||||
python3 scripts/forge_amorcer.py --confirmer
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
FRERES = RACINE.parent
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import underlay as underlay_mod # noqa: E402
|
||||
|
||||
|
||||
def _depots_declares() -> list[dict]:
|
||||
"""`serveur_ops_depots` du plan du SITE — la source que le runner lit."""
|
||||
import yaml
|
||||
plan = underlay_mod.lire_plan_site("serveurs.yml") or {}
|
||||
for srv in (plan.get("serveurs") or {}).values():
|
||||
d = ((srv.get("variables") or {}).get("serveur_ops_depots"))
|
||||
if d:
|
||||
return list(d)
|
||||
return []
|
||||
|
||||
|
||||
def _api(base: str, chemin: str, methode: str = "GET", corps: dict | None = None,
|
||||
auth: str = "") -> tuple[int, object]:
|
||||
req = urllib.request.Request(f"{base}/api/v1{chemin}", method=methode)
|
||||
req.add_header("Authorization", f"Basic {auth}")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
donnees = json.dumps(corps).encode() if corps is not None else None
|
||||
import ssl
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE # la racine step-ca n'est pas encore au magasin du poste
|
||||
try:
|
||||
with urllib.request.urlopen(req, donnees, timeout=25, context=ctx) as r:
|
||||
return r.status, json.loads(r.read() or b"null")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, (e.read() or b"").decode()[:200]
|
||||
except OSError as e:
|
||||
return 0, str(e)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--forge", help="URL de la forge (defaut : derivee du plan du site)")
|
||||
ap.add_argument("--organisation", default="genome")
|
||||
ap.add_argument("--utilisateur", default="forgejo-admin")
|
||||
ap.add_argument("--cle-publique",
|
||||
default=str(Path.home() / ".ssh" / "id_ed25519_ansible_chezlepro.pub"),
|
||||
help="cle publique a enregistrer dans la forge neuve (pour le push SSH)")
|
||||
ap.add_argument("--confirmer", action="store_true")
|
||||
a = ap.parse_args()
|
||||
|
||||
depots = _depots_declares()
|
||||
if not depots:
|
||||
print("Aucun `serveur_ops_depots` au plan du site : rien a amorcer.")
|
||||
return 2
|
||||
|
||||
# L'hote de la forge vient du plan, comme pour le runner.
|
||||
import yaml
|
||||
apps = (underlay_mod.lire_plan_site("applications.yml") or {}).get("applications") or {}
|
||||
hote = next((str(x.get("hote")) for x in apps.values()
|
||||
if str(x.get("groupe")) == "serveur_forgejo"), "")
|
||||
fqdn = next((str(f) for x in apps.values()
|
||||
if str(x.get("groupe")) == "serveur_forgejo"
|
||||
for f in (x.get("expose") or [])), "")
|
||||
base = a.forge or (f"https://{fqdn}" if fqdn else "")
|
||||
if not base:
|
||||
print("La forge du site ne se derive pas du plan : passer --forge.")
|
||||
return 2
|
||||
|
||||
print(f"Forge : {base} (machine {hote or '?'})")
|
||||
print(f"Organisation : {a.organisation}\n")
|
||||
|
||||
manquants, prets = [], []
|
||||
for d in depots:
|
||||
local = FRERES / str(d.get("dest"))
|
||||
if not (local / ".git").is_dir():
|
||||
manquants.append(f"{d['dest']} — absent du poste ({local})")
|
||||
else:
|
||||
prets.append((str(d["depot"]), local, str(d.get("branche") or "main")))
|
||||
|
||||
for nom, local, branche in prets:
|
||||
print(f" {nom:22} <- {local.name:20} branche {branche}")
|
||||
for m in manquants:
|
||||
print(f" MANQUANT {m}")
|
||||
|
||||
if manquants:
|
||||
print(f"\nREFUS : {len(manquants)} depot(s) declare(s) n'existent pas sur ce poste.\n"
|
||||
f"Amorcer une forge avec une liste incomplete la rendrait credible et fausse :\n"
|
||||
f"le runner clonerait ce qui est la et echouerait sur le reste, plus tard.")
|
||||
return 2
|
||||
|
||||
if not a.confirmer:
|
||||
print(f"\n{len(prets)} depot(s) seraient crees puis pousses.\n"
|
||||
f"Relancer avec : make forge-amorcer CONFIRMER=true")
|
||||
return 0
|
||||
|
||||
mdp = os.environ.get("SETOPS_FORGE_MDP", "")
|
||||
if not mdp:
|
||||
print("\nREFUS : mot de passe de l'administrateur de la forge absent.\n"
|
||||
" export SETOPS_FORGE_MDP=\"$(ansible-vault view … | …)\" puis relancer.\n"
|
||||
"Il n'est pas lu du depot : un secret ne traverse pas un argument de ligne\n"
|
||||
"de commande, ou il finirait dans l'historique du shell.")
|
||||
return 2
|
||||
auth = base64.b64encode(f"{a.utilisateur}:{mdp}".encode()).decode()
|
||||
|
||||
# LA FORGE NEUVE NE CONNAIT PERSONNE (2026-09-12). Les remotes `origin` poussent en
|
||||
# `ssh://git@…` ; une Forgejo fraiche rejette la cle de l'exploitant — `Permission
|
||||
# denied (publickey)` — parce qu'aucune cle n'y est enregistree. Amorcer sans cela
|
||||
# revient a creer des depots qu'on ne peut pas remplir.
|
||||
cle = Path(a.cle_publique)
|
||||
if cle.is_file():
|
||||
st, rep = _api(base, "/user/keys", "POST",
|
||||
{"title": f"amorcage-{cle.stem}", "key": cle.read_text().strip()}, auth)
|
||||
if st in (201, 200):
|
||||
print(f" cle publique {cle.name} : enregistree")
|
||||
elif st == 422:
|
||||
print(f" cle publique {cle.name} : deja la")
|
||||
else:
|
||||
print(f" cle publique {cle.name} : ECHEC {st} {rep}")
|
||||
return 2
|
||||
else:
|
||||
print(f" cle publique absente ({cle}) — le push SSH echouera")
|
||||
return 2
|
||||
|
||||
st, _ = _api(base, f"/orgs/{a.organisation}", auth=auth)
|
||||
if st == 404:
|
||||
st, rep = _api(base, "/orgs", "POST", {"username": a.organisation}, auth)
|
||||
print(f" organisation {a.organisation} : "
|
||||
+ ("creee" if st in (201, 200) else f"ECHEC {st} {rep}"))
|
||||
elif st == 200:
|
||||
print(f" organisation {a.organisation} : deja la")
|
||||
else:
|
||||
print(f" organisation {a.organisation} : ECHEC {st}")
|
||||
return 2
|
||||
|
||||
echecs = 0
|
||||
for nom, local, branche in prets:
|
||||
st, _ = _api(base, f"/repos/{a.organisation}/{nom}", auth=auth)
|
||||
if st == 404:
|
||||
st, rep = _api(base, f"/orgs/{a.organisation}/repos", "POST",
|
||||
{"name": nom, "private": False}, auth)
|
||||
# PUBLIC DANS LE PERIMETRE DE LA FORGE, ET C'EST VOULU (2026-09-12).
|
||||
#
|
||||
# Cree en `private` au premier essai — par reflexe. Le runner clone
|
||||
# ANONYMEMENT : la tache `git` de `serveur_ops` ne porte aucun identifiant,
|
||||
# et l'echec ne dit pas « depot prive », il dit :
|
||||
#
|
||||
# could not read Username for 'https://forge.genese.internal'
|
||||
#
|
||||
# La forge n'est joignable que depuis les zones du site, les supernets
|
||||
# locataires et le plan d'administration : « public » y signifie « lisible
|
||||
# par ceux qui ont deja franchi la frontiere ». Les secrets, eux, sont dans
|
||||
# des voutes chiffrees — c'est la qu'est la protection, pas dans un drapeau
|
||||
# de visibilite qui casserait la reproduction.
|
||||
if st not in (201, 200):
|
||||
print(f" {nom:22} ECHEC creation {st} {rep}")
|
||||
echecs += 1
|
||||
continue
|
||||
# `--force` JAMAIS : si le depot porte deja quelque chose, on veut le savoir,
|
||||
# pas l'ecraser. Un amorcage n'a de sens que sur une forge vide.
|
||||
r = subprocess.run(["git", "-C", str(local), "push", "origin", branche],
|
||||
capture_output=True, text=True, timeout=180)
|
||||
if r.returncode == 0:
|
||||
print(f" {nom:22} pousse ({branche})")
|
||||
else:
|
||||
print(f" {nom:22} ECHEC push : {(r.stderr or '').strip().splitlines()[-1][:90]}")
|
||||
echecs += 1
|
||||
|
||||
print(f"\n{len(prets) - echecs}/{len(prets)} depot(s) amorces.")
|
||||
return 1 if echecs else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -479,6 +479,36 @@ def inventaire() -> dict:
|
|||
# Les deux lignes vont ensemble : eteindre l'une sans allumer l'autre
|
||||
# laisse un Grafana sans aucune porte. Le role refuse desormais cette
|
||||
# combinaison, plutot que de la deployer.
|
||||
# LE POSTGRESQL DU SITE ETAIT HORS DE LA DOCTRINE (2026-09-12).
|
||||
#
|
||||
# `serveur_postgresql_tls_actif` vaut `false` par defaut, et le tenant le
|
||||
# met a `true` dans ses group_vars. Le SITE ne le mettait nulle part : son
|
||||
# PostgreSQL servait le certificat AUTO-SIGNE du paquet Debian
|
||||
# (`ssl-cert-snakeoil.pem`), pas celui de son AC.
|
||||
#
|
||||
# PERSONNE NE L'AVAIT VU parce qu'aucun client du site n'exigeait la
|
||||
# verification. Le premier a l'exiger — l'import du schema Icinga DB, qui
|
||||
# se connecte par le FQDN — a echoue sur `certificate verify failed`, et
|
||||
# c'est ainsi que l'ecart s'est montre. Le defaut n'a pas casse la
|
||||
# construction : la construction a revele le defaut.
|
||||
#
|
||||
# Ce n'est pas un blocage d'amorcage, c'est un trou de zero-confiance
|
||||
# est-ouest : le site chiffrait sans que personne ne verifie a qui.
|
||||
# ET LES CLIENTS DOIVENT ETRE NOMMES. Le tenant declare
|
||||
# `serveur_postgresql_reseaux_autorises: [supernet]` ; le site ne declarait
|
||||
# rien, donc `pg_hba` n'autorisait que le local :
|
||||
#
|
||||
# aucune entree dans pg_hba.conf pour l hote 10.37.36.11,
|
||||
# utilisateur icingadb, chiffrement SSL
|
||||
#
|
||||
# Les zones du site, et elles seules — pas un `0.0.0.0/0` qui rendrait le
|
||||
# verrou `hostssl` decoratif.
|
||||
"serveur_postgresql_reseaux_autorises": _zones_du_site,
|
||||
# `tls_force` pose `hostssl` : toute connexion NON chiffree est refusee.
|
||||
# On ne l'active qu'apres avoir confirme que le certificat servi vient bien
|
||||
# de l'AC — c'est fait, mesure a 17:31 ce jour.
|
||||
"serveur_postgresql_tls_force": True,
|
||||
"serveur_postgresql_tls_actif": True,
|
||||
"serveur_grafana_oidc_actif": False,
|
||||
"serveur_grafana_connexion_locale": True,
|
||||
# Le depot du site, pour les machines du site qui y deposent. Vide s'il n'y
|
||||
|
|
|
|||
Loading…
Reference in a new issue