forge du site : ses courriels partent, par le relais du site
La forge nommait infra-mail-01 en dur comme relais : une machine de locataire, absente du site. Elle derive maintenant son relais du groupe serveur_postfix. Le relais du site admet les machines dont un role declare un flux vers lui, lues dans le registre des flux (aujourd'hui la forge). Deploye au site ; un courriel de mot de passe oublie est arrive et a servi. Aussi : forge-comptes ne compte plus de changement en mode plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
548d35b1b5
commit
92a396b3fe
8 changed files with 140 additions and 6 deletions
32
CHANGELOG.md
32
CHANGELOG.md
|
|
@ -1,5 +1,37 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-12 (138) — La forge du site remet ses courriels
|
||||
|
||||
**95 preuves.** `make verifier` passe ; `test_relais_du_site.py` est nouveau, et échoue sur
|
||||
l'ancien défaut (témoin). Déployé au site après simulation ; éprouvé par Daniel : le lien
|
||||
« mot de passe oublié » est arrivé et a servi.
|
||||
|
||||
### Ce qui manquait
|
||||
|
||||
« Mot de passe oublié » répondait 200 et la forge du site n'a jamais remis un seul courriel :
|
||||
`lookup infra-mail-01.genese.internal ... server misbehaving`. Deux défauts, chacun suffisant :
|
||||
|
||||
- **`serveur_forgejo_smtp` nommait `infra-mail-01` en dur** : la machine des boîtes d'un
|
||||
locataire, où rien n'écoute le port 25 ; au site, ce nom n'existe pas. Il dérive désormais
|
||||
de l'inventaire (l'hôte qui porte `serveur_postfix`) ; sans relais, le courriel de la forge
|
||||
s'éteint (`ENABLED = false`) au lieu d'échouer en silence. Les forges des locataires visaient
|
||||
la même machine : elles viseront `edge-mta-01` à leur prochain déploiement.
|
||||
- **Le relais du site n'admettait que sa boucle locale** (`mynetworks`). `site_inventaire`
|
||||
y ajoute les machines dont un rôle déclare un flux `egress` vers `serveur_postfix`, lues dans
|
||||
le registre des flux (`reseaux_confiance_relais`) : aujourd'hui la forge seule.
|
||||
|
||||
### Ce qui était déjà juste
|
||||
|
||||
Mesuré, une fois l'instrument corrigé : la forge joint le relais (STARTTLS, certificat vérifié),
|
||||
le relais sort sur le port 25 (`mx.chezlepro.ca` et Google répondent depuis 69.70.26.62), et
|
||||
`mx.chezlepro.ca` accepte `forge@genese.internal`. Une première sonde (`head -c 120` sur une
|
||||
bannière de 39 octets) avait fait croire ces chemins fermés.
|
||||
|
||||
### Aussi
|
||||
|
||||
`make forge-comptes` ne compte plus un changement en mode plan : le mot « jeton » de
|
||||
« jeton a creer » trompait `changed_when`, qui lit désormais les seules lignes d'action.
|
||||
|
||||
## 2026-10-11 (137) — Un mainteneur demande au site par un ticket, et la console du site le voit
|
||||
|
||||
**95 preuves.** `make verifier` passe ; `test_forge_cles.py` couvre aussi les collaborations.
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -337,6 +337,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
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_sonde_tcp.py
|
||||
python3 scripts/tests/test_restauration.py
|
||||
python3 scripts/tests/test_repertoires_partages.py
|
||||
|
|
|
|||
|
|
@ -35,8 +35,9 @@
|
|||
# En simulation, le script ne fait que montrer (sans --confirmer) : il peut tourner.
|
||||
check_mode: false
|
||||
register: forge_comptes
|
||||
changed_when: >-
|
||||
['cree ', 'collab ', 'retire ', 'jeton '] | select('in', forge_comptes.stdout) | list | length > 0
|
||||
# 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]
|
||||
|
|
|
|||
|
|
@ -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,7 +68,7 @@ ACCOUNT_LINKING = auto
|
|||
{% endif %}
|
||||
|
||||
[mailer]
|
||||
ENABLED = true
|
||||
ENABLED = {{ 'true' if serveur_forgejo_smtp else 'false' }}
|
||||
PROTOCOL = smtp
|
||||
SMTP_ADDR = {{ serveur_forgejo_smtp }}
|
||||
SMTP_PORT = 25
|
||||
|
|
|
|||
|
|
@ -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) ---
|
||||
|
|
|
|||
|
|
@ -375,6 +375,8 @@ def inventaire() -> dict:
|
|||
for s in serveurs.values()
|
||||
if str(s.get("etat", "actif")) == "actif"
|
||||
and (par_reseau.get(s.get("reseau")) or {}).get("sous_reseau")})
|
||||
import resoudre_flux as _rf # noqa: PLC0415 — il importe ce module a son tour, plus tard
|
||||
_relais_confiance = reseaux_confiance_relais(serveurs, _groupes_de, _rf.charger_flux())
|
||||
|
||||
# LE RESOLVEUR D'AMORCAGE SE DERIVE, IL NE S'ECRIT PAS (2026-08-25).
|
||||
#
|
||||
|
|
@ -777,6 +779,11 @@ def inventaire() -> dict:
|
|||
# n'a ni l'un ni l'autre, et n'heberge aucune boite — il lui faut seulement
|
||||
# de quoi faire SORTIR une alerte, en son propre nom.
|
||||
"serveur_postfix_mode": "relais",
|
||||
# QUI PEUT FAIRE RELAYER (2026-10-12). Le defaut du role n'admet que la boucle
|
||||
# locale : juste tant qu'Icinga, sur la meme machine, etait le seul a ecrire. La
|
||||
# forge du site envoie ses courriels (mot de passe oublie) par ce relais ; refusee,
|
||||
# elle n'aurait rien pu remettre hors du site. Le registre des flux dit qui l'emploie.
|
||||
"serveur_postfix_reseaux_confiance": _relais_confiance,
|
||||
# LE GRAFANA DU SITE N'A PAS DE SSO — pas par oubli, par D-87.
|
||||
#
|
||||
# Le SSO d'un tenant supposerait que le site emprunte l'identite d'un
|
||||
|
|
@ -1056,6 +1063,24 @@ def inventaire() -> dict:
|
|||
return out
|
||||
|
||||
|
||||
def reseaux_confiance_relais(serveurs: dict, groupes_de, flux_par_role: dict) -> str:
|
||||
"""`mynetworks` du relais du site : la boucle locale, et les machines qui le DECLARENT.
|
||||
|
||||
Une machine peut faire relayer si l'un des roles qu'elle porte declare un flux `egress`
|
||||
vers `serveur_postfix` (la forge, un `client_smtp`). Le registre des flux le dit deja ;
|
||||
une liste ecrite ici le suivrait avec retard. Pur : `test_relais_du_site.py`.
|
||||
"""
|
||||
def _pairs(fl: dict) -> list:
|
||||
p = fl.get("pair")
|
||||
return p if isinstance(p, list) else [p]
|
||||
emetteurs = {r for r, liste in flux_par_role.items() for fl in liste
|
||||
if fl.get("sens") == "egress" and "serveur_postfix" in _pairs(fl)}
|
||||
ips = sorted(f"{s['ip']}/32" for n, s in serveurs.items()
|
||||
if str(s.get("etat", "actif")) == "actif" and s.get("ip")
|
||||
and emetteurs & set(groupes_de(n)))
|
||||
return " ".join(["127.0.0.0/8", "[::1]/128", *ips])
|
||||
|
||||
|
||||
def _racine_backup_du_site() -> str:
|
||||
"""La racine que `serveur_backup_site` passe a sa dependance `serveur_backup`."""
|
||||
meta = Path(__file__).resolve().parent.parent / "roles" / "serveur_backup_site" / "meta" / "main.yml"
|
||||
|
|
|
|||
65
scripts/tests/test_relais_du_site.py
Normal file
65
scripts/tests/test_relais_du_site.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
"""La forge du site trouve son relais de courriel, et le relais la laisse passer.
|
||||
|
||||
POURQUOI (2026-10-12). « Mot de passe oublie » sur la forge du site n'a jamais remis un
|
||||
courriel. Deux defauts, chacun suffisant :
|
||||
1. `serveur_forgejo_smtp` valait `infra-mail-01.<domaine>` en dur : la machine des boites
|
||||
d'un locataire, absente du site ;
|
||||
2. le relais du site (`mynetworks`) n'admettait que sa boucle locale.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import site_inventaire as si # noqa: E402
|
||||
from resoudre_flux import charger_flux # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
echecs: list[str] = []
|
||||
|
||||
# 1. La forge ne nomme plus de machine : elle lit le groupe du relais.
|
||||
defauts = (RACINE / "roles/serveur_forgejo/defaults/main.yml").read_text(encoding="utf-8")
|
||||
ligne = next((l for l in defauts.splitlines() if l.startswith("serveur_forgejo_smtp:")), "")
|
||||
bloc = defauts[defauts.find(ligne):].split("\n\n")[0]
|
||||
if "infra-mail-01" in bloc or "groups['serveur_postfix']" not in bloc:
|
||||
echecs.append("serveur_forgejo_smtp ne derive pas du groupe `serveur_postfix`")
|
||||
|
||||
# 2. Le relais admet les emetteurs declares, et eux seuls (pur).
|
||||
serveurs = {"forge": {"ip": "10.0.33.11"}, "edge": {"ip": "10.0.37.11"},
|
||||
"mon": {"ip": "10.0.36.11"}, "vieille": {"ip": "10.0.33.99", "etat": "retire"}}
|
||||
groupes = {"forge": {"serveur_forgejo"}, "edge": {"serveur_nginx"},
|
||||
"mon": {"serveur_postfix", "serveur_icinga"}, "vieille": {"serveur_forgejo"}}
|
||||
obtenu = si.reseaux_confiance_relais(serveurs, lambda n: groupes[n], charger_flux()).split()
|
||||
if obtenu != ["127.0.0.0/8", "[::1]/128", "10.0.33.11/32"]:
|
||||
echecs.append(f"reseaux_confiance_relais = {obtenu}, attendu boucle + la forge seule")
|
||||
|
||||
# 3. Sur le site monte, s'il l'est : la machine du relais admet sa forge.
|
||||
try:
|
||||
inv = si.inventaire()
|
||||
except Exception: # noqa: BLE001 — aucun site monte : la partie pure suffit
|
||||
inv = {}
|
||||
hv = (inv.get("_meta") or {}).get("hostvars") or {}
|
||||
relais = (inv.get("serveur_postfix") or {}).get("hosts") or []
|
||||
forges = (inv.get("serveur_forgejo") or {}).get("hosts") or []
|
||||
for r in relais:
|
||||
confiance = str((hv.get(r) or {}).get("serveur_postfix_reseaux_confiance") or "").split()
|
||||
for f in forges:
|
||||
ip = f"{(hv.get(f) or {}).get('ansible_host')}/32"
|
||||
if ip not in confiance:
|
||||
echecs.append(f"{r} n'admet pas sa forge {f} ({ip}) : {confiance}")
|
||||
|
||||
for e in echecs:
|
||||
print(f"ECHEC : {e}")
|
||||
if not echecs:
|
||||
print(f"OK : la forge lit son relais ; le relais admet ses emetteurs declares"
|
||||
f"{' (site monte verifie)' if relais else ''}.")
|
||||
return 1 if echecs else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in a new issue