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