From 91d3a736e7e33178cc051361a954538e90f0530e Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 7 Aug 2026 21:10:47 -0400 Subject: [PATCH] rotation : les comptes de secours peuvent enfin changer de mot de passe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le runbook promettait cette rotation ; le code ne savait pas la faire. Les trois roles ne posaient le mot de passe qu'a la CREATION — regenerer la voute aurait produit le mensonge silencieux corrige toute la journee. Chaque role sait desormais CHANGER un mot de passe existant, idempotent par empreinte du secret applique. Pour Nextcloud le secret passe par l'environnement, pas par la ligne de commande ou il serait visible dans la table des processus. `grafana-cli` ecrivait dans une base FANTOME en annoncant « changed successfully » a chaque fois : il prend `paths.data` a `/data`, le paquet Debian range la base dans /var/lib/grafana. Demasque par le champ `updated` du compte, reste a l'heure du deploiement initial malgre quatre reinitialisations « reussies ». Un message de succes n'est pas une preuve ; l'etat l'est. Verifie par authentification reelle : Grafana 200, Nextcloud 200. Forgejo ferme l'API par doctrine (D-41) — seule preuve disponible : le retour de la commande. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 39 +++++++++++++++ roles/serveur_forgejo/defaults/main.yml | 3 ++ roles/serveur_forgejo/tasks/main.yml | 44 +++++++++++++++++ roles/serveur_grafana/defaults/main.yml | 8 ++++ roles/serveur_grafana/tasks/main.yml | 50 ++++++++++++++++++++ roles/serveur_nextcloud/defaults/main.yml | 3 ++ roles/serveur_nextcloud/tasks/configurer.yml | 40 ++++++++++++++++ 7 files changed, 187 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7017c7..0883a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### La rotation des comptes de secours devient possible — elle ne l'était pas + +Le runbook §6.6 promettait de régénérer les comptes de secours ; **le code ne savait pas le +faire**. Les trois rôles ne posaient le mot de passe qu'à la *création* : + +``` +grafana GF_SECURITY_ADMIN_PASSWORD n'agit qu'à la création du compte +forgejo admin user create `creates: .admin-created` — une seule fois +nextcloud maintenance:install seulement à l'installation +``` + +Régénérer la voûte sans cela aurait produit exactement le mensonge silencieux corrigé toute +la journée : la voûte dit une chose, le service en a une autre. + +Chaque rôle sait désormais **changer** un mot de passe existant, avec une idempotence par +**empreinte du secret appliqué** — on ne peut pas relire un hachage, donc on mémorise ce qu'on +a posé. Pour Nextcloud, le secret passe par l'environnement (`--password-from-env`) et non par +la ligne de commande, où il serait visible dans la table des processus. + +### `grafana-cli` écrivait dans une base fantôme + +Et disait « Admin password changed successfully ✔ » à chaque fois. + +La CLI prend `paths.data` par défaut à `/data` ; le paquet Debian range la base dans +`/var/lib/grafana`. Elle créait donc `/usr/share/grafana/data/grafana.db`, y écrivait, et +annonçait le succès — pendant que le serveur lisait l'autre fichier. + +**Ce qui l'a démasqué** : le champ `updated` du compte, resté à l'heure du déploiement initial +malgré quatre réinitialisations « réussies ». Un message de succès n'est pas une preuve ; +l'état l'est. `--configOverrides=cfg:default.paths.data=…` est désormais imposé. + +**Vérifié par authentification réelle** : Grafana `200`, Nextcloud `200`. Pour Forgejo, +`ENABLE_BASIC_AUTHENTICATION = false` ferme l'API par doctrine (D-41) — la seule preuve +disponible est le retour de la commande, qui confirme le changement. + +Quatre secrets régénérés : `vault_grafana_admin`, `vault_forgejo_admin`, +`vault_nextcloud_admin`, `vault_sysadmin_amorcage`. + + ### Icinga Web 2 cesse de nommer des personnes Le dernier `porte_par: liste-uid` du catalogue. `serveur_icingaweb2_admins` valait un `uid` diff --git a/roles/serveur_forgejo/defaults/main.yml b/roles/serveur_forgejo/defaults/main.yml index 15a23af..87ad0fe 100644 --- a/roles/serveur_forgejo/defaults/main.yml +++ b/roles/serveur_forgejo/defaults/main.yml @@ -82,3 +82,6 @@ serveur_forgejo_db_sslrootcert: "/etc/step/certs/root_ca.crt" # LDAP (group-ldap-mapper). Voir docs/autorisation.md. serveur_forgejo_oidc_groupe_claim: "groups" serveur_forgejo_oidc_groupe_admin: "sysadmin" + +# Empreinte du mot de passe admin APPLIQUE (rotation). Voir tasks/main.yml. +serveur_forgejo_marqueur_admin: "/etc/forgejo/.setops-admin-sha256" diff --git a/roles/serveur_forgejo/tasks/main.yml b/roles/serveur_forgejo/tasks/main.yml index 4c9be3b..5b51227 100644 --- a/roles/serveur_forgejo/tasks/main.yml +++ b/roles/serveur_forgejo/tasks/main.yml @@ -196,3 +196,47 @@ when: - serveur_forgejo_oidc_actif | bool - not ansible_check_mode + +# --- ROTATION DU COMPTE DE SECOURS ------------------------------------------- +# `admin user create` porte `creates: .admin-created` : il ne tourne qu'UNE fois. +# Changer la voute ne changeait donc rien, et la voute se mettait a mentir. Meme +# raisonnement que pour Grafana — idempotence par EMPREINTE du secret applique. +- name: Empreinte du mot de passe admin en place + ansible.builtin.slurp: + src: "{{ serveur_forgejo_marqueur_admin }}" + register: serveur_forgejo_marque + failed_when: false + changed_when: false + no_log: true + +- name: Appliquer le mot de passe du compte de secours (rotation) + ansible.builtin.command: + argv: + - "{{ serveur_forgejo_binaire }}" + - "--config" + - "{{ serveur_forgejo_config }}" + - admin + - user + - change-password + - "--username" + - "{{ serveur_forgejo_admin_user }}" + - "--password" + - "{{ serveur_forgejo_admin_password }}" + environment: + GITEA_WORK_DIR: "{{ serveur_forgejo_data }}" + become: true + become_user: "{{ serveur_forgejo_utilisateur }}" + changed_when: true + no_log: true + when: >- + (serveur_forgejo_marque.content | default('') | b64decode | trim) + != (serveur_forgejo_admin_password | hash('sha256')) + +- name: Retenir l'empreinte appliquee + ansible.builtin.copy: + content: "{{ serveur_forgejo_admin_password | hash('sha256') }}\n" + dest: "{{ serveur_forgejo_marqueur_admin }}" + owner: root + group: root + mode: "0600" + no_log: true diff --git a/roles/serveur_grafana/defaults/main.yml b/roles/serveur_grafana/defaults/main.yml index 4695fd7..c28c089 100644 --- a/roles/serveur_grafana/defaults/main.yml +++ b/roles/serveur_grafana/defaults/main.yml @@ -50,3 +50,11 @@ serveur_grafana_oidc_role_path: "contains(roles[*], 'grafana-admin') && 'Admin' serveur_grafana_depot_cle_url: "https://apt.grafana.com/gpg-full.key" serveur_grafana_depot_cle_fichier: "/etc/apt/keyrings/grafana.asc" serveur_grafana_depot_source: "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" + +# Empreinte du mot de passe admin APPLIQUE — permet la rotation sans lire le secret +# en place (impossible : il est hache). Voir tasks/main.yml. +serveur_grafana_marqueur_admin: "/etc/grafana/.setops-admin-sha256" + +# Chemins du paquet Debian — la CLI ne les devine pas (voir tasks/main.yml). +serveur_grafana_homepath: "/usr/share/grafana" +serveur_grafana_donnees: "/var/lib/grafana" diff --git a/roles/serveur_grafana/tasks/main.yml b/roles/serveur_grafana/tasks/main.yml index eabe9ad..2d34853 100644 --- a/roles/serveur_grafana/tasks/main.yml +++ b/roles/serveur_grafana/tasks/main.yml @@ -102,3 +102,53 @@ enabled: true state: started daemon_reload: true + +# --- ROTATION DU COMPTE DE SECOURS ------------------------------------------- +# `GF_SECURITY_ADMIN_PASSWORD` n'agit qu'a la CREATION du compte : changer la voute +# ne changeait rien, et la voute se mettait a mentir en silence. Le runbook de +# reprise (docs/autorisation.md §6) promet pourtant cette rotation — c'est ce qui +# transforme une livraison en transfert. Constate le 2026-08-07. +# +# Idempotence par EMPREINTE : on garde le sha256 du secret applique. Tant qu'il ne +# change pas, on ne touche a rien ; des qu'il change, on reapplique. On ne peut pas +# lire le mot de passe en place, donc on memorise ce qu'on a pose. +- name: Empreinte du mot de passe admin en place + ansible.builtin.slurp: + src: "{{ serveur_grafana_marqueur_admin }}" + register: serveur_grafana_marque + failed_when: false + changed_when: false + no_log: true + +- name: Appliquer le mot de passe du compte de secours (rotation) + ansible.builtin.command: + argv: + - grafana-cli + - "--homepath={{ serveur_grafana_homepath }}" + # `paths.data` DOIT etre impose : la CLI le prend par defaut a + # `/data`, alors que le paquet Debian range la base dans + # /var/lib/grafana. Sans lui, la CLI cree une base FANTOME, y ecrit, et + # annonce « Admin password changed successfully » — pendant que le serveur + # lit l'autre. Constate le 2026-08-07 : le compte n'avait pas bouge depuis + # le deploiement initial, malgre quatre reinitialisations « reussies ». + - "--configOverrides=cfg:default.paths.data={{ serveur_grafana_donnees }}" + - admin + - reset-admin-password + - "{{ serveur_grafana_admin_password }}" + no_log: true + # La tache ne s'execute QUE si l'empreinte differe : quand elle tourne, elle + # change reellement le mot de passe. + changed_when: true + when: >- + (serveur_grafana_marque.content | default('') | b64decode | trim) + != (serveur_grafana_admin_password | hash('sha256')) + notify: Redemarrer grafana + +- name: Retenir l'empreinte appliquee + ansible.builtin.copy: + content: "{{ serveur_grafana_admin_password | hash('sha256') }}\n" + dest: "{{ serveur_grafana_marqueur_admin }}" + owner: root + group: root + mode: "0600" + no_log: true diff --git a/roles/serveur_nextcloud/defaults/main.yml b/roles/serveur_nextcloud/defaults/main.yml index 9d9f6e9..0ee18a8 100644 --- a/roles/serveur_nextcloud/defaults/main.yml +++ b/roles/serveur_nextcloud/defaults/main.yml @@ -112,3 +112,6 @@ serveur_nextcloud_bruteforce_whitelist: "{{ (sous_reseau_admin | default('')) }} # verse les membres du groupe d'habilitation. serveur_nextcloud_oidc_groupe_claim: "groups" serveur_nextcloud_oidc_groupe_admin: "sysadmin" + +# Empreinte du mot de passe admin APPLIQUE (rotation). Voir tasks/configurer.yml. +serveur_nextcloud_marqueur_admin: "/etc/setops-nextcloud-admin-sha256" diff --git a/roles/serveur_nextcloud/tasks/configurer.yml b/roles/serveur_nextcloud/tasks/configurer.yml index 951bfec..aa346a5 100644 --- a/roles/serveur_nextcloud/tasks/configurer.yml +++ b/roles/serveur_nextcloud/tasks/configurer.yml @@ -77,3 +77,43 @@ when: - serveur_nextcloud_bruteforce_whitelist | length > 0 - "'/' in serveur_nextcloud_bruteforce_whitelist" + +# --- ROTATION DU COMPTE DE SECOURS ------------------------------------------- +# `maintenance:install --admin-pass` ne tourne qu'a l'INSTALLATION. Changer la +# voute ne changeait rien. `user:resetpassword` lit le secret dans l'environnement +# (`--password-from-env`) : il ne passe donc pas par la ligne de commande, ou il +# serait visible dans la table des processus. +- name: Empreinte du mot de passe admin en place + ansible.builtin.slurp: + src: "{{ serveur_nextcloud_marqueur_admin }}" + register: serveur_nextcloud_marque + failed_when: false + changed_when: false + no_log: true + +- name: Appliquer le mot de passe du compte de secours (rotation) + ansible.builtin.command: + argv: + - php + - "{{ serveur_nextcloud_racine }}/occ" + - "user:resetpassword" + - "--password-from-env" + - "{{ serveur_nextcloud_admin_user }}" + environment: + OC_PASS: "{{ serveur_nextcloud_admin_password }}" + become: true + become_user: "{{ serveur_nextcloud_utilisateur | default('www-data') }}" + changed_when: true + no_log: true + when: >- + (serveur_nextcloud_marque.content | default('') | b64decode | trim) + != (serveur_nextcloud_admin_password | hash('sha256')) + +- name: Retenir l'empreinte appliquee + ansible.builtin.copy: + content: "{{ serveur_nextcloud_admin_password | hash('sha256') }}\n" + dest: "{{ serveur_nextcloud_marqueur_admin }}" + owner: root + group: root + mode: "0600" + no_log: true