diff --git a/CHANGELOG.md b/CHANGELOG.md index f82e39c..09b8051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # CHANGELOG — Set-OPS +## 2026-07-07 — Reconstruction from-zero PROUVÉE (preuve de portabilité « sans réserve ») + +Les 14 VM du lab (+ sauvegardes + AC) supprimées, puis `make myDay` a reconstruit l'écosystème +POC **de rien** : 13 hôtes déployés (0 échec), et `make valider` **entièrement vert** (Prometheus +6/6 UP, 6 vhosts HTTPS, courriel bout-en-bout remis, 6 dépôts restic restaurés) — le tout sous +pare-feu actif. 4 bugs de portabilité débusqués et corrigés dans le moteur : + +### Corrigé +- **client_pki : empreinte du root CA dérivée dynamiquement** (au lieu d'une valeur figée en Vault). + Une AC régénérée (from-zero) a une empreinte neuve ; le rôle la lit désormais de l'autorité + elle-même (`step certificate fingerprint`, délégué au nœud step-ca), source de vérité. + `client_pki_ca_fingerprint_override` permet un épinglage explicite. +- **serveur_keycloak : assignation de rôle tolérante aux utilisateurs absents.** Sur un annuaire + vide (from-zero), assigner un rôle à un user inexistant échouait ; on vérifie désormais son + existence (Keycloak fédère LDAP à la demande) et on saute proprement sinon. +- **serveur_prometheus : ne scrute que les hôtes ACTIFS** (`client_metrique ∩ hotes_actifs`). Un + hôte planifié (non déployé) n'est plus une cible morte. +- **Pare-feu nftables compatible Docker.** Le ruleset résolu (a) remplace **uniquement la table + `setops_flux`** au lieu de `flush ruleset` (préserve les tables Docker : DNAT/forward des + conteneurs) et (b) autorise `docker0` + `ct established,related` dans la chaîne `forward`. Sans + ça, `forward policy drop` coupait Collabora (conteneur). Diagnostic prouvé au niveau paquet. + +### Ajouté +- **`playbooks/proxmox/supprimer_vm_debian.yml`** — suppression de VM par VMID (from-zero), avec + garde-fous : n'agit que sur les VMID présents dans le cluster, refuse si le modèle est ciblé, + secrets `no_log`. + ## 2026-07-07 ### Ajouté diff --git a/playbooks/proxmox/supprimer_vm_debian.yml b/playbooks/proxmox/supprimer_vm_debian.yml new file mode 100644 index 0000000..355fbbf --- /dev/null +++ b/playbooks/proxmox/supprimer_vm_debian.yml @@ -0,0 +1,116 @@ +--- +# DESTRUCTIF : supprime des VM Proxmox par VMID. Réservé à un from-zero explicite. +# Sécurités : n'agit QUE sur les VMID passés en `cibles_vmid` ET présents dans le +# cluster ; refuse si le modèle (proxmox_clone_vmid_modele) est dans la liste. +- name: Supprimer des VM Debian du cluster Proxmox (from-zero) + hosts: localhost + connection: local + gather_facts: false + become: false + + vars: + setops_inventaires: [lab, principal, production] + setops_gv_proxmox: [proxmox.yml, proxmox.vault.yml, proxmox.local.yml] + # cibles_vmid : liste d'entiers, passée en -e (sinon dérivée des hôtes actifs). + cibles_vmid: "{{ groups['hotes_actifs'] | map('extract', hostvars, 'proxmox_vmid') | map('int') | list }}" + + tasks: + - name: Localiser les fichiers de variables Proxmox + ansible.builtin.stat: + path: "{{ playbook_dir }}/../../instance/inventories/{{ item.0 }}/group_vars/{{ item.1 }}" + loop: "{{ setops_inventaires | product(setops_gv_proxmox) | list }}" + loop_control: + label: "{{ item.0 }}/{{ item.1 }}" + register: proxmox_fichiers_variables + + - name: Charger les variables Proxmox non sensibles + ansible.builtin.include_vars: + file: "{{ item.stat.path }}" + loop: "{{ proxmox_fichiers_variables.results }}" + when: + - item.stat.exists + - item.stat.path is not search('vault') + + - name: Charger les secrets Proxmox (Vault) + ansible.builtin.include_vars: + file: "{{ item.stat.path }}" + loop: "{{ proxmox_fichiers_variables.results }}" + when: + - item.stat.exists + - item.stat.path is search('vault') + no_log: true + + - name: Résoudre les identifiants d'API + ansible.builtin.set_fact: + api_host: "{{ proxmox_api_host | default(lookup('env', 'PROXMOX_API_HOST'), true) }}" + api_port: "{{ proxmox_api_port | default('8006', true) }}" + api_user: "{{ proxmox_api_user | default(lookup('env', 'PROXMOX_API_USER'), true) }}" + api_tid: "{{ proxmox_api_token_id | default(lookup('env', 'PROXMOX_API_TOKEN_ID'), true) }}" + api_secret: "{{ proxmox_api_token_secret | default(lookup('env', 'PROXMOX_API_TOKEN_SECRET'), true) }}" + no_log: true + + - name: Interroger l'inventaire du cluster + ansible.builtin.uri: + url: "https://{{ api_host }}:{{ api_port }}/api2/json/cluster/resources?type=vm" + method: GET + headers: + Authorization: "PVEAPIToken={{ api_user }}!{{ api_tid }}={{ api_secret }}" + validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" + register: cluster_vms + check_mode: false + no_log: true + + - name: Établir la carte VMID -> nœud pour nos cibles présentes + ansible.builtin.set_fact: + vm_a_supprimer: >- + {{ cluster_vms.json.data + | selectattr('vmid', 'in', cibles_vmid | map('int') | list) + | list }} + + - name: "SÉCURITÉ — le modèle ({{ proxmox_clone_vmid_modele }}) n'est PAS ciblé" + ansible.builtin.assert: + that: "(proxmox_clone_vmid_modele | int) not in (cibles_vmid | map('int') | list)" + fail_msg: "REFUS : le VMID du modèle est dans la liste à supprimer." + + - name: Récapitulatif avant suppression + ansible.builtin.debug: + msg: >- + Suppression de {{ vm_a_supprimer | length }} VM : + {{ vm_a_supprimer | map(attribute='name') | list }} + (VMID {{ vm_a_supprimer | map(attribute='vmid') | list }}) + + - name: Arrêter chaque VM (force) + community.general.proxmox_kvm: + api_host: "{{ api_host }}" + api_port: "{{ api_port | int }}" + api_user: "{{ api_user }}" + api_token_id: "{{ api_tid }}" + api_token_secret: "{{ api_secret }}" + validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" + node: "{{ item.node }}" + vmid: "{{ item.vmid | int }}" + state: stopped + force: true + timeout: 120 + loop: "{{ vm_a_supprimer }}" + loop_control: + label: "{{ item.name }} ({{ item.vmid }})" + no_log: true + failed_when: false + + - name: Supprimer chaque VM + community.general.proxmox_kvm: + api_host: "{{ api_host }}" + api_port: "{{ api_port | int }}" + api_user: "{{ api_user }}" + api_token_id: "{{ api_tid }}" + api_token_secret: "{{ api_secret }}" + validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" + node: "{{ item.node }}" + vmid: "{{ item.vmid | int }}" + state: absent + timeout: 120 + loop: "{{ vm_a_supprimer }}" + loop_control: + label: "{{ item.name }} ({{ item.vmid }})" + no_log: true diff --git a/roles/client_pki/tasks/main.yml b/roles/client_pki/tasks/main.yml index 5373efb..9a6708e 100644 --- a/roles/client_pki/tasks/main.yml +++ b/roles/client_pki/tasks/main.yml @@ -1,12 +1,27 @@ --- -- name: Exiger les secrets PKI (Vault) +# L'empreinte du root CA est la SOURCE DE VÉRITÉ de l'autorité elle-même : on la +# dérive à chaud (robuste au from-zero — une AC régénérée a une empreinte neuve). +# `client_pki_ca_fingerprint_override` permet d'épingler explicitement si besoin. +- name: Dériver l'empreinte du root CA depuis l'autorité + ansible.builtin.command: + cmd: "step certificate fingerprint {{ serveur_step_ca_steppath | default('/etc/step-ca') }}/certs/root_ca.crt" + delegate_to: "{{ groups['serveur_step_ca'][0] }}" + changed_when: false + check_mode: false + register: client_pki_fingerprint_ac + +- name: Retenir l'empreinte (dérivée de l'AC, sauf override explicite) + ansible.builtin.set_fact: + client_pki_ca_fingerprint: "{{ client_pki_ca_fingerprint_override | default(client_pki_fingerprint_ac.stdout | trim, true) }}" + +- name: Exiger l'empreinte AC et le mot de passe provisioner (Vault) ansible.builtin.assert: that: - client_pki_ca_fingerprint | length > 0 - client_pki_provisioner_password | length > 0 fail_msg: >- - client_pki_ca_fingerprint et client_pki_provisioner_password - sont requis (a fournir via Ansible Vault). + Empreinte du root CA indisponible (step-ca joignable et déployé ?) ; + client_pki_provisioner_password requis (via Ansible Vault). - name: Assurer le repertoire des trousseaux apt ansible.builtin.file: diff --git a/roles/serveur_keycloak/tasks/rbac-oidc.yml b/roles/serveur_keycloak/tasks/rbac-oidc.yml index 37bd2f0..8437cad 100644 --- a/roles/serveur_keycloak/tasks/rbac-oidc.yml +++ b/roles/serveur_keycloak/tasks/rbac-oidc.yml @@ -71,6 +71,14 @@ KC={{ serveur_keycloak_home }}/bin/kcadm.sh "$KC" config credentials --server http://localhost:8080 --realm master \ --user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null + # L'utilisateur doit exister dans le realm (Keycloak fédère LDAP à la demande). + # from-zero : l'annuaire peut être vide -> on n'échoue pas ; l'assignation + # s'appliquera quand l'utilisateur existera (synchro / 1re connexion). + if ! "$KC" get users -r {{ serveur_keycloak_realm }} -q username={{ item.user }} --fields username 2>/dev/null \ + | grep -q '"{{ item.user }}"'; then + echo SETOPS_SKIP + exit 0 + fi if "$KC" get-roles -r {{ serveur_keycloak_realm }} --uusername {{ item.user }} 2>/dev/null \ | grep -q '"{{ item.role }}"'; then echo SETOPS_OK diff --git a/roles/serveur_prometheus/templates/prometheus.yml.j2 b/roles/serveur_prometheus/templates/prometheus.yml.j2 index 264c88a..e1fedcc 100644 --- a/roles/serveur_prometheus/templates/prometheus.yml.j2 +++ b/roles/serveur_prometheus/templates/prometheus.yml.j2 @@ -14,7 +14,7 @@ scrape_configs: ca_file: {{ serveur_prometheus_metriques_ca }} {% endif %} static_configs: - - targets: {{ groups.get(serveur_prometheus_groupe_metriques, []) + - targets: {{ (groups.get(serveur_prometheus_groupe_metriques, []) | intersect(groups.get('hotes_actifs', []))) | map('extract', hostvars) | selectattr('ansible_host', 'defined') | map(attribute='ansible_host') diff --git a/scripts/resoudre_flux.py b/scripts/resoudre_flux.py index 4173d05..bdb7aa5 100644 --- a/scripts/resoudre_flux.py +++ b/scripts/resoudre_flux.py @@ -232,7 +232,10 @@ def _rendre_nft(hote: str, regles: list[str]) -> str: f"#!/usr/sbin/nft -f\n" f"# GENERE par scripts/resoudre_flux.py depuis les meta/flux.yml — hote {hote}.\n" f"# APERCU inspectable — NON active. L'activation reste un geste dedie, teste par noeud.\n\n" - f"flush ruleset\n\n" + f"# Remplace UNIQUEMENT la table setops (pas de `flush ruleset` : préserve les\n" + f"# tables de Docker — DNAT/forward des conteneurs, ex. Collabora).\n" + f"table inet setops_flux {{}}\n" + f"delete table inet setops_flux\n\n" f"table inet setops_flux {{\n" f" chain input {{\n" f" type filter hook input priority 0; policy drop;\n\n" @@ -243,7 +246,12 @@ def _rendre_nft(hote: str, regles: list[str]) -> str: f" ip6 nexthdr icmpv6 accept\n\n" f"{corps}\n" f" }}\n\n" - f" chain forward {{ type filter hook forward priority 0; policy drop; }}\n" + f" chain forward {{\n" + f" type filter hook forward priority 0; policy drop;\n" + f" ct state established,related accept\n" + f" iifname \"docker0\" accept\n" + f" oifname \"docker0\" accept\n" + f" }}\n" f" chain output {{ type filter hook output priority 0; policy accept; }}\n" f"}}\n" )