Compare commits
48 commits
v2026.10.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e7d704fdcc | |||
| d152e32328 | |||
| ee5aad5f41 | |||
| 968caed3b1 | |||
| 65a4268871 | |||
| ca731eedc4 | |||
| a9428be752 | |||
| 810670cd30 | |||
| f9e45b1676 | |||
| f3bb9caee3 | |||
| 78c3505577 | |||
| 63f5fda091 | |||
| 203f7c5436 | |||
| 73fd941780 | |||
| 92a396b3fe | |||
| 548d35b1b5 | |||
| 6df04888ab | |||
| 3fc16c845d | |||
| 82a2c4a4c2 | |||
| d7717a4f98 | |||
| 9af39b1b60 | |||
| aa5e4e0897 | |||
| 0d5ec924cc | |||
| 1d582045f3 | |||
| 9592309f36 | |||
| 493a5dcbeb | |||
| 412133bd76 | |||
| ee0e33d452 | |||
| dd4ca09245 | |||
| 162604ad24 | |||
| 34e961fb61 | |||
| 422b68846b | |||
| 55f42add15 | |||
| 4ebca13058 | |||
| 279e583701 | |||
| 3afb756993 | |||
| c7378ed7f7 | |||
| 7a4c55bfc9 | |||
| 0f7c89fbb5 | |||
| 9ca9a5ffe1 | |||
| 2dce6eabd5 | |||
| 479ec3cc7d | |||
| 9a4b63544e | |||
| 3e2846cc5f | |||
| cca22f47ca | |||
| 4660a8892a | |||
| 6141bc4b2f | |||
| e8907285d5 |
156 changed files with 11386 additions and 1292 deletions
1768
CHANGELOG.md
1768
CHANGELOG.md
File diff suppressed because it is too large
Load diff
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -4,24 +4,3 @@
|
|||
|
||||
`AGENTS.md` est la **source d'autorité unique** de ce dépôt. Claude Code doit le lire
|
||||
et le respecter intégralement avant toute modification.
|
||||
|
||||
En cas de contradiction entre `CLAUDE.md` et `AGENTS.md`, **suivre `AGENTS.md`**.
|
||||
|
||||
Ce fichier est volontairement mince : il ne redéclare pas la doctrine (template, SSH,
|
||||
pare-feu, cloud-init, handlers, Makefile, groupes…). Toute cette doctrine vit dans
|
||||
`AGENTS.md`, pour éviter la duplication et les dérives qu'elle provoque.
|
||||
|
||||
## Les cinq règles absolues
|
||||
|
||||
1. **Un seul agent IA** travaille dans ce dépôt à la fois (Codex *ou* Claude, jamais les deux).
|
||||
2. **`instance/inventories/*/hosts.yml` est un artefact GÉNÉRÉ** depuis le plan
|
||||
(`instance/plan/`). On l'édite jamais à la main : on édite le plan, puis
|
||||
`make instancier` → `make instancier-appliquer`.
|
||||
3. **Aucun secret en clair** (mots de passe, clés privées, tokens, certificats) — Vault
|
||||
ou stockage hors dépôt uniquement.
|
||||
4. **Toute action destructive ou risquée exige une confirmation explicite** (ex.
|
||||
`CONFIRMER=true`, `confirm_destructive_action: true`). Sans elle, refuser l'exécution.
|
||||
5. **Rien n'est « prêt » sans validation.** Au minimum `--syntax-check` du playbook
|
||||
touché, `ansible-lint` si disponible, et l'entrée `CHANGELOG.md` correspondante.
|
||||
|
||||
Le détail de chacune de ces règles, et tout le reste, est dans `AGENTS.md`.
|
||||
|
|
|
|||
68
Makefile
68
Makefile
|
|
@ -318,10 +318,15 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_raser.py
|
||||
python3 scripts/tests/test_raser_resultat.py
|
||||
python3 scripts/tests/test_temoins.py
|
||||
python3 scripts/tests/test_genome_colis.py
|
||||
python3 scripts/tests/test_verifier_depot.py
|
||||
python3 scripts/tests/test_apps_nextcloud.py
|
||||
python3 scripts/tests/test_verrou_runner.py
|
||||
python3 scripts/tests/test_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_runbooks.py
|
||||
python3 scripts/tests/test_portee_console.py
|
||||
python3 scripts/tests/test_forge_cles.py
|
||||
python3 scripts/tests/test_devis_placement.py
|
||||
python3 scripts/tests/test_sonder.py
|
||||
python3 scripts/tests/test_fraicheur_icinga.py
|
||||
|
|
@ -330,9 +335,21 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_empreinte_plancher.py
|
||||
python3 scripts/tests/test_expositions_sans_port.py
|
||||
python3 scripts/tests/test_proxmox_fw_externe.py
|
||||
python3 scripts/tests/test_libelles_parefeu.py
|
||||
python3 scripts/tests/test_conditions_site.py
|
||||
python3 scripts/tests/test_relais_du_site.py
|
||||
python3 scripts/tests/test_sasl_local.py
|
||||
python3 scripts/tests/test_dovecot_sonde_et_imap.py
|
||||
python3 scripts/tests/test_parite_tls_site.py
|
||||
python3 scripts/tests/test_exportateur_pg_tls.py
|
||||
python3 scripts/tests/test_frontiere_pki.py
|
||||
python3 scripts/tests/test_sonde_tcp.py
|
||||
python3 scripts/tests/test_restauration.py
|
||||
python3 scripts/tests/test_repertoires_partages.py
|
||||
python3 scripts/tests/test_underlay_tunnel.py
|
||||
python3 scripts/tests/test_face_publique_relais.py
|
||||
python3 scripts/tests/test_mode_nat_sortant.py
|
||||
python3 scripts/tests/test_verifier_flux_site.py
|
||||
python3 scripts/tests/test_frontiere_refus.py
|
||||
python3 scripts/tests/test_contexte.py
|
||||
python3 scripts/tests/test_appels_locataire.py
|
||||
|
|
@ -1233,6 +1250,8 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops
|
|||
@# `underlay.vault.yml`, qui vit a cote de sa carte et hors depot. Sans le `-e @`, le
|
||||
@# role echoue sur son assertion — et l'echec ne dit pas qu'il manque un FICHIER,
|
||||
@# seulement que les valeurs sont vides.
|
||||
@# `SETOPS_UNDERLAY` (2026-10-13) : la carte lue par l'inventaire DESIGNE aussi la voute.
|
||||
@# Lire le symlink seul aurait charge les secrets de Chezlepro contre TechnoLibre.
|
||||
@set -e; \
|
||||
if [[ ! -f "playbooks/groupes/$(GROUPE).yml" ]]; then \
|
||||
printf 'Refus: aucun playbook pour le groupe %s.\n' "$(GROUPE)"; exit 2; \
|
||||
|
|
@ -1244,7 +1263,7 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops
|
|||
printf 'Groupes du site : %s\n' "$$connus"; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$(GROUPE).yml" \
|
||||
"$${supp[@]}" $(ARGS)
|
||||
|
|
@ -1266,6 +1285,12 @@ fiches: ## Regenere la fiche de chaque role (docs/roles/) — ROLE=<nom> pour un
|
|||
|
||||
.PHONY: site-deployer-tout
|
||||
site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true
|
||||
@# L'AMORCAGE DU CACHE (2026-10-13). Toute machine du site s'amorce sur le cache des son
|
||||
@# socle — or le cache n'existe qu'apres le sien. Aucune COUCHE ne peut le dire : une couche
|
||||
@# vaut pour toutes les machines a la fois. Le premier site construit depuis zero
|
||||
@# (TechnoLibre) l'a montre ; la machine du cache passe donc d'abord, seule, par son socle
|
||||
@# et son cache. Sur un site deja monte, ce passage ne change rien. Les regles nftables
|
||||
@# generees sont exigees avant tout (`verifier_flux_site.py`) : sans elles, repli generique.
|
||||
@# LE SITE AVAIT L'ORCHESTRATION, PAS LE MOYEN DE LA LANCER (2026-09-14).
|
||||
@#
|
||||
@# `playbooks/site.yml` est genere par `orchestrer.py` et ordonne les couches pour
|
||||
|
|
@ -1295,11 +1320,21 @@ site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des cou
|
|||
exit 2; \
|
||||
fi; \
|
||||
python3 scripts/verifier_genome_a_jour.py || exit 2; \
|
||||
python3 scripts/verifier_flux_site.py || exit 2; \
|
||||
python3 scripts/orchestrer.py verifier; \
|
||||
python3 scripts/orchestrer.py ecrire; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
|
||||
cache="$$(python3 scripts/site_inventaire.py --list | python3 -c \
|
||||
'import json,sys; print(" ".join((json.load(sys.stdin).get("serveur_cache_site") or {}).get("hosts") or []))')"; \
|
||||
if [[ -n "$$cache" ]]; then \
|
||||
printf '\n=== Amorcage : le cache du site d abord (%s) ===\n' "$$cache"; \
|
||||
for g in serveur_debian serveur_durci serveur_artefacts serveur_cache_site; do \
|
||||
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$$g.yml" \
|
||||
--limit serveur_cache_site "$${supp[@]}" $$mode $(ARGS); \
|
||||
done; \
|
||||
fi; \
|
||||
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
|
||||
--limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS)
|
||||
|
||||
|
|
@ -1353,7 +1388,7 @@ gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige r
|
|||
.PHONY: genome-etat
|
||||
genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS)
|
||||
|
|
@ -1368,11 +1403,31 @@ publier: ## Publie le genome d'un geste : eregion PUIS la forge du site, et le v
|
|||
.PHONY: publier
|
||||
genome-pousser: ## Le runner du site pousse le genome sur la forge du site — DEPOT=<nom> pour un seul
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
|
||||
|
||||
.PHONY: forge-cles
|
||||
forge-cles: ## Le site enregistre sur le depot de chaque locataire la cle de lecture que sa face publie — CONFIRMER=true pour ecrire
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/forge_cles.yml "$${supp[@]}" $(ARGS)
|
||||
|
||||
.PHONY: forge-comptes
|
||||
forge-comptes: ## Les comptes de la forge du site : un par mainteneur (plan du site), et le lecteur de la console — CONFIRMER=true pour ecrire
|
||||
@set -e; \
|
||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||
CONFIRMER="$(CONFIRMER)" ansible-playbook -i scripts/site_inventaire.py \
|
||||
playbooks/maintenance/forge_comptes.yml "$${supp[@]}" $(ARGS)
|
||||
|
||||
.PHONY: forge-etancheite
|
||||
forge-etancheite: ansible-runtime ## La forge du site laisse-t-elle chaque runner lire ses depots, et personne d'autre ? (mesure, aucune ecriture)
|
||||
python3 scripts/forge_etancheite.py
|
||||
|
||||
.PHONY: depots-perimes
|
||||
depots-perimes: ansible-runtime _instance-requise ## Depots qu'un runner ne declare plus : regarde, et retire avec CONFIRMER=true
|
||||
@# DESTRUCTIF SUR CONFIRMATION SEULEMENT. Sans `CONFIRMER=true`, ce geste REGARDE :
|
||||
|
|
@ -1444,6 +1499,9 @@ site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true
|
|||
NOEUD_PROXMOX="$$SETOPS_NOEUD" \
|
||||
VMID_MODELE="$${SETOPS_VMID_MODELE:-$(VMID_MODELE)}" \
|
||||
CLONE_COMPLET="$$SETOPS_CLONE_COMPLET" \
|
||||
CLE_SSH_PUBLIQUE="$${SETOPS_CLE_PUBLIQUE:-$(CLE_SSH_PUBLIQUE)}" \
|
||||
DNS="$${SETOPS_DNS:-$(DNS)}" \
|
||||
DOMAINE_RECHERCHE="$${SETOPS_DOMAINE:-$(DOMAINE_RECHERCHE)}" \
|
||||
FORMAT_DISQUE="$(FORMAT_DISQUE)"; \
|
||||
printf '=== %s : %s s ===\n' "$$m" "$$(( $$(date +%s) - t0 ))"; \
|
||||
done; \
|
||||
|
|
@ -1673,7 +1731,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
|
|||
done; \
|
||||
done; \
|
||||
if [[ -z "$$vault_file" && -L underlay.yml ]]; then \
|
||||
vault_file="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
||||
vault_file="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||
fi; \
|
||||
if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \
|
||||
read -r premiere_ligne < "$$vault_file" || true; \
|
||||
|
|
|
|||
130
docs/audit/preuve-2026-10-09.md
Normal file
130
docs/audit/preuve-2026-10-09.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-10-09
|
||||
|
||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (94 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
|
||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
| 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 : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 119 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. |
|
||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
|
||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 35 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
|
||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, 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, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 78 scripts expliques et atteignables, 151 cibles make documentees, 69 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (148 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 50 document(s) declarent leur lecteur (47 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (11 groupe(s) au catalogue). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 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, 97 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 : 74 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 201 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 102 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 (137 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 304 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 — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
|
||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (94 preuves, 69 roles, 42 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e9215a2` (publie le 2026-10-07). |
|
||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
|
||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
|
||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
|
||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
|
||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
|
||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 4 ecosysteme(s) (OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d'amorcage, sorties du |
|
||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 144 etapes, 150 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
||||
| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
|
||||
| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
|
||||
| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
|
||||
| P87 | La frontiere designe chaque locataire par les identites qu'il publie | — | ✅ OK | 2 locataire(s) : la frontiere les designe par les identites qu'ils publient (supernet, administration, tunnel, groupes, routes, sortie). |
|
||||
| P88 | Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : regles WAN, redirections et tunnel disent exactement ce que leur face reseau ouvre a tous. |
|
||||
| P89 | Ce que l'administration atteint chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : par la gestion, le VPN et le tunnel, l'administration atteint exactement ce que leur face reseau lui ouvre. |
|
||||
| P90 | Ce qui sort d'un locataire vers l'Internet, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : la frontiere laisse sortir exactement ce que leur face reseau publie. |
|
||||
| P91 | La fiche du site est deposee a jour, et l'inventaire se genere sans le site | — | ✅ OK | 2 locataire(s) : fiche du site deposee a jour, et inventaire genere sans le site identique a l'octet pres. |
|
||||
| P92 | Chaque locataire a publie sa face reseau a jour | — | ✅ OK | 2 locataire(s) : face reseau publiee, a jour. |
|
||||
| P93 | La face publie, machine par machine, les parametres de clonage de l'inventaire | — | ✅ OK | 26 machine(s) : la face publie exactement les parametres de clonage de l'inventaire. |
|
||||
| P94 | Creer, raser et placer un locataire par sa face visent les memes machines | — | ✅ OK | 2 locataire(s) : creer, raser et placer visent par la face les memes machines que par l'instance montee. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||
|
||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||
|
||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||
comme declarations d'intention, non comme preuves :
|
||||
|
||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||
contre une flotte vivante.
|
||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||
|
||||
_Rapport genere le 2026-10-09._
|
||||
131
docs/audit/preuve-2026-10-11.md
Normal file
131
docs/audit/preuve-2026-10-11.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-10-11
|
||||
|
||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (95 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
| 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 : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 123 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. |
|
||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
|
||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 35 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
|
||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, 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, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 81 scripts expliques et atteignables, 154 cibles make documentees, 69 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (150 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 51 document(s) declarent leur lecteur (48 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (11 groupe(s) au catalogue). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 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, 97 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 : 77 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 175 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 103 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 (141 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 278 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 — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
|
||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (95 preuves, 69 roles, 42 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e9215a2` (publie le 2026-10-07). |
|
||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
|
||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
|
||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
|
||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 11 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
|
||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
|
||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 4 ecosysteme(s) (OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d'amorcage, sorties du |
|
||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 148 etapes, 153 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
||||
| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
|
||||
| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
|
||||
| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
|
||||
| P87 | La frontiere designe chaque locataire par les identites qu'il publie | — | ✅ OK | 2 locataire(s) : la frontiere les designe par les identites qu'ils publient (supernet, administration, tunnel, groupes, routes, sortie). |
|
||||
| P88 | Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : regles WAN, redirections et tunnel disent exactement ce que leur face reseau ouvre a tous. |
|
||||
| P89 | Ce que l'administration atteint chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : par la gestion, le VPN et le tunnel, l'administration atteint exactement ce que leur face reseau lui ouvre. |
|
||||
| P90 | Ce qui sort d'un locataire vers l'Internet, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : la frontiere laisse sortir exactement ce que leur face reseau publie. |
|
||||
| P91 | La fiche du site est deposee a jour, et l'inventaire se genere sans le site | — | ✅ OK | 2 locataire(s) : fiche du site deposee a jour, et inventaire genere sans le site identique a l'octet pres. |
|
||||
| P92 | Chaque locataire a publie sa face reseau a jour | — | ✅ OK | 2 locataire(s) : face reseau publiee, a jour. |
|
||||
| P93 | La face publie, machine par machine, les parametres de clonage de l'inventaire | — | ✅ OK | 26 machine(s) : la face publie exactement les parametres de clonage de l'inventaire. |
|
||||
| P94 | Creer, raser et placer un locataire par sa face visent les memes machines | — | ✅ OK | 2 locataire(s) : creer, raser et placer visent par la face les memes machines que par l'instance montee. |
|
||||
| P95 | Chaque locataire publie la cle qui lira son depot du genome, et sa flotte la connait | — | ✅ OK | 2 locataire(s) : depot et cle de lecture publies, cle connue de leur flotte. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||
|
||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||
|
||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||
comme declarations d'intention, non comme preuves :
|
||||
|
||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||
contre une flotte vivante.
|
||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||
|
||||
_Rapport genere le 2026-10-11._
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
||||
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
|
||||
source: 3fe7e3c
|
||||
date: 2026-09-29
|
||||
source: e9215a2
|
||||
date: 2026-10-07
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ Une directive qu'aucune garde ne vérifie finit par ne plus être vraie. Chaque
|
|||
| `socle-identite` | **est** la chaîne d'identité, ne peut pas se déléguer à elle-même | keycloak, openldap |
|
||||
| `ldap-direct` | protocole non-OIDC lié à LDAP | dovecot, postfix |
|
||||
| `interne-sans-auth` | interface joignable **dans** le tenant sans authentification, non exposée | prometheus, loki |
|
||||
| `sans-auth-humaine` | aucun point d'authentification humaine | 23 |
|
||||
| `sans-auth-humaine` | aucun point d'authentification humaine | 24 |
|
||||
|
||||
La preuve refuse **l'oubli et le mensonge** : un rôle sans déclaration, une portée
|
||||
inventée, un `web-sso` sans accès de secours ou sans posture de formulaire, un `web-sso
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ README de rôles). Cette page comble ces deux trous.
|
|||
|
||||
| Ce qu'on compte | Combien | Comment on le mesure |
|
||||
|---|---|---|
|
||||
| rôles | 69 | `roles/*/` |
|
||||
| README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 46 | `docs/*.md` |
|
||||
| pièces d'audit | 51 | `docs/audit/*` |
|
||||
| rôles | 70 | `roles/*/` |
|
||||
| README de rôles | 70 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 47 | `docs/*.md` |
|
||||
| pièces d'audit | 53 | `docs/audit/*` |
|
||||
| unités de wiki | 27 | `wiki/*.md` |
|
||||
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
||||
|
|
@ -36,7 +36,7 @@ README de rôles). Cette page comble ces deux trous.
|
|||
| Sujet | Documents |
|
||||
|---|---|
|
||||
| **Autorité / gouvernance** | `AGENTS.md` (source d'autorité), `CLAUDE.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md` |
|
||||
| **Le modèle (plan)** | `docs/architecture-set-ops.md` (survol) → `docs/plan-et-generation.md` (à fond) → `docs/meta-classe.md` (concept) → `docs/conception-contextes.md` (site et locataire : deux classes, un contrat) |
|
||||
| **Le modèle (plan)** | `docs/architecture-set-ops.md` (survol) → `docs/plan-et-generation.md` (à fond) → `docs/meta-classe.md` (concept) → `docs/conception-contextes.md` (site et locataire : deux classes, un contrat) → `docs/depots-prives-conception.md` (chaque runner ne lit que ses dépôts ; chantier) |
|
||||
| **Services, maturité, dette** | `docs/catalogue-services.md` (**la carte de maturité + la cruft y sont déjà**) |
|
||||
| **Exploitation / VM** | `docs/vm-lifecycle.md`, `docs/procedure-template-debian13-proxmox.md`, `docs/config-proxmox.md`, `docs/nomenclature-vm.md`, `docs/multi-instances.md` |
|
||||
| **Conceptions de domaine** | `docs/identite-sso.md`, `docs/courriel-conception.md`, `docs/bindings-conception.md`, `docs/dns-interne.md`, `docs/dimensionnement-ressources.md`, `docs/integrations-vm.md` |
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ Ce que la reconstruction couvre, par capacité :
|
|||
| Resolveur du site | `serveur_resolveur_site` | designe LE resolveur que les ecosystemes de ce site interrogent tant qu'ils n'ont pas le leur. Marqueur : `serveur_resolveur` installe. |
|
||||
| Depot de sauvegarde du site | `serveur_backup_site` | designe LE depot ou les ecosystemes de ce site posent leur etat tant qu'ils n'ont pas le leur. Marqueur : `serveur_backup` installe. |
|
||||
| DNS public du site | `serveur_dns_public` | **secondaire** public de toutes les zones `autorite: primaire-cache` des locataires du site : le locataire ecrit, le site sert. Transfert signe TSIG, aucune adresse de confiance, aucune zone `.internal`. Phase 1 : non expose a Internet |
|
||||
| Relais de la face publique | `serveur_relais` | un VPS **hors du site** qui loue ses adresses fixes quand le site n'a qu'une adresse DHCP : il repond a l'ARP de son hebergeur pour elles (proxy) et les **route** en /32 dans un tunnel WireGuard que la frontiere initie. Aucune traduction : l'adresse du client arrive intacte. Inventorie depuis la carte, comme un hyperviseur |
|
||||
| Agents de flotte | `client_metrique`, `client_journal`, `client_smtp`, `client_sante` | collecte, relais et rapport de santé sur toute la flotte |
|
||||
|
||||
*Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail`
|
||||
|
|
@ -201,7 +202,7 @@ dans le plan.
|
|||
## Ordre de déploiement — le raisonnement
|
||||
|
||||
> **L'ordre exécutable n'est pas ici.** Il vit dans `docs/couches-deploiement.yml` et
|
||||
> `docs/dependances-groupes.yml`, que P08 prouve cohérents (42 groupes classés, aucun
|
||||
> `docs/dependances-groupes.yml`, que P08 prouve cohérents (43 groupes classés, aucun
|
||||
> cycle, aucune arête en arrière) et que `make reconstruire` suit. Ce qui suit en est le
|
||||
> **raisonnement**, utile pour comprendre pourquoi cet ordre-là — et pour placer un
|
||||
> service nouveau. Les phases sont franchies : les « intégrations à prévoir » ci-dessous
|
||||
|
|
|
|||
|
|
@ -184,7 +184,9 @@ les publie désormais (`sources_declarees`). La frontière, en trois temps : les
|
|||
l'inventaire d'un locataire se génère sans son site, à l'octet près (P91). Le locataire publie sa face
|
||||
réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox, la frontière et la
|
||||
découverte des locataires du site la lisent. La matérialisation aussi : la face publie les
|
||||
paramètres de clonage (P93) ; `locataire-creer`, `locataire-raser` et `placement-plan TENANT=`
|
||||
paramètres de clonage (P93), sauf la clé du runner du site. Le site l'ajoute lui-même en
|
||||
clonant le runner du locataire (`clonage_rendu`, 2026-10-11) : elle ne transite plus par la
|
||||
face, et un locataire peut calculer sa face sans le plan de son site ; `locataire-creer`, `locataire-raser` et `placement-plan TENANT=`
|
||||
nomment leur locataire au lieu de le monter, et visent les mêmes machines, à l'argument près de
|
||||
la ligne `ansible-playbook` (P94, `test_appels_locataire.py`) ; `reconstruire-locataire` les
|
||||
emploie. Reste : la preuve par reconstruction.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,19 @@ couches:
|
|||
- serveur_debian
|
||||
- serveur_durci
|
||||
|
||||
# LE DEPOT DE L'ETAT AVANT TOUT CE QUI EN RESTAURE (2026-10-13). L'AC, les bases, la
|
||||
# supervision demandent au depot « mon incarnation precedente attend-elle ? » au moment de
|
||||
# naitre — et refusent, a juste titre, un depot ILLISIBLE (conclure « rien a restaurer »
|
||||
# faute de le joindre ferait naitre neuf ce qui attend). Range dans `services`, le depot
|
||||
# n'existait pas encore : le premier site construit depuis zero (TechnoLibre) s'est arrete
|
||||
# sur sa PKI. Il ne depend que du socle.
|
||||
- nom: depot_etat
|
||||
raison: "Le depot de sauvegarde : il doit repondre (vide ou non) avant que l'AC, les bases et la supervision ne naissent."
|
||||
groupes:
|
||||
- serveur_backup
|
||||
# Le depot du site, meme nature : il recoit l'etat des locataires et le sien.
|
||||
- serveur_backup_site
|
||||
|
||||
- nom: pki_racine
|
||||
raison: "L'autorité de certification interne (step_ca) : la racine de confiance de tous les flux TLS est-ouest."
|
||||
groupes:
|
||||
|
|
@ -75,7 +88,6 @@ couches:
|
|||
- serveur_rspamd
|
||||
- serveur_dovecot
|
||||
- serveur_postfix
|
||||
- serveur_backup
|
||||
# La source d'artefacts vient AVANT ceux qui installent des paquets — c'est tout
|
||||
# son objet. Placee plus tard, elle serait remplie apres avoir servi.
|
||||
- serveur_artefacts
|
||||
|
|
@ -90,8 +102,10 @@ couches:
|
|||
- serveur_forge_site
|
||||
# Le resolveur du site, meme nature : prete aux locataires pendant leur jeunesse.
|
||||
- serveur_resolveur_site
|
||||
# Le depot de sauvegarde du site, meme nature : il recoit l'etat des locataires.
|
||||
- serveur_backup_site
|
||||
# LE RELAIS (2026-10-13) : la face publique d'un site qui n'a qu'une adresse DHCP. Un
|
||||
# equipement HORS du site (VPS), inventorie depuis la carte comme un hyperviseur. Il
|
||||
# vient avant tout ce qui se publie : sans lui, une adresse publique ne mene nulle part.
|
||||
- serveur_relais
|
||||
# Le DNS public du site vient APRES les services : il tire les zones que les
|
||||
# autoritatifs des locataires ecrivent, et n'a rien a servir avant eux.
|
||||
- serveur_dns_public
|
||||
|
|
|
|||
149
docs/depots-prives-conception.md
Normal file
149
docs/depots-prives-conception.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Dépôts privés sur la forge du site — conception du chantier
|
||||
|
||||
> **Pour qui :** le mainteneur qui décide de ce chantier puis le conduit, et quiconque se demande
|
||||
> pourquoi un runner ne lit que ses propres dépôts.
|
||||
|
||||
Préparé le 2026-10-11. Décisions D1 à D3 prises le même jour par Daniel : les trois recommandations.
|
||||
|
||||
## 1. Le constat
|
||||
|
||||
Sur la forge du site, l'organisation `genome` porte cinq dépôts, tous **publics** :
|
||||
|
||||
| Dépôt | Contenu | Qui en a besoin |
|
||||
|---|---|---|
|
||||
| `set-ops-public` | le moteur | tous les runners |
|
||||
| `set-ops-modeles` | les modèles génériques | le runner du site |
|
||||
| `site-chezlepro` | la carte de la fabric, le plan du site | le runner du site |
|
||||
| `ops-chezlepro` | le plan, l'inventaire et la face de Chezlepro | son runner, le runner du site |
|
||||
| `ops-technolibre` | le plan, l'inventaire et la face de Technolibre | son runner, le runner du site |
|
||||
|
||||
Mesuré le 2026-10-11 : depuis le runner de Technolibre, `git ls-remote` sans identifiant lit la
|
||||
tête de `ops-chezlepro` (`b4fbd85`) et de `site-chezlepro` (`51dae24`). Chaque locataire peut donc
|
||||
lire le plan de l'autre, et la carte du site. **Aucun n'en a besoin** : chaque runner de locataire
|
||||
ne clone que le moteur et son propre dépôt ; ce que le site lui donne arrive par la fiche déposée
|
||||
dans son dépôt.
|
||||
|
||||
D'où ça vient : le 2026-09-12, à la première reconstruction du site, les dépôts créés privés ont
|
||||
cassé le clonage anonyme des runners. Ils sont passés publics, avec l'argument que seuls ceux
|
||||
qui ont franchi la frontière joignent la forge (`scripts/forge_amorcer.py`). Les autres locataires
|
||||
en font partie.
|
||||
|
||||
## 2. La cible
|
||||
|
||||
- **Publics** : `set-ops-public` et `set-ops-modeles`. Ils sont génériques, identiques pour tous.
|
||||
- **Privés** : `site-chezlepro` et chaque `ops-<locataire>`.
|
||||
- **Le runner du site** lit et pousse tous les dépôts avec sa clé, déjà enregistrée comme clé de
|
||||
déploiement en écriture par `genome_pousser.yml`. Mesuré le 2026-10-11 : il lit les cinq dépôts
|
||||
en SSH, avec une vérification stricte de la clé d'hôte. Rien à créer pour lui.
|
||||
- **Le runner d'un locataire** lit son seul dépôt, avec une clé de déploiement **en lecture
|
||||
seule** : la clé publique de son runner, déjà déclarée au plan (`ssh_baseline_cles_admin`) et
|
||||
dont la privée lui est remise à l'armement depuis sa voûte (`vault_runner_cle_ssh`).
|
||||
- **Aucun secret ne change de main.** Le locataire publie la clé publique de son runner dans sa
|
||||
face réseau ; le site l'enregistre sur le dépôt de ce locataire, avec ses propres
|
||||
identifiants d'administration de la forge (`vault_forgejo_admin`, déjà dans sa voûte).
|
||||
|
||||
### Éprouvé le 2026-10-11 (étape 1)
|
||||
|
||||
Clés de lecture enregistrées par `make forge-cles`, flux posé sur `site-forge-01` et à la
|
||||
frontière. Depuis chaque runner, `git ls-remote` en SSH, clé d'hôte de la forge épinglée (lue
|
||||
sur sa machine par l'accès d'administration) :
|
||||
|
||||
| Depuis | son dépôt | l'autre locataire | `site-chezlepro` | `set-ops-public` |
|
||||
|---|---|---|---|---|
|
||||
| runner de Technolibre | lu (`0172dd8`) | refusé | refusé | refusé |
|
||||
| runner de Chezlepro | lu (`74d5567`) | refusé | refusé | refusé |
|
||||
|
||||
Le refus vient de la forge (« Deploy Key … is not authorized to read »), **alors que les dépôts
|
||||
sont encore publics** : une clé de déploiement ne vaut que pour le dépôt qui l'a enregistrée.
|
||||
Conséquence : le moteur, public, continue de se cloner en HTTPS sans identifiant ; seuls les
|
||||
dépôts privés passent par SSH.
|
||||
|
||||
## 3. Le nœud : l'insémination
|
||||
|
||||
Le site amorce le runner d'un locataire **avant** que celui-ci ait reçu sa clé stable (elle
|
||||
arrive à l'armement, geste humain, après). Pendant l'insémination, le rôle `serveur_ops` clone
|
||||
pourtant le dépôt du locataire : avec un dépôt privé, ce clonage échoue. Même chose au premier
|
||||
déploiement du runner du site, dont la clé est neuve.
|
||||
|
||||
**Remède proposé : le colis.** Quand un runner ne peut pas encore lire un dépôt privé, le
|
||||
contrôleur qui joue `serveur_ops` (le runner du site pendant l'insémination, le poste
|
||||
ailleurs) lui apporte ce dépôt en `git bundle`. Le mécanisme existe déjà : `genome_colis.py` porte
|
||||
le génome du poste au runner du site. Le runner clone le colis, puis pointe `origin` vers l'URL
|
||||
SSH de la forge. Ses tirages suivants passent par sa clé, une fois armé.
|
||||
|
||||
## 4. Les décisions à prendre
|
||||
|
||||
**D1 — Comment le runner d'un locataire lit-il son dépôt ?**
|
||||
- *Clé de déploiement en SSH (recommandé).* Le runner continue de tirer lui-même ; il garde son
|
||||
autonomie (émancipation, `make` lancé sur le runner, garde `verifier_genome_a_jour`). Coût : un
|
||||
flux nouveau, du runner de chaque locataire vers la forge du site, en SSH.
|
||||
- *Colis seulement.* Aucun flux nouveau, aucune clé de déploiement pour les locataires : le
|
||||
contrôleur apporte toujours le dépôt. Mais le runner ne tire plus son dépôt lui-même, et sa
|
||||
garde de génome n'a plus de forge à laquelle se comparer.
|
||||
|
||||
**D2 — Sur quelle écoute SSH ?**
|
||||
- *Le `sshd` de la machine, port 22 (recommandé pour commencer).* Le runner du site et le poste
|
||||
l'utilisent déjà pour pousser ; l'utilisateur `git` y est contraint par la forge. Le flux
|
||||
n'ouvre que les adresses des runners de locataires, pas leurs supernets.
|
||||
- *Le serveur SSH intégré de Forgejo, sur un port dédié.* Il n'expose pas le `sshd` de la
|
||||
machine, mais sa cohabitation avec le `sshd` existant reste à éprouver avant de s'y fier.
|
||||
|
||||
**D3 — La preuve.** Ce chantier touche l'insémination : je propose de le prouver par une
|
||||
reconstruction complète de Technolibre (raser, créer, inséminer, armer, monter), dépôts
|
||||
privés, avant de reprendre le lot 3b.
|
||||
|
||||
## 5. Ce qui change dans le code
|
||||
|
||||
État au 2026-10-11 : tout ce tableau est écrit, sauf la bascule elle-même. La clé d'hôte de
|
||||
la forge est déclarée dans le plan du site (`forge_ssh_cle_hote`), offerte par la fiche
|
||||
(`serveur_ops_forge_cle_hote`) et copiée par chaque locataire. Le moteur reste cloné en HTTPS.
|
||||
|
||||
| Pièce | Changement |
|
||||
|---|---|
|
||||
| `serveur_ops_depots` | un drapeau `prive: true` sur les dépôts `instance`, `tenant` et `hebergeur` |
|
||||
| `roles/serveur_ops` | URL SSH et clé du runner pour les dépôts privés ; colis du contrôleur quand le runner ne peut pas encore les lire ; clé d'hôte de la forge épinglée |
|
||||
| `contexte.py` (face) | la face publie la clé publique de lecture du runner (`genome.cle_lecture`) |
|
||||
| `contexte.py` (fiche) | la fiche du site publie la clé d'hôte SSH de sa forge, que le runner du locataire épingle |
|
||||
| nouvelle cible `forge-cles` | le site enregistre, d'après les faces, la clé de lecture de chaque locataire sur son dépôt, et retire celles qui ne sont plus publiées |
|
||||
| `forge_amorcer.py` | crée les dépôts privés comme privés |
|
||||
| flux | `serveur_forge_site` : entrée SSH depuis les runners des locataires hébergés (paire nouvelle, résolue depuis les faces) ; `serveur_ops` : sortie correspondante |
|
||||
| `genome_etat.yml` | lit la forge par SSH ou authentifié : l'API anonyme répondrait 404 pour un dépôt privé |
|
||||
| `sonde-genome-servi` | ne compte plus que les dépôts publics (au moins le moteur) : c'est ce qu'un écosystème neuf clone sans rien détenir ; les privés sont suivis par `genome-etat`, `forge-cles` et les runners |
|
||||
|
||||
Inchangés : `sonde-genome-a-jour` (son repère ne compare que `set-ops-public`), le poste (il
|
||||
pousse déjà en SSH), la console (`etat_forge` lit `origin`, qui deviendra l'URL SSH).
|
||||
|
||||
## 6. Les preuves à écrire
|
||||
|
||||
- **Hors ligne (`make prouver`)** : chaque dépôt `instance`, `tenant` ou `hebergeur` est déclaré
|
||||
privé ; chaque face publie une clé de lecture, et cette clé est dans `ssh_baseline_cles_admin`.
|
||||
- **Sur le terrain (`make forge-etancheite`)**, avec témoins :
|
||||
- sans identifiant, un dépôt privé refuse et un dépôt public répond ;
|
||||
- le runner d'un locataire lit son dépôt et **ne lit pas** celui de l'autre ;
|
||||
- les clés de déploiement de chaque dépôt sont exactement : runner du site en écriture, runner
|
||||
du locataire en lecture.
|
||||
- **Par reconstruction** : D3.
|
||||
|
||||
## 7. L'ordre de bascule
|
||||
|
||||
**Fait le 2026-10-11, étapes 1 à 5.** Bascule par `make forge-cles CONFIRMER=true`, mesurée
|
||||
ETANCHE par `make forge-etancheite`. Preuve par reconstruction de Technolibre (§D3) : le runner
|
||||
neuf a reçu son dépôt en colis à l'insémination (il n'avait pas encore de clé), l'a tiré en SSH
|
||||
une fois armé, et la flotte est remontée avec ses témoins d'état intacts. La reconstruction a
|
||||
d'abord révélé un dossier SSH manquant sur une machine neuve (corrigé, `3fc16c8`).
|
||||
|
||||
Chaque étape se vérifie avant la suivante, et la bascule se défait d'un appel par dépôt
|
||||
(repasser public).
|
||||
|
||||
1. **Éprouver** sur la forge du site, sans rien basculer : enregistrer la clé de lecture du
|
||||
runner de Technolibre sur `ops-technolibre`, ouvrir le flux pour ce seul runner, et mesurer
|
||||
qu'il lit son dépôt en SSH.
|
||||
2. **Construire** le code et les preuves (§5, §6) ; `make verifier`.
|
||||
3. **Déployer sans basculer** : flux et frontière (`make frontiere-appliquer CONFIRMER=true`,
|
||||
lancé par Daniel), clés de déploiement, remotes SSH sur les trois runners. Les dépôts sont
|
||||
encore publics : chaque runner doit déjà tirer en SSH.
|
||||
4. **Basculer** `site-chezlepro`, `ops-chezlepro` et `ops-technolibre` en privé. Mesurer avec
|
||||
`make forge-etancheite`, puis la veille des trois consoles.
|
||||
5. **Prouver** par la reconstruction de Technolibre (D3).
|
||||
6. Reprendre le lot 3b : les demandes vivent alors dans le dépôt de chaque locataire, lisibles
|
||||
par lui et par le site seulement.
|
||||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
|||
|
||||
## Le trou qu'il comble
|
||||
|
||||
`scripts/prouver.py` porte 94 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 95 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||
propriétaire, que rien n'est codé en dur.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ flux:
|
|||
port: 5432 # entier ou liste [80, 443]
|
||||
protocole: tcp # tcp | udp
|
||||
pair: [serveur_keycloak, serveur_forgejo] # QUI — voir « Résolution du pair »
|
||||
chiffrement: tls-requis # tls-requis | tls | starttls | ssh | tls-cible | clair | n-a (bonus audit)
|
||||
chiffrement: tls-requis # tls-requis | tls | starttls | ssh | tls-cible | wireguard | clair | n-a (bonus audit)
|
||||
# tls-requis : chiffré + pair vérifié (verify-full) | tls : chiffré | starttls : mise à niveau opportuniste
|
||||
# ssh : transport SSH (chiffré, hôte vérifié) | tls-cible : TLS visé mais pas encore appliqué (feuille de route)
|
||||
# clair : non chiffré (local ou terminé à l'edge) | n-a : sans objet
|
||||
|
|
@ -66,6 +66,31 @@ Un **résolveur** (miroir de `instancier`) agrège, par serveur, les `flux.yml`
|
|||
- **le registre d'audit** : `docs/registre-flux.md` (généré) + une cible `make flux`.
|
||||
|
||||
## Activation prudente
|
||||
|
||||
### Descriptions conservées par les pare-feux
|
||||
|
||||
La `raison` non vide du flux et son rôle alimentent les champs natifs : `comment`
|
||||
pour nftables et Proxmox, `description` pour le filtrage OPNsense (`descr` pour ses
|
||||
redirections). Un commentaire de fichier `# ...` ne survit pas au chargement nftables.
|
||||
Les règles communes (administration, connexions établies, refus) portent aussi leur motif.
|
||||
|
||||
Les libellés sont sur une ligne. Le commentaire nftables est borné à 127 octets UTF-8 ;
|
||||
OPNsense garde sa clé `setops:...` complète et borne le texte qui suit pour rester dans
|
||||
255 octets. Une raison abrégée reste disponible en entier dans le registre des flux.
|
||||
Le commentaire Proxmox conserve aussi le pair nommé dans le devis.
|
||||
|
||||
Une correction de description apparaît dans `make proxmox-fw-plan` et
|
||||
`make frontiere-plan`. Son application met à jour le champ sur la règle existante,
|
||||
puis le relit : elle ne recrée pas la règle et ne modifie ni son activation ni sa
|
||||
politique. Les confirmations habituelles restent requises pour appliquer ces devis.
|
||||
`make test` vérifie notamment cette convergence et les limites des champs.
|
||||
|
||||
Références des champs natifs : [nftables](https://netfilter.org/projects/nftables/manpage.html),
|
||||
[API Proxmox](https://github.com/proxmox/pve-firewall/blob/master/src/PVE/API2/Firewall/Rules.pm),
|
||||
[descriptions OPNsense](https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/DescriptionField.php).
|
||||
|
||||
### Activer le filtrage
|
||||
|
||||
Activer nftables = **action destructive** (peut couper l'accès) → confirmation explicite +
|
||||
déploiement graduel (garder l'accès SSH/Ansible, tester par nœud).
|
||||
|
||||
|
|
@ -119,7 +144,7 @@ Le registre confondait deux situations :
|
|||
|
||||
| Situation | Exemple | `partage` |
|
||||
|---|---|---|
|
||||
| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 12345 (SASL réseau) | absent |
|
||||
| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 24 (LMTP réseau) | absent |
|
||||
| le rôle **décrit** celle d'un autre | `serveur_backup` emprunte le sshd de `serveur_debian` | `true` |
|
||||
|
||||
Sans cette distinction, la seule co-location légitime de la flotte — `tcp/22` sur le
|
||||
|
|
@ -130,4 +155,3 @@ ignorée, ce qui est pire que de ne pas l'avoir.
|
|||
**Corollaire à retenir** : un port qu'on **subit** (le défaut amont d'un logiciel) doit être
|
||||
imposé et déclaré comme les autres. Celui d'Alloy ne l'était pas, et c'est la seule raison
|
||||
pour laquelle la collision a pu durer des semaines.
|
||||
|
||||
|
|
|
|||
|
|
@ -430,3 +430,12 @@ Reste à trancher sur une interface **physique** : `ip ?`, `access-group ?`, `sp
|
|||
déclare 53 en UDP et TCP, la récursion depuis la racine que le choix souverain implique.
|
||||
Le `block out` de la section 5 est donc un vrai default-deny **assumé**, et le devis
|
||||
l'énonce désormais au lieu de le poser en silence.
|
||||
- **La sortie générale n'existe plus — 2026-10-10.** Le 123 était déjà passé à la frontière
|
||||
(2026-09-11). Le 443 et le 80 du socle sont devenus conditionnels (`seulement_si:
|
||||
artefacts_amorcage` vide) : le site et ses locataires prennent apt par le cache du site,
|
||||
et ne les reçoivent plus. Mesurée à la frontière, cette sortie ne servait plus qu'à ce
|
||||
qu'aucun rôle ne déclarait : télémétrie de Grafana, d'Alloy et de Loki et vérification
|
||||
de version de Collabora (coupées à la source), cartes de rspamd et repère du génome
|
||||
(déclarés dans `serveur_rspamd` et `serveur_ops_site`). Chaque sortie vers Internet porte
|
||||
désormais le nom du rôle qui en a besoin. `test_conditions_site.py` refuse qu'un rôle
|
||||
universel en rouvre une sans condition.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
| `serveur_backup_site` | ingress | 22 | tcp | voisins_site | ssh | Les écosystèmes de ce site déposent leur état ici tant qu'ils n'ont pas leur propre dépôt. SFTP par un utilisateur restreint, jamais un compte d'administration : le site reçoit des octets chiffrés par restic, il ne peut pas les lire. |
|
||||
| `serveur_cache_site` | ingress | 3142 | tcp | voisins_site | clair | Servir les caches des écosystèmes voisins. Debian n'est ainsi téléchargé qu'une fois pour tout le site, et le cache ne voit que des requêtes AGRÉGÉES — jamais quelle machine installe quoi. |
|
||||
| `serveur_cache_site` | egress | 80 | tcp | externe | clair | Remplir le cache depuis les dépôts Debian amont. En clair parce que les dépôts apt sont signés : l'intégrité vient de la signature, pas du transport. |
|
||||
| `serveur_cache_site` | egress | 443 | tcp | externe | tls-requis | Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga). Le cache les relaie pour que la flotte n'ait pas à sortir elle-même. |
|
||||
| `serveur_cache_site` | egress | 443 | tcp | externe | tls-requis | Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga), et le dépôt de binaires (roues PyPI, collections Galaxy, versions GitHub, Nextcloud, Forgejo). Le site les relaie pour que la flotte n'ait pas à sortir elle-même. |
|
||||
| `serveur_collabora` | ingress | 9980 | tcp | edge | clair | Éditeur servi au navigateur via l'edge (WebSocket WOPI ; TLS terminé à l'edge). |
|
||||
| `serveur_collabora` | ingress | 9980 | tcp | localhost | clair | Vérifications WOPI serveur→Collabora depuis Nextcloud co-localisé. |
|
||||
| `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). |
|
||||
|
|
@ -31,9 +31,9 @@
|
|||
| `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 | 80 | tcp | externe | clair | Dépôts apt en clair et redirections des miroirs, pour un écosystème SANS amont d'artefacts. 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. |
|
||||
| `serveur_debian` | egress | 443 | tcp | externe | tls-requis | Dépôts apt en direct, pour un écosystème SANS aucune source d'artefacts. Le site et ses locataires passent par le cache du site et ne la reçoivent pas. |
|
||||
| `serveur_debian` | egress | frag-needed | icmp | externe | n-a | ICMP « fragmentation nécessaire » sortant : c'est ainsi que nos hôtes signalent l'overlay à 1450 aux correspondants distants. |
|
||||
| `serveur_dns_public` | ingress | 53 | udp | voisins_site | clair | NOTIFY des primaires des locataires du site : une zone publique a change. Le contenu d'une zone publique n'a rien de secret ; ce qui compte est QUI peut le modifier, et c'est TSIG qui le garde, sur le transfert. |
|
||||
| `serveur_dns_public` | ingress | 53 | tcp | voisins_site | clair | Repli TCP des notifications des primaires des locataires. |
|
||||
|
|
@ -43,8 +43,8 @@
|
|||
| `serveur_dns_public` | egress | 5300 | udp | primaires_dns_locataires | clair | SOA demande au primaire avant chaque transfert : c'est lui qui dit si la zone a change. Sans lui, le secondaire garde sa premiere copie pour toujours. |
|
||||
| `serveur_dovecot` | ingress | 24 | tcp | serveur_postfix | tls-requis | Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify). |
|
||||
| `serveur_dovecot` | ingress | 993 | tcp | externe | tls-requis | Accès courriel des utilisateurs (IMAPS). Frontière publique gérée à l'OPNsense. |
|
||||
| `serveur_dovecot` | ingress | 12345 | tcp | serveur_postfix | tls | Authentification SASL déléguée : Postfix valide les identifiants de soumission contre Dovecot. |
|
||||
| `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 | 22 | tcp | runners_locataires | ssh | Chaque runner de locataire lit son propre depot du genome, prive, avec sa cle de deploiement en lecture seule. Il ne lit ni le depot d'un autre locataire, ni celui du site. |
|
||||
| `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, 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. |
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
| `serveur_keycloak` | ingress | 8080 | tcp | edge | clair | Console et endpoints OIDC servis au navigateur et aux applications via l'edge (TLS terminé à l'edge). |
|
||||
| `serveur_keycloak` | egress | 636 | tcp | serveur_openldap | tls-requis | Fédération de l'annuaire OpenLDAP (LDAPS). |
|
||||
| `serveur_keycloak` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Persistance Keycloak dans PostgreSQL (verify-full). |
|
||||
| `serveur_loki` | ingress | 1514 | tcp | fabric | clair | Journaux de la frontiere. Elle n'accueille aucun agent et n'emet que du syslog ; sans ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part. |
|
||||
| `serveur_loki` | ingress | 1514 | tcp | fabric | tls-requis | Journaux de la frontiere, en TLS mutuel. Elle n'accueille aucun agent et n'emet que du syslog ; sans ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part. |
|
||||
| `serveur_loki` | ingress | 3100 | tcp | client_journal | tls | Réception des journaux poussés par Alloy (client_journal) en HTTPS (http_tls_config, cert step-ca). |
|
||||
| `serveur_loki` | ingress | 3100 | tcp | localhost | clair | Requêtes de Grafana co-localisé (datasource Loki en localhost). |
|
||||
| `serveur_loki` | ingress | 3100 | tcp | fabric | tls | Journaux des hyperviseurs. La machine qui PORTE les VM ecrivait ses journaux nulle part ailleurs que sur son propre disque — invisibles le jour ou c'est elle qui casse. |
|
||||
|
|
@ -89,6 +89,7 @@
|
|||
| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. |
|
||||
| `serveur_ops_site` | egress | 22 | tcp | serveur_ops_tenant | ssh | Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne transporte aucun secret : la voute est remise par un humain. |
|
||||
| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. |
|
||||
| `serveur_ops_site` | egress | 443 | tcp | externe | tls-requis | Repere du genome : `git ls-remote` anonyme sur eregion (forge.alliance-boreale.ca) pour dire si la forge du site a pris du retard. Lecture seule, sans identifiant. |
|
||||
| `serveur_ops_site` | egress | 8006 | tcp | fabric | tls-requis | API de l'hyperviseur : créer, cloner et détruire les VM de la fabric. Le seul flux par lequel un écosystème peut en matérialiser un autre. |
|
||||
| `serveur_ops_tenant` | ingress | 22 | tcp | runner_site | ssh | Insémination : le runner du SITE amorce ce runner-ci — socle, moteur, plan, plancher de résolution — jusqu'à ce qu'un humain lui remette sa voûte. Vers cette machine seule, jamais vers le reste de l'écosystème. |
|
||||
| `serveur_postfix` | ingress | 25 | tcp | externe, client_smtp | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). |
|
||||
|
|
@ -96,11 +97,10 @@
|
|||
| `serveur_postfix` | ingress | 587 | tcp | flotte, externe | starttls | Soumission authentifiée (STARTTLS obligatoire) : agents internes et clients de courriel des utilisateurs. |
|
||||
| `serveur_postfix` | egress | 24 | tcp | serveur_dovecot | tls-requis | Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié. |
|
||||
| `serveur_postfix` | egress | 25 | tcp | externe | starttls | Relais sortant vers les MX distants (STARTTLS opportuniste). |
|
||||
| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS). |
|
||||
| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire, et mots de passe de soumission vérifiés par le Dovecot local (LDAPS). |
|
||||
| `serveur_postfix` | egress | 11332 | tcp | localhost | clair | Filtre milter rspamd co-localisé (antispam + signature DKIM). |
|
||||
| `serveur_postfix` | egress | 12345 | tcp | serveur_dovecot | tls | Validation SASL des identifiants de soumission contre Dovecot. |
|
||||
| `serveur_postgresql` | ingress | 5432 | tcp | serveur_keycloak, serveur_forgejo, serveur_icinga, serveur_nextcloud | tls-requis | Connexions applicatives à PostgreSQL (verify-full ; pg_hba hostssl). |
|
||||
| `serveur_postgresql` | ingress | 9187 | tcp | serveur_prometheus | clair | Metriques PostgreSQL lues par l'observatoire. Series, pas verdicts : ce qui derive lentement — cache qui decroche, connexions qui montent, bases qui grossissent — n'a que le graphe pour se faire voir. En clair pour l'instant, contrairement a `client_metrique` : dette inscrite, a fermer par `--web.config.file` + `client_pki`. |
|
||||
| `serveur_postgresql` | ingress | 9187 | tcp | serveur_prometheus | tls | Metriques PostgreSQL lues par l'observatoire, en HTTPS verifie. Series, pas verdicts : ce qui derive lentement — cache qui decroche, connexions qui montent, bases qui grossissent — n'a que le graphe pour se faire voir. |
|
||||
| `serveur_powerdns` | ingress | 5300 | tcp | dns_public_site | clair | AXFR signe TSIG par le serveur DNS public du site, vers l'instance publique uniquement. |
|
||||
| `serveur_powerdns` | ingress | 5300 | udp | dns_public_site | clair | Interrogation du SOA par le secondaire du site avant chaque transfert. |
|
||||
| `serveur_powerdns` | ingress | derive | udp | flotte | clair | Zone souveraine. 53 seul sur son hôte, 5300 sur la loopback derrière le résolveur. |
|
||||
|
|
@ -109,9 +109,11 @@
|
|||
| `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). |
|
||||
| `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). |
|
||||
| `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. |
|
||||
| `serveur_prometheus` | egress | 9100 | tcp | frontiere | clair | Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni charge dans la supervision. |
|
||||
| `serveur_prometheus` | egress | 9100 | tcp | frontiere | tls | Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni charge dans la supervision. |
|
||||
| `serveur_prometheus` | egress | 9100 | tcp | fabric | clair | Scrape des hyperviseurs : la machine qui PORTE les VM etait invisible de la supervision qui les surveille. Elle ne peut pas pousser (route par defaut gelee, D-57), donc on la tire. |
|
||||
| `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. |
|
||||
| `serveur_relais` | ingress | 22 | tcp | externe | ssh | La console d'administration, sur l'adresse PROPRE du VPS seulement. Les adresses relayees ne menent jamais a ce SSH : elles partent dans le tunnel. |
|
||||
| `serveur_relais` | ingress | derive | udp | externe | wireguard | La frontiere du site ouvre le tunnel depuis son adresse DHCP (port derive : 51800 + index du site). Seule sa cle est admise ; le reste de l'Internet ne recoit aucune reponse. |
|
||||
| `serveur_resolveur` | ingress | 53 | udp | flotte | clair | Toute la flotte du tenant résout ici — et nulle part ailleurs. |
|
||||
| `serveur_resolveur` | ingress | 53 | tcp | flotte | clair | Réponses longues et bascule TCP, obligatoires en DNS. |
|
||||
| `serveur_resolveur` | egress | 53 | udp | externe | clair | Récursion depuis les serveurs racine. Aucun transitaire : l'écosystème ne confie ses questions à personne. |
|
||||
|
|
@ -119,6 +121,8 @@
|
|||
| `serveur_resolveur_site` | ingress | 53 | tcp | voisins_site | clair | Réponses longues et bascule TCP, obligatoires en DNS. |
|
||||
| `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. |
|
||||
| `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. |
|
||||
| `serveur_rspamd` | egress | 80 | tcp | externe | clair | Carte de hachages SURBL (sa-update.surbl.org), publiee en clair par l'amont. Une carte alteree ne peut que mal classer un courriel ; elle n'execute rien. |
|
||||
| `serveur_rspamd` | egress | 443 | tcp | externe | tls-requis | Cartes de rspamd (maps.rspamd.com) : messageries gratuites et jetables, redirecteurs, listes blanches DMARC/SPF/DKIM, hameconnage. Sans elles l'antispam garde les copies du paquet, qui vieillissent. |
|
||||
| `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. |
|
||||
| `serveur_web_dorsal` | ingress | 80 | tcp | edge, serveur_web_frontal | clair | Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics). |
|
||||
| `serveur_web_dorsal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot de chaque app (Forgejo souverain) au deploiement. |
|
||||
|
|
@ -128,10 +132,11 @@
|
|||
|
||||
## Synthèse chiffrement
|
||||
|
||||
- **clair** : 51 flux
|
||||
- **clair** : 49 flux
|
||||
- **n-a** : 8 flux
|
||||
- **ssh** : 8 flux
|
||||
- **ssh** : 10 flux
|
||||
- **starttls** : 6 flux
|
||||
- **tls** : 10 flux
|
||||
- **tls-cible** : 2 flux
|
||||
- **tls-requis** : 34 flux
|
||||
- **tls-requis** : 37 flux
|
||||
- **wireguard** : 1 flux
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri
|
|||
Aucune de ces responsabilités n'est laissée à la parole :
|
||||
|
||||
```
|
||||
make prouver le dépôt est-il cohérent avec lui-même (94 preuves, zéro réseau)
|
||||
make prouver le dépôt est-il cohérent avec lui-même (95 preuves, zéro réseau)
|
||||
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
||||
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
||||
make expositions-plan chaque service publié répond-il, et depuis où
|
||||
|
|
|
|||
|
|
@ -219,6 +219,26 @@ runbooks:
|
|||
Deposer chez chaque locataire la fiche que le site lui destine. Son runner n'a pas
|
||||
le depot du site : c'est par elle que son inventaire se genere sans lui. A commiter
|
||||
dans le depot de chaque locataire.
|
||||
- cible: forge-cles
|
||||
nature: ecriture
|
||||
fixes: {CONFIRMER: "true"}
|
||||
pourquoi: >-
|
||||
Enregistrer sur le depot de chaque locataire la cle de lecture que sa face publie, et
|
||||
retirer celle qui ne l'est plus. Sans elle, son runner ne lit plus son depot prive.
|
||||
- cible: forge-comptes
|
||||
nature: ecriture
|
||||
fixes: {CONFIRMER: "true"}
|
||||
pourquoi: >-
|
||||
Un compte par mainteneur declare, ouvert sur les seuls depots de ses locataires :
|
||||
c'est par un ticket qu'il demande au site. Et le lecteur de la console du site, qui
|
||||
met ces demandes dans « A traiter ».
|
||||
- cible: forge-etancheite
|
||||
nature: mesure
|
||||
portee: poste
|
||||
pourquoi: >-
|
||||
Sans identifiant, chaque depot prive exige une authentification et le moteur
|
||||
repond ; chaque runner de locataire lit son depot et pas celui d'un autre. Du
|
||||
poste, qui voit le site et les runners.
|
||||
- cible: site-verifier
|
||||
nature: mesure
|
||||
pourquoi: "Le playbook du site correspond-il encore aux couches declarees ?"
|
||||
|
|
@ -236,6 +256,19 @@ runbooks:
|
|||
Cloner les VM d'un locataire NOMME, d'apres la face reseau qu'il publie : le
|
||||
runner du site materialise sans monter son depot. Memes machines, memes
|
||||
parametres que `flotte-creer` sur l'instance montee (P93, P94).
|
||||
# UNE SEULE MACHINE, A LA DEMANDE D'UN MAINTENEUR (2026-10-11). `locataire-creer`
|
||||
# clone TOUTES les VM manquantes ; une demande de locataire en vise une. Les
|
||||
# parametres viennent de la meme face reseau (`clonage`), par le meme chemin
|
||||
# (`PARAMS_CLONAGE`). Sans `TENANT`, `creer-vm` exige une instance montee — que le
|
||||
# site n'a pas : l'oubli se refuse, il ne cree rien de travers.
|
||||
- cible: creer-vm
|
||||
nature: ecriture
|
||||
variables: [TENANT, HOTE]
|
||||
duree: "~4 min 30"
|
||||
pourquoi: >-
|
||||
Cloner UNE machine d'un locataire nomme, d'apres sa face reseau publiee. Elle
|
||||
n'existe pour le site que si son mainteneur l'a mise au plan, a applique
|
||||
l'inventaire, puis a republie sa face.
|
||||
- cible: locataire-raser
|
||||
nature: destructif
|
||||
variables: [TENANT, INSTANCE]
|
||||
|
|
|
|||
|
|
@ -78,6 +78,16 @@
|
|||
when:
|
||||
- artefacts_amorcage is defined
|
||||
- artefacts_amorcage | string | length > 0
|
||||
- not (artefacts_amorcage_direct | default(false) | bool)
|
||||
|
||||
# LA MACHINE QUI PORTE LE CACHE VA DROIT CHEZ DEBIAN (2026-10-13). Le fichier herite du
|
||||
# gabarit vise un cache qui n'existe pas encore — le sien — ou une adresse perimee :
|
||||
# l'inventaire le dit par `artefacts_amorcage_direct`, et on le retire.
|
||||
- name: Retirer l'amorcage sur la machine qui porte le cache
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/apt.conf.d/00-setops-amorcage-artefacts
|
||||
state: absent
|
||||
when: artefacts_amorcage_direct | default(false) | bool
|
||||
|
||||
# RETIRER CE QUI DESIGNE UN CACHE QUI N'EXISTE PLUS (2026-09-10).
|
||||
#
|
||||
|
|
|
|||
19
playbooks/groupes/serveur_relais.yml
Normal file
19
playbooks/groupes/serveur_relais.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
- name: Appliquer le groupe serveur_relais
|
||||
hosts: serveur_relais
|
||||
become: true
|
||||
module_defaults:
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: 300
|
||||
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Vérifier que la cible est Debian
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == "Debian"
|
||||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
roles:
|
||||
- serveur_relais
|
||||
43
playbooks/maintenance/forge_cles.yml
Normal file
43
playbooks/maintenance/forge_cles.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
# LE SITE ENREGISTRE LA CLE DE LECTURE DE CHAQUE LOCATAIRE SUR SON DEPOT (2026-10-11).
|
||||
#
|
||||
# Voir `docs/depots-prives-conception.md` et `scripts/forge_cles.py`. Le script tourne SUR le
|
||||
# runner du site : il y lit les faces que le site tire (et rien d'autre de ses locataires), et
|
||||
# il y joint la forge. Le mot de passe d'administration vient de la voute du site, par
|
||||
# l'environnement, et n'est jamais affiche.
|
||||
#
|
||||
# make forge-cles montre ce qui serait fait
|
||||
# make forge-cles CONFIRMER=true l'ecrit, puis relit
|
||||
- name: Les cles de lecture des locataires, sur la forge du site
|
||||
hosts: serveur_ops_site
|
||||
gather_facts: false
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
|
||||
vars:
|
||||
forge_cles_racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
|
||||
forge_cles_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}"
|
||||
forge_cles_confirmer: "{{ lookup('env', 'CONFIRMER') == 'true' }}"
|
||||
tasks:
|
||||
- name: Exiger le mot de passe d'administration de la forge (voute du site)
|
||||
ansible.builtin.assert:
|
||||
that: vault_forgejo_admin | default('') | length > 0
|
||||
fail_msg: "vault_forgejo_admin absent : la voute du site n'est pas chargee."
|
||||
no_log: true
|
||||
|
||||
- name: Confronter les faces aux cles de la forge
|
||||
ansible.builtin.command:
|
||||
argv: "{{ ['python3', 'scripts/forge_cles.py']
|
||||
+ (['--confirmer'] if (forge_cles_confirmer | bool) else []) }}"
|
||||
chdir: "{{ forge_cles_racine }}/{{ forge_cles_moteur }}"
|
||||
environment:
|
||||
FORGEJO_ADMIN: "{{ vault_forgejo_admin }}"
|
||||
PATH: "{{ forge_cles_racine }}/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
# En simulation, le script ne fait que montrer (sans --confirmer) : il peut tourner.
|
||||
check_mode: false
|
||||
register: forge_cles
|
||||
changed_when: "'ajoutee' in forge_cles.stdout or 'retiree' in forge_cles.stdout or 'rendu ' in forge_cles.stdout"
|
||||
failed_when: forge_cles.rc not in [0]
|
||||
|
||||
- name: Ce que la forge porte
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ forge_cles.stdout_lines }}"
|
||||
47
playbooks/maintenance/forge_comptes.yml
Normal file
47
playbooks/maintenance/forge_comptes.yml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
# LES COMPTES DE LA FORGE DU SITE (2026-10-11, lot 3b).
|
||||
#
|
||||
# Voir `docs/depots-prives-conception.md` et `scripts/forge_comptes.py`. Le script tourne SUR le
|
||||
# runner du site : il y lit le plan du site (`mainteneurs`) et les faces de ses locataires, et
|
||||
# il y joint la forge. Le mot de passe d'administration vient de la voute du site, par
|
||||
# l'environnement, et n'est jamais affiche.
|
||||
#
|
||||
# make forge-comptes montre ce qui serait fait
|
||||
# make forge-comptes CONFIRMER=true l'ecrit, puis relit
|
||||
- name: Les comptes de la forge du site
|
||||
hosts: serveur_ops_site
|
||||
gather_facts: false
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur | default('setops') }}"
|
||||
vars:
|
||||
forge_comptes_racine: "{{ serveur_ops_racine | default('/opt/setops') }}"
|
||||
forge_comptes_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}"
|
||||
forge_comptes_confirmer: "{{ lookup('env', 'CONFIRMER') == 'true' }}"
|
||||
tasks:
|
||||
- name: Exiger le mot de passe d'administration de la forge (voute du site)
|
||||
ansible.builtin.assert:
|
||||
that: vault_forgejo_admin | default('') | length > 0
|
||||
fail_msg: "vault_forgejo_admin absent : la voute du site n'est pas chargee."
|
||||
no_log: true
|
||||
|
||||
- name: Confronter le plan du site aux comptes de la forge
|
||||
ansible.builtin.command:
|
||||
argv: "{{ ['python3', 'scripts/forge_comptes.py']
|
||||
+ (['--confirmer'] if (forge_comptes_confirmer | bool) else []) }}"
|
||||
chdir: "{{ forge_comptes_racine }}/{{ forge_comptes_moteur }}"
|
||||
environment:
|
||||
FORGEJO_ADMIN: "{{ vault_forgejo_admin }}"
|
||||
PATH: "{{ forge_comptes_racine }}/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
# En simulation, le script ne fait que montrer (sans --confirmer) : il peut tourner.
|
||||
check_mode: false
|
||||
register: forge_comptes
|
||||
# LES LIGNES D'ACTION, PAS DES MOTS : « jeton a creer », dans le plan, contient « jeton »
|
||||
# et faisait compter un changement alors que rien n'etait ecrit (2026-10-11).
|
||||
changed_when: forge_comptes.stdout is search('(?m)^ (cree|collab|retire|equipe|jeton) ')
|
||||
# 1 = un ecart DIT (un courriel a remplir, par exemple) : il s'affiche, il n'empeche pas
|
||||
# le reste. 2 = le script a refuse de travailler.
|
||||
failed_when: forge_comptes.rc not in [0, 1]
|
||||
|
||||
- name: Ce que la forge porte
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ forge_comptes.stdout_lines }}"
|
||||
|
|
@ -61,6 +61,11 @@
|
|||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
|
||||
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
|
||||
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_compares }}"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#
|
||||
# make genome-pousser tous les depots portes par le runner
|
||||
# make genome-pousser DEPOT=<nom> un seul (le nom du depot sur la forge)
|
||||
# make genome-pousser ARGS=--check ce que la poussee ferait, depot par depot, sans rien ecrire
|
||||
#
|
||||
# L'ECRITURE S'AUTHENTIFIE, LA LECTURE NON. `serveur_ops` clone en anonyme, par doctrine.
|
||||
# Pousser est un autre acte : le runner s'y presente avec SA cle — celle que `serveur_ops`
|
||||
|
|
@ -95,6 +96,181 @@
|
|||
# depot par depot, et l'exploitant doit aller cliquer. La cible s'en charge : la
|
||||
# declaration est idempotente (422 = deja la), et sa portee est exactement ce qu'on
|
||||
# pousse, jamais plus large.
|
||||
# UNE FORGE NEUVE, UN RUNNER NEUF (2026-10-13, premier site construit depuis zero). Le
|
||||
# runner du site ne fabrique sa cle qu'APRES avoir clone le genome, et il ne peut cloner
|
||||
# que ce que cette commande a pousse — qui exige sa cle. Personne n'allait au bout. On
|
||||
# fabrique donc la cle ici si elle manque, A L'IDENTIQUE de `serveur_ops` (meme commande,
|
||||
# meme commentaire, `creates:`) : le role la retrouvera et ne la refera pas.
|
||||
- name: Assurer le répertoire SSH du runner
|
||||
ansible.builtin.file:
|
||||
path: "{{ genome_racine }}/.ssh"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: Fabriquer la clé du runner si elle n'existe pas encore
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
ssh-keygen -t {{ serveur_ops_cle_type | default('ed25519') }} -N ''
|
||||
-C "setops@{{ inventory_hostname }}.{{ domaine_interne }}"
|
||||
-f {{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}
|
||||
creates: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}"
|
||||
|
||||
# L'ORGANISATION ET SES DEPOTS N'ETAIENT CREES PAR PERSONNE : au site de Chezlepro, une
|
||||
# fois, a la main. Meme forme qu'elle : organisation PUBLIQUE ; le moteur et les modeles
|
||||
# PUBLICS (le runner les clone en anonyme) ; la carte du site et les plans des
|
||||
# locataires PRIVES (`serveur_ops_roles_prives`, lus en SSH par cle de deploiement).
|
||||
- name: L'organisation du génome existe-t-elle ?
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}"
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
# UNE LECTURE, DONC AUSSI EN SIMULATION : sans elle, `--check` sautait la sonde et la
|
||||
# tache suivante echouait sur un statut absent (2026-10-14).
|
||||
check_mode: false
|
||||
register: genome_org
|
||||
|
||||
- name: Créer l'organisation du génome
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/orgs"
|
||||
method: POST
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
body_format: json
|
||||
body:
|
||||
username: "{{ serveur_ops_forge_organisation | default('genome') }}"
|
||||
visibility: public
|
||||
status_code: 201
|
||||
no_log: true
|
||||
when: genome_org.status == 404
|
||||
|
||||
- name: Quels dépôts poussés existent déjà sur la forge ?
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation | default('genome') }}/{{ item.depot }}"
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
no_log: true
|
||||
check_mode: false
|
||||
register: genome_depots_forge
|
||||
|
||||
# LA SIMULATION DIT CE QUE LA POUSSEE FERAIT, PUIS S'ARRETE (2026-10-14).
|
||||
#
|
||||
# Tout ce qui suit est `uri` en ecriture ou `command` : `--check` le saute, et le verdict
|
||||
# final comparait alors une forge intacte a des colis jamais recus. On ne lit donc que la
|
||||
# forge et le poste, on compare, et on rend la main avant la premiere ecriture.
|
||||
- name: Simulation — la branche que porte la forge
|
||||
ansible.builtin.uri:
|
||||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.item.depot }}/branches/{{ item.item.branche | default('main') }}
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_depots_forge.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }}"
|
||||
register: genome_sim_forge
|
||||
retries: 4
|
||||
delay: 3
|
||||
until: genome_sim_forge is succeeded
|
||||
check_mode: false
|
||||
no_log: true
|
||||
when:
|
||||
- ansible_check_mode
|
||||
- item.status == 200
|
||||
|
||||
# `--left-right` : a gauche ce que la forge porte et que le poste n'a pas (le runner
|
||||
# refusera, `--ff-only`), a droite ce qui serait pousse. rc 128 : le poste ne connait
|
||||
# pas le commit de la forge — l'avance rapide est impossible a juger d'ici.
|
||||
- name: Simulation — l'écart entre la forge et le poste
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- git
|
||||
- -C
|
||||
- "{{ genome_freres }}/{{ item.item.item.dest }}"
|
||||
- rev-list
|
||||
- --count
|
||||
- --left-right
|
||||
- "{{ item.json.commit.id }}...refs/heads/{{ item.item.item.branche | default('main') }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
loop: "{{ genome_sim_forge.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.item.depot }}"
|
||||
register: genome_sim_ecart
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when:
|
||||
- ansible_check_mode
|
||||
- item.status | default(0) == 200
|
||||
|
||||
- name: Simulation — ce que la poussée ferait
|
||||
vars:
|
||||
genome_sim_lignes: >-
|
||||
{%- set out = [] -%}
|
||||
{%- if genome_org.status == 404 -%}
|
||||
{%- set _ = out.append('organisation ' ~ (serveur_ops_forge_organisation | default('genome')) ~ ' : à créer') -%}
|
||||
{%- endif -%}
|
||||
{%- for r in genome_sim_ecart.results -%}
|
||||
{%- set d = r.item.item.item.depot -%}
|
||||
{%- if r.item.item.status == 404 -%}
|
||||
{%- set _ = out.append(d ~ ' : dépôt à créer, puis tout poussé') -%}
|
||||
{%- elif r.item.status | default(0) == 404 -%}
|
||||
{%- set _ = out.append(d ~ ' : dépôt vide, tout poussé') -%}
|
||||
{%- elif r.rc != 0 -%}
|
||||
{%- set _ = out.append(d ~ ' : la forge porte ' ~ r.item.json.commit.id[:7]
|
||||
~ ', inconnu du poste — avance rapide impossible à juger d ici') -%}
|
||||
{%- else -%}
|
||||
{%- set lr = r.stdout.split() | map('int') | list -%}
|
||||
{%- if lr[0] > 0 -%}
|
||||
{%- set _ = out.append(d ~ ' : la forge a ' ~ lr[0] ~ ' commit(s) absent(s) du poste — le runner REFUSERA (--ff-only)') -%}
|
||||
{%- elif lr[1] == 0 -%}
|
||||
{%- set _ = out.append(d ~ ' : à jour (' ~ r.item.json.commit.id[:7] ~ ')') -%}
|
||||
{%- else -%}
|
||||
{%- set _ = out.append(d ~ ' : ' ~ r.item.json.commit.id[:7] ~ ' → ' ~ lr[1] ~ ' commit(s) en avance rapide') -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ genome_sim_lignes }}"
|
||||
when: ansible_check_mode
|
||||
|
||||
- name: Simulation — s'arrêter avant la première écriture
|
||||
ansible.builtin.meta: end_host
|
||||
when: ansible_check_mode
|
||||
|
||||
- name: Créer les dépôts qui manquent
|
||||
ansible.builtin.uri:
|
||||
url: "https://forge.{{ domaine_interne }}/api/v1/orgs/{{ serveur_ops_forge_organisation | default('genome') }}/repos"
|
||||
method: POST
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
body_format: json
|
||||
body:
|
||||
name: "{{ item.item.depot }}"
|
||||
private: "{{ item.item.role in (serveur_ops_roles_prives | default(['instance', 'tenant', 'hebergeur'])) }}"
|
||||
default_branch: "{{ item.item.branche | default('main') }}"
|
||||
auto_init: false
|
||||
status_code: 201
|
||||
loop: "{{ genome_depots_forge.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }}"
|
||||
no_log: true
|
||||
when: item.status == 404
|
||||
|
||||
- name: Lire la clé publique du runner
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ genome_racine }}/.ssh/id_{{ serveur_ops_cle_type | default('ed25519') }}.pub"
|
||||
|
|
@ -160,6 +336,7 @@
|
|||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.dest }}"
|
||||
register: genome_emballe
|
||||
changed_when: true
|
||||
|
||||
- name: Porter les colis jusqu'au runner
|
||||
|
|
@ -215,11 +392,16 @@
|
|||
|
||||
- name: Demander à la forge ce qu'elle porte maintenant
|
||||
# On interroge son API plutôt que `git ls-remote` : c'est la forge elle-même qui
|
||||
# répond, et la lecture est anonyme — aucun justificatif n'entre ici.
|
||||
# répond. Lecture authentifiée depuis le 2026-10-11 (dépôts privés).
|
||||
ansible.builtin.uri:
|
||||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }}
|
||||
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
|
||||
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
|
|
@ -255,6 +437,50 @@
|
|||
label: "{{ item.depot }}"
|
||||
index_var: idx
|
||||
|
||||
# LES RELEASES AUSSI (2026-10-07). Le colis ne portait que la branche : `v2026.08.21` et
|
||||
# `v2026.10.07` manquaient a la forge du site. Il porte desormais les etiquettes ; on
|
||||
# demande a la forge si elle a TOUTES celles du poste. 404 = aucune etiquette sur ce
|
||||
# depot, ce qui est juste quand le poste n'en a pas non plus.
|
||||
- name: Demander à la forge les étiquettes qu'elle porte
|
||||
ansible.builtin.uri:
|
||||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/git/refs/tags
|
||||
# AUTHENTIFIE (2026-10-11) : les depots des locataires et du site deviennent prives, et
|
||||
# l'API anonyme y repondrait 404 — un « absent » qui n'en serait pas un.
|
||||
url_username: forgejo-admin
|
||||
url_password: "{{ vault_forgejo_admin }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
register: genome_forge_etiquettes
|
||||
retries: 4
|
||||
delay: 3
|
||||
until: genome_forge_etiquettes is succeeded
|
||||
changed_when: false
|
||||
|
||||
- name: La forge porte-t-elle toutes les releases du poste ?
|
||||
vars:
|
||||
genome_etiquettes_poste: "{{ (genome_emballe.results[idx].stdout | from_json).etiquettes | default([]) }}"
|
||||
genome_etiquettes_forge: >-
|
||||
{{ (genome_forge_etiquettes.results[idx].json | default([]) if genome_forge_etiquettes.results[idx].status == 200 else [])
|
||||
| map(attribute='ref') | map('regex_replace', '^refs/tags/', '') | list }}
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- genome_etiquettes_poste | difference(genome_etiquettes_forge) | length == 0
|
||||
fail_msg: >-
|
||||
{{ item.depot }} : la forge n'a pas
|
||||
{{ genome_etiquettes_poste | difference(genome_etiquettes_forge) | join(', ') }}.
|
||||
success_msg: >-
|
||||
{{ item.depot }} : {{ genome_etiquettes_poste | length }} etiquette(s) du poste, toutes sur la forge.
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
index_var: idx
|
||||
|
||||
# LE RUNNER DU SITE AVANCE SON PROPRE CLONE, ET SA CONSOLE NE LE SAIT PAS (2026-09-20).
|
||||
#
|
||||
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le
|
||||
|
|
|
|||
|
|
@ -21,13 +21,17 @@
|
|||
# vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja
|
||||
# (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent
|
||||
# (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge.
|
||||
setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# LA CARTE DESIGNEE (2026-10-13) : `SETOPS_UNDERLAY` quand il est pose, le symlink sinon.
|
||||
# Lire le symlink seul aurait clone les machines du site de TechnoLibre sur le cluster
|
||||
# de Chezlepro, avec ses jetons — l'inventaire, lui, suivait deja la variable.
|
||||
setops_carte_underlay: "{{ lookup('ansible.builtin.env', 'SETOPS_UNDERLAY') | default(playbook_dir ~ '/../../underlay.yml', true) }}"
|
||||
setops_proxmox_hebergeur: "{{ setops_carte_underlay | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# LA VOUTE DE L'UNDERLAY — les secrets du MONDE PHYSIQUE (jeton d'API du cluster).
|
||||
# Meme derivation que le fichier ci-dessus : le symlink `underlay.yml` designe deja
|
||||
# l'hebergeur, on n'a rien a redeclarer. Depuis le 2026-08-22 ces secrets ne vivent
|
||||
# plus dans la voute d'un tenant : recopies chez chacun, ils ne pouvaient plus etre
|
||||
# revoques isolement (docs/frontiere-physique-virtuel.md).
|
||||
setops_voute_underlay: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/underlay.vault.yml"
|
||||
setops_voute_underlay: "{{ setops_carte_underlay | realpath | dirname }}/underlay.vault.yml"
|
||||
|
||||
tasks:
|
||||
- name: Verifier les fichiers de variables Proxmox optionnels
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@
|
|||
# vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja
|
||||
# (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent
|
||||
# (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge.
|
||||
setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# LA CARTE DESIGNEE (2026-10-13) : `SETOPS_UNDERLAY` quand il est pose, le symlink sinon.
|
||||
# Lire le symlink seul aurait clone les machines du site de TechnoLibre sur le cluster
|
||||
# de Chezlepro, avec ses jetons — l'inventaire, lui, suivait deja la variable.
|
||||
setops_carte_underlay: "{{ lookup('ansible.builtin.env', 'SETOPS_UNDERLAY') | default(playbook_dir ~ '/../../underlay.yml', true) }}"
|
||||
setops_proxmox_hebergeur: "{{ setops_carte_underlay | realpath | dirname }}/proxmox-hebergeur.yml"
|
||||
# cibles_vmid : liste d'entiers, passée en -e (sinon dérivée des hôtes actifs).
|
||||
cibles_vmid: "{{ groups['hotes_actifs'] | map('extract', hostvars, 'proxmox_vmid') | map('int') | list }}"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@
|
|||
- name: Couche socle — groupe serveur_durci
|
||||
import_playbook: groupes/serveur_durci.yml
|
||||
|
||||
# --- couche depot_etat ---
|
||||
- name: Couche depot_etat — groupe serveur_backup
|
||||
import_playbook: groupes/serveur_backup.yml
|
||||
- name: Couche depot_etat — groupe serveur_backup_site
|
||||
import_playbook: groupes/serveur_backup_site.yml
|
||||
|
||||
# --- couche pki_racine ---
|
||||
- name: Couche pki_racine — groupe serveur_step_ca
|
||||
import_playbook: groupes/serveur_step_ca.yml
|
||||
|
|
@ -50,8 +56,6 @@
|
|||
import_playbook: groupes/serveur_nginx.yml
|
||||
- name: Couche services — groupe serveur_rspamd
|
||||
import_playbook: groupes/serveur_rspamd.yml
|
||||
- name: Couche services — groupe serveur_backup
|
||||
import_playbook: groupes/serveur_backup.yml
|
||||
- name: Couche services — groupe serveur_artefacts
|
||||
import_playbook: groupes/serveur_artefacts.yml
|
||||
- name: Couche services — groupe serveur_cache_site
|
||||
|
|
@ -60,8 +64,8 @@
|
|||
import_playbook: groupes/serveur_forge_site.yml
|
||||
- name: Couche services — groupe serveur_resolveur_site
|
||||
import_playbook: groupes/serveur_resolveur_site.yml
|
||||
- name: Couche services — groupe serveur_backup_site
|
||||
import_playbook: groupes/serveur_backup_site.yml
|
||||
- name: Couche services — groupe serveur_relais
|
||||
import_playbook: groupes/serveur_relais.yml
|
||||
- name: Couche services — groupe serveur_dns_public
|
||||
import_playbook: groupes/serveur_dns_public.yml
|
||||
- name: Couche services — groupe serveur_resolveur
|
||||
|
|
|
|||
|
|
@ -11,11 +11,23 @@
|
|||
gather_facts: false
|
||||
become: false
|
||||
tasks:
|
||||
- name: Interroger l'API des cibles actives
|
||||
# UN PROMETHEUS QUI VIENT DE REDEMARRER N'A PAS ENCORE RELEVE SES CIBLES (2026-10-08).
|
||||
# Depuis qu'obs-01 est sauvegarde, son premier depot (copie a froid, en fin de
|
||||
# deploiement) relance Prometheus : a la reconstruction de Technolibre, la recette l'a
|
||||
# interroge 18 s apres, et deux cibles n'avaient pas encore de releve reussi — saines
|
||||
# quelques minutes plus tard. On redemande donc, une minute au plus, avant de juger :
|
||||
# une vraie panne echoue toujours, une minute plus tard.
|
||||
- name: Interroger l'API des cibles actives (jusqu'a ce que toutes soient relevees, une minute au plus)
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:9090/api/v1/targets?state=active"
|
||||
return_content: true
|
||||
register: prom_targets
|
||||
until: >-
|
||||
((prom_targets.content | default('{}') | from_json).get('data', {}).get('activeTargets', [])
|
||||
| rejectattr('health', 'equalto', 'up') | list | length == 0)
|
||||
or (prom_targets.attempts | default(0) | int >= 6)
|
||||
retries: 6
|
||||
delay: 10
|
||||
|
||||
- name: Analyser up / down
|
||||
vars:
|
||||
|
|
|
|||
|
|
@ -83,6 +83,29 @@ client_backup_catalogue:
|
|||
# PAS DE `serveur_web_frontal` (retire le 2026-09-30) : le frontal RELAIE, il ne sert
|
||||
# rien lui-meme — ses vhosts, son WAF et sa page 404 se redeploient depuis le depot.
|
||||
# `/srv/web` venait de l'epoque ou il servait du statique ; ses instantanes etaient vides.
|
||||
# LES METRIQUES ET LES JOURNAUX (2026-10-07). Ils repartaient de zero a chaque
|
||||
# reconstruction. Ils s'ecrivent en continu : une copie A FROID (service arrete le temps
|
||||
# de copier, `setops-copie-a-froid`) les pose dans le repertoire de preparation, et c'est
|
||||
# cette copie que restic emporte. `vivant` dit ou vit la base, pour la restauration et les
|
||||
# temoins, qui la comparent a l'instantane.
|
||||
serveur_prometheus:
|
||||
nom: prometheus
|
||||
vivant: "{{ serveur_prometheus_donnees | default('/var/lib/prometheus/metrics2') }}"
|
||||
commande: >-
|
||||
/usr/local/sbin/setops-copie-a-froid
|
||||
{{ serveur_prometheus_service | default('prometheus') }}
|
||||
{{ serveur_prometheus_donnees | default('/var/lib/prometheus/metrics2') }}
|
||||
{{ client_backup_staging }}/prometheus/metrics2 --blocs-immuables
|
||||
chemins: ["{{ client_backup_staging }}/prometheus"]
|
||||
serveur_loki:
|
||||
nom: loki
|
||||
vivant: "{{ serveur_loki_chemin | default('/var/lib/loki') }}"
|
||||
commande: >-
|
||||
/usr/local/sbin/setops-copie-a-froid
|
||||
{{ serveur_loki_service | default('loki') }}
|
||||
{{ serveur_loki_chemin | default('/var/lib/loki') }}
|
||||
{{ client_backup_staging }}/loki/donnees --lier chunks --copier chunks/index
|
||||
chemins: ["{{ client_backup_staging }}/loki"]
|
||||
serveur_icinga:
|
||||
# L'AC D'ICINGA FAIT L'IDENTITE DE SA BASE (2026-10-07). Icinga 2 derive l'environnement
|
||||
# d'Icinga DB de la cle publique de son AC, et chaque identifiant d'hote, de service et
|
||||
|
|
|
|||
84
roles/client_backup/files/setops-copie-a-froid.sh
Normal file
84
roles/client_backup/files/setops-copie-a-froid.sh
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/bin/bash
|
||||
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
|
||||
#
|
||||
# setops-copie-a-froid SERVICE SOURCE DESTINATION [--blocs-immuables] [--lier REL]... [--copier REL]...
|
||||
#
|
||||
# Copie COHERENTE d'une base qui ecrit en continu (Prometheus, Loki) : le service est
|
||||
# arrete le temps de la copie, puis redemarre — QUOI QU'IL ARRIVE a la copie, et seulement
|
||||
# s'il tournait avant. La destination n'est remplacee qu'une fois la copie complete.
|
||||
#
|
||||
# POURQUOI A FROID (decision de l'exploitant, 2026-10-07 : « on ne prend pas de risque »).
|
||||
# Copier a chaud un WAL qu'on ecrit, c'est sauvegarder un fichier a moitie ecrit. L'autre
|
||||
# voie — ne garder que les blocs fermes — perdait jusqu'aux deux dernieres heures de
|
||||
# metriques a chaque reconstruction. Mesure au site : 2,9 Go de Prometheus se lisent en
|
||||
# 4,2 s, son WAL seul en 0,3 s ; Loki entier en 2 s.
|
||||
#
|
||||
# `--blocs-immuables` : les sous-dossiers nommes par un ULID (26 caracteres, `01…`) sont
|
||||
# les blocs de Prometheus, que rien ne reecrit apres leur fermeture. On les recopie par
|
||||
# LIENS DURS — instantane, sans place — et le reste (WAL, `chunks_head`) par une vraie
|
||||
# copie. Si la destination est sur un autre systeme de fichiers, on copie tout.
|
||||
#
|
||||
# `--lier REL` lie tout un sous-dossier ; `--copier REL` y copie vraiment un sous-dossier qui,
|
||||
# lui, bouge (2026-10-07). Pour Loki : `--lier chunks --copier chunks/index`. Ses morceaux sont
|
||||
# adresses par leur contenu (`debut:fin:somme`, decode sur site-mon-01) et ne bougent jamais ;
|
||||
# mais `chunks/index/` loge aussi `delete_requests.gz`, la base du compacteur, mesuree
|
||||
# reecrite trois semaines apres sa naissance. La lier, c'etait laisser la sauvegarde changer
|
||||
# avec elle. Copier 6 550 morceaux arretait Loki 13 s ; les lier prend 40 ms.
|
||||
set -euo pipefail
|
||||
|
||||
service="${1:?service}"
|
||||
source="${2:?source}"
|
||||
dest="${3:?destination}"
|
||||
shift 3
|
||||
blocs="" lier=() copier=()
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--blocs-immuables) blocs=1; shift ;;
|
||||
--lier) lier+=("$2"); shift 2 ;;
|
||||
--copier) copier+=("$2"); shift 2 ;;
|
||||
*) echo "copie a froid : option inconnue : $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -d "${source}" ]] || { echo "copie a froid : ${source} n'existe pas — rien a copier." >&2; exit 0; }
|
||||
|
||||
tmp="${dest}.en-cours"
|
||||
rm -rf "${tmp}"
|
||||
mkdir -p "$(dirname "${dest}")"
|
||||
|
||||
actif=0
|
||||
if systemctl is-active --quiet "${service}"; then actif=1; fi
|
||||
relancer() { if (( actif )); then systemctl start "${service}"; fi; }
|
||||
|
||||
debut=$(date +%s%N)
|
||||
if (( actif )); then systemctl stop "${service}"; fi
|
||||
trap relancer EXIT
|
||||
|
||||
lie() { cp -al "$1" "$2" 2>/dev/null || { rm -rf "$2"; cp -a "$1" "$2"; }; }
|
||||
mkdir -p "${tmp}"
|
||||
for e in "${source}"/* "${source}"/.[!.]*; do
|
||||
[[ -e "${e}" ]] || continue
|
||||
n="$(basename "${e}")"
|
||||
if [[ -n "${blocs}" && -d "${e}" && ${#n} -eq 26 && "${n}" == 01* ]] \
|
||||
|| [[ " ${lier[*]:-} " == *" ${n} "* ]]; then
|
||||
lie "${e}" "${tmp}/${n}"
|
||||
else
|
||||
cp -a "${e}" "${tmp}/${n}"
|
||||
fi
|
||||
done
|
||||
# Ce qui bouge, a l'interieur d'un dossier lie : remplace par une vraie copie.
|
||||
for c in "${copier[@]:+${copier[@]}}"; do
|
||||
if [[ -e "${source}/${c}" ]]; then
|
||||
rm -rf "${tmp:?}/${c}"
|
||||
mkdir -p "$(dirname "${tmp}/${c}")"
|
||||
cp -a "${source}/${c}" "${tmp}/${c}"
|
||||
fi
|
||||
done
|
||||
|
||||
trap - EXIT
|
||||
relancer
|
||||
fin=$(date +%s%N)
|
||||
|
||||
rm -rf "${dest}"
|
||||
mv "${tmp}" "${dest}"
|
||||
echo "copie a froid de ${source} : ${service} $( (( actif )) && echo "arrete (jusqu a la relance, rejeu du WAL non compris) $(( (fin - debut) / 1000000 )) ms" || echo "n'etait pas actif")"
|
||||
|
|
@ -19,7 +19,8 @@ VERDICTS, PAR JEU :
|
|||
clés et certificats de l'AC, AC d'Icinga et son environnement, clé DKIM,
|
||||
`instanceid` de Nextcloud, mot de passe d'une entrée de l'annuaire ; une
|
||||
base dont plus aucune ligne d'avant ne subsiste ; une ligne d'historique
|
||||
perdue. Exactement ce que la reconstruction du
|
||||
perdue ; des métriques qui ne couvrent plus l'avant-rasage ; des journaux
|
||||
dont aucun morceau d'avant ne subsiste. Exactement ce que la reconstruction du
|
||||
2026-09-30 avait perdu sans que rien ne le dise.
|
||||
|
||||
Code de sortie : 0 sans écart, 1 avec au moins un écart, 2 erreur d'usage.
|
||||
|
|
@ -30,6 +31,7 @@ import argparse
|
|||
import base64
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
|
@ -174,6 +176,80 @@ def instance_id(r: Rapport, avant: Path, vivant: Path) -> None:
|
|||
r.ecarts.append(f"IDENTITE instanceid : {a} -> {v or '(absent)'}")
|
||||
|
||||
|
||||
# --- Les bases copiees a froid : Prometheus et Loki (2026-10-07) ----------------------
|
||||
#
|
||||
# Leurs FICHIERS ne se comparent pas : Prometheus fusionne ses blocs, Loki compacte son
|
||||
# index, et la retention (15 j, 31 j) en retire chaque jour. On compare ce qui compte.
|
||||
|
||||
def blocs_prometheus(racine: Path) -> list[tuple[int, int]]:
|
||||
out = []
|
||||
for meta in racine.glob("01*/meta.json") if racine.is_dir() else []:
|
||||
try:
|
||||
m = json.loads(meta.read_text())
|
||||
out.append((int(m["minTime"]), int(m["maxTime"])))
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def segments_wal(racine: Path) -> list[str]:
|
||||
return sorted(p.name for p in (racine / "wal").glob("[0-9]*")) if (racine / "wal").is_dir() else []
|
||||
|
||||
|
||||
def jeu_prometheus(avant: Path, vivant: Path) -> Rapport:
|
||||
"""Les métriques vivantes couvrent-elles encore l'avant-rasage ?
|
||||
|
||||
Le repère : le DÉBUT du bloc le plus récent de l'instantané — quelques heures avant le
|
||||
rasage. Un bloc vivant doit couvrir cet instant. La rétention ne l'a pas retiré (il a
|
||||
moins de 15 jours) ; une base qui n'a pas été restaurée ne peut pas le couvrir : elle
|
||||
est née après la reconstruction. Une base de moins de deux heures n'a pas encore de
|
||||
bloc : son WAL fait alors le repère, et doit se retrouver dans la base vivante, ou y
|
||||
avoir été fermé en bloc.
|
||||
"""
|
||||
r = Rapport("prometheus")
|
||||
a, v = blocs_prometheus(avant), blocs_prometheus(vivant)
|
||||
r.compte = len(a)
|
||||
if a:
|
||||
t = max(debut for debut, _ in a)
|
||||
if not any(debut <= t < fin for debut, fin in v):
|
||||
r.ecarts.append(f"aucun bloc vivant ne couvre {t} (debut du dernier bloc d'avant) : "
|
||||
f"metriques d'avant non restaurees")
|
||||
if v and min(d for d, _ in v) > min(d for d, _ in a):
|
||||
r.changes.append(f"plus ancienne donnee : {min(d for d, _ in a)} -> {min(d for d, _ in v)} (retention)")
|
||||
else:
|
||||
wal = segments_wal(avant)
|
||||
if wal and not v and wal[0] not in segments_wal(vivant):
|
||||
r.ecarts.append(f"le WAL d'avant ({wal[0]}...) n'est pas dans la base vivante, et elle n'a "
|
||||
f"aucun bloc : metriques d'avant non restaurees")
|
||||
if len(v) != len(a):
|
||||
r.changes.append(f"{len(a)} bloc(s) avant, {len(v)} maintenant (fusions, retention, nouveaux)")
|
||||
return r
|
||||
|
||||
|
||||
def jeu_loki(avant: Path, vivant: Path) -> Rapport:
|
||||
"""Des morceaux de journal d'avant subsistent-ils ? Aucun : non restaurés.
|
||||
|
||||
La rétention (31 j) en retire chaque jour : une perte PARTIELLE est listée. Une perte
|
||||
TOTALE, elle, ne vient que d'une base qui n'a pas été remise.
|
||||
"""
|
||||
r = Rapport("loki")
|
||||
# LES MORCEAUX, PAS L'INDEX (2026-10-08). `chunks/index/` loge l'index TSDB et la base du
|
||||
# compacteur : il les fusionne et les remplace. A la reconstruction de Technolibre, trois
|
||||
# de ces fichiers « d'avant » manquaient, et le temoin les disait retires « (retention ?) »
|
||||
# — aucun journal ne l'etait (Loki rendait ceux d'avant le rasage, minute par minute).
|
||||
morceaux = lambda racine: {rel for rel in arbre(racine / "chunks") if not rel.startswith("index/")} # noqa: E731
|
||||
a = morceaux(avant)
|
||||
v = morceaux(vivant)
|
||||
r.compte = len(a)
|
||||
if a and not (a & v):
|
||||
r.ecarts.append(f"aucun des {len(a)} morceau(x) de journal d'avant ne subsiste : journaux non restaures")
|
||||
elif a - v:
|
||||
r.retires.append(f"{len(a - v)} morceau(x) d'avant retire(s) (retention ?)")
|
||||
if v - a:
|
||||
r.nouveaux.append(f"{len(v - a)} morceau(x) nouveau(x)")
|
||||
return r
|
||||
|
||||
|
||||
# --- L'annuaire ----------------------------------------------------------------------
|
||||
|
||||
def entrees_ldif(texte: str) -> dict[str, dict[str, list[str]]]:
|
||||
|
|
@ -317,6 +393,8 @@ def main(argv: list[str] | None = None, interroger=psql) -> int:
|
|||
ap.add_argument("--heure", default="")
|
||||
ap.add_argument("--hote", default="")
|
||||
ap.add_argument("--jeu", action="append", default=[], help="NOM=CHEMIN[:CHEMIN...]")
|
||||
ap.add_argument("--vivant", action="append", default=[],
|
||||
help="NOM=CHEMIN : ou vit la base d'un jeu copie a froid")
|
||||
ap.add_argument("--ldif", default="", help="chemin du LDIF dans l'instantané")
|
||||
ap.add_argument("--ldif-vivant", type=Path, default=None)
|
||||
ap.add_argument("--dump", default="", help="chemin du pg_dumpall dans l'instantané")
|
||||
|
|
@ -327,9 +405,17 @@ def main(argv: list[str] | None = None, interroger=psql) -> int:
|
|||
print(" SANS OBJET : ce noeud ne detient aucun etat.")
|
||||
return 0
|
||||
ecart = False
|
||||
vivants = dict(v.partition("=")[::2] for v in a.vivant)
|
||||
for spec in a.jeu:
|
||||
nom, _, chemins = spec.partition("=")
|
||||
if nom == "openldap":
|
||||
# La copie a froid est le SEUL sous-dossier de la preparation du jeu.
|
||||
copie = lambda: next((d for d in sorted((a.avant / chemins.split(":")[0].lstrip("/")).glob("*")) # noqa: E731
|
||||
if d.is_dir()), a.avant / "absent")
|
||||
if nom == "prometheus" and nom in vivants:
|
||||
r, unite = jeu_prometheus(copie(), Path(vivants[nom])), "bloc(s)"
|
||||
elif nom == "loki" and nom in vivants:
|
||||
r, unite = jeu_loki(copie(), Path(vivants[nom])), "morceau(x)"
|
||||
elif nom == "openldap":
|
||||
r, unite = jeu_annuaire(a.avant / a.ldif.lstrip("/"), a.ldif_vivant or Path("/nonexistent")), "entree(s)"
|
||||
elif nom == "postgresql":
|
||||
r, unite = jeu_postgresql(a.avant / a.dump.lstrip("/"), interroger), "table(s)"
|
||||
|
|
|
|||
|
|
@ -56,6 +56,17 @@
|
|||
group: root
|
||||
mode: "0700"
|
||||
|
||||
# LA COPIE A FROID (2026-10-07) : les jeux qui s'ecrivent en continu (Prometheus, Loki)
|
||||
# arretent leur service le temps de copier. L'outil est deploye avec le script qui l'appelle.
|
||||
- name: Déployer l'outil de copie à froid
|
||||
when: client_backup_jobs | length > 0
|
||||
ansible.builtin.copy:
|
||||
src: setops-copie-a-froid.sh
|
||||
dest: /usr/local/sbin/setops-copie-a-froid
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Déployer l'unité et le timer systemd
|
||||
when: client_backup_jobs | length > 0
|
||||
ansible.builtin.template:
|
||||
|
|
@ -116,6 +127,7 @@
|
|||
state: absent
|
||||
loop:
|
||||
- /usr/local/sbin/setops-sauvegarder.sh
|
||||
- /usr/local/sbin/setops-copie-a-froid
|
||||
- /etc/systemd/system/setops-sauvegarde.service
|
||||
- /etc/systemd/system/setops-sauvegarde.timer
|
||||
- /etc/systemd/system/setops-sauvegarde-avant-raser.service
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@
|
|||
# choisir [--instantane ID] (pour Ansible) JSON de l'instantane candidat
|
||||
# fichiers [opts] CHEMIN... remettre des repertoires (rsync --delete)
|
||||
# --proprietaire U:G --remplacer --vers DOSSIER (repetition, rien n'est touche)
|
||||
# --depuis CHEMIN_DANS_L_INSTANTANE (un seul CHEMIN) : ce qui a ete sauvegarde
|
||||
# ailleurs que la ou ca vit (copie a froid)
|
||||
# annuaire [opts] BASE_DN remplacer la base LDAP (slapadd)
|
||||
# --essai (slapadd -u a blanc, rien n'est touche)
|
||||
# base [opts] NOM rejouer une base depuis le pg_dumpall
|
||||
|
|
@ -58,6 +60,9 @@ JEUX=({% for j in client_backup_jobs %} "{{ j.nom }}"{% endfor %} )
|
|||
# Les chemins de chaque jeu, separes par « : » — ce que les temoins extraient et comparent.
|
||||
declare -A CHEMINS_JEU=({% for j in client_backup_jobs %} ["{{ j.nom }}"]="{{ j.chemins | join(':') }}"{% endfor %} )
|
||||
ETIQUETTE_AVANT_RASER="{{ client_backup_etiquette_avant_raser }}"
|
||||
# Ou vit la base des jeux copies a froid (Prometheus, Loki) : leur instantane porte la
|
||||
# copie, pas la base. Les temoins comparent l'une a l'autre.
|
||||
declare -A VIVANT_JEU=({% for j in client_backup_jobs if j.vivant is defined %} ["{{ j.nom }}"]="{{ j.vivant }}"{% endfor %} )
|
||||
|
||||
dire() { printf '%s\n' "$*" >&2; }
|
||||
mourir() { dire "setops-restaurer: $*"; exit 1; }
|
||||
|
|
@ -154,22 +159,28 @@ vide() { # vrai si le chemin est absent, ou un repertoire sans contenu
|
|||
}
|
||||
|
||||
cmd_fichiers() {
|
||||
local proprio="" remplacer=0 vers="" chemins=() src dest n
|
||||
local proprio="" remplacer=0 vers="" depuis="" chemins=() src dest n
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--proprietaire) proprio="$2"; shift 2 ;;
|
||||
--remplacer) remplacer=1; shift ;;
|
||||
--vers) vers="$2"; shift 2 ;;
|
||||
--depuis) depuis="$2"; shift 2 ;;
|
||||
--instantane) INSTANTANE="$2"; shift 2 ;;
|
||||
*) chemins+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "${chemins[*]:-}" ]] || mourir "fichiers : aucun chemin"
|
||||
# LA COPIE A FROID (2026-10-07) : l'instantane porte la copie posee dans le repertoire de
|
||||
# preparation, pas la base. `--depuis` dit ou la prendre ; le CHEMIN, ou la remettre.
|
||||
# (Un second element, plutot qu'une longueur : la sequence accolade-diese ouvre un
|
||||
# commentaire Jinja dans ce gabarit.)
|
||||
if [[ -n "${depuis}" && -n "${chemins[1]:-}" ]]; then mourir "fichiers --depuis : un seul chemin"; fi
|
||||
resoudre_instantane
|
||||
if [[ -z "${INSTANTANE}" ]]; then echo "RIEN : aucune incarnation precedente."; return 0; fi
|
||||
extraire "${chemins[@]}"
|
||||
if [[ -n "${depuis}" ]]; then extraire "${depuis}"; else extraire "${chemins[@]}"; fi
|
||||
for c in "${chemins[@]}"; do
|
||||
src="${TMP}${c}"
|
||||
src="${TMP}${depuis:-${c}}"
|
||||
dest="${vers}${c}"
|
||||
if [[ ! -e "${src}" ]]; then echo "ABSENT de ${INSTANTANE} : ${c}"; continue; fi
|
||||
if ! vide "${dest}"; then
|
||||
|
|
@ -358,6 +369,7 @@ cmd_temoins() {
|
|||
IFS=: read -r -a morceaux <<< "${CHEMINS_JEU[${j}]}"
|
||||
chemins+=("${morceaux[@]}")
|
||||
args+=(--jeu "${j}=${CHEMINS_JEU[${j}]}")
|
||||
if [[ -n "${VIVANT_JEU[${j}]:-}" ]]; then args+=(--vivant "${j}=${VIVANT_JEU[${j}]}"); fi
|
||||
done
|
||||
extraire "${chemins[@]}"
|
||||
if [[ -s "${TMP}${LDIF}" ]]; then
|
||||
|
|
|
|||
|
|
@ -56,9 +56,17 @@ rapporter() { # $1=code $2=texte
|
|||
|
||||
maintenant=$(date +%s)
|
||||
|
||||
# LE PLUS RECENT DE TOUS LES GROUPES, PAS LE PREMIER (2026-10-08). `--latest 1` rend le
|
||||
# dernier instantane de CHAQUE groupe — machine ET liste de chemins. Ajouter un jeu change la
|
||||
# liste : deux groupes, et `d[0]` etait l'ancien, fige. Sur site-mon-01, des le lendemain de
|
||||
# l'arrivee de prometheus, loki et icinga : « EN RETARD : 26 h » sur une sauvegarde de 2 h 30,
|
||||
# et « PERIME » promis a 50 h. On prend donc le plus recent de tous.
|
||||
json=$(restic snapshots --latest 1 --json 2>/dev/null)
|
||||
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||
'import sys,json,re,datetime
|
||||
d=json.load(sys.stdin) or []
|
||||
t=lambda x: datetime.datetime.fromisoformat(re.sub(r"Z$","+00:00",re.sub(r"(\.\d{6})\d+",r"\1",x["time"]))).timestamp()
|
||||
print(max(d,key=t)["time"] if d else "")' 2>/dev/null)
|
||||
if [[ -z "${horodatage}" ]]; then
|
||||
# ON NE DISTINGUE PAS ICI « depot vide » de « depot injoignable », et c'est voulu :
|
||||
# dans les deux cas il n'y a PAS de sauvegarde exploitable, et c'est ce qui compte.
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ client_backup_groupes_etat:
|
|||
- serveur_rspamd
|
||||
- serveur_web_dorsal
|
||||
- serveur_icinga
|
||||
- serveur_prometheus
|
||||
- serveur_loki
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ client_journal_config: "/etc/alloy/config.alloy"
|
|||
|
||||
# Point de collecte Loki (obs-01, serveur_loki, VLAN 14).
|
||||
# Hôte Loki dérivé du groupe serveur_loki (FQDN via le plancher) ; override par instance au besoin.
|
||||
client_journal_loki_url: "http://{{ (groups.get('serveur_loki') or ['obs-01']) | first }}.{{ domaine_interne }}:3100/loki/api/v1/push"
|
||||
# LE SCHEMA SUIT L'INTERRUPTEUR TLS (2026-10-12) — meme defaut que Grafana le 2026-09-28 :
|
||||
# une URL `http://` ecrite a cote d'un `client_journal_loki_tls: true` parlerait en clair a
|
||||
# un Loki qui n'ecoute qu'en HTTPS.
|
||||
client_journal_loki_url: >-
|
||||
{{ 'https' if client_journal_loki_tls | bool else 'http' }}://{{
|
||||
(groups.get('serveur_loki') or ['obs-01']) | first }}.{{ domaine_interne }}:3100/loki/api/v1/push
|
||||
|
||||
# Depot apt officiel Grafana (partage avec serveur_loki / serveur_grafana).
|
||||
client_journal_depot_cle_url: "{{ client_journal_depot_schema }}://apt.grafana.com/gpg-full.key"
|
||||
|
|
@ -18,10 +23,16 @@ client_journal_depot_source: "deb [signed-by=/etc/apt/keyrings/grafana.asc] {{ c
|
|||
# --- TLS (zero-confiance) : Alloy pousse vers Loki en https + verifie root_ca ---
|
||||
client_journal_loki_tls: false
|
||||
client_journal_loki_ca: "/etc/step/certs/root_ca.crt"
|
||||
# UNE MACHINE SANS PKI PEUT QUAND MEME VERIFIER (2026-10-12). Un hyperviseur n'a pas
|
||||
# `client_pki` : aucune racine sous /etc/step. Pour verifier Loki, il lui suffit du
|
||||
# certificat PUBLIC de l'autorite — pas d'un certificat a lui. Non vide, ce contenu est
|
||||
# pose a `client_journal_loki_ca` (au site, `ac-racine-site.crt` lu par l'inventaire).
|
||||
client_journal_loki_ca_contenu: ""
|
||||
|
||||
# Port de l'interface HTTP locale d'Alloy (diagnostic). DECLARE ici parce que son defaut
|
||||
# amont — 12345 — entre en collision avec `serveur_dovecot_sasl_port`, qui vaut 12345 lui
|
||||
# aussi et qui est un choix DELIBERE de Set-OPS (SASL reseau, pour la soumission :587).
|
||||
# amont — 12345 — entrait en collision avec `serveur_dovecot_sasl_port`, qui valait 12345
|
||||
# lui aussi (SASL reseau, pour la soumission :587 ; retire le 2026-10-12 — il passait en
|
||||
# clair). Le port reste declare : le defaut amont, lui, n'a pas change.
|
||||
#
|
||||
# Sur `infra-mail-01`, les deux cohabitent : le premier demarre gagne, l'autre boucle sur
|
||||
# « bind: address already in use ». Constate le 2026-08-08 lors de la premiere
|
||||
|
|
@ -81,6 +92,17 @@ client_journal_syslog_protocole: "tcp"
|
|||
# du site — jamais fabrique ici.
|
||||
client_journal_syslog_emetteur: "frontiere"
|
||||
|
||||
# LE RECEPTEUR SYSLOG EN TLS, AUTHENTIFIANT SES CLIENTS (2026-10-12). Le syslog de la
|
||||
# frontiere passait en clair. Vrai : Alloy sert le certificat de la machine (client_pki,
|
||||
# recopie la ou `alloy` le lit, et a chaque renouvellement) et EXIGE un certificat client
|
||||
# signe par une autorite de `client_journal_syslog_clients_ca_contenu` — au site, celle de
|
||||
# la frontiere seule : personne d'autre ne peut y ecrire.
|
||||
client_journal_syslog_tls: false
|
||||
client_journal_syslog_tls_dir: "/etc/alloy/tls"
|
||||
client_journal_syslog_tls_source_cert: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.crt"
|
||||
client_journal_syslog_tls_source_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key"
|
||||
client_journal_syslog_clients_ca_contenu: ""
|
||||
|
||||
# LE JOURNAL D'AUDIT, EXPEDIE COMME LE RESTE (2026-09-11).
|
||||
#
|
||||
# DERIVE DE L'APPARTENANCE AU GROUPE, pas declare par instance : `auditd` vient avec
|
||||
|
|
|
|||
|
|
@ -184,6 +184,104 @@
|
|||
append: true
|
||||
notify: Redemarrer alloy
|
||||
|
||||
# --- RECEPTEUR SYSLOG EN TLS (2026-10-12) --------------------------------------------
|
||||
# Meme pont que node_exporter, Loki, Dovecot : le certificat de la machine, recopie la ou le
|
||||
# service le lit, recopie a chaque renouvellement. Avant la configuration : Alloy redemarre
|
||||
# avec des fichiers deja en place.
|
||||
- name: Syslog TLS — repertoire des certificats (alloy)
|
||||
ansible.builtin.file:
|
||||
path: "{{ client_journal_syslog_tls_dir }}"
|
||||
state: directory
|
||||
owner: "{{ client_journal_utilisateur }}"
|
||||
group: "{{ client_journal_utilisateur }}"
|
||||
mode: "0700"
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
|
||||
- name: Syslog TLS — script de synchronisation du certificat
|
||||
ansible.builtin.template:
|
||||
src: setops-alloy-cert-sync.sh.j2
|
||||
dest: /usr/local/sbin/setops-alloy-cert-sync
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
|
||||
- name: Syslog TLS — unites de synchronisation (service + path)
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.s }}"
|
||||
dest: "{{ item.d }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop:
|
||||
- { s: "setops-alloy-cert-sync.service.j2", d: "/etc/systemd/system/setops-alloy-cert-sync.service" }
|
||||
- { s: "setops-alloy-cert-sync.path.j2", d: "/etc/systemd/system/setops-alloy-cert-sync.path" }
|
||||
loop_control:
|
||||
label: "{{ item.d | basename }}"
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
|
||||
- name: Syslog TLS — activer la surveillance du certificat (path)
|
||||
ansible.builtin.systemd:
|
||||
name: setops-alloy-cert-sync.path
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Syslog TLS — synchroniser le certificat maintenant
|
||||
ansible.builtin.command: /usr/local/sbin/setops-alloy-cert-sync
|
||||
changed_when: false
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
# SEULS CES EMETTEURS PEUVENT ECRIRE. Sans certificat d'autorite, le recepteur n'accepterait
|
||||
# aucun client : on refuse de deployer plutot que d'eteindre la journalisation de la
|
||||
# frontiere en silence. Un CERTIFICAT, pas seulement une valeur non vide — et cette forme-la
|
||||
# echappe a P32, qui ne lit pas les `when:` et aurait exige la variable de chaque locataire.
|
||||
- name: Syslog TLS — exiger les autorites des emetteurs admis
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- client_journal_syslog_clients_ca_contenu is search('BEGIN CERTIFICATE')
|
||||
fail_msg: >-
|
||||
client_journal_syslog_tls est vrai, mais aucune autorite d'emetteur n'est fournie
|
||||
(client_journal_syslog_clients_ca_contenu) : le recepteur refuserait tout le monde.
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
|
||||
- name: Syslog TLS — poser les autorites des emetteurs admis
|
||||
ansible.builtin.copy:
|
||||
content: "{{ client_journal_syslog_clients_ca_contenu }}"
|
||||
dest: "{{ client_journal_syslog_tls_dir }}/clients.crt"
|
||||
owner: "{{ client_journal_utilisateur }}"
|
||||
group: "{{ client_journal_utilisateur }}"
|
||||
mode: "0644"
|
||||
notify: Redemarrer alloy
|
||||
when:
|
||||
- client_journal_syslog_ecoute | length > 0
|
||||
- client_journal_syslog_tls | bool
|
||||
|
||||
- name: Poser l'autorite qui signe Loki (machine sans PKI)
|
||||
ansible.builtin.copy:
|
||||
content: "{{ client_journal_loki_ca_contenu }}"
|
||||
dest: "{{ client_journal_loki_ca }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Redemarrer alloy
|
||||
when: client_journal_loki_ca_contenu | length > 0
|
||||
|
||||
- name: Deployer la configuration Alloy
|
||||
ansible.builtin.template:
|
||||
src: config.alloy.j2
|
||||
|
|
@ -194,12 +292,17 @@
|
|||
notify: Redemarrer alloy
|
||||
|
||||
# Le port d'ecoute est IMPOSE ici plutot que subi : voir defaults/main.yml.
|
||||
- name: Imposer le port de l interface locale d Alloy
|
||||
#
|
||||
# AUCUN RAPPORT D'USAGE (mesure du 2026-10-10). Par defaut, Alloy envoie a
|
||||
# stats.grafana.org la liste de ses composants actifs : relevé a la frontiere depuis
|
||||
# CHAQUE machine des trois ecosystemes, ~70 connexions par jour et par locataire. Il
|
||||
# passait par la sortie 443 generale du socle, qui n'existe plus.
|
||||
- name: Imposer le port de l interface locale d Alloy, sans rapport d usage
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/default/alloy
|
||||
regexp: "^CUSTOM_ARGS="
|
||||
line: >-
|
||||
CUSTOM_ARGS="--server.http.listen-addr={{ client_journal_http_adresse }}:{{ client_journal_http_port }}"
|
||||
CUSTOM_ARGS="--server.http.listen-addr={{ client_journal_http_adresse }}:{{ client_journal_http_port }} --disable-reporting"
|
||||
create: false
|
||||
notify: Redemarrer alloy
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,16 @@ loki.source.syslog "frontiere" {
|
|||
protocol = "{{ client_journal_syslog_protocole }}"
|
||||
label_structured_data = true
|
||||
labels = { job = "syslog", host = "{{ client_journal_syslog_emetteur }}" }
|
||||
{% if client_journal_syslog_tls | bool %}
|
||||
// TLS, ET UN CERTIFICAT CLIENT EXIGE : un `ca_file` suffit a l'exiger dans ce
|
||||
// recepteur (`client_auth_type` n'y est pas accepte — `alloy validate`, 2026-10-12).
|
||||
tls_config {
|
||||
cert_file = "{{ client_journal_syslog_tls_dir }}/alloy.crt"
|
||||
key_file = "{{ client_journal_syslog_tls_dir }}/alloy.key"
|
||||
ca_file = "{{ client_journal_syslog_tls_dir }}/clients.crt"
|
||||
min_version = "TLS12"
|
||||
}
|
||||
{% endif %}
|
||||
}
|
||||
forward_to = [loki.relabel.syslog.receiver]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# Gere par Set-OPS (role client_journal). Ne pas editer a la main.
|
||||
[Unit]
|
||||
Description=Set-OPS — surveille le certificat step_ca du recepteur syslog d'Alloy
|
||||
|
||||
[Path]
|
||||
PathChanged={{ client_journal_syslog_tls_source_cert }}
|
||||
Unit=setops-alloy-cert-sync.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Gere par Set-OPS (role client_journal). Ne pas editer a la main.
|
||||
[Unit]
|
||||
Description=Set-OPS — synchronise le certificat step_ca vers le recepteur syslog d'Alloy
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/setops-alloy-cert-sync
|
||||
18
roles/client_journal/templates/setops-alloy-cert-sync.sh.j2
Normal file
18
roles/client_journal/templates/setops-alloy-cert-sync.sh.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/sh
|
||||
# Gere par Set-OPS (role client_journal). Ne pas editer a la main.
|
||||
# Recopie le certificat step_ca (client_pki, root:root) la ou `alloy` le lit, pour le
|
||||
# recepteur syslog en TLS. Idempotent ; appele au deploiement ET au renouvellement (.path).
|
||||
set -eu
|
||||
|
||||
SRC_CERT="{{ client_journal_syslog_tls_source_cert }}"
|
||||
SRC_KEY="{{ client_journal_syslog_tls_source_cle }}"
|
||||
DST="{{ client_journal_syslog_tls_dir }}"
|
||||
|
||||
[ -f "$SRC_CERT" ] && [ -f "$SRC_KEY" ] || exit 0
|
||||
|
||||
install -o {{ client_journal_utilisateur }} -g {{ client_journal_utilisateur }} -m 0644 "$SRC_CERT" "$DST/alloy.crt"
|
||||
install -o {{ client_journal_utilisateur }} -g {{ client_journal_utilisateur }} -m 0600 "$SRC_KEY" "$DST/alloy.key"
|
||||
|
||||
# Le recepteur lit son certificat au demarrage : un nouveau certificat sur disque resterait
|
||||
# sans effet jusqu'au prochain redemarrage (renouvellement-cert-recharger-consommateur).
|
||||
systemctl try-restart {{ client_journal_service }} 2>/dev/null || true
|
||||
|
|
@ -110,6 +110,11 @@
|
|||
when: not (common_packages_desarmer_maj_auto | bool)
|
||||
failed_when: false # une unite absente sur cette image n'est pas une faute
|
||||
|
||||
# AVANT TOUTE OPERATION APT : pas de rattrapage herite du gabarit (voir minuteurs.yml).
|
||||
- name: Empecher le rattrapage des minuteurs apt herite du gabarit
|
||||
ansible.builtin.include_tasks: minuteurs.yml
|
||||
when: not (common_packages_desarmer_maj_auto | bool)
|
||||
|
||||
- name: Mettre à jour le cache APT
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
|
|
|
|||
69
roles/common_packages/tasks/minuteurs.yml
Normal file
69
roles/common_packages/tasks/minuteurs.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
# LE RATTRAPAGE HERITE DU GABARIT (2026-10-08).
|
||||
#
|
||||
# `apt-daily.timer` et `apt-daily-upgrade.timer` sont PERSISTANTS : systemd garde la date
|
||||
# de leur dernier passage (`/var/lib/systemd/timers/stamp-*`) et rattrape un passage
|
||||
# manque. Une VM clonee herite de ces traces — celle d'`apt-daily` datait du 2026-06-21,
|
||||
# jour de fabrication du gabarit. Elle « rattrape » donc des son demarrage, avec un delai
|
||||
# aleatoire jusqu'a 60 min : a la reconstruction de Technolibre, `apt-daily-upgrade` est
|
||||
# parti cinq minutes apres la naissance d'`infra-pki-01`, en pleine serie d'operations apt
|
||||
# du role. Il tenait le verrou du CACHE DES ARCHIVES, qu'`apt-get` n'attend jamais
|
||||
# (`DPkg::Lock::Timeout` ne couvre que dpkg) :
|
||||
#
|
||||
# 'apt-get autoremove' failed: E: Could not get lock /var/cache/apt/archives/lock.
|
||||
#
|
||||
# Une trace plus ancienne que la naissance de la machine n'est pas la sienne : on la remet a
|
||||
# l'heure, et on relance le minuteur, qui recalcule son prochain passage (le matin suivant)
|
||||
# sans rattraper. Les mises a jour automatiques restent armees (decision du 2026-09-11) ;
|
||||
# seul disparait un passage herite. Sur une machine en service, la trace est recente :
|
||||
# rien ne change.
|
||||
- name: Naissance de la machine (cle d'hote SSH, posee au premier demarrage)
|
||||
ansible.builtin.stat:
|
||||
path: /etc/ssh/ssh_host_ed25519_key.pub
|
||||
register: common_packages_naissance
|
||||
|
||||
- name: Traces des minuteurs apt persistants
|
||||
ansible.builtin.stat:
|
||||
path: "/var/lib/systemd/timers/stamp-{{ item }}"
|
||||
loop: "{{ common_packages_unites_maj_auto | select('match', '.*[.]timer$') | list }}"
|
||||
register: common_packages_traces
|
||||
|
||||
- name: Remettre a l'heure une trace heritee du gabarit
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.stat.path }}"
|
||||
state: touch
|
||||
mode: "0644"
|
||||
loop: >-
|
||||
{{ common_packages_traces.results | selectattr('stat.exists')
|
||||
| selectattr('stat.mtime', 'lt', common_packages_naissance.stat.mtime | default(0))
|
||||
| list }}
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
register: common_packages_traces_remises
|
||||
when: common_packages_naissance.stat.exists
|
||||
|
||||
# Relancer recalcule le prochain passage d'apres la trace neuve. Relancer un minuteur ne
|
||||
# declenche pas sa cible.
|
||||
- name: Relancer les minuteurs dont la trace a ete remise
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ item.item.item }}"
|
||||
state: restarted
|
||||
loop: "{{ common_packages_traces_remises.results | default([]) | selectattr('changed') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.item }}"
|
||||
|
||||
# UN PASSAGE DEJA PARTI se termine avant nos operations apt, plutot que de se les disputer.
|
||||
# `is-active` rend 0 si l'une au moins des unites tourne.
|
||||
# `check_mode: false` : une LECTURE. Sautee en simulation, elle n'avait jamais de `rc`, la
|
||||
# condition `until` ne se remplissait pas, et `--check` echouait la apres 30 essais sur
|
||||
# chaque machine (mesure le 2026-10-13, premiere simulation du site de TechnoLibre).
|
||||
- name: Attendre la fin d'un passage apt automatique deja en cours
|
||||
ansible.builtin.command:
|
||||
argv: ["systemctl", "is-active", "--quiet", "apt-daily.service", "apt-daily-upgrade.service"]
|
||||
register: common_packages_passage
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
until: common_packages_passage.rc != 0
|
||||
retries: 30
|
||||
delay: 10
|
||||
|
|
@ -7,25 +7,25 @@ table inet setops_filter {
|
|||
chain input {
|
||||
type filter hook input priority 0; policy {{ nftables_baseline_input_policy }};
|
||||
|
||||
iif "lo" accept
|
||||
ct state established,related accept
|
||||
ct state invalid drop
|
||||
iif "lo" accept comment "socle: communications locales"
|
||||
ct state established,related accept comment "socle: connexions etablies et associees"
|
||||
ct state invalid drop comment "socle: paquets invalides"
|
||||
|
||||
{% if nftables_baseline_allow_icmp | bool %}
|
||||
ip protocol icmp accept
|
||||
ip6 nexthdr icmpv6 accept
|
||||
ip protocol icmp accept comment "socle: diagnostic et erreurs reseau IPv4"
|
||||
ip6 nexthdr icmpv6 accept comment "socle: diagnostic et erreurs reseau IPv6"
|
||||
{% endif %}
|
||||
|
||||
{% if nftables_baseline_allow_ssh | bool %}
|
||||
tcp dport {{ nftables_baseline_ssh_port }} accept
|
||||
tcp dport {{ nftables_baseline_ssh_port }} accept comment "administration: acces SSH du gabarit de repli"
|
||||
{% endif %}
|
||||
|
||||
{% for port in nftables_baseline_extra_tcp_ports %}
|
||||
tcp dport {{ port }} accept
|
||||
tcp dport {{ port }} accept comment "repli: port TCP supplementaire declare par l'instance"
|
||||
{% endfor %}
|
||||
|
||||
{% for port in nftables_baseline_extra_udp_ports %}
|
||||
udp dport {{ port }} accept
|
||||
udp dport {{ port }} accept comment "repli: port UDP supplementaire declare par l'instance"
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,13 @@ quarante minutes sur la **première** machine.
|
|||
| | |
|
||||
|---|---|
|
||||
| **dépôts apt** | ce rôle — un cache, servi à la flotte |
|
||||
| **téléchargements directs** (Forgejo, Keycloak, Nextcloud, oauth2-proxy, collections) | le **cache du contrôleur** : il télécharge une fois et pousse par SSH |
|
||||
| **téléchargements directs** (Forgejo, Keycloak, Nextcloud et ses applications, oauth2-proxy) | ce rôle — le **dépôt de binaires** (`/var/lib/setops/artefacts-directs`, servi sous `/setops-binaires` sur le même port) |
|
||||
| **ce que le runner installe** (roues Python, collections Ansible, `verrou-runner.yml`) | ce rôle — le même dépôt de binaires |
|
||||
|
||||
Deux mécanismes parce que ce sont deux problèmes : on ne sert pas un dépôt apt par `scp`.
|
||||
Les rôles consommateurs prennent ces fichiers au dépôt du site d'abord, vérifient leur somme
|
||||
(ou leur signature), et ne sortent sur Internet qu'en repli, en le disant. Depuis le
|
||||
2026-10-09, une reconstruction ne demande plus rien à PyPI, Galaxy ni à l'app store de
|
||||
Nextcloud quand le dépôt est rempli.
|
||||
|
||||
## Ce qu'il ne couvre pas encore
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,14 @@
|
|||
label: "{{ item }}"
|
||||
when: serveur_artefacts_directs_actif | bool
|
||||
|
||||
# CE QUE LE RUNNER INSTALLE (2026-10-09) : ses roues Python et ses collections, verrouillees
|
||||
# a l'octet dans `verrou-runner.yml`, a la racine du moteur. Lues, pas recopiees.
|
||||
- name: Lire le verrou du runner (roues Python, collections Ansible)
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ role_path }}/../../verrou-runner.yml"
|
||||
name: serveur_artefacts_verrou_runner
|
||||
when: serveur_artefacts_directs_actif | bool
|
||||
|
||||
# `force: false` ET UN `dest` NOMME PAR LA VERSION : une publication ne change pas sous
|
||||
# le meme nom, donc un fichier deja present est le bon. Une nouvelle version porte un
|
||||
# autre nom et se telecharge d'elle-meme — le depot se met a jour en suivant les roles,
|
||||
|
|
@ -211,10 +219,18 @@
|
|||
ansible.builtin.get_url:
|
||||
url: "{{ item.url }}"
|
||||
dest: "{{ serveur_artefacts_directs_dir }}/{{ item.nom }}"
|
||||
checksum: "{{ ('sha256:' ~ item.sha256) if item.sha256 is defined else omit }}"
|
||||
mode: "0644"
|
||||
force: false
|
||||
timeout: 60
|
||||
loop: "{{ serveur_artefacts_directs }}"
|
||||
# Les applications de Nextcloud (2026-10-08) : declarees par `serveur_nextcloud`, lues
|
||||
# telles quelles — meme `nom`, meme `url`, rien de recopie ici.
|
||||
# Et le verrou du runner (2026-10-09). Un artefact qui declare sa somme est verifie ici :
|
||||
# le site ne tient pas un fichier altere en chemin.
|
||||
loop: >-
|
||||
{{ serveur_artefacts_directs + (serveur_nextcloud_apps | default([]))
|
||||
+ (serveur_artefacts_verrou_runner.python | default([]))
|
||||
+ (serveur_artefacts_verrou_runner.collections | default([])) }}
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
register: serveur_artefacts_directs_tires
|
||||
|
|
|
|||
|
|
@ -62,9 +62,14 @@ for noeud in {{ serveur_backup_noeuds_attendus | sort | join(' ') }}; do
|
|||
continue
|
||||
fi
|
||||
|
||||
# Le plus recent de tous les groupes (machine ET liste de chemins), pas le premier : voir
|
||||
# `client_backup/templates/verifier-mon-depot.sh.j2` (2026-10-08).
|
||||
json=$(restic -r "${depot}" snapshots --latest 1 --json 2>/dev/null)
|
||||
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||
'import sys,json,re,datetime
|
||||
d=json.load(sys.stdin) or []
|
||||
t=lambda x: datetime.datetime.fromisoformat(re.sub(r"Z$","+00:00",re.sub(r"(\.\d{6})\d+",r"\1",x["time"]))).timestamp()
|
||||
print(max(d,key=t)["time"] if d else "")' 2>/dev/null)
|
||||
if [[ -z "${horodatage}" ]]; then
|
||||
rapporter "${noeud}" 2 "AUCUN INSTANTANE dans ${depot} — depot present mais vide."
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -42,5 +42,6 @@ flux:
|
|||
pair: externe
|
||||
chiffrement: tls-requis
|
||||
raison: >-
|
||||
Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga). Le cache
|
||||
les relaie pour que la flotte n'ait pas à sortir elle-même.
|
||||
Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga), et le
|
||||
dépôt de binaires (roues PyPI, collections Galaxy, versions GitHub, Nextcloud,
|
||||
Forgejo). Le site les relaie pour que la flotte n'ait pas à sortir elle-même.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
# Surcharge de l'unité du paquet. Le `coolwsd.xml` livré (439 lignes commentées) reste
|
||||
# intact : le remplacer par un gabarit maison obligerait à suivre son évolution à chaque
|
||||
# version amont, et ferait perdre les commentaires qui expliquent chaque réglage.
|
||||
#
|
||||
# AUCUNE VÉRIFICATION DE VERSION (mesure du 2026-10-10) : par défaut coolwsd interroge
|
||||
# collabora.co.uk toutes les 10 heures, et le dit aux utilisateurs par une fenêtre. Les
|
||||
# mises à jour arrivent par apt ; `fetch_update_check=0` coupe l'appel, la fenêtre suit.
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/coolwsd --version --o:sys_template_path=/opt/cool/systemplate \
|
||||
|
|
@ -12,6 +16,8 @@ ExecStart=/usr/bin/coolwsd --version --o:sys_template_path=/opt/cool/systemplate
|
|||
--o:ssl.termination={{ serveur_collabora_ssl_termination | bool | lower }} \
|
||||
--o:server_name={{ serveur_collabora_server_name }} \
|
||||
--o:storage.wopi.alias_group[0].host=https://{{ serveur_collabora_domaine_nextcloud }}:443 \
|
||||
--o:fetch_update_check=0 \
|
||||
--o:allow_update_popup=false \
|
||||
--o:admin_console.enable={{ serveur_collabora_admin_actif | bool | lower }}{% if serveur_collabora_admin_actif | bool %} \
|
||||
--o:admin_console.username={{ serveur_collabora_admin_utilisateur }} \
|
||||
--o:admin_console.password={{ serveur_collabora_admin_mot_de_passe }}{% endif %}
|
||||
|
|
|
|||
|
|
@ -9,22 +9,39 @@ flux:
|
|||
chiffrement: ssh
|
||||
raison: "Plan de gestion : administration et déploiement Ansible par SSH (inter-nœud ; l'accès depuis l'extérieur est filtré à l'OPNsense)."
|
||||
|
||||
# --- Sortie générale de la flotte ---
|
||||
# Portée par le socle parce qu'elle vaut pour TOUS les hôtes, sans exception.
|
||||
# Non déclarée, la frontière la bloquerait : son `block out` final est un vrai
|
||||
# default-deny, et la flotte perdrait ses mises à jour et son horloge en silence.
|
||||
# --- Sortie apt directe : SEULEMENT SANS AMONT D'ARTEFACTS (2026-10-10) ---
|
||||
#
|
||||
# C'ÉTAIT UNE SORTIE 80/443 VERS TOUT INTERNET, POUR TOUTE MACHINE. Sa raison, « dépôts
|
||||
# apt », n'était plus vraie chez les locataires : leurs 26 machines prennent TOUTES leurs
|
||||
# sources en `http://` par le cache du site (`artefacts_amorcage`), aucune n'a de dépôt
|
||||
# HTTPS. Mesuré à la frontière, elle ne servait plus qu'à ce qu'aucun rôle ne déclarait :
|
||||
# télémétrie de Grafana, d'Alloy et de Loki, vérification de version de Collabora,
|
||||
# cartes de rspamd, repère du génome. Les trois premiers sont coupés à la source ; les
|
||||
# deux derniers ont désormais leur propre flux, dans leur rôle.
|
||||
#
|
||||
# ELLE NE RESTE QUE LÀ OÙ IL N'Y A AUCUN AMONT. Le site non plus ne la reçoit pas : ses
|
||||
# machines visent son propre cache depuis le 2026-09-14 (`site_inventaire.py`,
|
||||
# `artefacts_amorcage`), et le cache, lui, porte ses propres sorties 80/443
|
||||
# (`serveur_cache_site`, `serveur_artefacts`). Seul un écosystème sans aucune source
|
||||
# d'artefacts — un laboratoire isolé — prend encore ses paquets en direct.
|
||||
- sens: egress
|
||||
port: 443
|
||||
protocole: tcp
|
||||
pair: externe
|
||||
chiffrement: tls-requis
|
||||
raison: "Dépôts apt en HTTPS (Debian, Smallstep, Grafana, Icinga, Forgejo, Nextcloud) — sans quoi aucun correctif de sécurité n'entre."
|
||||
seulement_si: {variable: artefacts_amorcage, non_vide: false}
|
||||
raison: >-
|
||||
Dépôts apt en direct, pour un écosystème SANS aucune source d'artefacts. Le site
|
||||
et ses locataires passent par le cache du site et ne la reçoivent pas.
|
||||
- sens: egress
|
||||
port: 80
|
||||
protocole: tcp
|
||||
pair: externe
|
||||
chiffrement: clair
|
||||
raison: "Dépôts apt en clair et redirections HTTP des miroirs (l'intégrité vient de la signature des paquets, pas du transport)."
|
||||
seulement_si: {variable: artefacts_amorcage, non_vide: false}
|
||||
raison: >-
|
||||
Dépôts apt en clair et redirections des miroirs, pour un écosystème SANS amont
|
||||
d'artefacts. L'intégrité vient de la signature des paquets, pas du transport.
|
||||
# --- Decouverte de MTU de chemin (PMTUD) ---
|
||||
# L'overlay EVPN plafonne a 1450 : tout ce qui traverse la frontiere depend de ces
|
||||
# deux messages. Bloques, la connexion s'etablit, les petites requetes passent et les
|
||||
|
|
|
|||
|
|
@ -34,16 +34,15 @@ bout-en-bout en mono-domaine.
|
|||
| `serveur_dovecot_ldap_filter` | `(&(objectClass=inetOrgPerson)(mail=%{user}))` | Login = adresse courriel |
|
||||
| `serveur_dovecot_tls_actif` | `true` | Pont de certificat step_ca |
|
||||
| `serveur_dovecot_lmtp_reseau` | `true` | LMTP :24 pour un MTA **distant** (edge-mta) |
|
||||
| `serveur_dovecot_sasl_reseau` | `false` | Auth SASL réseau :12345 pour la soumission :587 |
|
||||
|
||||
## Flux (`meta/flux.yml`)
|
||||
`993/tcp` IMAPS (utilisateurs) · `24/tcp` LMTP ← Postfix (TLS vérifié) ·
|
||||
`12345/tcp` SASL ← Postfix · `636/tcp` → OpenLDAP (userdb/passdb).
|
||||
`636/tcp` → OpenLDAP (userdb/passdb).
|
||||
|
||||
## Notes / limites
|
||||
- **Mono-domaine** : le chemin ne conserve pas le domaine. Le multi-domaine demandera de le
|
||||
préserver des deux côtés (raffinement Étape B).
|
||||
- LMTP et SASL **réseau** doivent être restreints au nœud MTA par pare-feu — c'est ce que
|
||||
- LMTP **réseau** doit être restreint au nœud MTA par pare-feu — c'est ce que
|
||||
fait `nftables_baseline` à partir de `meta/flux.yml` (`pair: serveur_postfix`).
|
||||
- Si Postfix est **co-localisé**, le rôle bascule sur les sockets sous le chroot
|
||||
(`/var/spool/postfix/private`) plutôt que sur le réseau (détection automatique).
|
||||
|
|
@ -52,3 +51,13 @@ bout-en-bout en mono-domaine.
|
|||
## Prérequis
|
||||
- `serveur_dovecot requiert serveur_openldap actif` (`docs/dependances-groupes.yml`).
|
||||
- `client_pki` appliqué sur le nœud (couche `pki_client`, avant la couche `services`).
|
||||
- **Authentification de la soumission : locale à Postfix** (2026-10-12). Le port SASL
|
||||
réseau `12345` est retiré : le client SASL Dovecot de Postfix ne chiffre pas, et les mots
|
||||
de passe y passaient en clair. `serveur_postfix` appelle `tasks/auth_locale.yml`, qui
|
||||
pose sur sa machine un Dovecot d'**authentification seule** (cœur + LDAP, aucun port),
|
||||
joint par un socket local et vérifiant sur l'annuaire en LDAPS.
|
||||
- **IMAP en TLS direct seulement** (2026-10-12) : l'écoute en clair 143, ouverte par défaut,
|
||||
est éteinte (`inet_listener imap { port = 0 }`) ; seul le 993 répond.
|
||||
- **Sonde « boites »** : `doveadm auth lookup` du compte d'amorçage (`amorcage_acces_courriel`),
|
||||
c'est-à-dire une recherche dans le **passdb** LDAP, sans mot de passe. `doveadm user` ne
|
||||
prouvait rien : le userdb `static` (`static_allow_all_users`) répond pour n'importe quel nom.
|
||||
|
|
|
|||
|
|
@ -40,11 +40,14 @@ serveur_dovecot_postfix_socket_dir: "/var/spool/postfix/private"
|
|||
serveur_dovecot_lmtp_reseau: true
|
||||
serveur_dovecot_lmtp_port: 24
|
||||
|
||||
# --- Auth SASL réseau : pour la soumission :587 servie par un Postfix DISTANT (edge-mta).
|
||||
# Dovecot vérifie le mot de passe (passdb LDAP prouvé) pour Postfix. À restreindre au MTA
|
||||
# par pare-feu ; le trafic d'auth devrait être isolé/chiffré en prod.
|
||||
serveur_dovecot_sasl_reseau: false
|
||||
serveur_dovecot_sasl_port: 12345
|
||||
# --- Authentification de la soumission : LOCALE A POSTFIX, plus jamais par le reseau ---
|
||||
# Jusqu'au 2026-10-12, Postfix (edge-mta) interrogeait ce Dovecot sur :12345 ; le client
|
||||
# SASL Dovecot de Postfix ne chiffre pas, et les mots de passe passaient en clair. Ce port
|
||||
# n'existe plus : `serveur_postfix` pose a cote de lui un Dovecot d'authentification seule
|
||||
# (`tasks/auth_locale.yml`), qui verifie sur l'annuaire en LDAPS.
|
||||
serveur_dovecot_auth_locale_paquets:
|
||||
- dovecot-core
|
||||
- dovecot-ldap
|
||||
|
||||
# TLS sur le port LMTP. Doit rester ACCORDE avec `serveur_postfix_lmtp_tls_verify` du
|
||||
# cote emetteur : l'un sans l'autre, la livraison interne s'arrete — soit elle part en
|
||||
|
|
@ -52,7 +55,13 @@ serveur_dovecot_sasl_port: 12345
|
|||
serveur_dovecot_lmtp_tls: true
|
||||
|
||||
# --- Sonde de supervision -----------------------------------------------------------
|
||||
# UN COMPTE QUI DOIT EXISTER. Le resoudre prouve la chaine entiere : demon, configuration,
|
||||
# bind LDAP, mappage userdb. Variable, donc mise en defaut avec un nom inexistant sans
|
||||
# toucher a l'annuaire.
|
||||
serveur_dovecot_sonde_utilisateur: "daniel"
|
||||
# UN COMPTE QUI DOIT EXISTER, sous son IDENTIFIANT DE CONNEXION (l'attribut `mail` : c'est
|
||||
# ce que cherche le filtre de connexion). Le trouver prouve la chaine : demon, configuration,
|
||||
# liaison LDAPS, filtre. Variable, donc mise en defaut avec un nom inexistant sans toucher a
|
||||
# l'annuaire.
|
||||
#
|
||||
# Le compte d'amorcage, pas un nom ecrit (2026-10-12) : ce defaut valait `daniel`, qui
|
||||
# n'existe chez aucun locataire — et la sonde, qui interrogeait le userdb statique, le disait
|
||||
# trouve. Le compte d'amorcage est le seul que Set-OPS sait exister ; son adresse est un
|
||||
# intrant obligatoire de toute instance.
|
||||
serveur_dovecot_sonde_utilisateur: "{{ amorcage_acces_courriel | default('') }}"
|
||||
|
|
|
|||
|
|
@ -7,12 +7,6 @@ flux:
|
|||
pair: serveur_postfix
|
||||
chiffrement: tls-requis
|
||||
raison: "Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify)."
|
||||
- sens: ingress
|
||||
port: 12345
|
||||
protocole: tcp
|
||||
pair: serveur_postfix
|
||||
chiffrement: tls
|
||||
raison: "Authentification SASL déléguée : Postfix valide les identifiants de soumission contre Dovecot."
|
||||
- sens: ingress
|
||||
port: 993
|
||||
protocole: tcp
|
||||
|
|
|
|||
77
roles/serveur_dovecot/tasks/auth_locale.yml
Normal file
77
roles/serveur_dovecot/tasks/auth_locale.yml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
# AUTHENTIFICATION LOCALE POUR POSTFIX (2026-10-12) — voir templates/auth-locale.conf.j2.
|
||||
#
|
||||
# Appelee par `serveur_postfix` la ou la soumission existe, APRES que Postfix a demarre :
|
||||
# le socket vit dans `/var/spool/postfix/private`, que Postfix cree a son premier demarrage.
|
||||
# Ni IMAP, ni LMTP, ni aucun port : la machine n'entre pas dans le groupe `serveur_dovecot`,
|
||||
# et aucun de ses flux ne s'ouvre ici.
|
||||
- name: Authentification locale — resoudre l'annuaire (role partage)
|
||||
ansible.builtin.include_role:
|
||||
name: resoudre_annuaire
|
||||
vars:
|
||||
# Le compte de Dovecot, comme sur la machine des boites : meme verification, meme droit.
|
||||
resoudre_annuaire_bind_cn: "dovecot"
|
||||
resoudre_annuaire_secret: "vault_ldap_bind_dovecot"
|
||||
|
||||
- name: Authentification locale — adopter la connexion annuaire
|
||||
ansible.builtin.set_fact:
|
||||
serveur_dovecot_ldap_uris: "{{ resoudre_annuaire_uri }}:{{ resoudre_annuaire_port }}"
|
||||
serveur_dovecot_ldap_base: "{{ resoudre_annuaire_base_dn }}"
|
||||
serveur_dovecot_ldap_bind_dn: "{{ resoudre_annuaire_bind_dn }}"
|
||||
serveur_dovecot_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}"
|
||||
no_log: true
|
||||
|
||||
- name: Authentification locale — exiger le mot de passe de liaison (Vault)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- serveur_dovecot_ldap_bind_password | length > 0
|
||||
fail_msg: "vault_ldap_bind_dovecot requis : l'authentification locale se lie a l'annuaire sous le compte de Dovecot."
|
||||
|
||||
# Le coeur et le pilote LDAP, RIEN d'autre : sans `dovecot-imapd` ni `dovecot-lmtpd`, aucun
|
||||
# protocole n'est declare et Dovecot ne lance aucun service de connexion.
|
||||
- name: Authentification locale — installer Dovecot (coeur + LDAP)
|
||||
ansible.builtin.apt:
|
||||
name: "{{ serveur_dovecot_auth_locale_paquets }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
register: serveur_dovecot_auth_locale_apt
|
||||
|
||||
- name: Authentification locale — assurer le repertoire du socket (sous le chroot Postfix)
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_dovecot_postfix_socket_dir }}"
|
||||
state: directory
|
||||
owner: postfix
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Authentification locale — deployer la configuration (drop-in Dovecot 2.4)
|
||||
ansible.builtin.template:
|
||||
src: auth-locale.conf.j2
|
||||
dest: /etc/dovecot/conf.d/99-setops.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0640"
|
||||
no_log: true # contient le mot de passe de liaison LDAP
|
||||
notify: Redémarrer dovecot
|
||||
|
||||
- name: Authentification locale — desactiver l'auth systeme (LDAP est l'unique autorite)
|
||||
ansible.builtin.replace:
|
||||
path: /etc/dovecot/conf.d/10-auth.conf
|
||||
regexp: '^!include auth-system\.conf\.ext'
|
||||
replace: '#!include auth-system.conf.ext'
|
||||
notify: Redémarrer dovecot
|
||||
# En simulation, le paquet qu'on vient d'« installer » n'a pose aucun fichier.
|
||||
when: not (ansible_check_mode and serveur_dovecot_auth_locale_apt is changed)
|
||||
|
||||
- name: Authentification locale — valider la configuration (doveconf)
|
||||
ansible.builtin.command: doveconf -n
|
||||
changed_when: false
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Authentification locale — activer et demarrer Dovecot
|
||||
when: not ansible_check_mode
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ serveur_dovecot_service }}"
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
|
|
@ -15,6 +15,18 @@
|
|||
serveur_dovecot_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}"
|
||||
no_log: true
|
||||
|
||||
# UNE INSTANCE QUI DEMANDE ENCORE L'ANCIEN PORT (2026-10-12). `serveur_dovecot_sasl_reseau`
|
||||
# ouvrait :12345, ou Postfix envoyait les mots de passe de soumission EN CLAIR. Le gabarit
|
||||
# ne le lit plus ; une instance qui le pose encore croirait l'avoir — on le dit.
|
||||
- name: Refuser l'authentification reseau retiree (serveur_dovecot_sasl_reseau)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not (serveur_dovecot_sasl_reseau | default(false) | bool)
|
||||
fail_msg: >-
|
||||
`serveur_dovecot_sasl_reseau` est retire (2026-10-12) : il faisait passer les mots de
|
||||
passe de soumission en clair. Postfix verifie desormais par un Dovecot local
|
||||
(serveur_dovecot/tasks/auth_locale.yml). Retire la ligne des group_vars de l'instance.
|
||||
|
||||
- name: Exiger le mot de passe de bind LDAP (Vault)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ ssl_server_cert_file = {{ serveur_dovecot_tls_dir }}/cert.pem
|
|||
ssl_server_key_file = {{ serveur_dovecot_tls_dir }}/key.pem
|
||||
ssl_min_protocol = TLSv1.2
|
||||
|
||||
{# LE 143 NE S'OUVRE PLUS (2026-10-12). L'IMAP en clair ecoutait par defaut a cote du 993 :
|
||||
le pare-feu le bloquait et `ssl = required` refusait toute connexion avant STARTTLS, mais
|
||||
le port repondait. Directive : tout transit chiffre — on n'offre que le TLS direct (993,
|
||||
RFC 8314). `port = 0` eteint l'ecoute ; eprouve sur banc Debian 13 (le 993 reste). #}
|
||||
service imap-login {
|
||||
inet_listener imap {
|
||||
port = 0
|
||||
}
|
||||
}
|
||||
|
||||
# --- Auth / annuaire LDAP (serveur_openldap) ---
|
||||
auth_mechanisms = plain login
|
||||
ldap_uris = {{ serveur_dovecot_ldap_uris }}
|
||||
|
|
@ -69,16 +79,6 @@ service lmtp {
|
|||
}
|
||||
{% endif %}
|
||||
|
||||
{% if serveur_dovecot_sasl_reseau %}
|
||||
# --- Auth SASL réseau (soumission :587 depuis le MTA distant edge-mta) ---
|
||||
# À restreindre au nœud MTA par pare-feu.
|
||||
service auth {
|
||||
inet_listener sasl {
|
||||
port = {{ serveur_dovecot_sasl_port }}
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
# --- Sockets pour Postfix (remise LMTP + auth SASL) ---
|
||||
# Seulement si Postfix est co-localisé (l'utilisateur postfix existe). Sinon Dovecot
|
||||
# tourne en autonome (IMAP) ; les sockets seront ajoutés au déploiement de serveur_postfix.
|
||||
|
|
|
|||
43
roles/serveur_dovecot/templates/auth-locale.conf.j2
Normal file
43
roles/serveur_dovecot/templates/auth-locale.conf.j2
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Géré par Set-OPS (rôle serveur_dovecot, authentification locale). Ne pas éditer à la main.
|
||||
# Syntaxe Dovecot 2.4.
|
||||
{#
|
||||
AUTHENTIFICATION SEULE, A COTE DE POSTFIX (2026-10-12).
|
||||
|
||||
Postfix confiait la verification des mots de passe de soumission a la machine des
|
||||
boites, par `smtpd_sasl_path = inet:<mailstore>:12345`. Le client SASL Dovecot de
|
||||
Postfix ne sait pas chiffrer : chaque mot de passe traversait le reseau en clair, et le
|
||||
registre le disait `tls`. Directive du 2026-10-12 : tout transit chiffre.
|
||||
|
||||
On ne chiffre pas ce flux, on le supprime. Ce Dovecot-ci ne sert ni IMAP ni LMTP (aucun
|
||||
paquet de protocole n'est installe) et n'ecoute AUCUN port : Postfix lui parle par un
|
||||
socket local, et il verifie le mot de passe sur l'annuaire en LDAPS — le chemin que
|
||||
Postfix emprunte deja sur cette machine. Le mot de passe ne voyage plus que dans TLS.
|
||||
|
||||
Eprouve sur banc Debian 13 avant d'ecrire le role : processus `dovecot`, `auth`,
|
||||
`anvil`, `log`, `config` seulement ; 235 avec le bon mot de passe, 535 avec un faux,
|
||||
AUTH non offert hors TLS.
|
||||
#}
|
||||
dovecot_config_version = 2.4.0
|
||||
|
||||
# Garder l'adresse complète (user@domaine) comme identité — domaines virtuels.
|
||||
auth_username_format = %{user}
|
||||
auth_mechanisms = plain login
|
||||
ldap_uris = {{ serveur_dovecot_ldap_uris }}
|
||||
ldap_base = {{ serveur_dovecot_ldap_base }}
|
||||
ldap_auth_dn = {{ serveur_dovecot_ldap_bind_dn }}
|
||||
ldap_auth_dn_password = {{ serveur_dovecot_ldap_bind_password }}
|
||||
|
||||
{# Le meme passdb que la machine des boites : une identite, un mot de passe, une regle.
|
||||
Pas de userdb : une verification SASL n'en consulte aucun. #}
|
||||
passdb ldap {
|
||||
ldap_filter = {{ serveur_dovecot_ldap_filter }}
|
||||
ldap_bind = yes
|
||||
}
|
||||
|
||||
service auth {
|
||||
unix_listener {{ serveur_dovecot_postfix_socket_dir }}/auth {
|
||||
user = postfix
|
||||
group = postfix
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,33 @@
|
|||
#!/bin/bash
|
||||
# GENERE par Set-OPS (role serveur_dovecot). Ne pas editer a la main.
|
||||
#
|
||||
# SONDE « boites » — Dovecot repond et resout un utilisateur dans l'annuaire
|
||||
# SONDE « boites » — Dovecot repond et trouve un compte dans l'annuaire
|
||||
#
|
||||
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
|
||||
# Mise en defaut PAR PARAMETRE : `serveur_dovecot_sonde_utilisateur` (compte inexistant).
|
||||
set -uo pipefail
|
||||
|
||||
UTILISATEUR={{ serveur_dovecot_sonde_utilisateur }}
|
||||
UTILISATEUR={{ serveur_dovecot_sonde_utilisateur | quote }}
|
||||
|
||||
[[ -n "${UTILISATEUR}" ]] || { echo "Aucun compte a chercher (serveur_dovecot_sonde_utilisateur vide)."; exit 3; }
|
||||
systemctl is-active --quiet dovecot || { echo "Dovecot n'est pas actif."; exit 2; }
|
||||
|
||||
# ON RESOUT UN UTILISATEUR REEL, PAS UN PING DE SERVICE. C'est le bind LDAP qui tombe le
|
||||
# plus souvent, et il ne se voit que quand on demande un compte : le demon reste debout,
|
||||
# le port reste ouvert, et toute authentification echoue.
|
||||
sortie=$(doveadm user "${UTILISATEUR}" 2>&1)
|
||||
if ! printf '%s' "${sortie}" | grep -qE '^uid[[:space:]]'; then
|
||||
echo "Dovecot ne resout pas « ${UTILISATEUR} » — l'annuaire ne repond plus : ${sortie:0:70}"
|
||||
# LE PASSDB, PAS LE USERDB (2026-10-12). La sonde demandait `doveadm user`, et le userdb est
|
||||
# `static` avec `static_allow_all_users` (requis par la remise LMTP) : il repond pour
|
||||
# N'IMPORTE QUEL nom. Elle disait « daniel » resolu chez deux locataires ou ce compte
|
||||
# n'existe pas — verte quoi qu'il arrive.
|
||||
#
|
||||
# `doveadm auth lookup` interroge le passdb : Dovecot cherche le compte dans l'annuaire,
|
||||
# sous SON compte de liaison, en LDAPS, avec le filtre de connexion. Aucune tentative de
|
||||
# mot de passe — rien ne compte pour le verrouillage du compte. Mesure : 0 s'il existe,
|
||||
# 67 s'il n'existe pas, une erreur si l'annuaire ne repond plus.
|
||||
sortie=$(doveadm auth lookup "${UTILISATEUR}" 2>&1)
|
||||
rc=$?
|
||||
if [[ ${rc} -eq 67 ]]; then
|
||||
echo "Dovecot ne trouve pas « ${UTILISATEUR} » dans l'annuaire : plus personne de ce nom ne peut se connecter."
|
||||
exit 2
|
||||
elif [[ ${rc} -ne 0 ]] || ! printf '%s' "${sortie}" | grep -q '^passdb: '; then
|
||||
echo "Dovecot n'interroge plus l'annuaire (rc=${rc}) : ${sortie:0:70}"
|
||||
exit 2
|
||||
fi
|
||||
echo "Dovecot : « ${UTILISATEUR} » resolu dans l'annuaire, service actif."
|
||||
echo "Dovecot : « ${UTILISATEUR} » trouve dans l'annuaire (passdb LDAP), service actif."
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ serveur_forge_site_port: "{{ serveur_forgejo_port | default(443) }}"
|
|||
# `serveur_ops_depots` nomme des depots sous ce proprietaire, pas ailleurs.
|
||||
serveur_forge_site_organisation: "genome"
|
||||
|
||||
# COMBIEN DE DEPOTS AU MINIMUM. Un site sert au moins le moteur et sa propre carte ; en
|
||||
# dessous de deux, la forge n'a pas de quoi faire naitre un ecosysteme. On ne fixe pas un
|
||||
# compte exact : le nombre de locataires change, la sonde ne doit pas changer avec.
|
||||
serveur_forge_site_sonde_depots_minimum: 2
|
||||
# COMBIEN DE DEPOTS PUBLICS AU MINIMUM. La sonde lit l'API SANS identifiant : elle voit ce
|
||||
# qu'un ecosysteme neuf peut cloner sans rien detenir. Depuis le 2026-10-11, la carte du
|
||||
# site et les plans des locataires sont PRIVES (`docs/depots-prives-conception.md`) : il
|
||||
# reste au moins le moteur. Les depots prives se surveillent ailleurs — `make genome-etat`,
|
||||
# `make forge-cles`, et chaque runner qui les tire. On ne fixe pas un compte exact : le
|
||||
# nombre de depots change, la sonde ne doit pas changer avec.
|
||||
serveur_forge_site_sonde_depots_minimum: 1
|
||||
|
|
|
|||
|
|
@ -39,3 +39,22 @@ flux:
|
|||
# de le demander : seul le registre distingue « j'ouvre » de « j'autorise une source
|
||||
# de plus sur l'écoute d'un autre ».
|
||||
partage: true
|
||||
|
||||
# LA LECTURE DE SON DEPOT PAR LE RUNNER D'UN LOCATAIRE (2026-10-11,
|
||||
# `docs/depots-prives-conception.md`). Les depots des locataires deviennent prives : chacun
|
||||
# se lit avec une cle de deploiement en lecture seule, et une cle de deploiement ne passe
|
||||
# que par SSH. `runners_locataires` : les seuls runners, lus dans les faces publiees — pas
|
||||
# les supernets. L'utilisateur `git` de ce `sshd` est contraint par la forge : la cle n'y
|
||||
# ouvre aucune session, seulement le depot qui l'a enregistree.
|
||||
- sens: ingress
|
||||
port: 22
|
||||
protocole: tcp
|
||||
pair: runners_locataires
|
||||
chiffrement: ssh
|
||||
raison: >-
|
||||
Chaque runner de locataire lit son propre depot du genome, prive, avec sa cle de
|
||||
deploiement en lecture seule. Il ne lit ni le depot d'un autre locataire, ni celui
|
||||
du site.
|
||||
# CE ROLE N'OUVRE AUCUNE ECOUTE — il emprunte le `sshd` du socle, comme
|
||||
# `serveur_ops_tenant` (voir P33).
|
||||
partage: true
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ nombre=${lecture%% *}
|
|||
vides=${lecture#*|}
|
||||
|
||||
if (( nombre < MINIMUM )); then
|
||||
echo "La forge du genome ne porte que ${nombre} depot(s) pour « ${ORG} » (minimum ${MINIMUM}) : un ecosysteme neuf n'a rien a cloner."
|
||||
echo "La forge du genome ne sert que ${nombre} depot(s) public(s) pour « ${ORG} » (minimum ${MINIMUM}) : un ecosysteme neuf n'a rien a cloner."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -47,8 +47,16 @@ serveur_forgejo_tls_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_host
|
|||
serveur_forgejo_groupe: "serveur_forgejo"
|
||||
serveur_forgejo_db_host: "" # dérivé (FQDN) par resoudre_base au déploiement
|
||||
|
||||
# Relais courriel.
|
||||
serveur_forgejo_smtp: "infra-mail-01.{{ domaine_interne }}"
|
||||
# Relais courriel : DERIVE de l'inventaire, l'hote qui porte `serveur_postfix` (2026-10-12).
|
||||
#
|
||||
# Le defaut nommait `infra-mail-01`, la machine des BOITES d'un locataire, ou rien n'ecoute
|
||||
# le port 25 ; au site, ce nom n'existe meme pas. La forge du site n'a donc jamais remis un
|
||||
# seul courriel : « mot de passe oublie » repondait 200 et le journal disait
|
||||
# `lookup infra-mail-01.genese.internal ... server misbehaving`. Sans relais dans
|
||||
# l'ecosysteme, la valeur est vide et le courriel de la forge s'eteint (`app.ini`).
|
||||
serveur_forgejo_smtp: >-
|
||||
{{ ((groups['serveur_postfix'] | default([]) | first) ~ '.' ~ domaine_interne)
|
||||
if (groups['serveur_postfix'] | default([])) else '' }}
|
||||
|
||||
# Secrets OBLIGATOIRES (Ansible Vault).
|
||||
serveur_forgejo_secret_key: "{{ vault_forgejo_secret_key | default('') }}" # rempli depuis la voute (vault_forgejo_secret_key)
|
||||
|
|
|
|||
|
|
@ -68,8 +68,11 @@ ACCOUNT_LINKING = auto
|
|||
{% endif %}
|
||||
|
||||
[mailer]
|
||||
ENABLED = true
|
||||
PROTOCOL = smtp
|
||||
ENABLED = {{ 'true' if serveur_forgejo_smtp else 'false' }}
|
||||
{# STARTTLS EXIGE, CERTIFICAT VERIFIE (2026-10-12). `smtp` seul remettait en clair :
|
||||
Forgejo le signalait (« insecure SMTP protocol to non-local address »). Le relais
|
||||
porte un certificat de l'autorite de l'ecosysteme, que la machine reconnait. #}
|
||||
PROTOCOL = smtp+starttls
|
||||
SMTP_ADDR = {{ serveur_forgejo_smtp }}
|
||||
SMTP_PORT = 25
|
||||
FROM = forge@{{ domaine_interne }}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,17 @@ Environment=GF_SERVER_DOMAIN={{ serveur_grafana_hostname }}
|
|||
Environment=GF_SERVER_ROOT_URL={{ serveur_grafana_root_url }}
|
||||
Environment=GF_SECURITY_ADMIN_PASSWORD={{ serveur_grafana_admin_password }}
|
||||
Environment=GF_USERS_ALLOW_SIGN_UP=false
|
||||
{# GRAFANA N'APPELLE PERSONNE (mesure du 2026-10-10). Par defaut il interroge grafana.com
|
||||
(versions, greffons) et envoie ses compteurs d'usage a stats.grafana.org : ~124 et ~70
|
||||
connexions par jour, depuis le site et depuis chaque locataire. Les avatars viennent de
|
||||
gravatar.com, les nouvelles de grafana.com. Rien de tout cela ne sert une console
|
||||
interne ; les mises a jour arrivent par apt, comme le reste. #}
|
||||
Environment=GF_ANALYTICS_REPORTING_ENABLED=false
|
||||
Environment=GF_ANALYTICS_CHECK_FOR_UPDATES=false
|
||||
Environment=GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES=false
|
||||
Environment=GF_ANALYTICS_FEEDBACK_LINKS_ENABLED=false
|
||||
Environment=GF_SECURITY_DISABLE_GRAVATAR=true
|
||||
Environment=GF_NEWS_NEWS_FEED_ENABLED=false
|
||||
{# LA SYNTHESE COMME PAGE D'ACCUEIL (2026-10-04) : a la connexion, la question « faut-il
|
||||
agir ? » plutot que la liste des tableaux. Le fichier est pose par ce role (tableau de
|
||||
synthese) ; sans lui, Grafana revient a son accueil par defaut, sans erreur. #}
|
||||
|
|
|
|||
|
|
@ -79,89 +79,6 @@
|
|||
passe deviné : renseigner la clé dans la voûte de cet écosystème.
|
||||
when: serveur_icingaweb2_auth == 'db'
|
||||
|
||||
# LE CLIENT POSTGRESQL DES MODULES `community.postgresql`, SUR CETTE MACHINE (2026-10-03).
|
||||
#
|
||||
# Les requetes ci-dessous s'executent sur l'hote de la vigie, pas sur le serveur de base.
|
||||
# Au site, les deux sont la meme machine : `serveur_postgresql` y avait deja pose
|
||||
# `python3-psycopg2`, et l'absence ne se voyait pas. Chez un locataire, la vigie vit sur
|
||||
# `mon-01` et la base sur `data-sql-01` — premier passage apres que la base est devenue
|
||||
# commune a tous les modes : `No module named 'psycopg2'`, masque par `no_log`.
|
||||
- name: Installer le client PostgreSQL de Python (modules community.postgresql)
|
||||
ansible.builtin.apt:
|
||||
name: python3-psycopg2
|
||||
state: present
|
||||
|
||||
# LE SCHEMA NE SE CHARGE QU'UNE FOIS. Le rejouer sur une base deja peuplee echouerait sur
|
||||
# les objets existants — on lit donc d'abord si la table des comptes est la.
|
||||
- name: La base de la console est-elle déjà en place ?
|
||||
community.postgresql.postgresql_query:
|
||||
login_host: "{{ serveur_icingaweb2_db_host }}"
|
||||
login_user: "{{ serveur_icingaweb2_db_utilisateur }}"
|
||||
login_password: "{{ serveur_icingaweb2_db_motdepasse }}"
|
||||
login_db: "{{ serveur_icingaweb2_db_base }}"
|
||||
ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}"
|
||||
ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}"
|
||||
query: "SELECT to_regclass('public.icingaweb_user') IS NOT NULL AS presente"
|
||||
register: serveur_icingaweb2_schema_etat
|
||||
changed_when: false
|
||||
no_log: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Charger le schéma de la console (comptes, préférences, migrations)
|
||||
community.postgresql.postgresql_script:
|
||||
login_host: "{{ serveur_icingaweb2_db_host }}"
|
||||
login_user: "{{ serveur_icingaweb2_db_utilisateur }}"
|
||||
login_password: "{{ serveur_icingaweb2_db_motdepasse }}"
|
||||
login_db: "{{ serveur_icingaweb2_db_base }}"
|
||||
ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}"
|
||||
ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}"
|
||||
path: "{{ serveur_icingaweb2_schema }}"
|
||||
no_log: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- not (serveur_icingaweb2_schema_etat.query_result[0].presente | default(false))
|
||||
|
||||
# LE HACHAGE EST CELUI QUE L'APPLICATION VERIFIE, PAS UN QU'ON CHOISIT. `password_hash()`
|
||||
# de PHP est exactement la fonction qu'Icinga Web 2 appelle a la connexion ; produire un
|
||||
# hachage d'un autre outil donnerait une base valide et une connexion impossible.
|
||||
- name: Hacher le mot de passe du compte d'amorçage
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- php
|
||||
- -r
|
||||
- 'echo password_hash($argv[1], PASSWORD_DEFAULT);'
|
||||
- "{{ serveur_icingaweb2_admin_motdepasse }}"
|
||||
register: serveur_icingaweb2_hachage
|
||||
changed_when: false
|
||||
no_log: true
|
||||
when:
|
||||
- serveur_icingaweb2_auth == 'db'
|
||||
- not ansible_check_mode
|
||||
|
||||
# CREE S'IL MANQUE, JAMAIS ECRASE. Un mot de passe change dans l'interface appartient a
|
||||
# celui qui l'a change : le moteur ne le reprend pas au deploiement suivant. Il ne pose
|
||||
# que le compte qui permet d'entrer la premiere fois.
|
||||
- name: Poser le compte d'amorçage s'il n'existe pas
|
||||
community.postgresql.postgresql_query:
|
||||
login_host: "{{ serveur_icingaweb2_db_host }}"
|
||||
login_user: "{{ serveur_icingaweb2_db_utilisateur }}"
|
||||
login_password: "{{ serveur_icingaweb2_db_motdepasse }}"
|
||||
login_db: "{{ serveur_icingaweb2_db_base }}"
|
||||
ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}"
|
||||
ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}"
|
||||
query: >-
|
||||
INSERT INTO icingaweb_user (name, active, password_hash)
|
||||
VALUES (%s, 1, %s) ON CONFLICT (name) DO NOTHING
|
||||
positional_args:
|
||||
- "{{ serveur_icingaweb2_admin_utilisateur }}"
|
||||
- "{{ serveur_icingaweb2_hachage.stdout }}"
|
||||
no_log: true
|
||||
when:
|
||||
- serveur_icingaweb2_auth == 'db'
|
||||
- not ansible_check_mode
|
||||
|
||||
|
||||
# ON NE RESOUT PAS UN ANNUAIRE QUAND ON N'EN A PAS (2026-09-14).
|
||||
#
|
||||
# `resoudre_annuaire` etait appele SANS CONDITION. Au site — qui n'a ni LDAP ni Keycloak —
|
||||
|
|
@ -259,6 +176,94 @@
|
|||
| map('regex_replace', '^(?:Purg|Remv) (\\S+).*$', '\\1') | list)
|
||||
is subset(serveur_icingaweb2_apache_restes)
|
||||
|
||||
# LA BASE ET LE COMPTE D'AMORCAGE APRES LES PAQUETS (2026-10-13). Ce bloc venait AVANT
|
||||
# l'installation : il lit `/usr/share/icingaweb2/schema/pgsql.schema.sql` et hache par `php`,
|
||||
# deux choses que les paquets apportent. Invisible la ou la console etait deja installee ;
|
||||
# sur la premiere machine neuve (site de TechnoLibre), le schema n'existait pas encore.
|
||||
|
||||
# LE CLIENT POSTGRESQL DES MODULES `community.postgresql`, SUR CETTE MACHINE (2026-10-03).
|
||||
#
|
||||
# Les requetes ci-dessous s'executent sur l'hote de la vigie, pas sur le serveur de base.
|
||||
# Au site, les deux sont la meme machine : `serveur_postgresql` y avait deja pose
|
||||
# `python3-psycopg2`, et l'absence ne se voyait pas. Chez un locataire, la vigie vit sur
|
||||
# `mon-01` et la base sur `data-sql-01` — premier passage apres que la base est devenue
|
||||
# commune a tous les modes : `No module named 'psycopg2'`, masque par `no_log`.
|
||||
- name: Installer le client PostgreSQL de Python (modules community.postgresql)
|
||||
ansible.builtin.apt:
|
||||
name: python3-psycopg2
|
||||
state: present
|
||||
|
||||
# LE SCHEMA NE SE CHARGE QU'UNE FOIS. Le rejouer sur une base deja peuplee echouerait sur
|
||||
# les objets existants — on lit donc d'abord si la table des comptes est la.
|
||||
- name: La base de la console est-elle déjà en place ?
|
||||
community.postgresql.postgresql_query:
|
||||
login_host: "{{ serveur_icingaweb2_db_host }}"
|
||||
login_user: "{{ serveur_icingaweb2_db_utilisateur }}"
|
||||
login_password: "{{ serveur_icingaweb2_db_motdepasse }}"
|
||||
login_db: "{{ serveur_icingaweb2_db_base }}"
|
||||
ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}"
|
||||
ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}"
|
||||
query: "SELECT to_regclass('public.icingaweb_user') IS NOT NULL AS presente"
|
||||
register: serveur_icingaweb2_schema_etat
|
||||
changed_when: false
|
||||
no_log: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Charger le schéma de la console (comptes, préférences, migrations)
|
||||
community.postgresql.postgresql_script:
|
||||
login_host: "{{ serveur_icingaweb2_db_host }}"
|
||||
login_user: "{{ serveur_icingaweb2_db_utilisateur }}"
|
||||
login_password: "{{ serveur_icingaweb2_db_motdepasse }}"
|
||||
login_db: "{{ serveur_icingaweb2_db_base }}"
|
||||
ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}"
|
||||
ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}"
|
||||
path: "{{ serveur_icingaweb2_schema }}"
|
||||
no_log: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- not (serveur_icingaweb2_schema_etat.query_result[0].presente | default(false))
|
||||
|
||||
# LE HACHAGE EST CELUI QUE L'APPLICATION VERIFIE, PAS UN QU'ON CHOISIT. `password_hash()`
|
||||
# de PHP est exactement la fonction qu'Icinga Web 2 appelle a la connexion ; produire un
|
||||
# hachage d'un autre outil donnerait une base valide et une connexion impossible.
|
||||
- name: Hacher le mot de passe du compte d'amorçage
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- php
|
||||
- -r
|
||||
- 'echo password_hash($argv[1], PASSWORD_DEFAULT);'
|
||||
- "{{ serveur_icingaweb2_admin_motdepasse }}"
|
||||
register: serveur_icingaweb2_hachage
|
||||
changed_when: false
|
||||
no_log: true
|
||||
when:
|
||||
- serveur_icingaweb2_auth == 'db'
|
||||
- not ansible_check_mode
|
||||
|
||||
# CREE S'IL MANQUE, JAMAIS ECRASE. Un mot de passe change dans l'interface appartient a
|
||||
# celui qui l'a change : le moteur ne le reprend pas au deploiement suivant. Il ne pose
|
||||
# que le compte qui permet d'entrer la premiere fois.
|
||||
- name: Poser le compte d'amorçage s'il n'existe pas
|
||||
community.postgresql.postgresql_query:
|
||||
login_host: "{{ serveur_icingaweb2_db_host }}"
|
||||
login_user: "{{ serveur_icingaweb2_db_utilisateur }}"
|
||||
login_password: "{{ serveur_icingaweb2_db_motdepasse }}"
|
||||
login_db: "{{ serveur_icingaweb2_db_base }}"
|
||||
ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}"
|
||||
ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}"
|
||||
query: >-
|
||||
INSERT INTO icingaweb_user (name, active, password_hash)
|
||||
VALUES (%s, 1, %s) ON CONFLICT (name) DO NOTHING
|
||||
positional_args:
|
||||
- "{{ serveur_icingaweb2_admin_utilisateur }}"
|
||||
- "{{ serveur_icingaweb2_hachage.stdout }}"
|
||||
no_log: true
|
||||
when:
|
||||
- serveur_icingaweb2_auth == 'db'
|
||||
- not ansible_check_mode
|
||||
|
||||
|
||||
- name: Ajouter www-data au groupe icingaweb2 (lecture de la config)
|
||||
ansible.builtin.user:
|
||||
name: www-data
|
||||
|
|
|
|||
|
|
@ -45,7 +45,10 @@ flux:
|
|||
port: 1514
|
||||
protocole: tcp
|
||||
pair: fabric
|
||||
chiffrement: clair
|
||||
# EN TLS, CLIENT AUTHENTIFIE (2026-10-12) : Alloy sert le certificat de la machine et
|
||||
# exige un certificat emis par l'autorite de la frontiere ; la frontiere verifie Alloy
|
||||
# contre la racine du site (scripts/frontiere_pki.py).
|
||||
chiffrement: tls-requis
|
||||
raison: >-
|
||||
Journaux de la frontiere. Elle n'accueille aucun agent et n'emet que du syslog ; sans
|
||||
ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part.
|
||||
Journaux de la frontiere, en TLS mutuel. Elle n'accueille aucun agent et n'emet que du
|
||||
syslog ; sans ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part.
|
||||
|
|
|
|||
|
|
@ -195,6 +195,56 @@
|
|||
group: loki
|
||||
mode: "0750"
|
||||
|
||||
# --- LA BASE D'UNE INCARNATION PRECEDENTE (2026-10-07) --------------------------------
|
||||
#
|
||||
# Les journaux repartaient de zero a chaque reconstruction. client_backup en garde une
|
||||
# copie a froid (jeu `loki`) ; on la remet ICI, avant que Loki ne demarre sur sa
|
||||
# configuration : une base sans aucun morceau de journal est une base neuve. Service
|
||||
# arrete pendant la remise ; « Activer et demarrer Loki » plus bas le relance.
|
||||
- name: La base de Loki a-t-elle deja des morceaux de journal ?
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_loki_chemin }}/chunks"
|
||||
recurse: true
|
||||
register: serveur_loki_morceaux
|
||||
|
||||
- name: Restauration — la base d'une incarnation precedente ?
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: restaurer.yml
|
||||
vars:
|
||||
client_backup_restaurer_jeu: serveur_loki
|
||||
client_backup_restaurer_vierge: "{{ serveur_loki_morceaux.matched == 0 }}"
|
||||
|
||||
- name: Restauration — remettre la base, service arrete
|
||||
when: client_backup_restauration.etat == 'a_restaurer'
|
||||
block:
|
||||
- name: Restauration — arreter Loki
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ serveur_loki_service }}"
|
||||
state: stopped
|
||||
|
||||
- name: Restauration — remettre la copie a froid a sa place
|
||||
changed_when: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/local/sbin/setops-restaurer
|
||||
- fichiers
|
||||
- --instantane
|
||||
- "{{ client_backup_restauration.instantane }}"
|
||||
- --depuis
|
||||
- "{{ client_backup_restauration.chemins[0] }}/donnees"
|
||||
- --remplacer
|
||||
- --proprietaire
|
||||
- loki:loki
|
||||
- "{{ serveur_loki_chemin }}"
|
||||
|
||||
- name: Restauration — acter la base remise
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: acter.yml
|
||||
vars:
|
||||
client_backup_acter_etat: restaure
|
||||
|
||||
# --- TLS : synchroniser le cert step_ca AVANT d'activer http_tls_config ---
|
||||
- name: TLS — repertoire des certificats (loki)
|
||||
ansible.builtin.file:
|
||||
|
|
|
|||
|
|
@ -39,3 +39,7 @@ compactor:
|
|||
|
||||
limits_config:
|
||||
retention_period: {{ serveur_loki_retention }}
|
||||
|
||||
{# Pas de rapport d'usage a stats.grafana.org (actif par defaut, mesure du 2026-10-10). #}
|
||||
analytics:
|
||||
reporting_enabled: false
|
||||
|
|
|
|||
|
|
@ -74,6 +74,31 @@ serveur_nextcloud_http_port: 80
|
|||
# IP de l'edge → trusted_proxies (pour voir la vraie IP client, pas throttler l'edge).
|
||||
serveur_nextcloud_edge_ip: "{{ hostvars[(groups['serveur_nginx'] | default([]) | first) | default('')].ansible_host | default('') }}"
|
||||
|
||||
# --- LES APPLICATIONS NE VIENNENT PLUS DE L'APP STORE (2026-10-08) -------------------
|
||||
#
|
||||
# `occ app:install` les tirait d'apps.nextcloud.com a chaque construction. Le 2026-10-08,
|
||||
# l'app store servait son catalogue (11,7 Mo) a 77 ko/s — 18,8 Mo/s ailleurs depuis la meme
|
||||
# machine : Nextcloud a abandonne, et la reconstruction de Technolibre s'est arretee. Le
|
||||
# site tient desormais ces archives dans son depot de binaires, comme celle de Nextcloud
|
||||
# (`serveur_artefacts`), et le role les installe sans l'app store.
|
||||
#
|
||||
# EPINGLEES, VERIFIEES DEUX FOIS : la somme de l'archive (ci-dessous), puis la signature de
|
||||
# son code (`occ integrity:check-app`, contre l'autorite de signature de Nextcloud, hors
|
||||
# ligne ; eprouve : une ligne ajoutee a un fichier donne INVALID_HASH). Versions relevees
|
||||
# sur Chezlepro, ou elles tournent sur Nextcloud 34.0.4. `nom` et `url` portent la version
|
||||
# en toutes lettres : `test_apps_nextcloud.py` verifie qu'elles disent la meme.
|
||||
serveur_nextcloud_apps:
|
||||
- id: user_oidc
|
||||
version: "8.11.0"
|
||||
nom: "nextcloud-app-user_oidc-8.11.0.tar.gz"
|
||||
url: "https://github.com/nextcloud-releases/user_oidc/releases/download/v8.11.0/user_oidc-v8.11.0.tar.gz"
|
||||
sha256: "887920504c2f0e0d055abd66225ee7b66d756f50fe5a3739779d6b2ce108633f"
|
||||
- id: richdocuments
|
||||
version: "11.1.2"
|
||||
nom: "nextcloud-app-richdocuments-11.1.2.tar.gz"
|
||||
url: "https://github.com/nextcloud-releases/richdocuments/releases/download/v11.1.2/richdocuments-v11.1.2.tar.gz"
|
||||
sha256: "96766a94cf417a716e47ed89c6ff2411c54954caa916afcdddc4d9668dffb557"
|
||||
|
||||
# --- SSO OIDC (Keycloak) — app user_oidc ---
|
||||
serveur_nextcloud_oidc_actif: true
|
||||
serveur_nextcloud_oidc_nom: "{{ organisation | default('Organisation') }}"
|
||||
|
|
|
|||
125
roles/serveur_nextcloud/tasks/app_tierce.yml
Normal file
125
roles/serveur_nextcloud/tasks/app_tierce.yml
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
# UNE APPLICATION NEXTCLOUD, SANS L'APP STORE (2026-10-08) — voir `serveur_nextcloud_apps`.
|
||||
#
|
||||
# entree : serveur_nextcloud_app_id (user_oidc, richdocuments)
|
||||
#
|
||||
# Le depot du SITE d'abord, l'amont ensuite (meme patron que l'archive de Nextcloud,
|
||||
# `installer.yml`) ; la somme verifiee dans les deux cas ; puis la signature du code ; puis
|
||||
# l'activation. Une version installee qui differe de l'epinglee ARRETE : une mise a niveau
|
||||
# d'application est une decision (et un `occ upgrade`), pas un effet de bord.
|
||||
- name: "Application Nextcloud — l'application epinglee ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.set_fact:
|
||||
serveur_nextcloud_app: >-
|
||||
{{ serveur_nextcloud_apps | selectattr('id', 'equalto', serveur_nextcloud_app_id) | first }}
|
||||
|
||||
- name: "Application Nextcloud — deja presente ? ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ serveur_nextcloud_racine }}/apps/{{ serveur_nextcloud_app.id }}/appinfo/info.xml"
|
||||
register: serveur_nextcloud_app_info
|
||||
failed_when: false
|
||||
|
||||
- name: "Application Nextcloud — la version presente est-elle l'epinglee ? ({{ serveur_nextcloud_app_id }})"
|
||||
vars:
|
||||
presente: >-
|
||||
{{ (serveur_nextcloud_app_info.content | b64decode
|
||||
| regex_search('<version>([^<]+)</version>', '\\1') | default([''], true) | first)
|
||||
if serveur_nextcloud_app_info.content is defined else '' }}
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- presente | length == 0 or presente == serveur_nextcloud_app.version
|
||||
fail_msg: >-
|
||||
{{ serveur_nextcloud_app.id }} {{ presente }} est installee, {{ serveur_nextcloud_app.version }}
|
||||
est epinglee. Une mise a niveau d'application se decide (et passe par `occ upgrade`) :
|
||||
ajuster `serveur_nextcloud_apps`, ou mettre a niveau a la main.
|
||||
quiet: true
|
||||
|
||||
- name: "Application Nextcloud — l'obtenir et la deposer ({{ serveur_nextcloud_app_id }})"
|
||||
when: serveur_nextcloud_app_info.content is not defined
|
||||
block:
|
||||
- name: "Application Nextcloud — le cache du controleur ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_nextcloud_cache_local }}"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
# `failed_when: false` : le depot est une COMMODITE ; sans lui, on retombe sur l'amont.
|
||||
# La somme est verifiee ici aussi : un fichier altere chez le site ne passe pas.
|
||||
- name: "Application Nextcloud — au depot du site, avant de sortir sur Internet ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ setops_depot_binaires }}/{{ serveur_nextcloud_app.nom }}"
|
||||
dest: "{{ serveur_nextcloud_cache_local }}/{{ serveur_nextcloud_app.nom }}"
|
||||
checksum: "sha256:{{ serveur_nextcloud_app.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 30
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: serveur_nextcloud_app_site
|
||||
failed_when: false
|
||||
when: setops_depot_binaires | default('') | length > 0
|
||||
|
||||
- name: "Application Nextcloud — dans le cache du controleur ? ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_nextcloud_cache_local }}/{{ serveur_nextcloud_app.nom }}"
|
||||
checksum_algorithm: sha256
|
||||
register: serveur_nextcloud_app_cache
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
# ON LE DIT, FORT : une reconstruction qui sort sur Internet doit se voir.
|
||||
- name: "Application Nextcloud — absente du depot du site : prise en amont ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ serveur_nextcloud_app.nom }} n'est pas au depot du site
|
||||
({{ setops_depot_binaires | default('aucun depot declare') }}) : prise sur Internet.
|
||||
when: not (serveur_nextcloud_app_cache.stat.exists
|
||||
and serveur_nextcloud_app_cache.stat.checksum == serveur_nextcloud_app.sha256)
|
||||
|
||||
- name: "Application Nextcloud — en amont (repli) ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ serveur_nextcloud_app.url }}"
|
||||
dest: "{{ serveur_nextcloud_cache_local }}/{{ serveur_nextcloud_app.nom }}"
|
||||
checksum: "sha256:{{ serveur_nextcloud_app.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 60
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
when: not (serveur_nextcloud_app_cache.stat.exists
|
||||
and serveur_nextcloud_app_cache.stat.checksum == serveur_nextcloud_app.sha256)
|
||||
|
||||
- name: "Application Nextcloud — deposer le code dans apps/ ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ serveur_nextcloud_cache_local }}/{{ serveur_nextcloud_app.nom }}"
|
||||
dest: "{{ serveur_nextcloud_racine }}/apps"
|
||||
owner: "{{ serveur_nextcloud_utilisateur }}"
|
||||
group: "{{ serveur_nextcloud_utilisateur }}"
|
||||
creates: "{{ serveur_nextcloud_racine }}/apps/{{ serveur_nextcloud_app.id }}/appinfo/info.xml"
|
||||
|
||||
- name: "Application Nextcloud — signature du code, puis activation ({{ serveur_nextcloud_app_id }})"
|
||||
become: true
|
||||
become_user: "{{ serveur_nextcloud_utilisateur }}"
|
||||
environment:
|
||||
PGSSLMODE: "{{ serveur_nextcloud_pg_sslmode }}"
|
||||
PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}"
|
||||
when: not ansible_check_mode
|
||||
block:
|
||||
# Contre l'autorite de signature de Nextcloud, sans reseau. Rien a dire = intact.
|
||||
- name: "Application Nextcloud — le code est-il celui que Nextcloud a signe ? ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.command:
|
||||
argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "integrity:check-app", "{{ serveur_nextcloud_app.id }}"]
|
||||
register: serveur_nextcloud_app_integrite
|
||||
changed_when: false
|
||||
failed_when: serveur_nextcloud_app_integrite.rc != 0 or serveur_nextcloud_app_integrite.stdout | trim | length > 0
|
||||
|
||||
- name: "Application Nextcloud — activer ({{ serveur_nextcloud_app_id }})"
|
||||
ansible.builtin.shell:
|
||||
executable: /bin/bash
|
||||
cmd: |
|
||||
set -eo pipefail
|
||||
OCC="php {{ serveur_nextcloud_racine }}/occ"
|
||||
APP="{{ serveur_nextcloud_app.id }}"
|
||||
if $OCC app:list --enabled | grep -qw "$APP"; then echo SETOPS_OK
|
||||
else $OCC app:enable "$APP" && echo SETOPS_CHANGED; fi
|
||||
register: serveur_nextcloud_app_active
|
||||
changed_when: "'SETOPS_CHANGED' in serveur_nextcloud_app_active.stdout"
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
---
|
||||
# L'APPLICATION VIENT DU DEPOT DU SITE, PLUS DE L'APP STORE (2026-10-08) : `app_tierce.yml`.
|
||||
- name: Installer l'app richdocuments (depot du site, somme, signature, activation)
|
||||
ansible.builtin.include_tasks: app_tierce.yml
|
||||
vars:
|
||||
serveur_nextcloud_app_id: richdocuments
|
||||
|
||||
- name: Collabora / Office (occ, en www-data avec TLS PG)
|
||||
become: true
|
||||
become_user: "{{ serveur_nextcloud_utilisateur }}"
|
||||
|
|
@ -7,16 +13,6 @@
|
|||
PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}"
|
||||
when: not ansible_check_mode
|
||||
block:
|
||||
- name: Installer + activer l'app richdocuments (idempotent)
|
||||
ansible.builtin.shell:
|
||||
executable: /bin/bash
|
||||
cmd: |
|
||||
set -eo pipefail
|
||||
OCC="php {{ serveur_nextcloud_racine }}/occ"
|
||||
if $OCC app:list | grep -qw richdocuments; then echo SETOPS_OK; else $OCC app:install richdocuments && echo SETOPS_CHANGED; fi
|
||||
register: serveur_nextcloud_office_app
|
||||
changed_when: "'SETOPS_CHANGED' in serveur_nextcloud_office_app.stdout"
|
||||
|
||||
- name: URL serveur -> Collabora (locale, rapide)
|
||||
ansible.builtin.command:
|
||||
argv: ["php", "{{ serveur_nextcloud_racine }}/occ", "config:app:set", "richdocuments", "wopi_url", "--value={{ serveur_nextcloud_wopi_url }}"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
---
|
||||
# L'APPLICATION VIENT DU DEPOT DU SITE, PLUS DE L'APP STORE (2026-10-08) : `app_tierce.yml`.
|
||||
- name: Installer l'app user_oidc (depot du site, somme, signature, activation)
|
||||
ansible.builtin.include_tasks: app_tierce.yml
|
||||
vars:
|
||||
serveur_nextcloud_app_id: user_oidc
|
||||
|
||||
- name: SSO OIDC (occ, en www-data avec TLS PG)
|
||||
become: true
|
||||
become_user: "{{ serveur_nextcloud_utilisateur }}"
|
||||
|
|
@ -7,16 +13,6 @@
|
|||
PGSSLROOTCERT: "{{ serveur_nextcloud_ca_racine }}"
|
||||
when: not ansible_check_mode
|
||||
block:
|
||||
- name: Installer + activer l'app user_oidc (idempotent)
|
||||
ansible.builtin.shell:
|
||||
executable: /bin/bash
|
||||
cmd: |
|
||||
set -eo pipefail
|
||||
OCC="php {{ serveur_nextcloud_racine }}/occ"
|
||||
if $OCC app:list | grep -qw user_oidc; then echo SETOPS_OK; else $OCC app:install user_oidc && echo SETOPS_CHANGED; fi
|
||||
register: serveur_nextcloud_oidc_app
|
||||
changed_when: "'SETOPS_CHANGED' in serveur_nextcloud_oidc_app.stdout"
|
||||
|
||||
- name: Ne plus annoncer le formulaire de connexion locale
|
||||
# Masquage, pas fermeture : `?direct=1` reste le chemin de secours documente par
|
||||
# l'amont. Voir serveur_nextcloud_connexion_locale dans defaults/main.yml.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ serveur_ops_paquets:
|
|||
# Ansible est ÉPINGLÉ sur la même famille que le poste du mainteneur (core 2.18) : un
|
||||
# écosystème qui se reconstruit avec une version différente de celle qui l'a construit ne
|
||||
# reproduit pas la même chose, et l'écart ne se voit qu'au premier échec.
|
||||
serveur_ops_ansible: "ansible-core>=2.18,<2.19"
|
||||
serveur_ops_ansible: "ansible-core==2.18.19"
|
||||
|
||||
# --- D'OÙ VIENT LE GÉNOME ----------------------------------------------------
|
||||
#
|
||||
|
|
@ -101,6 +101,29 @@ serveur_ops_forge_amont_ac_depot: "{{ serveur_ops_racine }}/.ac-amont.crt"
|
|||
serveur_ops_depots:
|
||||
- { depot: "set-ops-public", dest: "Set-OPS-public", role: "moteur" }
|
||||
|
||||
# --- LES DEPOTS PRIVES (2026-10-11, `docs/depots-prives-conception.md`) --------
|
||||
#
|
||||
# Le plan d'un ecosysteme et la carte du site ne regardent que leur runner — et le runner du
|
||||
# site. Sur la forge du site, ils sont PRIVES : chaque runner les lit en SSH, avec SA cle,
|
||||
# enregistree comme cle de deploiement en lecture sur son seul depot (`make forge-cles`).
|
||||
# Le moteur et les modeles restent publics, et se clonent comme avant, en HTTPS anonyme :
|
||||
# une cle de deploiement ne vaut que pour le depot qui l'a enregistree (mesure du jour).
|
||||
serveur_ops_roles_prives: ["instance", "tenant", "hebergeur"]
|
||||
# La cle d'hote SSH de la forge, epinglee : le runner sait a qui il parle avant de presenter
|
||||
# sa cle. Au site, elle vient de son plan (`forge_ssh_cle_hote`) ; chez un locataire, de sa
|
||||
# fiche (`serveur_ops_forge_cle_hote`, copie verifiee par `make site-intrants`).
|
||||
serveur_ops_forge_cle_hote: ""
|
||||
serveur_ops_forge_ssh_hote: "{{ serveur_ops_forge_url | urlsplit('hostname') }}"
|
||||
serveur_ops_forge_known_hosts: "{{ serveur_ops_racine }}/.ssh/known_hosts_forge"
|
||||
serveur_ops_cle_privee: "{{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}"
|
||||
# Ou le CONTROLEUR fabrique ses colis avant de les porter (jamais dans un depot).
|
||||
serveur_ops_colis_local: "{{ lookup('env', 'TMPDIR') | default('/tmp', true) }}"
|
||||
# La commande SSH des depots prives, posee dans LEUR config git (`core.sshCommand`) : la
|
||||
# console, la garde du genome et `make monter` la reprennent sans rien savoir de tout ceci.
|
||||
serveur_ops_ssh_prive: >-
|
||||
ssh -i {{ serveur_ops_cle_privee }} -o BatchMode=yes -o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ serveur_ops_forge_known_hosts }}
|
||||
|
||||
# --- LES DEUX SYMLINKS (D-80) ------------------------------------------------
|
||||
#
|
||||
# `instance` dit QUEL tenant on pilote.
|
||||
|
|
|
|||
|
|
@ -108,80 +108,140 @@
|
|||
become: false
|
||||
run_once: true
|
||||
|
||||
# LE CONTROLEUR ET LA CIBLE DOIVENT PARTAGER LEUR PYTHON, et ca ne se devine pas.
|
||||
# LE VERROU DU RUNNER (2026-10-09) — `verrou-runner.yml`, a la racine du moteur.
|
||||
#
|
||||
# `pip download` rend les roues du systeme qui telecharge. `ansible-core`, `proxmoxer` et
|
||||
# `requests` sont universelles (`py3-none-any`), mais `pyyaml` porte du C compile : une
|
||||
# roue `cp313` posee sur un python 3.12 ne s'installe pas. L'echec serait « No matching
|
||||
# distribution found » a l'installation — un message qui accuse le depot local alors que
|
||||
# le coupable est l'ecart entre deux machines. On le mesure ici, ou on peut encore le dire.
|
||||
- name: Lire le Python du controleur
|
||||
ansible.builtin.command:
|
||||
argv: ["python3", "-c", "import sys; print('%d.%d' % sys.version_info[:2])"]
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
changed_when: false
|
||||
# LIRE UNE VERSION N'EST PAS CHANGER, ET `--check` RENDAIT LA GARDE MUETTE (2026-09-14).
|
||||
#
|
||||
# Sans `check_mode: false`, cette commande est sautee en mode idempotent : la garde qui
|
||||
# suit compare alors contre du VIDE et refuse.
|
||||
#
|
||||
# Le controleur tourne Python et site-ops-01 tourne Python 3.13
|
||||
#
|
||||
# L'espace apres « Python » est tout le diagnostic — il n'y avait rien a comparer. Un
|
||||
# essai a blanc rendait donc un echec sur le runner, alors que le deploiement reel passe.
|
||||
check_mode: false
|
||||
register: serveur_ops_python_controleur
|
||||
# Les roues ne sont plus resolues par `pip download` contre PyPI, a chaque passage : elles
|
||||
# sont ECRITES, une par une, avec leur somme, et le SITE les tient dans son depot de
|
||||
# binaires. Le runner de Technolibre portait deux versions de six bibliotheques — PyPI avait
|
||||
# bouge entre l'insemination et le montage. Desormais, ce qui s'installe est ce que le
|
||||
# verrou nomme, et rien d'autre.
|
||||
- name: Lire le verrou du runner (roues Python, collections Ansible)
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ playbook_dir }}/../../verrou-runner.yml"
|
||||
name: serveur_ops_verrou
|
||||
|
||||
- name: Exiger que le controleur et la cible partagent leur version de Python
|
||||
# LA CIBLE DOIT PORTER LE PYTHON DES ROUES VERROUILLEES. `pyyaml`, `cffi`, `markupsafe`...
|
||||
# portent du C compile pour un Python donne : une roue `cp313` posee sur un 3.12 ne
|
||||
# s'installe pas, et l'echec accuserait le depot local. Le controleur, lui, ne compile plus
|
||||
# rien : il ne fait que transporter des fichiers ecrits.
|
||||
- name: Exiger que la cible porte le Python des roues verrouillees
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- serveur_ops_python_controleur.stdout | trim
|
||||
== ansible_python_version.split('.')[0:2] | join('.')
|
||||
- ansible_python_version.split('.')[0:2] | join('.') == serveur_ops_verrou.python_cible | string
|
||||
fail_msg: >-
|
||||
Le controleur tourne Python {{ serveur_ops_python_controleur.stdout | trim }} et
|
||||
{{ inventory_hostname }} tourne Python
|
||||
{{ ansible_python_version.split('.')[0:2] | join('.') }}. Les roues telechargees ne
|
||||
s'installeraient pas sur la cible, et l'echec accuserait le depot local au lieu de
|
||||
cet ecart. Materialiser depuis un controleur de meme famille — pendant une
|
||||
insemination, c'est le runner du SITE, qui porte le meme Debian que la machine
|
||||
qu'il amorce.
|
||||
{{ inventory_hostname }} tourne Python {{ ansible_python_version }} ; les roues de
|
||||
`verrou-runner.yml` sont faites pour Python {{ serveur_ops_verrou.python_cible }}.
|
||||
Monter le verrou (nouvelles roues, nouvelles sommes), ou garder la cible sur ce Python.
|
||||
|
||||
# `argv` ET NON `cmd` : le chemin du controleur peut contenir une ESPACE (« Espace
|
||||
# Chezlepro/… » chez le mainteneur). Meme piege que pour ansible-galaxy le 2026-08-23.
|
||||
- name: Telecharger les roues sur le controleur (une seule fois par empreinte)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "python3"
|
||||
- "-m"
|
||||
- "pip"
|
||||
- "download"
|
||||
- "--dest"
|
||||
- "{{ serveur_ops_cache_roues }}"
|
||||
- "--requirement"
|
||||
- "{{ serveur_ops_roues_source }}"
|
||||
- "{{ serveur_ops_ansible }}"
|
||||
- "pyyaml"
|
||||
# LE DEPOT DU SITE D'ABORD, L'AMONT ENSUITE — la somme verifiee dans les deux cas.
|
||||
# `failed_when: false` : le depot est une COMMODITE ; sans lui, on retombe sur l'amont, et on
|
||||
# le dit (tache suivante).
|
||||
- name: Prendre les roues verrouillees au depot du site
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ setops_depot_binaires }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_cache_roues }}/{{ item.nom }}"
|
||||
checksum: "sha256:{{ item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 30
|
||||
loop: "{{ serveur_ops_verrou.python }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
register: serveur_ops_roues_telechargees
|
||||
changed_when: "'Saved' in serveur_ops_roues_telechargees.stdout"
|
||||
# LE GARDE-FOU SUIT LE RESULTAT, PAS LE GESTE (lecon du 2026-08-27) : on saute quand le
|
||||
# cache PORTE deja des roues, pas quand une commande a deja tourne un jour.
|
||||
when: not ansible_check_mode
|
||||
failed_when: false
|
||||
when:
|
||||
- setops_depot_binaires | default('') | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Deposer les roues sur le runner
|
||||
- name: Les roues verrouillees sont-elles dans le cache du controleur ?
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_ops_cache_roues }}/{{ item.nom }}"
|
||||
checksum_algorithm: sha256
|
||||
loop: "{{ serveur_ops_verrou.python }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
register: serveur_ops_roues_cache
|
||||
|
||||
# ON LE DIT, FORT : une reconstruction qui sort sur Internet doit se voir.
|
||||
- name: Dire les roues absentes du depot du site (prises en amont)
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ serveur_ops_roues_cache.results | rejectattr('stat.exists') | map(attribute='item.nom') | list }}
|
||||
— absentes du depot du site ({{ setops_depot_binaires | default('aucun depot declare') }}) : prises sur Internet.
|
||||
run_once: true
|
||||
# PAS EN SIMULATION : `--check` ne telecharge rien depuis le site, le cache reste vide et
|
||||
# ce message accuserait le depot a tort (vu le 2026-10-09 sur le runner de Technolibre).
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- serveur_ops_roues_cache.results | rejectattr('stat.exists') | list | length > 0
|
||||
|
||||
- name: Prendre en amont les roues que le site n'a pas (repli)
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ item.item.url }}"
|
||||
dest: "{{ serveur_ops_cache_roues }}/{{ item.item.nom }}"
|
||||
checksum: "sha256:{{ item.item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 60
|
||||
# ABSENTE OU ALTEREE : `pip --no-index` ne verifie pas les sommes, un fichier present mais
|
||||
# different passerait. On le reprend, et `get_url` le verifie.
|
||||
loop: "{{ serveur_ops_roues_cache.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- not item.stat.exists or item.stat.checksum != item.item.sha256
|
||||
|
||||
# LES ROUES DU VERROU, ET ELLES SEULES. Copier le cache entier deposait aussi les versions
|
||||
# d'hier (deux `cryptography`, deux `urllib3`...) ; `--no-index` choisissait alors la plus
|
||||
# haute, que personne n'avait choisie.
|
||||
# LE DOSSIER D'ABORD (2026-10-09). L'ancienne copie du cache ENTIER (`src: dossier/`) creait
|
||||
# sa destination ; une copie fichier par fichier ne la cree pas. Sur un runner neuf, la
|
||||
# reconstruction de Technolibre s'est arretee la (« Destination directory … does not exist »)
|
||||
# — invisible en simulant sur un runner ou le dossier existait deja.
|
||||
- name: Assurer le depot des roues sur le runner
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_roues_depot }}"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Deposer sur le runner les roues du verrou
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_cache_roues }}/"
|
||||
dest: "{{ serveur_ops_roues_depot }}/"
|
||||
src: "{{ serveur_ops_cache_roues }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_roues_depot }}/{{ item.nom }}"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
directory_mode: "0755"
|
||||
loop: "{{ serveur_ops_verrou.python }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Relever les roues deposees sur le runner
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_ops_roues_depot }}"
|
||||
patterns: "*.whl"
|
||||
register: serveur_ops_roues_deposees
|
||||
|
||||
- name: Retirer du runner les roues que le verrou ne nomme pas
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: >-
|
||||
{{ serveur_ops_roues_deposees.files
|
||||
| rejectattr('path', 'search', '/(' ~ (serveur_ops_verrou.python | map(attribute='nom')
|
||||
| map('regex_escape') | join('|')) ~ ')$') | list }}
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
|
||||
# `--no-index` EST LE POINT, PAS UNE OPTIMISATION. Sans lui, pip resterait capable de
|
||||
# sortir vers PyPI le jour ou le depot local serait incomplet — et la reussite dependrait
|
||||
# alors d'un flux que ce tenant n'a pas le droit d'avoir. Un repli silencieux vers
|
||||
|
|
@ -289,7 +349,8 @@
|
|||
version: "{{ item.branche | default('main') }}"
|
||||
update: true
|
||||
force: false
|
||||
loop: "{{ serveur_ops_depots }}"
|
||||
# LES DEPOTS PUBLICS SEULEMENT (2026-10-11). Les prives suivent, plus bas, en SSH.
|
||||
loop: "{{ serveur_ops_depots | rejectattr('role', 'in', serveur_ops_roles_prives) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }} → {{ item.dest }}"
|
||||
become: true
|
||||
|
|
@ -300,6 +361,186 @@
|
|||
delay: 6
|
||||
until: serveur_ops_clone is succeeded
|
||||
|
||||
# --- LES DEPOTS PRIVES : EN SSH, OU EN COLIS TANT QUE LE RUNNER NE PEUT PAS ---------------
|
||||
#
|
||||
# (2026-10-11, `docs/depots-prives-conception.md`.) Le plan d'un ecosysteme et la carte du
|
||||
# site sont prives sur la forge du site. Le runner les lit en SSH, avec SA cle, que le site a
|
||||
# enregistree comme cle de deploiement sur ce seul depot (`make forge-cles`).
|
||||
#
|
||||
# MAIS PAS ENCORE A L'INSEMINATION. Le site amorce le runner d'un locataire AVANT l'armement,
|
||||
# donc avant que la voute lui remette sa cle stable : la forge ne le connait pas. Le
|
||||
# controleur qui joue ce role — le runner du site, ou le poste — porte deja ce depot, frere
|
||||
# du moteur ; il le lui apporte en COLIS (`git bundle`). Le runner pointe ensuite `origin`
|
||||
# vers la forge en SSH : une fois arme, il tire lui-meme. Rien n'est devine : si le
|
||||
# controleur n'a pas le depot, on refuse.
|
||||
- name: Exiger la cle d'hote de la forge pour lire un depot prive
|
||||
ansible.builtin.assert:
|
||||
that: serveur_ops_forge_cle_hote | length > 0
|
||||
fail_msg: >-
|
||||
Ce runner lit des depots prives ({{ serveur_ops_depots
|
||||
| selectattr('role', 'in', serveur_ops_roles_prives) | map(attribute='depot') | join(', ') }}),
|
||||
mais `serveur_ops_forge_cle_hote` est vide. Au site : `forge_ssh_cle_hote` dans son plan.
|
||||
Chez un locataire : la copier depuis sa fiche (`make site-intrants`).
|
||||
when: serveur_ops_depots | selectattr('role', 'in', serveur_ops_roles_prives) | list | length > 0
|
||||
|
||||
# LE DOSSIER SSH D'ABORD (mesure du 2026-10-11, insemination de Technolibre). Il n'etait cree
|
||||
# que plus bas, avec la cle du poste : sur un runner existant il etait la, et les trois
|
||||
# deploiements comme l'epreuve du colis n'ont rien vu. Une machine NEUVE s'est arretee ici :
|
||||
# « Destination directory /opt/setops/.ssh does not exist ». La tache du bas reste, inchangee.
|
||||
- name: Assurer le repertoire SSH du poste (avant d'y epingler la forge)
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0700"
|
||||
when: serveur_ops_forge_cle_hote | length > 0
|
||||
|
||||
- name: Epingler la cle d'hote de la forge
|
||||
ansible.builtin.copy:
|
||||
content: "{{ serveur_ops_forge_ssh_hote }} {{ serveur_ops_forge_cle_hote | trim }}\n"
|
||||
dest: "{{ serveur_ops_forge_known_hosts }}"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
when: serveur_ops_forge_cle_hote | length > 0
|
||||
|
||||
- name: Le runner lit-il deja chaque depot prive en SSH ?
|
||||
ansible.builtin.command:
|
||||
argv: [git, ls-remote, "git@{{ serveur_ops_forge_ssh_hote }}:{{ serveur_ops_forge_organisation }}/{{ item.depot }}.git", HEAD]
|
||||
environment:
|
||||
GIT_SSH_COMMAND: "{{ serveur_ops_ssh_prive }}"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
loop: "{{ serveur_ops_depots | selectattr('role', 'in', serveur_ops_roles_prives) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
register: serveur_ops_prives_ssh
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Tirer en SSH les depots prives que le runner peut lire
|
||||
ansible.builtin.git:
|
||||
repo: "git@{{ serveur_ops_forge_ssh_hote }}:{{ serveur_ops_forge_organisation }}/{{ item.item.depot }}.git"
|
||||
dest: "{{ serveur_ops_racine }}/{{ item.item.dest }}"
|
||||
version: "{{ item.item.branche | default('main') }}"
|
||||
key_file: "{{ serveur_ops_cle_privee }}"
|
||||
ssh_opts: "-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ serveur_ops_forge_known_hosts }} -o IdentitiesOnly=yes"
|
||||
accept_hostkey: false
|
||||
update: true
|
||||
force: false
|
||||
loop: "{{ serveur_ops_prives_ssh.results | selectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (SSH)"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
register: serveur_ops_prives_tires
|
||||
|
||||
- name: Faire le colis des depots prives que le runner ne lit pas encore (controleur)
|
||||
ansible.builtin.command:
|
||||
argv: [git, -C, "{{ serveur_ops_freres }}/{{ item.item.dest }}", bundle, create,
|
||||
"{{ serveur_ops_colis_local }}/{{ item.item.depot }}.bundle",
|
||||
"refs/heads/{{ item.item.branche | default('main') }}"]
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (colis)"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
changed_when: false
|
||||
when: not ansible_check_mode
|
||||
vars:
|
||||
serveur_ops_freres: "{{ role_path | dirname | dirname | dirname }}"
|
||||
register: serveur_ops_colis_faits
|
||||
|
||||
- name: Assurer le dossier des colis sur le runner
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}/.colis"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0700"
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list | length > 0
|
||||
|
||||
- name: Porter les colis au runner
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_colis_local }}/{{ item.item.depot }}.bundle"
|
||||
dest: "{{ serveur_ops_racine }}/.colis/{{ item.item.depot }}.bundle"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0600"
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (colis)"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Recevoir chaque colis (clone neuf, ou avance rapide), puis viser la forge en SSH
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
d="{{ serveur_ops_racine }}/{{ item.item.dest }}"
|
||||
c="{{ serveur_ops_racine }}/.colis/{{ item.item.depot }}.bundle"
|
||||
b="{{ item.item.branche | default('main') }}"
|
||||
# `git bundle verify` EXIGE UN DEPOT (mesure du 2026-10-11, a l'epreuve du colis) : hors
|
||||
# d'un depot il refuse, et c'est justement le cas de l'insemination. Le clone verifie le
|
||||
# colis lui-meme (un colis tronque y echoue) ; un depot existant le verifie avant de tirer.
|
||||
if [ ! -d "$d/.git" ]; then
|
||||
git clone -q --branch "$b" "$c" "$d"
|
||||
echo "CLONE"
|
||||
else
|
||||
git -C "$d" bundle verify -q "$c" >/dev/null
|
||||
avant=$(git -C "$d" rev-parse HEAD)
|
||||
git -C "$d" fetch -q "$c" "refs/heads/$b:refs/remotes/colis/$b"
|
||||
git -C "$d" merge -q --ff-only "colis/$b"
|
||||
[ "$avant" = "$(git -C "$d" rev-parse HEAD)" ] || echo "AVANCE"
|
||||
fi
|
||||
git -C "$d" remote set-url origin "git@{{ serveur_ops_forge_ssh_hote }}:{{ serveur_ops_forge_organisation }}/{{ item.item.depot }}.git"
|
||||
git -C "$d" fetch -q origin 2>/dev/null && git -C "$d" branch -q --set-upstream-to "origin/$b" || true
|
||||
rm -f "$c"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }} (colis)"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
register: serveur_ops_colis_recus
|
||||
changed_when: "'CLONE' in serveur_ops_colis_recus.stdout or 'AVANCE' in serveur_ops_colis_recus.stdout"
|
||||
|
||||
- name: Dire quels depots prives sont venus par colis
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ item.item.depot }} : le runner ne le lit pas encore en SSH ({{ (item.stderr | default('')
|
||||
| trim).splitlines()[:1] | join('') }}). Apporte en colis par le controleur. S'il est
|
||||
arme et que ca dure : `make forge-cles` au site, et la cle de sa voute.
|
||||
loop: "{{ serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.depot }}"
|
||||
|
||||
- name: Retirer les colis qui resteraient (un passage interrompu en laisse)
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}/.colis"
|
||||
state: absent
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- serveur_ops_prives_ssh.results | rejectattr('rc', 'equalto', 0) | list | length == 0
|
||||
|
||||
- name: Poser la commande SSH de la forge dans chaque depot prive
|
||||
community.general.git_config:
|
||||
name: core.sshCommand
|
||||
scope: local
|
||||
repo: "{{ serveur_ops_racine }}/{{ item.dest }}"
|
||||
value: "{{ serveur_ops_ssh_prive | trim }}"
|
||||
loop: "{{ serveur_ops_depots | selectattr('role', 'in', serveur_ops_roles_prives) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
# CE QUI A AVANCE, ET CE QUE CA OBLIGE A RELIRE (2026-09-20).
|
||||
#
|
||||
# SEUL LE MOTEUR PORTE LE CODE DE LA CONSOLE. Un plan qui avance ne change pas
|
||||
|
|
@ -336,48 +577,107 @@
|
|||
become: false
|
||||
run_once: true
|
||||
|
||||
# `argv` ET NON `cmd` : le chemin du controleur peut contenir une ESPACE (« Espace
|
||||
# Chezlepro/… » chez le mainteneur), et `cmd` la lit comme un separateur d'arguments.
|
||||
# Constate le 2026-08-23 : ansible-galaxy recevait « /home/…/Espace » puis
|
||||
# « Chezlepro/… » comme deux arguments, et refusait — « positional collection_name arg
|
||||
# and --requirements-file are mutually exclusive ». Le message ne parlait pas du tout du
|
||||
# vrai probleme.
|
||||
- name: Telecharger les collections dans le cache du controleur (une seule fois)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "ansible-galaxy"
|
||||
- "collection"
|
||||
- "download"
|
||||
- "-r"
|
||||
- "{{ serveur_ops_requirements_controleur }}"
|
||||
- "-p"
|
||||
- "{{ serveur_ops_cache_collections }}"
|
||||
creates: "{{ serveur_ops_cache_collections }}/requirements.yml"
|
||||
# LES COLLECTIONS DU VERROU (2026-10-09), meme chemin que les roues : le depot du site
|
||||
# d'abord, Galaxy en repli dit, la somme verifiee. `collection download` sortait sur Galaxy
|
||||
# a chaque runner neuf.
|
||||
- name: Prendre les collections verrouillees au depot du site
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ setops_depot_binaires }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_cache_collections }}/{{ item.nom }}"
|
||||
checksum: "sha256:{{ item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 30
|
||||
loop: "{{ serveur_ops_verrou.collections }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when: not ansible_check_mode
|
||||
failed_when: false
|
||||
when:
|
||||
- setops_depot_binaires | default('') | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Deposer les collections sur le poste
|
||||
- name: Les collections verrouillees sont-elles dans le cache du controleur ?
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_ops_cache_collections }}/{{ item.nom }}"
|
||||
checksum_algorithm: sha256
|
||||
loop: "{{ serveur_ops_verrou.collections }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
register: serveur_ops_collections_cache
|
||||
|
||||
- name: Dire les collections absentes du depot du site (prises sur Galaxy)
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ serveur_ops_collections_cache.results | rejectattr('stat.exists') | map(attribute='item.nom') | list }}
|
||||
— absentes du depot du site : prises sur Internet.
|
||||
run_once: true
|
||||
# PAS EN SIMULATION : `--check` ne telecharge rien depuis le site, le cache reste vide et
|
||||
# ce message accuserait le depot a tort (vu le 2026-10-09 sur le runner de Technolibre).
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- serveur_ops_collections_cache.results | rejectattr('stat.exists') | list | length > 0
|
||||
|
||||
- name: Prendre sur Galaxy les collections que le site n'a pas (repli)
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ item.item.url }}"
|
||||
dest: "{{ serveur_ops_cache_collections }}/{{ item.item.nom }}"
|
||||
checksum: "sha256:{{ item.item.sha256 }}"
|
||||
mode: "0644"
|
||||
timeout: 60
|
||||
loop: "{{ serveur_ops_collections_cache.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.nom }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- not item.stat.exists or item.stat.checksum != item.item.sha256
|
||||
|
||||
- name: Assurer le depot des collections sur le runner
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}/.collections-hors-ligne"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Deposer sur le poste les collections du verrou
|
||||
register: serveur_ops_depot_collections
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_cache_collections }}/"
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/"
|
||||
src: "{{ serveur_ops_cache_collections }}/{{ item.nom }}"
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/{{ item.nom }}"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
directory_mode: "0755"
|
||||
loop: "{{ serveur_ops_verrou.collections }}"
|
||||
loop_control:
|
||||
label: "{{ item.nom }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
# Le meme format que celui qu'ecrivait `collection download` : des archives en RELATIF,
|
||||
# que l'installation lit depuis ce dossier (`chdir`).
|
||||
- name: Ecrire la liste hors ligne des collections du verrou
|
||||
register: serveur_ops_depot_collections_liste
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/requirements.yml"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
content: |
|
||||
# Gere par Set-OPS (role serveur_ops), d'apres verrou-runner.yml. Ne pas editer.
|
||||
collections:
|
||||
{% for c in serveur_ops_verrou.collections %}
|
||||
- name: {{ c.nom }}
|
||||
version: {{ (c.nom | replace('.tar.gz', '')).split('-') | last }}
|
||||
{% endfor %}
|
||||
when: not ansible_check_mode
|
||||
|
||||
# `chdir` OBLIGATOIRE : le `requirements.yml` produit par `collection download` nomme les
|
||||
# archives en RELATIF (`community-postgresql-4.2.0.tar.gz`), et ansible-galaxy les cherche
|
||||
# depuis le repertoire COURANT, pas depuis celui du fichier. Sans chdir : « Could not find
|
||||
# community-postgresql-4.2.0.tar.gz » alors que l'archive est bien la, a cote.
|
||||
# LE GARDE-FOU DOIT SUIVRE LE RESULTAT, PAS LE GESTE (2026-08-27). La condition
|
||||
# d'installation ne regardait que le DEPOT des archives. Le jour ou l'on a corrige le
|
||||
# CHEMIN d'installation, les archives etaient inchangees : le role a donc conclu qu'il
|
||||
# n'y avait rien a faire, et les collections sont restees a l'ancien endroit. Le meme
|
||||
# piege qu'en 2026-08-24, deplace d'un cran. On mesure desormais la DESTINATION.
|
||||
- name: Voir si les collections sont deja la ou le moteur les cherche
|
||||
ansible.builtin.stat:
|
||||
path: >-
|
||||
|
|
@ -431,6 +731,7 @@
|
|||
# d'autre ne l'aurait vu. Les conditions vont donc dans UNE liste.
|
||||
when:
|
||||
- serveur_ops_depot_collections is changed
|
||||
or serveur_ops_depot_collections_liste is changed
|
||||
or not serveur_ops_collections_en_place.stat.exists
|
||||
- not ansible_check_mode
|
||||
|
||||
|
|
|
|||
|
|
@ -70,3 +70,16 @@ flux:
|
|||
Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de
|
||||
resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne
|
||||
transporte aucun secret : la voute est remise par un humain.
|
||||
|
||||
# LE REPERE DU GENOME (2026-10-10). La sonde `sonde-genome-a-jour` compare la forge du
|
||||
# site a eregion par un `git ls-remote` anonyme, toutes les 15 minutes. Elle sortait par
|
||||
# la sortie 443 generale du socle, qui n'existe plus : la declarer ici la rend lisible.
|
||||
# Sans Internet, la sonde passe en INCONNU (code 3) — rien ne casse.
|
||||
- sens: egress
|
||||
port: 443
|
||||
protocole: tcp
|
||||
pair: externe
|
||||
chiffrement: tls-requis
|
||||
raison: >-
|
||||
Repere du genome : `git ls-remote` anonyme sur eregion (forge.alliance-boreale.ca)
|
||||
pour dire si la forge du site a pris du retard. Lecture seule, sans identifiant.
|
||||
|
|
|
|||
|
|
@ -227,6 +227,23 @@
|
|||
- serveur_ops_tenant_ssh_cle | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
# LA CLE QUI LIT LE DEPOT DU GENOME (2026-10-11). Le site l'enregistre sur le depot de ce
|
||||
# locataire d'apres sa face (`serveur_ops_tenant_cle_publique`) : si la voute en remettait une
|
||||
# autre, le runner arme ne pourrait plus tirer son plan, et l'echec viendrait bien plus tard.
|
||||
- name: Exiger que la cle du runner soit celle que la face publie pour lire le genome
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (serveur_ops_tenant_ssh_publique.stdout.split()[:2] | join(' '))
|
||||
== (serveur_ops_tenant_cle_publique.split()[:2] | join(' '))
|
||||
fail_msg: >-
|
||||
La cle SSH du runner (issue de la voute) n'est PAS `serveur_ops_tenant_cle_publique` :
|
||||
la forge du site n'autorise que celle-ci a lire le depot de ce locataire. Corriger la
|
||||
declaration (puis republier la face), ou la voute.
|
||||
when:
|
||||
- serveur_ops_tenant_ssh_cle | length > 0
|
||||
- serveur_ops_tenant_cle_publique | default('') | length > 0
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Dire qu'aucune identite n'est conservee
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
|
|
|
|||
|
|
@ -38,12 +38,13 @@ serveur_postfix_mailstore_hote: ""
|
|||
serveur_postfix_lmtp_port: 24
|
||||
|
||||
# --- Soumission :587 (clients authentifiés) ---
|
||||
# SASL délégué à Dovecot (passdb LDAP prouvé) sur le nœud mail-store, via son auth réseau.
|
||||
# SASL délégué à un Dovecot d'AUTHENTIFICATION SEULE, sur cette machine, par socket local
|
||||
# (`serveur_dovecot/tasks/auth_locale.yml`) ; il vérifie sur l'annuaire en LDAPS. Jusqu'au
|
||||
# 2026-10-12, Postfix interrogeait le Dovecot du mailstore sur :12345 — en clair, faute de
|
||||
# TLS dans ce protocole.
|
||||
# 587 (STARTTLS requis) et 465 (TLS direct) ; seuls les authentifiés relaient. Publics depuis le
|
||||
# 2026-09-29 : les clients de courriel y arrivent par l'adresse publique du locataire.
|
||||
serveur_postfix_submission_actif: false
|
||||
serveur_postfix_sasl_hote: "{{ serveur_postfix_mailstore_hote }}"
|
||||
serveur_postfix_sasl_port: 12345
|
||||
|
||||
# --- Annuaire LDAP (validation domaines/boîtes/alias) ---
|
||||
# Connexion à l'annuaire (serveur, base, bind_dn, bind_password) : fournie par le rôle partagé
|
||||
|
|
@ -59,6 +60,26 @@ serveur_postfix_tls_source_cert: "/etc/step/certs/{{ ansible_fqdn | default(ansi
|
|||
serveur_postfix_tls_source_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key"
|
||||
serveur_postfix_tls_dir: "/etc/postfix/tls"
|
||||
|
||||
# TOUT CE QUI TRANSITE EST CHIFFRE, A L'INTERNE COMME A L'EXTERNE (directive du 2026-10-12).
|
||||
#
|
||||
# Un RELAIS ne parle qu'a des emetteurs de son ecosysteme (la forge) et a des MX distants :
|
||||
# il peut EXIGER le TLS des deux cotes. `may` laissait une forge sans STARTTLS remettre en
|
||||
# clair, sans que rien ne le dise. Le serveur de courriel COMPLET d'un locataire reste en
|
||||
# `may` pour l'instant : un MX public qui exige le TLS refuse le courrier des serveurs qui ne
|
||||
# l'offrent pas (RFC 3207) — c'est une decision de l'Etape B, pas un reglage par defaut.
|
||||
serveur_postfix_tls_entrant: "{{ 'encrypt' if serveur_postfix_mode == 'relais' else 'may' }}"
|
||||
# EN SORTIE, LE RELAIS VERIFIE AUSSI QUI IL JOINT (2026-10-12) : `encrypt` chiffrait vers un
|
||||
# serveur dont il ne verifiait pas l'identite (« Untrusted » au journal). Il n'ecrit qu'a
|
||||
# quelques destinataires connus ; un MX au certificat invalide fait refuser le message au lieu
|
||||
# de le livrer, et c'est voulu. Mesure avant : mx.chezlepro.ca et Gmail « Verified ».
|
||||
serveur_postfix_tls_sortant: "{{ 'verify' if serveur_postfix_mode == 'relais' else 'may' }}"
|
||||
# Les autorites PUBLIQUES du systeme : celles des MX de l'Internet, pas celle de l'ecosysteme.
|
||||
# UN FICHIER, PAS UN DOSSIER : `smtp` tourne en chroot ; `CAfile` est lu AVANT d'y entrer,
|
||||
# `CApath` dedans — sur une copie que Debian refait au redemarrage, et qui vieillit entre deux.
|
||||
serveur_postfix_tls_ca_publiques: "/etc/ssl/certs/ca-certificates.crt"
|
||||
# Une ligne par session TLS au journal : ce qui se dit chiffre doit pouvoir se MESURER.
|
||||
serveur_postfix_tls_journal: 1
|
||||
|
||||
# Zero-confiance : verifier le TLS de la livraison LMTP vers Dovecot (cert step-ca).
|
||||
serveur_postfix_lmtp_tls_verify: false
|
||||
serveur_postfix_lmtp_tls_cafile: "/etc/step/certs/root_ca.crt"
|
||||
|
|
|
|||
|
|
@ -36,12 +36,6 @@ flux:
|
|||
pair: serveur_dovecot
|
||||
chiffrement: tls-requis
|
||||
raison: "Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié."
|
||||
- sens: egress
|
||||
port: 12345
|
||||
protocole: tcp
|
||||
pair: serveur_dovecot
|
||||
chiffrement: tls
|
||||
raison: "Validation SASL des identifiants de soumission contre Dovecot."
|
||||
- sens: egress
|
||||
port: 11332
|
||||
protocole: tcp
|
||||
|
|
@ -53,7 +47,7 @@ flux:
|
|||
protocole: tcp
|
||||
pair: serveur_openldap
|
||||
chiffrement: tls-requis
|
||||
raison: "Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS)."
|
||||
raison: "Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire, et mots de passe de soumission vérifiés par le Dovecot local (LDAPS)."
|
||||
- sens: egress
|
||||
port: 25
|
||||
protocole: tcp
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@
|
|||
# 465 (`submissions`, TLS DIRECT, 2026-09-29) A COTE DU 587 (STARTTLS). La RFC 8314 recommande
|
||||
# le TLS direct pour la soumission : aucun echange en clair avant le chiffrement. Memes
|
||||
# restrictions que le 587 : authentifie, sinon refuse.
|
||||
- name: Activer la soumission :587 et :465 (clients authentifiés, SASL Dovecot distant)
|
||||
- name: Activer la soumission :587 et :465 (clients authentifiés, SASL Dovecot local)
|
||||
ansible.builtin.blockinfile:
|
||||
path: /etc/postfix/master.cf
|
||||
marker: "# {mark} SETOPS SUBMISSION 587"
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_sasl_auth_enable=yes
|
||||
-o smtpd_sasl_type=dovecot
|
||||
-o smtpd_sasl_path=inet:{{ serveur_postfix_sasl_hote }}:{{ serveur_postfix_sasl_port }}
|
||||
-o smtpd_sasl_path=private/auth
|
||||
-o smtpd_sasl_security_options=noanonymous
|
||||
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
|
||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
-o smtpd_tls_wrappermode=yes
|
||||
-o smtpd_sasl_auth_enable=yes
|
||||
-o smtpd_sasl_type=dovecot
|
||||
-o smtpd_sasl_path=inet:{{ serveur_postfix_sasl_hote }}:{{ serveur_postfix_sasl_port }}
|
||||
-o smtpd_sasl_path=private/auth
|
||||
-o smtpd_sasl_security_options=noanonymous
|
||||
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
|
||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||
|
|
@ -231,6 +231,17 @@
|
|||
state: started
|
||||
daemon_reload: true
|
||||
|
||||
# LES MOTS DE PASSE NE QUITTENT PLUS LA MACHINE (2026-10-12). `smtpd_sasl_path` visait
|
||||
# `inet:<mailstore>:12345`, que le client SASL Dovecot de Postfix ne sait pas chiffrer :
|
||||
# chaque soumission y envoyait son mot de passe en clair. Le socket `private/auth` est
|
||||
# servi par un Dovecot d'authentification seule, pose ici, qui verifie en LDAPS. Apres le
|
||||
# demarrage de Postfix : c'est lui qui cree `private/` a sa premiere mise en route.
|
||||
- name: Authentification de la soumission — Dovecot local (aucun port)
|
||||
ansible.builtin.include_role:
|
||||
name: serveur_dovecot
|
||||
tasks_from: auth_locale.yml
|
||||
when: serveur_postfix_submission_actif | bool
|
||||
|
||||
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
|
||||
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
|
||||
# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ local_recipient_maps =
|
|||
qu'elle sorte au nom du SITE — pas d'un locataire. #}
|
||||
relayhost =
|
||||
{# `mynetworks` seul decide qui peut faire relayer : la loopback, donc les services de
|
||||
cette machine, et rien d'autre. Un relais ouvert est repere et exploite en heures. #}
|
||||
cette machine, et les machines dont un role declare un flux vers ce relais (la forge ;
|
||||
au site, `site_inventaire.reseaux_confiance_relais`). Rien d'autre : un relais ouvert
|
||||
est repere et exploite en heures. #}
|
||||
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination
|
||||
{% else %}
|
||||
# --- Boîtes virtuelles : validation LDAP + remise LMTP vers le nœud mail-store (Dovecot) ---
|
||||
|
|
@ -37,9 +39,14 @@ lmtp_host_lookup = native
|
|||
# --- TLS (certificat step_ca) ---
|
||||
smtpd_tls_cert_file = {{ serveur_postfix_tls_dir }}/cert.pem
|
||||
smtpd_tls_key_file = {{ serveur_postfix_tls_dir }}/key.pem
|
||||
smtpd_tls_security_level = may
|
||||
smtpd_tls_security_level = {{ serveur_postfix_tls_entrant }}
|
||||
smtpd_tls_protocols = >=TLSv1.2, !SSLv2, !SSLv3
|
||||
smtp_tls_security_level = may
|
||||
smtpd_tls_mandatory_protocols = >=TLSv1.2
|
||||
smtpd_tls_loglevel = {{ serveur_postfix_tls_journal }}
|
||||
smtp_tls_security_level = {{ serveur_postfix_tls_sortant }}
|
||||
smtp_tls_CAfile = {{ serveur_postfix_tls_ca_publiques }}
|
||||
smtp_tls_mandatory_protocols = >=TLSv1.2
|
||||
smtp_tls_loglevel = {{ serveur_postfix_tls_journal }}
|
||||
{% if serveur_postfix_lmtp_tls_verify | default(false) %}
|
||||
# Zero-confiance : livraison LMTP vers Dovecot chiffree ET verifiee (cert step-ca).
|
||||
# `wrappermode` = TLS des la connexion : c'est ce que sait offrir un `inet_listener`
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ serveur_postgresql_exportateur_service: prometheus-postgres-exporter
|
|||
serveur_postgresql_exportateur_port: 9187
|
||||
serveur_postgresql_exportateur_utilisateur: setops_metriques
|
||||
serveur_postgresql_exportateur_mot_de_passe: "{{ vault_pg_exportateur | default('') }}"
|
||||
# L'EXPORTATEUR SERT EN TLS QUAND LA BASE LE FAIT (2026-10-12). Meme interrupteur que
|
||||
# PostgreSQL, pas un second : la machine a alors son certificat (client_pki), et Prometheus
|
||||
# lit ce meme interrupteur (`meta/metriques.yml`, `tls_si`) pour choisir son schema. Deux
|
||||
# interrupteurs pourraient diverger — l'un servirait en TLS, l'autre lirait en clair.
|
||||
serveur_postgresql_exportateur_tls_dir: "/etc/prometheus-postgres-exporter/tls"
|
||||
serveur_postgresql_exportateur_web_config: "/etc/prometheus-postgres-exporter/web-config.yml"
|
||||
|
||||
# --- RESTAURATION (2026-09-30) — voir tasks/main.yml ---------------------------------
|
||||
#
|
||||
|
|
|
|||
|
|
@ -20,20 +20,12 @@ flux:
|
|||
port: 9187
|
||||
protocole: tcp
|
||||
pair: [serveur_prometheus]
|
||||
# `clair`, ET C'EST UNE DETTE QU'ON ECRIT PLUTOT QUE DE LA TAIRE (2026-09-14).
|
||||
#
|
||||
# `client_metrique` sert deja ses metriques en TLS, certificat synchronise par
|
||||
# `client_pki`. Cet exportateur-ci ne le fait pas encore : il faudrait lui poser un
|
||||
# `--web.config.file` et l'abonner au meme renouvellement.
|
||||
#
|
||||
# CE QUI TRANSITE N'EST PAS ANODIN : nombre de connexions, tailles des bases, ratios
|
||||
# de cache. Pas de donnee applicative, mais l'etat interne d'un serveur de donnees.
|
||||
# Le flux ne sort pas de la zone, et la frontiere ne le laisse passer que depuis
|
||||
# l'observatoire — ce n'est pas une excuse, c'est ce qui rend la dette tenable en
|
||||
# attendant.
|
||||
chiffrement: clair
|
||||
# DETTE FERMEE (2026-10-12). Inscrite `clair` le 2026-09-14 : l'exportateur sert
|
||||
# desormais en HTTPS (`--web.config.file`, certificat de la machine resynchronise a chaque
|
||||
# renouvellement), et Prometheus verifie contre l'autorite des que
|
||||
# `serveur_postgresql_tls_actif` est vrai — partout, au site comme chez les locataires.
|
||||
chiffrement: tls
|
||||
raison: >-
|
||||
Metriques PostgreSQL lues par l'observatoire. Series, pas verdicts : ce qui derive
|
||||
lentement — cache qui decroche, connexions qui montent, bases qui grossissent — n'a
|
||||
que le graphe pour se faire voir. En clair pour l'instant, contrairement a
|
||||
`client_metrique` : dette inscrite, a fermer par `--web.config.file` + `client_pki`.
|
||||
Metriques PostgreSQL lues par l'observatoire, en HTTPS verifie. Series, pas verdicts :
|
||||
ce qui derive lentement — cache qui decroche, connexions qui montent, bases qui
|
||||
grossissent — n'a que le graphe pour se faire voir.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ exportateur:
|
|||
service: prometheus-postgres-exporter
|
||||
port: 9187
|
||||
job: postgresql
|
||||
# LE SCHEMA SUIT UN INTERRUPTEUR DE LA CIBLE (2026-10-12). Prometheus lit cette variable
|
||||
# dans les hostvars de chaque hote du groupe : toutes vraies, il collecte en HTTPS et
|
||||
# verifie contre l'autorite ; sinon en HTTP. Le role, lui, sert en TLS sur le meme
|
||||
# interrupteur — une seule valeur decide des deux bouts.
|
||||
tls_si: serveur_postgresql_tls_actif
|
||||
|
||||
# LES PANNEAUX : ce qui merite d'etre REGARDE DANS LE TEMPS.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -330,6 +330,100 @@
|
|||
state: present
|
||||
when: serveur_postgresql_exportateur_actif | bool
|
||||
|
||||
# --- TLS DE L'EXPORTATEUR (2026-10-12) -----------------------------------------------
|
||||
# Le 9187 etait le seul port de metriques en clair, chez les locataires comme au site :
|
||||
# une dette inscrite au registre le 2026-09-14. Meme pont que `client_metrique` : le
|
||||
# certificat de la machine, copie la ou `prometheus` le lit, recopie a chaque renouvellement.
|
||||
- name: Exportateur TLS — repertoire des certificats (prometheus)
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_postgresql_exportateur_tls_dir }}"
|
||||
state: directory
|
||||
owner: prometheus
|
||||
group: prometheus
|
||||
mode: "0700"
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
|
||||
- name: Exportateur TLS — script de synchronisation du certificat
|
||||
ansible.builtin.template:
|
||||
src: setops-pg-exportateur-cert-sync.sh.j2
|
||||
dest: /usr/local/sbin/setops-pg-exportateur-cert-sync
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
|
||||
- name: Exportateur TLS — unites de synchronisation (service + path)
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.s }}"
|
||||
dest: "{{ item.d }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop:
|
||||
- { s: "setops-pg-exportateur-cert-sync.service.j2", d: "/etc/systemd/system/setops-pg-exportateur-cert-sync.service" }
|
||||
- { s: "setops-pg-exportateur-cert-sync.path.j2", d: "/etc/systemd/system/setops-pg-exportateur-cert-sync.path" }
|
||||
loop_control:
|
||||
label: "{{ item.d | basename }}"
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
|
||||
- name: Exportateur TLS — activer la surveillance du certificat (path)
|
||||
ansible.builtin.systemd:
|
||||
name: setops-pg-exportateur-cert-sync.path
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Exportateur TLS — synchroniser le certificat maintenant
|
||||
ansible.builtin.command: /usr/local/sbin/setops-pg-exportateur-cert-sync
|
||||
changed_when: false
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Exportateur TLS — garde-fou, ne pas activer sans certificat en place
|
||||
ansible.builtin.stat:
|
||||
path: "{{ serveur_postgresql_exportateur_tls_dir }}/exportateur.key"
|
||||
register: serveur_postgresql_exportateur_cle
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Exportateur TLS — exiger le certificat synchronise
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- serveur_postgresql_exportateur_cle.stat.exists
|
||||
fail_msg: >-
|
||||
Certificat de l'exportateur absent ({{ serveur_postgresql_exportateur_tls_dir }}) :
|
||||
la machine porte-t-elle client_pki ? TLS non active, plutot qu'un exportateur muet.
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Exportateur TLS — deployer la web-config
|
||||
ansible.builtin.template:
|
||||
src: web-config-exportateur.yml.j2
|
||||
dest: "{{ serveur_postgresql_exportateur_web_config }}"
|
||||
owner: root
|
||||
group: prometheus
|
||||
mode: "0640"
|
||||
notify: Redemarrer l exportateur de metriques
|
||||
when:
|
||||
- serveur_postgresql_exportateur_actif | bool
|
||||
- serveur_postgresql_tls_actif | bool
|
||||
|
||||
# LA CHAINE DE CONNEXION NE PASSE PAS PAR LA LIGNE DE COMMANDE. Un `DATA_SOURCE_NAME` en
|
||||
# argument serait lisible dans `ps` par tout le monde sur la machine ; dans un fichier a
|
||||
# 0600, il ne l'est que par root et le service.
|
||||
|
|
@ -340,7 +434,9 @@
|
|||
DATA_SOURCE_NAME="postgresql://{{ serveur_postgresql_exportateur_utilisateur
|
||||
}}:{{ serveur_postgresql_exportateur_mot_de_passe
|
||||
}}@127.0.0.1:5432/postgres?sslmode=disable"
|
||||
ARGS="--web.listen-address=:{{ serveur_postgresql_exportateur_port }}"
|
||||
ARGS="--web.listen-address=:{{ serveur_postgresql_exportateur_port }}{{
|
||||
(' --web.config.file=' ~ serveur_postgresql_exportateur_web_config)
|
||||
if serveur_postgresql_tls_actif | bool else '' }}"
|
||||
dest: /etc/default/prometheus-postgres-exporter
|
||||
owner: root
|
||||
group: root
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# Gere par Set-OPS (role serveur_postgresql). Ne pas editer a la main.
|
||||
[Unit]
|
||||
Description=Set-OPS — surveille le certificat step_ca de l'exportateur PostgreSQL
|
||||
|
||||
[Path]
|
||||
PathChanged={{ serveur_postgresql_tls_source_cert }}
|
||||
Unit=setops-pg-exportateur-cert-sync.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Gere par Set-OPS (role serveur_postgresql). Ne pas editer a la main.
|
||||
[Unit]
|
||||
Description=Set-OPS — synchronise le certificat step_ca vers l'exportateur PostgreSQL
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/setops-pg-exportateur-cert-sync
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
# Gere par Set-OPS (role serveur_postgresql). Ne pas editer a la main.
|
||||
# Synchronise le cert step_ca (client_pki, root:root) vers un emplacement lisible par
|
||||
# 'prometheus' (l'utilisateur de l'exportateur). Cle en 0600. Idempotent ; appele au
|
||||
# deploiement ET au renouvellement (unite .path).
|
||||
set -eu
|
||||
|
||||
SRC_CERT="{{ serveur_postgresql_tls_source_cert }}"
|
||||
SRC_KEY="{{ serveur_postgresql_tls_source_cle }}"
|
||||
DST="{{ serveur_postgresql_exportateur_tls_dir }}"
|
||||
|
||||
[ -f "$SRC_CERT" ] && [ -f "$SRC_KEY" ] || exit 0
|
||||
|
||||
install -o prometheus -g prometheus -m 0644 "$SRC_CERT" "$DST/exportateur.crt"
|
||||
install -o prometheus -g prometheus -m 0600 "$SRC_KEY" "$DST/exportateur.key"
|
||||
|
||||
# REDEMARRAGE, pas rechargement : meme famille que node_exporter (client_metrique), dont
|
||||
# l'ExecReload Debian envoie un SIGHUP que le binaire ne sait pas honorer. `try-restart` :
|
||||
# ne demarre pas un exportateur qu'on aurait volontairement arrete.
|
||||
systemctl try-restart {{ serveur_postgresql_exportateur_service }} 2>/dev/null || true
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Gere par Set-OPS (role serveur_postgresql). TLS de l'exportateur (exporter-toolkit).
|
||||
# Cert/cle step_ca synchronises par setops-pg-exportateur-cert-sync (proprietaire prometheus).
|
||||
tls_server_config:
|
||||
cert_file: {{ serveur_postgresql_exportateur_tls_dir }}/exportateur.crt
|
||||
key_file: {{ serveur_postgresql_exportateur_tls_dir }}/exportateur.key
|
||||
|
|
@ -95,6 +95,9 @@ serveur_powerdns_sonde_tolerance: 900
|
|||
# tire les zones, et l'adresse publique vers laquelle elles pointent. Le locataire ne les
|
||||
# recopie pas — il les recoit, comme ses adresses d'amorcage.
|
||||
serveur_powerdns_dns_public_site: "{{ dns_public_site | default('') }}"
|
||||
# LES SERVEURS PUBLICS DES SITES PAIRS (2026-10-14) — le second serveur de noms, sur un autre
|
||||
# site, joint par le tunnel entre les sites. Du contrat du site, comme `dns_public_site`.
|
||||
serveur_powerdns_dns_public_pairs: "{{ dns_public_pairs | default([]) }}"
|
||||
serveur_powerdns_ip_publique: "{{ ip_publique_site | default('') }}"
|
||||
# L'ADRESSE DES NOMS EXPOSES : celle DU LOCATAIRE (2026-09-29), que le site lui attribue et
|
||||
# que la frontiere redirige vers son web frontal. `ip_publique_site` ne porte plus que les
|
||||
|
|
@ -102,6 +105,9 @@ serveur_powerdns_ip_publique: "{{ ip_publique_site | default('') }}"
|
|||
serveur_powerdns_ip_expositions: "{{ ip_publique | default(ip_publique_site) | default('') }}"
|
||||
# Le nom du serveur de noms du site (contrat du site) : NS et SOA de chaque zone publique.
|
||||
serveur_powerdns_dns_public_nom: "{{ dns_public_nom | default('') }}"
|
||||
# LES SERVEURS DE NOMS DES SITES PAIRS (2026-10-14) : {nom : adresse publique}, du contrat du
|
||||
# site. Chaque zone publique les designe en plus du sien ; celle qui contient un nom en porte le A.
|
||||
serveur_powerdns_dns_public_noms_pairs: "{{ dns_public_noms_pairs | default({}) }}"
|
||||
# Le NOM de la cle TSIG, identique des deux cotes — derive du nom du depot du locataire,
|
||||
# par la meme regle que `site_inventaire.py` (`tsig-<depot sans ops->`). `realpath`, parce
|
||||
# que sur le poste l'inventaire passe par le lien `instance`.
|
||||
|
|
|
|||
|
|
@ -75,19 +75,33 @@
|
|||
# `allow-axfr-ips` SANS signature — mesure le 2026-09-16. Le serveur public du site
|
||||
# ne doit donc jamais y figurer.
|
||||
- serveur_powerdns_dns_public_site not in (serveur_powerdns_allow_axfr_ips | default([]))
|
||||
- serveur_powerdns_dns_public_pairs | intersect(serveur_powerdns_allow_axfr_ips | default([])) | length == 0
|
||||
- (vault_tsig_dns_public | default('')) | length > 0
|
||||
# LE NOM DU SERVEUR DE NOMS VIENT DU SITE. Sans lui, NS et SOA seraient vides.
|
||||
- serveur_powerdns_dns_public_nom is search('^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+\.?$')
|
||||
# PERSONNE NE LE REDECLARE : un enregistrement a ce nom lui donnerait une seconde
|
||||
# adresse, ou un CNAME a cote de son A.
|
||||
- (serveur_powerdns_dns_public_nom | trim | regex_replace('\\.$', '')) not in (serveur_powerdns_fqdn_declares | from_json)
|
||||
# LES SERVEURS DES SITES PAIRS, SOUS LES MEMES REGLES : un nom bien forme, une adresse
|
||||
# publique, distinct du notre, et que personne ne redeclare.
|
||||
- serveur_powerdns_dns_public_noms_pairs is mapping
|
||||
- (serveur_powerdns_dns_public_noms_pairs | dict2items
|
||||
| rejectattr('key', 'search', '^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+\.?$') | list | length) == 0
|
||||
- (serveur_powerdns_dns_public_noms_pairs.values()
|
||||
| select('search', '^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.)') | list | length) == 0
|
||||
- (serveur_powerdns_dns_public_noms_pairs.values()
|
||||
| reject('search', '^\d{1,3}(\.\d{1,3}){3}$') | list | length) == 0
|
||||
- (serveur_powerdns_dns_public_noms_pairs.keys() | map('trim') | map('regex_replace', '\.$', '')
|
||||
| intersect([serveur_powerdns_dns_public_nom | trim | regex_replace('\.$', '')]
|
||||
+ (serveur_powerdns_fqdn_declares | from_json)) | list | length) == 0
|
||||
fail_msg: >-
|
||||
Zones publiques refusees : zone `.internal` declaree publique, adresse publique
|
||||
absente ou privee ({{ serveur_powerdns_ip_publique | default('?') }}), serveur public
|
||||
du site liste dans `allow-axfr-ips` (TSIG deviendrait decoratif), ou
|
||||
`vault_tsig_dns_public` absent de la voute, `dns_public_nom` absent ou malforme
|
||||
({{ serveur_powerdns_dns_public_nom | default('?') }}), ou redeclare dans les
|
||||
enregistrements d'une zone.
|
||||
enregistrements d'une zone ; ou un serveur de site pair malforme, a une adresse non
|
||||
publique, ou redeclare ({{ serveur_powerdns_dns_public_noms_pairs | default({}) }}).
|
||||
quiet: true
|
||||
when: serveur_powerdns_zones_publiques | length > 0
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ allow-axfr-ips=127.0.0.1/32
|
|||
# Par defaut, un primaire notifie AUSSI les adresses de ses enregistrements NS. Ici, ce
|
||||
# sont les adresses PUBLIQUES des serveurs de noms : le primaire enverrait des
|
||||
# notifications vers Internet depuis l'interieur du locataire. `only-notify` les borne.
|
||||
only-notify={{ serveur_powerdns_dns_public_site }}/32
|
||||
also-notify={{ serveur_powerdns_dns_public_site }}
|
||||
# ET CEUX DES SITES PAIRS (2026-10-14) : le second serveur de noms tire la zone par le tunnel
|
||||
# entre les sites, et doit apprendre qu'elle a change comme le premier.
|
||||
only-notify={{ ([serveur_powerdns_dns_public_site] + serveur_powerdns_dns_public_pairs) | map('regex_replace', '$', '/32') | join(',') }}
|
||||
also-notify={{ ([serveur_powerdns_dns_public_site] + serveur_powerdns_dns_public_pairs) | join(',') }}
|
||||
|
||||
version-string=anonymous
|
||||
security-poll-suffix=
|
||||
|
|
|
|||
|
|
@ -21,10 +21,19 @@ $TTL {{ serveur_powerdns_ttl }}
|
|||
de 256 bits ne revele rien d'elle. #}
|
||||
{% set _cle = lookup('vars', 'vault_dnssec_' ~ (zone_publique | lower | regex_replace('[^a-z0-9]', '_')), default='') %}
|
||||
; DNSSEC : {{ ('signee, cle ' ~ (_cle | hash('sha256'))[:12]) if ((domaines_publics[zone_publique].dnssec | default(false) | bool) and _cle) else 'non signee' }}
|
||||
@ IN NS {{ _ns }}.
|
||||
{% if _ns.endswith('.' ~ zone_publique) %}
|
||||
{{ _ns[:-(zone_publique | length + 1)] }} IN A {{ serveur_powerdns_ip_publique }}
|
||||
{% endif %}
|
||||
{# ET LE SERVEUR DE CHAQUE SITE PAIR (2026-10-14). Il replique la zone par le tunnel entre les
|
||||
sites ; sans son NS, il la servirait sans que personne le designe. Meme regle pour son A :
|
||||
seule la zone qui contient son nom le porte, a l'adresse publique de SON site. #}
|
||||
{% set _ns_tous = {_ns: serveur_powerdns_ip_publique} %}
|
||||
{% for _n, _ip in (serveur_powerdns_dns_public_noms_pairs | default({})).items() %}
|
||||
{% set _ = _ns_tous.update({(_n | trim | regex_replace('\\.$', '')): _ip}) %}
|
||||
{% endfor %}
|
||||
{% for _n in _ns_tous %}
|
||||
@ IN NS {{ _n }}.
|
||||
{% endfor %}
|
||||
{% for _n, _ip in _ns_tous.items() if _n.endswith('.' ~ zone_publique) %}
|
||||
{{ _n[:-(zone_publique | length + 1)] }} IN A {{ _ip }}
|
||||
{% endfor %}
|
||||
|
||||
{# UNE ZONE PUBLIQUE NE PORTE QUE DES ADRESSES PUBLIQUES.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ serveur_prometheus_paquets:
|
|||
|
||||
serveur_prometheus_service: "prometheus"
|
||||
serveur_prometheus_config: "/etc/prometheus/prometheus.yml"
|
||||
# Le dossier de la base (defaut du paquet Debian, que `ARGS` ne change pas). Lu aussi par
|
||||
# client_backup : la copie a froid et la restauration le visent.
|
||||
serveur_prometheus_donnees: "/var/lib/prometheus/metrics2"
|
||||
|
||||
serveur_prometheus_intervalle: "15s"
|
||||
serveur_prometheus_retention: "15d"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ flux:
|
|||
port: 9100
|
||||
protocole: tcp
|
||||
pair: frontiere
|
||||
chiffrement: clair
|
||||
# HTTPS VERIFIE (2026-10-12) : certificat emis par l'autorite de la frontiere.
|
||||
chiffrement: tls
|
||||
raison: >-
|
||||
Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de
|
||||
supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni
|
||||
|
|
|
|||
|
|
@ -6,6 +6,58 @@
|
|||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
# --- LA BASE D'UNE INCARNATION PRECEDENTE (2026-10-07) --------------------------------
|
||||
#
|
||||
# Les metriques repartaient de zero a chaque reconstruction. client_backup en garde une
|
||||
# copie a froid (jeu `prometheus`) ; on la remet ICI, juste apres l'installation : le paquet
|
||||
# demarre Prometheus aussitot, mais sa base n'a encore aucun bloc (le premier se ferme au
|
||||
# bout de deux heures). Service arrete pendant la remise ; la tache « Activer et demarrer »
|
||||
# plus bas le relance.
|
||||
- name: La base de Prometheus a-t-elle deja des blocs ?
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_prometheus_donnees }}"
|
||||
file_type: directory
|
||||
patterns: "01*"
|
||||
register: serveur_prometheus_blocs
|
||||
|
||||
- name: Restauration — la base d'une incarnation precedente ?
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: restaurer.yml
|
||||
vars:
|
||||
client_backup_restaurer_jeu: serveur_prometheus
|
||||
client_backup_restaurer_vierge: "{{ serveur_prometheus_blocs.matched == 0 }}"
|
||||
|
||||
- name: Restauration — remettre la base, service arrete
|
||||
when: client_backup_restauration.etat == 'a_restaurer'
|
||||
block:
|
||||
- name: Restauration — arreter Prometheus
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ serveur_prometheus_service }}"
|
||||
state: stopped
|
||||
|
||||
- name: Restauration — remettre la copie a froid a sa place
|
||||
changed_when: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/local/sbin/setops-restaurer
|
||||
- fichiers
|
||||
- --instantane
|
||||
- "{{ client_backup_restauration.instantane }}"
|
||||
- --depuis
|
||||
- "{{ client_backup_restauration.chemins[0] }}/metrics2"
|
||||
- --remplacer
|
||||
- --proprietaire
|
||||
- prometheus:prometheus
|
||||
- "{{ serveur_prometheus_donnees }}"
|
||||
|
||||
- name: Restauration — acter la base remise
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: acter.yml
|
||||
vars:
|
||||
client_backup_acter_etat: restaure
|
||||
|
||||
# --- LES CIBLES QUE LES ROLES DECLARENT (2026-09-14) ----------------------------------
|
||||
#
|
||||
# `serveur_prometheus_cibles_supplementaires` existait depuis longtemps — une liste libre,
|
||||
|
|
|
|||
|
|
@ -43,9 +43,21 @@ scrape_configs:
|
|||
| map('extract', hostvars) | selectattr('ansible_host', 'defined')
|
||||
| map(attribute='ansible_host')
|
||||
| map('regex_replace', '$', ':' ~ (expo.port | default(0))) | list %}
|
||||
{# TLS SI LA DECLARATION LE DIT ET QUE CHAQUE CIBLE L'A (2026-10-12) : `tls_si` nomme
|
||||
l'interrupteur que le role exportateur lit lui aussi. Une seule cible sans lui, et le
|
||||
job reste en HTTP — mieux qu'un job HTTPS qui en perdrait une sans rien dire. #}
|
||||
{% set tls = namespace(oui=(expo.tls_si | default('') | length > 0) if expo else false) %}
|
||||
{% for h in (groups.get(role, []) | intersect(groups.get('hotes_actifs', [])) | sort) %}
|
||||
{% if tls.oui and not (hostvars[h][expo.tls_si] | default(false) | bool) %}{% set tls.oui = false %}{% endif %}
|
||||
{% endfor %}
|
||||
{% if expo and cibles %}
|
||||
|
||||
- job_name: {{ expo.job }}
|
||||
{% if tls.oui %}
|
||||
scheme: https
|
||||
tls_config:
|
||||
ca_file: {{ serveur_prometheus_metriques_ca }}
|
||||
{% endif %}
|
||||
static_configs:
|
||||
- targets: {{ cibles | to_json }}
|
||||
{% endif %}
|
||||
|
|
@ -55,6 +67,13 @@ scrape_configs:
|
|||
{% for job in serveur_prometheus_cibles_supplementaires %}
|
||||
|
||||
- job_name: {{ job.job }}
|
||||
{# UNE CIBLE HORS FLOTTE PEUT SERVIR EN TLS (2026-10-12) : la frontiere, certifiee par son
|
||||
autorite subordonnee, verifiee contre la meme racine que le reste. #}
|
||||
{% if job.tls | default(false) %}
|
||||
scheme: https
|
||||
tls_config:
|
||||
ca_file: {{ serveur_prometheus_metriques_ca }}
|
||||
{% endif %}
|
||||
static_configs:
|
||||
{% if job.hotes | default({}) %}
|
||||
{# UNE CIBLE NOMMEE PAR BLOC : l'etiquette `hote` s'attache a un `static_config`, pas a
|
||||
|
|
|
|||
63
roles/serveur_relais/README.md
Normal file
63
roles/serveur_relais/README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# serveur_relais
|
||||
|
||||
La **face publique d'un site qui n'a qu'une adresse DHCP**. Un VPS loue des adresses fixes,
|
||||
répond à l'ARP de son hébergeur pour elles (proxy ARP) et les **route en /32** dans un tunnel
|
||||
WireGuard que la frontière du site initie. Aucune traduction d'adresse : les adresses arrivent
|
||||
intactes sur la frontière, qui les porte et qui filtre. L'adresse du client reste visible de
|
||||
bout en bout (courriel, journaux, blocage des abus).
|
||||
|
||||
```
|
||||
Internet ──► hébergeur (ARP « qui a 51.79.x ? ») ──► relais : « c'est moi » (proxy ARP)
|
||||
──► route /32 vers wg0 ──► tunnel ──► frontière (porte les adresses, filtre)
|
||||
```
|
||||
|
||||
Né le 2026-10-13 au site de TechnoLibre (VPS OVH, Debian 13, frontière `requin`).
|
||||
|
||||
## Ce que le rôle pose
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `/etc/wireguard/wg0.conf` | le tunnel ; routes /32 et proxy ARP en `PostUp`, sans `Table` |
|
||||
| `/etc/wireguard/relais.key` | la clé privée, depuis la voûte du site |
|
||||
| `/etc/nftables.conf` | entrée : SSH et tunnel sur la console seulement ; transit : les adresses relayées, dans chaque sens, rien d'autre |
|
||||
| `/etc/sysctl.d/90-relais.conf` | `ip_forward` |
|
||||
| `/etc/systemd/resolved.conf.d/90-relais.conf` | LLMNR et mDNS coupés (ils écoutaient sur l'adresse publique) |
|
||||
|
||||
Le relais **ne décide d'aucun service** : il ne connaît que des adresses. Ce qui est publié,
|
||||
et à qui, se décide sur la frontière (`devis_opnsense.py`, `opnsense_face_publique`).
|
||||
|
||||
## D'où viennent les valeurs
|
||||
|
||||
Toutes **dérivées** par `scripts/site_inventaire.py` ; aucune n'est à recopier.
|
||||
|
||||
| Variable | Source |
|
||||
|---|---|
|
||||
| `serveur_relais_tunnel_adresse`, `serveur_relais_tunnel_mtu` | hôte `role: relais` et réseau `tunnel: wireguard` de `underlay.yml` |
|
||||
| `serveur_relais_tunnel_port` | 51800 + index du site |
|
||||
| `serveur_relais_pair_*` | hôte `role: frontiere` du même réseau, avec sa `cle_publique` |
|
||||
| `serveur_relais_adresses_relayees` | `opnsense_wan_ip` + `opnsense_ips_publiques` de `opnsense.yml` |
|
||||
| `serveur_relais_cle_privee` | `vault_relais_wg_cle_privee`, voûte du site |
|
||||
| `ansible_host` | `console:` de l'hôte relais — jamais une adresse relayée |
|
||||
|
||||
Le relais n'est **pas** dans `plan/serveurs.yml` : tout ce qu'une machine du plan reçoit
|
||||
descend des zones du site, que le VPS n'atteint pas. Comme un hyperviseur, c'est un
|
||||
équipement de la carte.
|
||||
|
||||
## Ce qui reste hors du rôle
|
||||
|
||||
- **Côté frontière** (OPNsense) : l'instance WireGuard, l'interface, la passerelle et les
|
||||
adresses virtuelles se posent par l'API, une fois. Le devis y pose ensuite les règles avec
|
||||
`reply-to` (voir `test_face_publique_relais.py`).
|
||||
- **La supervision** : le relais est hors du site et ne joint pas sa supervision. Ce qui le
|
||||
dit vivant aujourd'hui, c'est la passerelle `RELAIS_GW` de la frontière (surveillée par
|
||||
ping à travers le tunnel). Une sonde propre au rôle reste à écrire.
|
||||
- **Le durcissement commun** (`serveur_durci`) : il dérive son pare-feu des zones du site et
|
||||
écraserait celui du relais. Non appliqué ici.
|
||||
|
||||
## Déployer
|
||||
|
||||
```
|
||||
SETOPS_UNDERLAY=../SITE-<site>/underlay.yml make site-appliquer GROUPE=serveur_relais ARGS="--check --diff"
|
||||
```
|
||||
|
||||
Le pare-feu est validé (`nft -c`) avant d'être posé : la seule porte du VPS est son SSH.
|
||||
34
roles/serveur_relais/defaults/main.yml
Normal file
34
roles/serveur_relais/defaults/main.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
# LE RELAIS — la face publique d'un site qui n'a qu'une adresse DHCP (2026-10-13).
|
||||
#
|
||||
# Un VPS loue des adresses fixes ; il repond a l'ARP de son hebergeur pour elles (proxy) et
|
||||
# les ROUTE en /32 dans un tunnel WireGuard que la frontiere du site INITIE. Aucune
|
||||
# traduction : les adresses arrivent intactes sur la frontiere, qui les porte et qui filtre.
|
||||
# Le relais ne decide d'aucun service — il fait suivre, et refuse tout le reste.
|
||||
#
|
||||
# Tout ce qui suit est DERIVE par `scripts/site_inventaire.py` de la carte du site
|
||||
# (`underlay.yml` : reseau `relais`, hote `role: relais`) et de `opnsense.yml` (adresse du
|
||||
# site et pool des locataires). Rien n'est a recopier ici.
|
||||
serveur_relais_tunnel_adresse: "" # ex. 10.31.60.1/30
|
||||
serveur_relais_tunnel_port: 0 # 51800 + index du site
|
||||
serveur_relais_tunnel_mtu: 1420
|
||||
serveur_relais_pair_nom: frontiere
|
||||
serveur_relais_pair_adresse: "" # l'adresse de la frontiere dans le tunnel
|
||||
serveur_relais_pair_cle_publique: ""
|
||||
serveur_relais_adresses_relayees: [] # l'adresse du site, puis le pool des locataires
|
||||
serveur_relais_site: ""
|
||||
|
||||
# La console SSH reste sur l'adresse PROPRE du VPS : les adresses relayees ne lui sont
|
||||
# jamais attribuees, et ne mennent jamais a son SSH.
|
||||
serveur_relais_console: "{{ ansible_host }}"
|
||||
# L'interface ou l'hebergeur livre les adresses (celle de la route par defaut).
|
||||
serveur_relais_interface_publique: "{{ ansible_facts.default_ipv4.interface }}"
|
||||
serveur_relais_interface_tunnel: wg0
|
||||
|
||||
# La cle privee vit dans la voute du site : reconstruire le relais ne change pas sa cle,
|
||||
# donc ne touche pas a la frontiere.
|
||||
serveur_relais_cle_privee: "{{ vault_relais_wg_cle_privee | default('') }}"
|
||||
|
||||
serveur_relais_paquets:
|
||||
- wireguard-tools
|
||||
- nftables
|
||||
23
roles/serveur_relais/handlers/main.yml
Normal file
23
roles/serveur_relais/handlers/main.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
- name: Appliquer les reglages noyau du relais
|
||||
ansible.builtin.systemd:
|
||||
name: systemd-sysctl
|
||||
state: restarted
|
||||
|
||||
- name: Redemarrer systemd-resolved
|
||||
ansible.builtin.systemd:
|
||||
name: systemd-resolved
|
||||
state: restarted
|
||||
|
||||
# Le tunnel se recree : quelques secondes sans relais, la frontiere se reconnecte d'elle-meme
|
||||
# (maintien de connexion toutes les 25 s).
|
||||
- name: Redemarrer le tunnel du relais
|
||||
ansible.builtin.systemd:
|
||||
name: "wg-quick@{{ serveur_relais_interface_tunnel }}"
|
||||
state: restarted
|
||||
|
||||
# `nft -f` remplace le jeu entier d'un coup (`flush ruleset` en tete) : aucun instant sans
|
||||
# regle. Le fichier a ete valide (`nft -c`) avant d'etre pose.
|
||||
- name: Recharger le pare-feu du relais
|
||||
ansible.builtin.command: nft -f /etc/nftables.conf
|
||||
changed_when: true
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue