From 53c5f41a1a3fed9da0c724d448a191c546f2e444 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 2 Sep 2026 10:31:21 -0400 Subject: [PATCH] durcissement : le site recoit enfin serveur_durci MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban, ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE affirmait pourtant le contraire, ce qui rendait l ecart invisible. Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable : - aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire dynamique) -> commande nftables-site, branchee sur make flux - le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour un inventaire dynamique) -> host_var explicite - aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le generateur refuse desormais de produire des regles sans cet intrant. Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee — juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la veille. MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que dans son play, et le passage de serveur_durci le remettait au defaut sans rien dire. Un reglage qui depend de l ordre des plays revient en arriere. Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot (2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on --- CHANGELOG.md | 70 +++++++++++++++ Makefile | 5 ++ roles/serveur_backup_site/meta/main.yml | 27 +----- scripts/resoudre_flux.py | 109 ++++++++++++++++++++++-- scripts/site_inventaire.py | 70 ++++++++++++++- 5 files changed, 245 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b815204..445cf94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ # CHANGELOG — Set-OPS +## 2026-09-02 (2) — Les machines du site sont durcies : le commentaire disait vrai, le code n'en faisait que la moitie + +**56 preuves.** Les six machines du SITE — racine de l'AC, forge du genome, cache +d'artefacts, resolveur, depot de sauvegarde, runner — ne recevaient QUE `serveur_debian`. +`serveur_durci` ne leur etait jamais applique : ni auditd, ni fail2ban, ni apparmor, ni +sysctl, ni pare-feu. Un inventaire de tenant met chaque machine dans les DEUX groupes +(`inventory_host.py`) ; celui du site n'en mettait aucune dans le second. + +Le commentaire au-dessus de `GROUPE_SOCLE` affirmait pourtant : « elle veut le meme +durcissement SSH, les memes horloges, LE MEME PARE-FEU que n'importe quelle machine de la +flotte ». Personne ne pouvait voir l'ecart, puisque la documentation disait le contraire +de ce que le code faisait. Deuxieme fois en deux jours qu'un commentaire juste couvre une +implementation qui ne l'est pas. + +**Trouve en cherchant autre chose** : un depot de sauvegarde refusait une cle valide, et le +fichier de durcissement SSH fautif datait d'une version abandonnee le 2026-08-09. Rien ne +l'avait jamais remplace parce que rien n'appliquait le role qui le remplace. + +Etat obtenu, mesure sur les machines : auditd, fail2ban, apparmor et nftables actifs sur +les six, chacune avec son jeu de regles resolu, aucun service en echec. + +### Le pare-feu du site n'avait aucune des trois pieces qui le rendent applicable + +**1. Aucune regle n'etait generee pour le site.** `resoudre_flux.py nftables` lit un +`hosts.yml` ; le site a un inventaire DYNAMIQUE. Ses machines etaient donc invisibles pour +la generation. Deux absences se cachaient l'une l'autre : pas de regles, et pas de +pare-feu pour s'en apercevoir. D'ou `nftables-site`, qui traduit l'inventaire dynamique +vers la forme attendue plutot que de dupliquer la generation, et ecrit a cote du plan du +site. Branche sur `make flux`, pour que ca ne se demode pas. + +**2. Le chemin du jeu de regles sortait du depot.** `nftables_baseline` derive son chemin +de `inventory_dir` — juste pour un tenant, faux pour un inventaire dynamique dont le +`inventory_dir` est `scripts/`. Le role serait tombe sur son repli : une politique `drop` +sans les regles resolues. + +**3. Aucun plan d'administration.** `nftables_admin_ssh` est la seule chose qui empeche un +deploiement de couper la main qui le lance. Pour le site, il fallait comprendre que +l'exploitant administre PAR REBOND : la connexion est ouverte par la frontiere et arrive +avec l'adresse de sa patte DANS LA ZONE DE LA MACHINE VISEE, pas avec celle du poste. +Oublier une seule de ces adresses rend une zone entiere inadministrable. Le generateur +REFUSE desormais de produire des regles si cet intrant manque. + +### Le defaut que la lecture a attrape avant l'application + +Le jeu de regles produit pour `site-dns-01` autorisait `10.23.0.0/16` et `10.29.0.0/16` sur +le port 53 — et PAS `10.17.0.0/16`. `_supernets_voisins()` retire l'instance montee : juste +chez un tenant, ou nul n'est son propre voisin ; **faux du cote du site**, ou ca retire le +locataire qu'on pilote au moment ou l'on genere, c'est-a-dire presque toujours celui a qui +l'on tient le plus. + +Chezlepro resout encore sur le resolveur du site. L'appliquer aurait prive de DNS les +quinze machines reconstruites la veille. C'est le meme piege que `site_inventaire` +documente deja pour les ACL du resolveur : il s'est retendu ici parce que la regle est +portee par la FONCTION, pas par le lieu qui l'appelle. + +### Une valeur qui dependait de l'ordre des plays + +`serveur_backup_site` desserrait `MaxStartups` a `60:30:200` — un depot de site voit la +somme des locataires. La valeur vivait dans son `meta`, donc ne portait que dans SON play. +Le jour ou `serveur_durci` a apporte `ssh_hardening` a toutes les machines du site, son +passage — le dernier — a remis le depot au defaut, sans rien signaler. Passee en host_var : +tout play qui applique le role la voit. Un reglage qui depend de l'ordre des plays est un +reglage qui reviendra en arriere. + +### Verifie apres coup, pas seulement dans le journal + +Depuis un locataire, a travers le nouveau pare-feu : le cache repond, la forge repond, le +depot rend `2 snapshots`, le resolveur du site resout. L'AC du site, elle, refuse — et +c'est correct : son flux declare `pair: flotte`, un locataire a la sienne. + ## 2026-09-02 — Reconstruction de Chezlepro depuis zero : quatre defauts que seule une flotte rasee pouvait montrer **56 preuves. `make valider` : 0 echec sur 13 hotes, test de restitution compris.** diff --git a/Makefile b/Makefile index 9f003a5..3e541b4 100644 --- a/Makefile +++ b/Makefile @@ -565,6 +565,11 @@ site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declaree flux: ansible-runtime ## Regenere le registre des flux et les regles nftables depuis les meta/flux.yml python3 scripts/resoudre_flux.py registre python3 scripts/resoudre_flux.py nftables + @# Les machines du SITE aussi : leur inventaire est dynamique, donc invisible + @# pour la generation qui lit un `hosts.yml`. Deux absences se cachaient l'une + @# l'autre — aucune regle generee, et aucun pare-feu applique pour s'en rendre + @# compte. Sans underlay monte, la cible ne fait rien plutot que d'echouer. + @python3 scripts/resoudre_flux.py nftables-site || printf 'note : pas de site monte, aucune regle de site generee.\n' .PHONY: devis-reseau devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat diff --git a/roles/serveur_backup_site/meta/main.yml b/roles/serveur_backup_site/meta/main.yml index 39a3df3..0f188b8 100644 --- a/roles/serveur_backup_site/meta/main.yml +++ b/roles/serveur_backup_site/meta/main.yml @@ -47,29 +47,6 @@ dependencies: # traversable et non listable : voir `serveur_backup_site_parent`. serveur_backup_racine: "/srv/restic/site" - # SSH DURCI, ET DESSERRÉ LÀ OÙ CE DÉPÔT L'EXIGE (mesuré le 2026-09-01). - # - # DEUX DÉFAUTS EN UN, TROUVÉS EN LANÇANT DOUZE SAUVEGARDES D'UN COUP. - # - # 1. Le gabarit transporte des fichiers de durcissement PÉRIMÉS — dont - # `20-chezlepro-hardening.conf`, celui-là même que `ssh_hardening_fichiers_perimes` - # existe pour retirer. Il a été figé dans l'image, donc chaque VM clonée en hérite. - # Les machines du SITE ne recevaient jamais `ssh_hardening` (leur socle est - # `serveur_debian` seul, là où un locataire reçoit aussi `serveur_durci`) : rien ne - # venait donc jamais le corriger. Le dépôt tournait avec `MaxStartups 5:30:20` et - # `MaxSessions 2`, des valeurs abandonnées depuis le 2026-08-09. - # - # 2. `MaxStartups` compte les connexions NON ENCORE AUTHENTIFIÉES. Un dépôt - # d'écosystème en voit une poignée ; celui d'un SITE en voit la somme de tous ses - # locataires, et leurs minuteurs se réveillent à la même heure. Au-delà du seuil, - # sshd coupe AVANT la bannière : le client rapporte - # `kex_exchange_identification: Connection reset by peer`, qui accuse le réseau - # pour un refus applicatif. Une sauvegarde perdue qui se lit comme un incident - # réseau ne sera pas cherchée au bon endroit. - # - # 60 : large devant la somme des flottes servies. Le port 22 de ce dépôt n'est - # atteignable que depuis les locataires déclarés (`meta/flux.yml`) ; l'inondation dont - # `MaxStartups` protège est déjà couverte à la frontière, la panne qu'il créait ne - # l'était pas. Même arbitrage que `ssh_hardening` a fait pour la voie d'administration. + # SSH DURCI COMME PARTOUT AILLEURS. Le desserrage que ce depot exige ne se declare PAS + # ici : voir `serveur_backup_site_max_startups` dans `site_inventaire.py`, et pourquoi. - role: ssh_hardening - ssh_hardening_max_startups: "60:30:200" diff --git a/scripts/resoudre_flux.py b/scripts/resoudre_flux.py index 310dd9a..33fab21 100644 --- a/scripts/resoudre_flux.py +++ b/scripts/resoudre_flux.py @@ -223,7 +223,8 @@ def _ip_par_hote(data: dict) -> dict[str, str]: return table -def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict[str, str]) -> list[str]: +def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict[str, str], + site: bool = False) -> list[str]: """pair -> IP sources concretes (pour une regle ingress). [] = pas de regle inter-noeud.""" noms: set[str] = set() for p in (pair if isinstance(pair, list) else [pair]): @@ -243,9 +244,36 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict srcs = {ip_par_hote[h] for h in noms if h in ip_par_hote} demandes = pair if isinstance(pair, list) else [pair] if "admin" in demandes: - srcs |= set(_sources_admin_ssh()) + srcs |= set(_sources_admin_ssh_site() if site else _sources_admin_ssh()) if "voisins_site" in demandes: - srcs |= set(_supernets_voisins()) + # DU COTE DU SITE, « VOISIN » N'EXCLUT PERSONNE (mesure du 2026-09-02). + # + # `_supernets_voisins()` retire l'instance montee : c'est juste chez un TENANT, + # ou l'on declare des flux vers ses voisins et ou nul n'est son propre voisin. + # Applique au SITE, ce filtre retire le locataire qu'on pilote au moment ou l'on + # genere — c'est-a-dire, presque toujours, celui a qui l'on tient le plus. + # + # CE QUE CA AURAIT COUTE : le jeu de regles produit pour `site-dns-01` autorisait + # 10.23.0.0/16 et 10.29.0.0/16 sur le port 53, et PAS 10.17.0.0/16. Chezlepro + # resout encore sur le resolveur du site : l'appliquer aurait prive de DNS les + # quinze machines qu'on venait de reconstruire. Le defaut a ete vu en LISANT le + # fichier avant de le poser, pas en le posant. + # + # C'est le meme piege que `site_inventaire` documente pour les ACL du resolveur, + # et il s'est retendu ici parce que la regle est portee par la fonction, pas par + # le lieu qui l'appelle. On prend donc la decouverte SANS son filtre : le site + # sert tous ceux qu'il heberge, y compris l'instance montee. + if site: + try: + import devis_reseau + from inventory_rules import supernet_de + srcs |= {supernet_de(int(n["index"])) + for _nom, _p, n in devis_reseau.decouvrir_du_site() + if n.get("index") is not None} + except Exception: + pass + else: + srcs |= set(_supernets_voisins()) if "runner_site" in demandes: srcs |= set(_adresses_runner_site()) return sorted(srcs) @@ -383,17 +411,73 @@ def _ports_du_plan() -> dict[str, str]: return {} -def generer_nftables(flux_par_role: dict[str, list[dict]]) -> list[Path]: - data = yaml.safe_load(_inventaire().read_text(encoding="utf-8")) or {} +def _inventaire_site() -> dict: + """L'inventaire du SITE, rendu dans la forme qu'attendent les fonctions ci-dessus. + + Un site n'a pas de `hosts.yml` : son inventaire est un SCRIPT (`site_inventaire.py`), + parce que sa declaration EST deja sa forme finale — rien n'y est derive d'un index. + C'est voulu, et documente la-bas. + + La consequence, elle, ne l'etait pas : tout ce qui lit l'inventaire en YAML ignorait + purement et simplement les machines du site. `generer_nftables` n'ecrivait donc + aucune regle pour elles — et comme le pare-feu n'y etait pas non plus applique, + personne ne pouvait s'en apercevoir. Deux absences qui se cachaient l'une l'autre. + + On traduit ici la forme dynamique (`{groupe: {hosts: [...]}}` + `_meta.hostvars`) + vers la forme statique (`all.children..hosts.`), plutot que de dupliquer + la logique de generation. Une seule facon de produire des regles pour tout le monde. + """ + import site_inventaire + src = site_inventaire.inventaire() + hostvars = (src.get("_meta") or {}).get("hostvars") or {} + enfants: dict = {} + for groupe, contenu in src.items(): + if groupe == "_meta" or not isinstance(contenu, dict): + continue + enfants[groupe] = {"hosts": {h: hostvars.get(h) for h in (contenu.get("hosts") or [])}} + return {"all": {"children": enfants}} + + +def _sources_admin_ssh_site() -> list[str]: + """`nftables_admin_ssh` du PLAN DU SITE — la meme garde, l'autre cote de la frontiere. + + Le site n'a pas de `group_vars/all` : ses intrants vivent dans `plan/10-intrants.yml`. + Rendre [] ici priverait les regles de leur garde anti-verrouillage, donc on le REFUSE + plus haut plutot que de generer un jeu de regles qui coupe SSH. + """ + import underlay as underlay_mod + src = (underlay_mod.lire_plan_site("10-intrants.yml") or {}).get("nftables_admin_ssh") + if not src: + return [] + return [str(s) for s in src] if isinstance(src, list) else [str(src)] + + +def generer_nftables(flux_par_role: dict[str, list[dict]], site: bool = False) -> list[Path]: + if site: + data = _inventaire_site() + admin_ssh = _sources_admin_ssh_site() + if not admin_ssh: + raise ErreurFlux( + "Le plan du site ne declare pas `nftables_admin_ssh`. Sans lui, les regles " + "generees refuseraient SSH sur TOUTES les machines du site — et le seul " + "moyen de les reparer passe par SSH. Declarer l'intrant avant de generer.") + else: + data = yaml.safe_load(_inventaire().read_text(encoding="utf-8")) or {} + admin_ssh = _sources_admin_ssh() actifs_noms = set(_hotes_du_groupe(data, "hotes_actifs")) ip_par_hote = _ip_par_hote(data) - admin_ssh = _sources_admin_ssh() ports_plan = _ports_du_plan() regles_admin = [ f" ip saddr {{ {cidr} }} tcp dport 22 accept # administration (garde anti-lockout, intrant nftables_admin_ssh)" for cidr in admin_ssh ] - sortie_dir = _inventaire().parent.parent.parent / "flux-genere" + if site: + # A cote du plan du site, comme sa carte et sa voute : un artefact du site vit + # chez le site, pas dans l'instance montee au moment ou on l'a genere. + import underlay as underlay_mod + sortie_dir = (underlay_mod.chemin().resolve().parent / "flux-genere") + else: + sortie_dir = _inventaire().parent.parent.parent / "flux-genere" sortie_dir.mkdir(parents=True, exist_ok=True) ecrits: list[Path] = [] for hote in sorted(actifs_noms): @@ -424,7 +508,7 @@ def generer_nftables(flux_par_role: dict[str, list[dict]]) -> list[Path]: if not reels: continue ports = reels - srcs = _resoudre_sources(data, fl.get("pair"), actifs_noms, ip_par_hote) + srcs = _resoudre_sources(data, fl.get("pair"), actifs_noms, ip_par_hote, site=site) saddr = "" if not srcs else "ip saddr { " + ", ".join(srcs) + " } " local = "localhost" in (fl.get("pair") if isinstance(fl.get("pair"), list) else [fl.get("pair")]) for port in ports: @@ -544,8 +628,15 @@ def main(argv: list[str]) -> int: print(f"{len(ecrits)} aperçu(s) nftables générés (NON activés) :") for p in ecrits: print(f" {p}") + elif commande == "nftables-site": + valider(flux) + ecrits = generer_nftables(flux, site=True) + print(f"{len(ecrits)} aperçu(s) nftables du SITE générés (NON activés) :") + for p in ecrits: + print(f" {p}") else: - print(f"Commande inconnue : {commande} (registre | nftables | verifier)", file=sys.stderr) + print(f"Commande inconnue : {commande} " + "(registre | nftables | nftables-site | verifier)", file=sys.stderr) return 2 except ErreurFlux as e: print(f"ERREUR flux :\n{e}", file=sys.stderr) diff --git a/scripts/site_inventaire.py b/scripts/site_inventaire.py index b5428b1..fb23650 100755 --- a/scripts/site_inventaire.py +++ b/scripts/site_inventaire.py @@ -40,7 +40,24 @@ UTILISATEUR_DEFAUT = "ansible" # espece : elle veut le meme durcissement SSH, les memes horloges, le meme pare-feu que # n'importe quelle machine de la flotte. L'oublier lui aurait donne un SSH par defaut et # aucune regle de frontiere — la machine la plus puissante du site, la moins protegee. -GROUPE_SOCLE = "serveur_debian" +# +# CE COMMENTAIRE DISAIT VRAI, LE CODE N'EN FAISAIT QUE LA MOITIE (2026-09-02). +# +# Seul `serveur_debian` etait pose. `serveur_durci` — auditd, fail2ban, apparmor, +# sysctl, unattended-upgrades, journald, ssh_hardening, nftables — ne l'etait pas. Un +# inventaire de tenant met CHAQUE machine dans les deux (`inventory_host.py`) ; celui du +# site n'en mettait aucune dans le second. +# +# Ce que ca laissait sans protection : la racine de l'autorite de certification, la +# forge qui porte le genome, le cache d'artefacts par ou passe chaque paquet installe +# sur chaque machine de chaque locataire, et le depot ou tous deposent leur etat. Les +# machines les plus consequentes de l'installation etaient les moins durcies — et +# personne ne pouvait le voir, puisque le commentaire affirmait le contraire. +# +# TROUVE EN CHERCHANT AUTRE CHOSE : un depot de sauvegarde refusait une cle valide, et +# le fichier de durcissement SSH fautif datait d'une version abandonnee le 2026-08-09. +# Rien ne l'avait jamais remplace parce que rien n'appliquait le role qui le remplace. +GROUPE_SOCLE = ["serveur_debian", "serveur_durci"] # LE PLANCHER `/etc/hosts` SE DERIVE DE CE GROUPE, et de nul autre. # @@ -317,8 +334,54 @@ def inventaire() -> dict: "serveur_resolveur_reseaux_autorises": ( _zones_du_site + _supernets_tenants if "serveur_resolveur_site" in _groupes_de(nom) else _zones_du_site), + # LE PARE-FEU DE MACHINE, EN DEUX TEMPS (2026-09-02). + # + # `serveur_durci` porte `nftables_baseline`, qui refuse tout ce qui n'est pas + # declare. Chez un tenant, trois pieces le rendent applicable : un registre de + # flux resolu POUR SES HOTES, un chemin ou lire le jeu de regles genere, et un + # `nftables_admin_ssh` qui dit par ou l'administration entre. + # + # Le site n'en avait aucune : `resoudre_flux.py nftables` n'ecrit que pour + # l'instance montee, et le chemin par defaut (`inventory_dir/../../`) sort du + # depot pour un inventaire DYNAMIQUE, dont le `inventory_dir` est `scripts/`. + # + # Poser le pare-feu avant ces pieces, ce serait appliquer une politique + # `drop` sans aucune regle : les six machines du site tomberaient d'un coup, + # et le deploiement qui les repare passe justement par SSH. On pose donc les + # neuf autres roles d'abord — ils ne coupent rien — et le pare-feu ensuite, + # quand il aura de quoi laisser passer. + "nftables_baseline_enabled": True, + # LE CHEMIN DU JEU DE REGLES, DIT ICI PARCE QUE LE DEFAUT NE PEUT PAS LE DIRE. + # + # `nftables_baseline` derive son chemin de `inventory_dir` — juste pour un + # tenant, dont l'inventaire est un FICHIER sous `instance/inventories/...`. + # Le site a un inventaire DYNAMIQUE : son `inventory_dir` est `scripts/`, et + # le defaut pointait donc un cran AU-DESSUS du depot, sur un chemin qui + # n'existe pas. Le role serait tombe sur son repli — une politique `drop` + # sans les regles resolues. + # + # Les regles du site vivent a cote de son plan, comme sa carte et sa voute : + # `make flux` les y ecrit (`resoudre_flux.py nftables-site`). + "nftables_baseline_ruleset_genere": str( + (U.chemin().resolve().parent / "flux-genere" / f"{nom}.nft") + if U.chemin() else ""), # Ce que le marqueur verifiera : la liste ci-dessus les admet-elle vraiment ? "serveur_resolveur_site_supernets_tenants": _supernets_tenants, + # `MaxStartups` COMPTE LES CONNEXIONS NON AUTHENTIFIEES, et un depot de SITE + # en voit la somme de tous ses locataires — dont les minuteurs se reveillent + # a la meme heure. Au-dela du seuil, sshd coupe AVANT la banniere : le client + # rapporte `kex_exchange_identification: Connection reset by peer`, qui + # accuse le reseau pour un refus applicatif. Douze sauvegardes simultanees, + # une perdue (2026-09-01). + # + # EN host_var, ET NON EN PARAMETRE DE DEPENDANCE (regression du 2026-09-02). + # La valeur vivait dans le `meta` de `serveur_backup_site` : elle ne portait + # donc que dans le play de CE role. Le jour ou `serveur_durci` a apporte + # `ssh_hardening` a toutes les machines du site, son passage — le dernier — a + # remis le depot a la valeur par defaut, sans rien signaler. Un reglage qui + # depend de l'ordre des plays est un reglage qui reviendra en arriere. + # + # Ici, tout play qui applique `ssh_hardening` a cet hote la voit. # Les ecosystemes qui deposent leur etat ici, un compte Unix chacun. "serveur_backup_site_locataires": ( _locataires_backup @@ -333,7 +396,10 @@ def inventaire() -> dict: # plan declare pour toutes. **(srv.get("variables") or {}), } - groupes.setdefault(GROUPE_SOCLE, []).append(nom) + for _socle in GROUPE_SOCLE: + groupes.setdefault(_socle, []).append(nom) + if "serveur_backup_site" in _groupes_de(nom): + hostvars[nom]["ssh_hardening_max_startups"] = "60:30:200" groupes.setdefault(GROUPE_FLOTTE, []).append(nom) for integ in universelles: groupes.setdefault(integ, []).append(nom)