From 1363ebff59e9419e454139f35aba13b64b0abcb9 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 24 Aug 2026 15:50:37 -0400 Subject: [PATCH] acces : le runner peut enfin entrer, et trois silences fermes ssh_baseline gere les cles d'administration declarees au plan, avec un `etat` par entree : revoquer devient un changement de plan, pas une visite sur chaque machine. Pas d'exclusive -- il effacerait la cle de cloud-init et fermerait la flotte a tout le monde. cloud-init reecrivait /etc/hosts a chaque demarrage et effacait le plancher de resolution. Constate sur infra-dns-01 apres un redemarrage : six entrees perdues, revelees deux jours plus tard par un apt update qui ne resolvait plus. requirements.yml ne declarait pas ansible.posix ni community.docker, pourtant utilisees. Ca marchait chez le mainteneur, pas sur un runner. Et le cache des collections suivait l'existence du fichier au lieu de son contenu. Enfin : deux `when` sur une meme tache, c'est un seul -- le dernier. Le check-mode avait disparu en silence. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++ docs/audit/preuve-2026-08-24.md | 2 +- requirements.yml | 17 ++++++++ roles/hosts_statiques/tasks/main.yml | 28 +++++++++++++ roles/serveur_ops/defaults/main.yml | 13 +++++- roles/serveur_ops/tasks/main.yml | 16 ++++++- roles/ssh_baseline/defaults/main.yml | 24 +++++++++++ roles/ssh_baseline/tasks/main.yml | 12 ++++++ 8 files changed, 170 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9882603..8da2abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,67 @@ # CHANGELOG — Set-OPS +## 2026-08-24 — La clé du runner, et trois silences fermés en chemin + +Le poste d'exploitation fabriquait sa propre paire SSH depuis son premier déploiement, et +**n'avait jamais pu joindre un seul hôte**. Deux verrous, pas un : la clé n'était autorisée +nulle part, et `nftables_admin_ssh` ne listait pas son adresse. Les deux vont ensemble — +c'est tout le sens de P24, un seul intrant pour la flotte et la frontière. + +`ssh_baseline` gère désormais les clés d'administration déclarées au plan. **Chaque entrée +porte son `etat`** : passer à `absent` révoque sur toute la flotte au prochain passage. Une +liste purement additive ne sait pas retirer, et « révoquer » voudrait alors dire se +connecter à la main sur chaque machine. + +Pas d'`exclusive: true` : il effacerait la clé posée par cloud-init si le plan ne la +reprenait pas, et fermerait la flotte à tout le monde d'un seul déploiement. Le prix +assumé : une clé posée hors du plan n'est pas détectée ici. + +Vérifié depuis `ops-01` : les cinq hôtes répondent leur FQDN. + +### Cloud-init effaçait le plancher de résolution à chaque démarrage + +`manage_etc_hosts: true` traîne dans le gabarit doré. À chaque boot, cloud-init régénère +`/etc/hosts` depuis son propre modèle et **efface le plancher dérivé de l'inventaire** — +la seule façon qu'a une machine de nommer ses voisines sans DNS. + +Constaté sur `infra-dns-01`, redémarré lors d'un déplacement de disque : six entrées de +flotte perdues. Le défaut s'est manifesté deux jours plus tard sous la forme d'un +`apt update` qui ne résolvait plus le cache d'artefacts — un message qui ne parlait pas du +tout du vrai problème. `infra-edge-01` avait subi le même sort et s'était fait réparer +sans qu'on le sache, par un redéploiement. + +`hosts_statiques` dépose maintenant un fragment `cloud.cfg.d` qui neutralise cette +réécriture. Les cinq hôtes : six entrées, `manage_etc_hosts: false`, `apt` sans erreur. + +### Trois collections utilisées, aucune déclarée + +`requirements.yml` ne nommait que `community.postgresql` et `community.general`. Or les +rôles utilisent aussi **`ansible.posix`** (`serveur_backup`) et **`community.docker`** +(`serveur_collabora`). Ça marchait chez le mainteneur, où un gros lot est installé — et +échouait partout ailleurs. + +**Le runner n'a que ce que ce fichier déclare** : il n'aurait pas pu déployer les +sauvegardes. C'est le genre d'écart qu'on ne voit qu'en portant le moteur sur une autre +machine. + +À trancher : `community.docker` contredit le principe « zéro conteneur ». La dépendance +est déclarée parce qu'elle **existe** — mieux vaut une contradiction visible qu'un rôle qui +échoue en silence — mais la question reste ouverte : Collabora en natif, ou l'exception +assumée ? + +### Et le cache suivait la mauvaise chose + +Le téléchargement des collections était gardé par `creates: /requirements.yml` : +une collection **ajoutée** au fichier n'aurait jamais été récupérée, le témoin existant +déjà. Le cache est désormais indexé sur l'**empreinte** du fichier — changer une ligne +crée un cache neuf, donc un téléchargement. + +### Un `when` en écrase un autre + +En ajoutant une condition, j'en ai laissé une seconde juste en dessous. YAML garde la +dernière clé et jette l'autre, **en silence** : `not ansible_check_mode` avait disparu. +`ansible-lint` l'a vu ; personne d'autre ne l'aurait vu. Les conditions vont dans une liste. + ## 2026-08-24 — Deux runners, deux pouvoirs, aucun omnipotent Le travail d'un runner se divise en trois, et la ligne de partage est **celle des voûtes** : diff --git a/docs/audit/preuve-2026-08-24.md b/docs/audit/preuve-2026-08-24.md index d9e2f93..05caed1 100644 --- a/docs/audit/preuve-2026-08-24.md +++ b/docs/audit/preuve-2026-08-24.md @@ -36,7 +36,7 @@ | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 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 : 7 reseau(x), aucune collision avec la plage tenant. | -| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 55 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,192.168.254.2/32,192.168.255.2/32. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 55 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | | P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 48 groupe(s), 77 regle(s). | | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 15 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (1 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. | diff --git a/requirements.yml b/requirements.yml index c097f1a..740c21c 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,6 +1,23 @@ --- # Collections Ansible requises par Set-OPS. # Installer : ansible-galaxy collection install -r requirements.yml +# +# CE FICHIER EST LA SEULE SOURCE POUR UN RUNNER (2026-08-24). Le poste d'exploitation +# n'installe QUE ce qui est declare ici — il n'herite pas du gros lot qui traine sur le +# poste du mainteneur. Une collection utilisee par un role et absente d'ici marche chez +# le mainteneur et echoue partout ailleurs : c'est le genre d'ecart qu'on ne voit qu'en +# portant le moteur sur une autre machine. collections: - name: community.postgresql - name: community.general + # `serveur_backup` : ansible.posix.authorized_key. Utilisee depuis longtemps, jamais + # declaree — donc absente du runner, qui n'aurait pas pu deployer les sauvegardes. + - name: ansible.posix + # `serveur_collabora` : community.docker.docker_container. + # + # A TRANCHER. La doctrine du depot dit « zero conteneur » pour la plateforme webapp, et + # le principe general est le logiciel libre en natif. Collabora fait exception, en + # conteneur Docker. On declare la dependance parce qu'elle EXISTE — mieux vaut une + # contradiction visible qu'un role qui echoue en silence sur un runner — mais la + # question reste ouverte : Collabora en natif, ou l'exception assumee et documentee ? + - name: community.docker diff --git a/roles/hosts_statiques/tasks/main.yml b/roles/hosts_statiques/tasks/main.yml index 8dc585f..07c52e7 100644 --- a/roles/hosts_statiques/tasks/main.yml +++ b/roles/hosts_statiques/tasks/main.yml @@ -1,4 +1,32 @@ --- +# CLOUD-INIT REECRIT /etc/hosts A CHAQUE DEMARRAGE (2026-08-24). +# +# Le gabarit dore laisse `manage_etc_hosts: true`. A chaque boot, cloud-init regenere +# /etc/hosts depuis son propre modele — et efface le plancher de resolution que ce role +# vient de poser. La machine perd alors la seule facon qu'elle a de nommer ses voisines +# sans DNS, et personne ne le voit : rien n'echoue tant qu'on ne demande pas un nom. +# +# CONSTATE SUR infra-dns-01 : redemarre a 18h53 lors d'un deplacement de disque, il a +# perdu ses six entrees de flotte. Le defaut est apparu deux jours plus tard, sous la +# forme d'un `apt update` qui n'arrivait plus a resoudre le cache d'artefacts — un +# message qui ne parlait pas du tout du vrai probleme. +# +# On le desactive par un fragment plutot qu'en editant cloud.cfg : le gabarit reste +# intact, et `apt purge cloud-init` ou une remise a zero rend la machine a son etat +# d'origine. +- name: Empêcher cloud-init de réécrire le plancher à chaque démarrage + ansible.builtin.copy: + dest: /etc/cloud/cloud.cfg.d/99-setops-hosts.cfg + content: | + # GÉNÉRÉ par Set-OPS (rôle hosts_statiques). NE PAS éditer à la main. + # /etc/hosts est le PLANCHER DE RÉSOLUTION de l'écosystème, dérivé de l'inventaire. + # Laisser cloud-init le régénérer au démarrage l'effacerait en silence. + manage_etc_hosts: false + owner: root + group: root + mode: "0644" + when: hosts_statiques_actif | bool + # Alias d'exposition (best-effort) : dérivés du plan si celui-ci est disponible. - name: Vérifier la présence des registres du plan (sur le nœud de contrôle) ansible.builtin.stat: diff --git a/roles/serveur_ops/defaults/main.yml b/roles/serveur_ops/defaults/main.yml index 70b004b..7689a19 100644 --- a/roles/serveur_ops/defaults/main.yml +++ b/roles/serveur_ops/defaults/main.yml @@ -124,6 +124,17 @@ serveur_ops_cle_generer: true # devant son propre tiers. # # Effet de bord recherché : le poste devient déployable **hors ligne**. +# LE CACHE SUIT LE CONTENU DE `requirements.yml`, PAS SON EXISTENCE (2026-08-24). +# +# La premiere version gardait le telechargement par `creates: /requirements.yml` : +# une collection AJOUTEE au fichier n'aurait jamais ete recuperee, puisque le temoin +# existait deja. Le runner serait reste en retard sur le moteur, en silence -- exactement +# le defaut qui a fait qu'`ansible.posix` manquait sans que rien ne le dise. +# +# L'empreinte du fichier entre dans le chemin : changer une ligne de `requirements.yml` +# cree un cache neuf, donc un telechargement. Les anciens caches restent (ils ne genent +# pas) et documentent ce que les versions precedentes exigeaient. serveur_ops_cache_collections: >- - {{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/collections + {{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/collections-{{ + lookup('file', serveur_ops_requirements_controleur) | hash('sha1') | truncate(12, true, '') }} serveur_ops_requirements_controleur: "{{ playbook_dir }}/../../requirements.yml" diff --git a/roles/serveur_ops/tasks/main.yml b/roles/serveur_ops/tasks/main.yml index 7633b3e..fac8d83 100644 --- a/roles/serveur_ops/tasks/main.yml +++ b/roles/serveur_ops/tasks/main.yml @@ -139,6 +139,7 @@ when: not ansible_check_mode - name: Deposer les collections sur le poste + register: serveur_ops_depot_collections ansible.builtin.copy: src: "{{ serveur_ops_cache_collections }}/" dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/" @@ -159,10 +160,21 @@ -r requirements.yml -p {{ serveur_ops_racine }}/.ansible/collections chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne" - creates: "{{ serveur_ops_racine }}/.ansible/collections/ansible_collections/community/general" + # LE GARDE-FOU SUIVAIT LA MAUVAISE CHOSE (2026-08-24). Il etait `creates: …/community/general` : + # une collection AJOUTEE a requirements.yml n'aurait jamais ete installee, puisque le + # repertoire temoin existait deja. Le runner serait reste en retard sur le moteur, en + # silence. On suit desormais le DEPOT des archives : si les collections deposees + # changent, on reinstalle. + changed_when: true become: true become_user: "{{ serveur_ops_utilisateur }}" - when: not ansible_check_mode + # DEUX `when` SUR UNE MEME TACHE, C'EST UN SEUL — LE DERNIER (2026-08-24). En ajoutant + # la condition sur le depot, j'ai laisse celle du check-mode juste en dessous : YAML + # garde la derniere cle et jette l'autre, en silence. `ansible-lint` l'a vu ; personne + # d'autre ne l'aurait vu. Les conditions vont donc dans UNE liste. + when: + - serveur_ops_depot_collections is changed + - not ansible_check_mode # LES DEUX SYMLINKS (D-80). `instance` dit QUEL tenant on pilote. Le poste en pose un par # défaut ; l'exploitant le bascule comme sur le poste du mainteneur. diff --git a/roles/ssh_baseline/defaults/main.yml b/roles/ssh_baseline/defaults/main.yml index 214f81d..f8922d1 100644 --- a/roles/ssh_baseline/defaults/main.yml +++ b/roles/ssh_baseline/defaults/main.yml @@ -2,3 +2,27 @@ ssh_baseline_password_authentication: "no" ssh_baseline_permit_root_login: "no" ssh_baseline_pubkey_authentication: "yes" + +# --- QUI A LE DROIT D'ENTRER -------------------------------------------------- +# +# La cle du compte d'administration est posee par CLOUD-INIT, a la creation de la VM. +# Il n'existait donc aucun moyen declaratif d'en autoriser une SECONDE sur des machines +# deja nees — constate le 2026-08-24 : le poste d'exploitation fabriquait sa propre paire +# depuis son premier deploiement, et n'a jamais pu joindre un seul hote +# (`Permission denied (publickey)`). +# +# CHAQUE ENTREE PORTE SON `etat`, ET C'EST DELIBERE. Une liste purement additive ne sait +# pas RETIRER : une cle autorisee une fois le resterait pour toujours, et « revoquer » +# voudrait dire se connecter a la main sur chaque machine. Ici, passer une entree a +# `absent` la retire au prochain passage, sur toute la flotte. +# +# ON N'UTILISE PAS `exclusive: true` : il effacerait la cle posee par cloud-init si le +# plan ne la reprenait pas, et fermerait la flotte a tout le monde d'un seul deploiement. +# Le prix de ce choix est qu'une cle posee HORS du plan ne sera pas detectee ici. +# +# ssh_baseline_cles_admin: +# - cle: "ssh-ed25519 AAAA… setops@ops-01.exemple.internal" +# pourquoi: "poste d'exploitation — configure la flotte" +# etat: present # `absent` pour revoquer +ssh_baseline_admin_user: "{{ sudo_ansible_admin_user | default('ansible') }}" +ssh_baseline_cles_admin: [] diff --git a/roles/ssh_baseline/tasks/main.yml b/roles/ssh_baseline/tasks/main.yml index 6e47c8c..cd002bb 100644 --- a/roles/ssh_baseline/tasks/main.yml +++ b/roles/ssh_baseline/tasks/main.yml @@ -26,3 +26,15 @@ name: ssh enabled: true state: started + +# Voir defaults/main.yml pour le raisonnement (etat par entree, pas d'`exclusive`). +- name: Autoriser — ou révoquer — les clés d'administration déclarées au plan + ansible.posix.authorized_key: + user: "{{ ssh_baseline_admin_user }}" + key: "{{ item.cle }}" + state: "{{ item.etat | default('present') }}" + comment: "{{ item.pourquoi | default(omit) }}" + loop: "{{ ssh_baseline_cles_admin }}" + loop_control: + label: "{{ item.pourquoi | default(item.cle[:40]) }} → {{ item.etat | default('present') }}" + when: not ansible_check_mode