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
|
# 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
|
## 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.
|
**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_proxmox_fw_externe.py
|
||||||
python3 scripts/tests/test_libelles_parefeu.py
|
python3 scripts/tests/test_libelles_parefeu.py
|
||||||
python3 scripts/tests/test_conditions_site.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_sonde_tcp.py
|
||||||
python3 scripts/tests/test_restauration.py
|
python3 scripts/tests/test_restauration.py
|
||||||
python3 scripts/tests/test_repertoires_partages.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.
|
# En simulation, le script ne fait que montrer (sans --confirmer) : il peut tourner.
|
||||||
check_mode: false
|
check_mode: false
|
||||||
register: forge_comptes
|
register: forge_comptes
|
||||||
changed_when: >-
|
# LES LIGNES D'ACTION, PAS DES MOTS : « jeton a creer », dans le plan, contient « jeton »
|
||||||
['cree ', 'collab ', 'retire ', 'jeton '] | select('in', forge_comptes.stdout) | list | length > 0
|
# 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
|
# 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.
|
# le reste. 2 = le script a refuse de travailler.
|
||||||
failed_when: forge_comptes.rc not in [0, 1]
|
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_groupe: "serveur_forgejo"
|
||||||
serveur_forgejo_db_host: "" # dérivé (FQDN) par resoudre_base au déploiement
|
serveur_forgejo_db_host: "" # dérivé (FQDN) par resoudre_base au déploiement
|
||||||
|
|
||||||
# Relais courriel.
|
# Relais courriel : DERIVE de l'inventaire, l'hote qui porte `serveur_postfix` (2026-10-12).
|
||||||
serveur_forgejo_smtp: "infra-mail-01.{{ domaine_interne }}"
|
#
|
||||||
|
# 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).
|
# Secrets OBLIGATOIRES (Ansible Vault).
|
||||||
serveur_forgejo_secret_key: "{{ vault_forgejo_secret_key | default('') }}" # rempli depuis la voute (vault_forgejo_secret_key)
|
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 %}
|
{% endif %}
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
ENABLED = true
|
ENABLED = {{ 'true' if serveur_forgejo_smtp else 'false' }}
|
||||||
PROTOCOL = smtp
|
PROTOCOL = smtp
|
||||||
SMTP_ADDR = {{ serveur_forgejo_smtp }}
|
SMTP_ADDR = {{ serveur_forgejo_smtp }}
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ local_recipient_maps =
|
||||||
qu'elle sorte au nom du SITE — pas d'un locataire. #}
|
qu'elle sorte au nom du SITE — pas d'un locataire. #}
|
||||||
relayhost =
|
relayhost =
|
||||||
{# `mynetworks` seul decide qui peut faire relayer : la loopback, donc les services de
|
{# `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
|
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination
|
||||||
{% else %}
|
{% else %}
|
||||||
# --- Boîtes virtuelles : validation LDAP + remise LMTP vers le nœud mail-store (Dovecot) ---
|
# --- 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()
|
for s in serveurs.values()
|
||||||
if str(s.get("etat", "actif")) == "actif"
|
if str(s.get("etat", "actif")) == "actif"
|
||||||
and (par_reseau.get(s.get("reseau")) or {}).get("sous_reseau")})
|
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).
|
# 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
|
# 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.
|
# de quoi faire SORTIR une alerte, en son propre nom.
|
||||||
"serveur_postfix_mode": "relais",
|
"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 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
|
# Le SSO d'un tenant supposerait que le site emprunte l'identite d'un
|
||||||
|
|
@ -1056,6 +1063,24 @@ def inventaire() -> dict:
|
||||||
return out
|
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:
|
def _racine_backup_du_site() -> str:
|
||||||
"""La racine que `serveur_backup_site` passe a sa dependance `serveur_backup`."""
|
"""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"
|
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