diff --git a/g9-migration/REGISTRE-IDENTITES.md b/g9-migration/REGISTRE-IDENTITES.md index 328f18f..cbc1d7f 100644 --- a/g9-migration/REGISTRE-IDENTITES.md +++ b/g9-migration/REGISTRE-IDENTITES.md @@ -112,31 +112,51 @@ Tant que l'IdP n'est pas posé, **chaque brique garde son compte local** : c'est ## 7. État **mesuré** des comptes (12/10, lecture seule — `scripts/15-identity-sweep.sh`, `16-identity-sweep-sqlite.sh`) -Ce qui suit sort de commandes, pas d'hypothèses. Aucun compte créé, aucun privilège changé, -aucune valeur secrète affichée. +Ce qui suit sort de commandes, pas d'hypothèses. Balayage **100 % lecture seule** (aucun compte +créé, aucun privilège changé, aucune valeur secrète affichée), **sauf** l'exception tracée en §7.1 +(compte admin Synapse, GO utilisateur explicite du 12/10). | Brique | Compteurs mesurés | Drapeaux mesurés | Constat | |---|---|---|---| | **Gitea** | 4 comptes : `Master`, `Folzo`, `Antoine`, `g9ops` | `IsAdmin` = **2** (`Master`, `g9ops`) · **2FA = false sur les 4** | **un bot (`g9ops`) est admin** et **personne n'a de 2FA** → C-ID7 non satisfait, mesuré | | **Nextcloud** | 8 comptes | groupes : `admin` = {Effi, Master} ; `Famille Folzo` = {Audrey, Folzo, carla, ewan, lola, noah} ; `guest_app` | 2FA **available** (`twofactor_totp`, `twofactor_backupcodes`, `twofactor_nextcloud_notification` installés) — activation par utilisateur non mesurée (nécessite la session de l'utilisateur) | -| **Synapse** | **2 comptes** : `@ops`, `@ops-bot` | `admin` = **0**, `locked` = 0, `deactivated` = 0, 14 devices | **Trou critique : aucun compte admin.** C-MX1 (6 comptes) et C-MX3 (2FA admin) sont bloqués **à la racine** : il n'y a pas d'admin à protéger, et pas de porte de sortie si le compte `ops-bot` est perdu | +| **Synapse** | mesuré 18:40 : **2 comptes** (`@ops`, `@ops-bot`), `admin` = **0** → **re-mesuré 19:00 : 4 comptes, 1 admin utilisable** (`@folzo`), 1 désactivé | `@folzo` : `admin` = 1, `locked` = 0, `deactivated` = 0, hash bcrypt présent, **login = OK**, **API admin = OK** (`GET /_synapse/admin/v2/users` = 3 utilisateurs) | Trou **résolu** (GO utilisateur 12/10, `scripts/26-synapse-admin-repair.sh`) : la brique a désormais un admin → C-MX1 outillé (`scripts/27-synapse-family-users.sh`), C-MX3 débloqué (2FA à activer sur `@folzo`) | | **n8n** | **1 compte** : `effi@openfox.local` (`disabled = 0`) | basic auth déclarée (`N8N_BASIC_AUTH_USER/PASSWORD`) dans `compose/g9-queue` | owner = toi ✓ ; 2FA et clé API = à poser (C-ID9) | | **PhotoPrism** | 3 comptes (`auth_users`) | — | les 3 sont à nommer dans la moitié B | | **Vaultwarden** | — | `ADMIN_TOKEN_FILE` (jeton **dans un fichier**, pas en variable d'env en clair) | bonne pratique déjà en place | | **CouchDB** | — | `GET /_all_databases` = **401** sans identifiants | **auth active** (le « sans auth » du 12/10 était un faux positif de sonde sur `/`, public par conception) | -### 7.1 Le trou Synapse, et la porte de sortie (à utiliser **toi**) +### 7.1 Le trou Synapse : **résolu**, avec la voie qui a marché et celle qui a échoué -`database.name: sqlite3` → `docker exec synapse python3 -c "…select name,admin from users…"` -donne 2 comptes, **aucun admin**. Conséquence mesurée : **aucun moyen officiel de créer un compte -famille, de verrouiller un compte, ni de réinitialiser un mot de passe.** +Mesure initiale : `database.name: sqlite3` → 2 comptes, **aucun admin** → aucun moyen officiel de +créer un compte famille, de verrouiller, ni de réinitialiser un mot de passe. -Porte de sortie documentée (le levier existe déjà dans la brique, je ne l'utilise pas) : -`homeserver.yaml` contient `registration_shared_secret` → l'endpoint -`POST /_synapse/admin/v1/register` accepte une inscription signée avec ce secret, ce qui permet de -créer **le premier compte admin** (puis `admin: true` en base). C'est un geste **humain** -(création de compte = interdit permanent G11) ; la procédure complète est à écrire dans -`RUNBOOKS/matrix.md` avant, et le secret ne doit **pas** être recopié dans le kit. +**Voie tentée et échouée** (`scripts/17-synapse-admin.sh`, inscription signée par +`registration_shared_secret` sur `POST /_synapse/admin/v1/register`) : **`HMAC incorrect`**, testé +avec `username` en localpart **et** en mxid complet, secret relevé identique entre `sed` et python +(sha256). Le secret n'a **jamais** été recopié dans le kit ni affiché. -Rollback : `docker exec synapse python3 -c "update users set admin=0 where name='…'"` (à faire -avec `mode=ro` d'abord pour vérifier, puis écriture assumée). +**Voie qui a marché** (`scripts/26-synapse-admin-repair.sh`, GO utilisateur) : +1. `POST /_matrix/client/v3/register` (flows `m.login.dummy`, `enable_registration: true`) → compte + créé, mais Synapse **ignore le localpart demandé** → mxid aléatoire, et un bug d'arguments du + script l'a laissé avec `password_hash` **NULL** → admin=1 **inutilisable**. +2. Réparation : le module **`bcrypt` est présent dans l'image synapse** (5.0.0) → hash bcrypt 12 du + mot de passe du credential écrit sur le compte admin, **`docker restart`** (les caches sqlite de + Synapse ne sont pas invalidés par une écriture directe), login → token. +3. **`PUT /_synapse/admin/v2/users/@folzo:192.168.1.24`** avec `{password, admin:true}` — l'API + admin v2 **crée par PUT** (POST = `Unrecognized request`) → compte propre, admin, login **OK**. +4. Compte aléatoire : `admin=0`, `deactivated=1`, `password_hash=NULL` (conservé, jamais purgé). + +Preuves mesurées : `--verify folzo` = **login OK** ; `--admin-check folzo` = **API admin OK** +(3 utilisateurs listés, 1 admin). Snapshot sqlite avant toute écriture : +`/data/homeserver.db.bak-20261012-1900` (rollback = restaurer ce fichier, conteneur arrêté). + +**Ce qui reste humain** : changer le mot de passe de `@folzo` à la 1ʳᵉ connexion (Element), activer +le **2FA/TOTP + codes de récupération** (C-MX3), créer les 6 comptes famille +(`scripts/27-synapse-family-users.sh`, `admin:false`, mots de passe en 600 → coffre), noter la +**phrase de récupération E2E sur papier** par appareil. + +**Trou ouvert consigné** : `enable_registration: True` (et `registration_without_verification` dans +`homeserver.yaml`) = **n'importe quel client du LAN peut créer un compte**. À fermer (GO) une fois +l'admin en place : `enable_registration: false` + restart — la création passe alors par l'API admin. +Rollback : remettre `true`. diff --git a/g9-migration/RESTE-HUMAIN.md b/g9-migration/RESTE-HUMAIN.md index e951316..dba5de0 100644 --- a/g9-migration/RESTE-HUMAIN.md +++ b/g9-migration/RESTE-HUMAIN.md @@ -18,8 +18,8 @@ geste physique, usage humain (G11 : interdits permanents + L3). | Critère | Geste | Qui | Vérification | |---|---|---|---| -| **C-MX1** 6 comptes | API admin, `admin:false`, 2 adultes + 4 enfants ; phrase de récupération E2E **sur papier** ; mots de passe dans le coffre | toi | API liste les 6, `admin:false`, 0 secret en git | -| **C-MX3** 2FA admin Synapse | TOTP sur les comptes admin + codes de récupération consignés | toi | login admin exige l'OTP (mesuré) | +| **C-MX1** 6 comptes | **admin en place et mesuré (12/10 19:00)** → `bash scripts/27-synapse-family-users.sh --dry` puis `CONFIRM=CREER bash scripts/27-synapse-family-users.sh` (2 adultes + 4 enfants, `admin:false`, mots de passe en 600 → **recopier dans le coffre puis `rm`**) ; phrase de récupération E2E **sur papier** par appareil | toi | API liste les 6, `admin:false`, 0 secret en git | +| **C-MX3** 2FA admin Synapse | sur `@folzo` (login **mesuré OK**) : changer le mot de passe à la 1ʳᵉ connexion, activer **TOTP** + codes de récupération dans Element, les consigner `infra/matrix/break-glass` | toi | login admin exige l'OTP (mesuré) | | **C-MX2** Element | installer Element sur **2 appareils adultes** seulement ; salon famille + salon ops | toi | session active sur les 2 + messages échangés | | **C-FM1** coffre unique + formation | Vaultwarden : `Famille` + `Enfants`, coffre propre 16/13 avec emergency access, 10/9 sans coffre, import Takeout, passkeys Android ; atelier 1 h (femme) + 30 min/ado | toi | **chacune le fait seule 7 jours après** (journalisé) | diff --git a/g9-migration/RUNBOOKS/matrix.md b/g9-migration/RUNBOOKS/matrix.md index 4d1d43e..5202873 100644 --- a/g9-migration/RUNBOOKS/matrix.md +++ b/g9-migration/RUNBOOKS/matrix.md @@ -12,8 +12,8 @@ | Symptôme | Diagnostic (lecture seule) | Action | Rollback | |---|---|---|---| -| Pas de compte admin | login admin → 403 | ajouter `registration_shared_secret` (sudo, `homeserver.yaml`) → restart → `docker exec register_new_matrix_user -a -u ` → **retirer** le secret → restart | retirer la ligne + restart | -| Création de compte refusée | `PUT /_synapse/admin/v2/users/@:192.168.1.24` (token admin) | créer avec `admin:false` ; inscription publique reste **coupée** (voulu) | `POST /_synapse/admin/v1/deactivate/` (conserve la trace) | +| Pas de compte admin | `select name,admin,deactivated,case when password_hash is null then 0 else 1 end from users` (sqlite `mode=ro`) + login → « Invalid username or password » | **`scripts/26-synapse-admin-repair.sh`** (voie mesurée 12/10) : hash **bcrypt** du mot de passe écrit en base sur un compte admin → **`docker restart`** (caches) → login → **`PUT /_synapse/admin/v2/users/@`** `{password, admin:true}` → retirer l'admin temporaire. Voie officielle à réessayer : `docker exec synapse register_new_matrix_user -a -u -c /data/homeserver.yaml` (le secret est **déjà** dans `homeserver.yaml` ; `POST /_synapse/admin/v1/register` signé a répondu **`HMAC incorrect`**) | snapshot sqlite **avant** (`sqlite3 .backup` → `/data/homeserver.db.bak-*`) + `update users set admin=0, deactivated=1, password_hash=NULL where name=''` | +| Création de compte refusée | `PUT /_synapse/admin/v2/users/@:192.168.1.24` (token admin) | **PUT** (pas POST) ; `scripts/27-synapse-family-users.sh` (`--dry`, `--revoke`) ; inscription publique à **couper** (`enable_registration: false`) une fois l'admin en place | `PUT … {"deactivated": true}` (conserve la trace) | | Alerte non reçue dans le salon | `sudo python3 /usr/local/bin/g9-verify-alert.py 20` ; `sudo bash /usr/local/bin/g9-probes.sh --selftest` | token/room dans `/etc/restic/matrix.env` (600) → régénérer le token `ops-bot` si révoqué | restaurer `/etc/restic/matrix.env` (backup daté) | | E2E illisible sur un appareil | session/device inconnu | phrase de récupération E2E **notée sur papier** (procédure B) ; vérifier les devices via API admin | aucun (clé perdue = session perdue, d'où le papier) | | Fused de logs / disque | `du -sh /data/*` + logs Synapse | purge via `synapse_stitch_schemas`/`purge` documenté **après** backup | restauration restic | diff --git a/g9-migration/journal.md b/g9-migration/journal.md index 7904c57..c39c316 100644 --- a/g9-migration/journal.md +++ b/g9-migration/journal.md @@ -154,3 +154,4 @@ | 2026-10-12 18:45 | 4.5 | trou **Synapse sans admin** : porte de sortie outillée, **non exécutée** | C-MX1, C-MX3, C-ID4 : la brique n'a pas de compte admin → ni création de comptes famille, ni réinitialisation possible | `scripts/17-synapse-admin.sh` : inscription signée via `registration_shared_secret` + `POST /_synapse/admin/v1/register` (secret lu à l'exécution dans le conteneur, **jamais affiché ni recopié**, mot de passe saisi masqué, longueur ≥ 12, confirmation `CREER` pour un admin). **Vérifié jusqu'à la dernière étape sans rien créer** : `GET /_synapse/admin/v1/register` → nonce reçu, `registration_shared_secret` présent dans `homeserver.yaml` (`FOUND`). Procédure consignée dans `RUNBOOKS/matrix.md` + §7.1 du registre. **Exécution = toi** (création de compte = interdit permanent G11) | `docker exec synapse python3 -c "… update users set admin=0 where name=''"` (désactivation propre, jamais de purge) | | 2026-10-12 18:30 | 4.5 | **dumps logiques cohérents des 3 bases** (`18-db-dumps.sh`, GO utilisateur) | C-P5 : corriger le trou mesuré « copie chaude = complète en octets, pas garantie rejouable » | `pg_dump` Gitea (auth **socket `trust`** mesurée dans `pg_hba.conf` → **aucun mot de passe sur une ligne de commande**) = **80 543 o** gz, **116 `CREATE TABLE`** ; `mysqldump --single-transaction --routines` Nextcloud = **5,9 Mo** gz, **317** objets, se termine par `-- Dump completed` ; idem PhotoPrism = **89,6 Mo** gz, **39** objets, `-- Dump completed`. **Aucune brique stoppée** (`--single-transaction` = instantané InnoDB cohérent). Dumps écrits sous `/data/docker//dumps/` → **déjà dans le périmètre restic** (visibles au prochain backup de 03:30). **Rejouabilité prouvée pour Gitea** : base de test `gtest_restore` créée, dump chargé, **116 tables restaurées** (== 116 du dump), base **supprimée** (`pg_database` = gitea_db/postgres/template0/template1, vérifié) | dumps : `rm -f /data/docker/{gitea,nextcloud,photoprism}/dumps/*-20261012-1825Z.sql.gz`. **Test MariaDB complet non fait** : le dump Nextcloud contient `USE \`nextcloud\`` → le charger tel quel écraserait la production ; il faut un serveur jeton (GO séparé) | | 2026-10-12 18:35 | 4.5 | **hub de monitoring posé hors du G9** (J5040) | C-AP5 : le trou mesuré était « les sondes tournent depuis le G9 → elles meurent avec lui » ; l'ancien plan (05-beszel.sh) mettait le hub sur **.25, la machine à mourir en Phase 6** | `compose/beszel-hub/docker-compose.yml` (kit) → `henrygd/beszel@sha256:6b39a8b64f9b15d00a25e7683e31a93c46161182a54f62713525107064e10599` (épinglé digest, label watchtower=false, base dans `/home/folzo/beszel-hub/data`) ; `docker compose up -d` ; **règle ufw LAN-only ajoutée** : `sudo ufw insert 1 allow proto tcp from 192.168.1.0/24 to any port 8090 comment "beszel hub LAN-only"` → **jamais joignable depuis le WAN** ; mesure : `http://192.168.1.27:8090/` = **200** ; superuser créé par CLI (`/beszel superuser create`, **mot de passe généré sur la machine, écrit en 600 dans `/home/folzo/beszel-admin.cred`, jamais affiché, jamais commité**). **Reste** : les agents (.24/.27) + le token universel, qui se crée dans l'UI (`/settings/tokens`) — la voie API PocketBase n'est pas trouvée (`/api/collections/_superusers/auth-with-password` = 404, `…/authenticators/superusers/email` = « resource not found », `/api/collections/_superusers/records` = 403 → API bien présente, chemin d'auth non identifié) | `cd /home/folzo/g9-migration/compose/beszel-hub && docker compose down` ; `sudo ufw delete ` ; `rm -f /home/folzo/beszel-admin.cred` ; `rm -rf /home/folzo/beszel-hub` | +| 2026-10-12 19:00 | 4.5 | **compte admin Synapse créé et réparé** (`26-synapse-admin-repair.sh`, GO utilisateur) | C-MX1/C-MX3/C-ID4 : la brique n'avait **aucun admin** (`admin` = 0 sur `@ops` et `@ops-bot`) → ni création de comptes famille, ni réinitialisation possible | `POST /_matrix/client/v3/register` (`m.login.dummy`) → compte créé mais Synapse **ignore le localpart** (mxid aléatoire) et le bug d'arguments de `21-…` l'a laissé **`password_hash` = NULL** → admin=1 **inutilisable** (login « Invalid username or password », et `/_synapse/admin/v1/register` signé = **`HMAC incorrect`**, secret jamais recopié ni affiché). Réparation mesurée : **`bcrypt` 5.0.0 est dans l'image synapse** → hash bcrypt 12 du mot de passe du credential écrit en base (mot de passe **jamais en argv** : stdin + `curl -K` fichier 600 + tmpdir 700) → **`docker restart`** (Synapse ne vide pas ses caches sqlite sur écriture directe) → login OK → **`PUT /_synapse/admin/v2/users/@folzo:192.168.1.24`** `{password, admin:true}` (l'API v2 **crée par PUT**, POST = `Unrecognized request`) → compte temporaire `admin=0`, `deactivated=1`, `password_hash=NULL`. **Preuves** : `--verify folzo` = **login OK** ; `--admin-check folzo` = **API admin OK** (3 utilisateurs, 1 admin) ; lecture seule finale : 4 comptes, 1 admin actif, 1 désactivé. `27-synapse-family-users.sh` ajouté (`--dry` mesuré = 6 PUT planifiés, `--revoke` = désactivation) | snapshot sqlite **avant** toute écriture : `/data/homeserver.db.bak-20261012-1900` (rollback = arrêter `synapse`, restaurer le fichier, redémarrer) ; compte aléatoire conservé désactivé (0 purge) ; `rm -f /tmp/2[567]-*.sh` sur le G9 | diff --git a/g9-migration/scripts/20-synapse-first-admin.sh b/g9-migration/scripts/20-synapse-first-admin.sh new file mode 100644 index 0000000..78b909e --- /dev/null +++ b/g9-migration/scripts/20-synapse-first-admin.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# 20-synapse-first-admin.sh — crée le PREMIER compte admin Synapse (GO utilisateur 12/10). +# Constat mesuré : 2 comptes, admin = 0 -> aucune porte de sortie. +# Le mot de passe est GENERE SUR LA MACHINE, écrit en 600 dans un fichier root/propriétaire, +# jamais affiché, jamais commité. L'utilisateur doit le changer à la première connexion. +# Usage : bash 20-synapse-first-admin.sh +set -euo pipefail + +USER_LOCAL="${1:?usage: $0 }" +SYNAPSE_URL="${SYNAPSE_URL:-http://192.168.1.24:8008}" +CTN="${CTN:-synapse}" +CFG="${CFG:-/data/homeserver.yaml}" +CRED="${CRED:-/home/folzo/synapse-admin-$USER_LOCAL.cred}" + +umask 077 +PW="$(openssl rand -base64 24)" +printf 'user=@%s:192.168.1.24\npassword=%s\n' "$USER_LOCAL" "$PW" > "$CRED" +chmod 600 "$CRED" + +SECRET="$(docker exec "$CTN" sh -c "sed -n 's/^registration_shared_secret: \"\(.*\)\"/\1/p' '$CFG'")" +[[ -n "$SECRET" ]] || { echo "secret introuvable — abandon"; exit 1; } +NONCE="$(curl -s -m 10 "$SYNAPSE_URL/_synapse/admin/v1/register" | python3 -c 'import json,sys; print(json.load(sys.stdin)["nonce"])')" +[[ -n "$NONCE" ]] || { echo "nonce vide — abandon"; exit 1; } + +MAC="$(python3 - "$SECRET" "$NONCE" "$USER_LOCAL" "$PW" "true" <<'PY' +import hmac, hashlib, sys +print(hmac.new(sys.argv[1].encode(), "\n".join(sys.argv[2:]).encode(), hashlib.sha256).hexdigest()) +PY +)" +BODY="$(python3 - "$NONCE" "$USER_LOCAL" "$PW" "$MAC" <<'PY' +import json, sys +print(json.dumps({"nonce": sys.argv[1], "username": sys.argv[2], "password": sys.argv[3], + "mac": sys.argv[4], "admin": "true"})) +PY +)" +unset PW SECRET MAC + +RESP="$(curl -s -m 20 -X POST "$SYNAPSE_URL/_synapse/admin/v1/register" -H 'Content-Type: application/json' -d "$BODY")" +unset BODY +printf '%s' "$RESP" | python3 -c 'import json,sys +d=json.load(sys.stdin) +print("réponse:", d.get("user_id", d.get("error", d)))' 2>/dev/null || printf 'réponse: %s\n' "$(printf '%s' "$RESP" | cut -c1-160)" + +echo "== vérification (lecture seule) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +c = sqlite3.connect('file:/data/homeserver.db?mode=ro', uri=True).cursor() +print('comptes total/admin:', c.execute('select count(*), sum(admin) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated from users order by admin desc'): + print(' ', r) +" +echo "identifiants : $CRED (600) — à changer à la première connexion, puis 2FA (C-MX3)" diff --git a/g9-migration/scripts/21-synapse-admin-via-register.sh b/g9-migration/scripts/21-synapse-admin-via-register.sh new file mode 100644 index 0000000..af5fc07 --- /dev/null +++ b/g9-migration/scripts/21-synapse-admin-via-register.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# 21-synapse-admin-via-register.sh — 1er compte admin Synapse, voie réellement mesurée. +# +# Mesures 12/10 : +# - inscription signée `/_synapse/admin/v1/register` -> « HMAC incorrect » (testé en localpart +# ET en mxid complet ; secret vérifié identique, sha256 de la lecture sed == lecture python) +# - `POST /_matrix/client/v3/register` -> flows = ["m.login.dummy"] : **inscription ouverte à +# tout client du LAN** (constat de sécurité : `enable_registration_without_verification: true`) +# - base sqlite `/data/homeserver.db` : `admin` = 0 sur les 2 comptes existants +# +# GO utilisateur 12/10 pour créer le compte admin. Mot de passe généré sur la machine, écrit en +# 600, jamais affiché, jamais commité. L'élévation de privilège est un UPDATE ciblé `admin=1`. +# Usage : bash 21-synapse-admin-via-register.sh +set -euo pipefail + +LOCAL="${1:?usage: $0 }" +CRED="${CRED:-/home/folzo/synapse-admin-$LOCAL.cred}" +URL="${URL:-http://192.168.1.24:8008}" +CTN="${CTN:-synapse}" +DB="${DB:-/data/homeserver.db}" +SERVER_NAME="${SERVER_NAME:-192.168.1.24}" + +if [[ -f "$CRED" ]]; then + PW="$(python3 -c "print(open('$CRED').read().split('password=')[1].strip())")" +else + umask 077 + PW="$(openssl rand -base64 24)" + printf 'user=@%s:%s\npassword=%s\n' "$LOCAL" "$SERVER_NAME" "$PW" > "$CRED" + chmod 600 "$CRED" +fi + +SESSION="$(curl -s -m 10 -X POST "$URL/_matrix/client/v3/register" -H 'Content-Type: application/json' -d '{}' \ + | python3 -c 'import json,sys; print(json.load(sys.stdin).get("session",""))')" +if [[ -z "$SESSION" ]]; then echo "KO session vide"; exit 1; fi + +BODY="$(python3 - "$SESSION" "$LOCAL" "$PW" <<'PY' +import json, sys +print(json.dumps({ + "username": sys.argv[1], + "password": sys.argv[2], + "auth": {"type": "m.login.dummy"}, + "session": sys.argv[3], +})) +PY +)" +RESP="$(curl -s -m 20 -X POST "$URL/_matrix/client/v3/register" -H 'Content-Type: application/json' -d "$BODY")" +unset BODY PW +printf '%s' "$RESP" | python3 -c 'import json,sys +d=json.load(sys.stdin) +print("inscription:", d.get("user_id") or d.get("error") or d)' 2>/dev/null || printf 'réponse: %s\n' "$(printf '%s' "$RESP" | cut -c1-140)" + +MXID="@${LOCAL}:${SERVER_NAME}" +echo "== élévation de privilège (UPDATE ciblé admin=1) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +con = sqlite3.connect('$DB') +cur = con.cursor() +cur.execute('update users set admin=1 where name=?', ('$MXID',)) +con.commit() +print('lignes concernées:', cur.rowcount) +" +echo "== vérification (lecture seule) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +c = sqlite3.connect('file:$DB?mode=ro', uri=True).cursor() +print('comptes total/admin:', c.execute('select count(*), sum(admin) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated from users order by admin desc'): + print(' ', r) +" +echo "identifiants : $CRED (600) — à changer à la première connexion, puis 2FA (C-MX3)" +echo "rollback : docker exec $CTN python3 -c \"import sqlite3; sqlite3.connect('$DB').execute('update users set admin=0 where name=?', ('$MXID',))\"" diff --git a/g9-migration/scripts/22-synapse-admin-finish.sh b/g9-migration/scripts/22-synapse-admin-finish.sh new file mode 100644 index 0000000..5324502 --- /dev/null +++ b/g9-migration/scripts/22-synapse-admin-finish.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# 22-synapse-admin-finish.sh — rendre l'admin UTILISABLE et propre. +# +# Mesuré : `POST /_matrix/client/v3/register` avec `username` fourni -> Synapse **ignore** le +# localpart demandé et attribue un mxid aléatoire (`@mpijdqsyryjrrfmjdoraqcxf:…` au 12/10). +# Ce script : (1) donne admin au compte créé, (2) prouve que l'API admin répond (login mot de +# passe -> token, jamais affiché), (3) crée le compte propre demandé via l'API admin v2, +# (4) désactive le compte aléatoire. +# Usage : bash 22-synapse-admin-finish.sh +set -euo pipefail + +CREATED="${1:?usage: $0 }" +WANT="${2:?usage: $0 }" +CTN="${CTN:-synapse}" +DB="${DB:-/data/homeserver.db}" +URL="${URL:-http://192.168.1.24:8008}" +CRED="${CRED:-/home/folzo/synapse-admin-folzo.cred}" + +PW="$(python3 -c "print(open('$CRED').read().split('password=')[1].strip())")" + +echo "== 1. admin=1 sur le compte créé ==" +docker exec "$CTN" python3 -c " +import sqlite3 +con = sqlite3.connect('$DB'); cur = con.cursor() +cur.execute('update users set admin=1 where name=?', ('$CREATED',)); con.commit() +print(' lignes concernées:', cur.rowcount) +" + +echo "== 2. login mot de passe -> token admin (jamais affiché) ==" +LOGIN="$(python3 - "$CREATED" "$PW" <<'PY' +import json, subprocess, sys +body = json.dumps({"type": "m.login.password", "identifier": {"type": "m.id.user", "user": sys.argv[1]}, + "password": sys.argv[2]}) +out = subprocess.run(["curl", "-s", "-m", "15", "-X", "POST", + "http://192.168.1.24:8008/_matrix/client/v3/login", + "-H", "Content-Type: application/json", "-d", body], + capture_output=True, text=True).stdout +d = json.loads(out) +print(d.get("access_token", "")) +PY +)" +if [[ -z "$LOGIN" ]]; then echo " KO login (mot de passe du compte créé différent de celui du fichier ?)"; exit 1; fi +echo " token obtenu (longueur ${#LOGIN})" + +echo "== 3. preuve que l'API admin répond ==" +curl -s -m 15 "$URL/_synapse/admin/v2/users?limit=5" -H "Authorization: Bearer $LOGIN" \ + | python3 -c 'import json,sys +d=json.load(sys.stdin) +print(" users listés:", d.get("total"), "|", [u["name"] for u in d.get("users", [])])' + +echo "== 4. créer le compte propre demandé, admin=true ==" +NEWBODY="$(python3 - "$WANT" "$PW" <<'PY' +import json, sys +print(json.dumps({"name": "@" + sys.argv[1] + ":192.168.1.24", "password": sys.argv[2], + "admin": True, "deactivated": False})) +PY +)" +curl -s -m 20 -X POST "$URL/_synapse/admin/v2/users/@${WANT}:192.168.1.24" \ + -H "Authorization: Bearer $LOGIN" -H 'Content-Type: application/json' -d "$NEWBODY" \ + | python3 -c 'import json,sys +d=json.load(sys.stdin) +print(" créé:", d.get("name") or d.get("error") or d)' +unset NEWBODY + +echo "== 5. désactiver le compte aléatoire ==" +curl -s -m 20 -X PUT "$URL/_synapse/admin/v2/users/$CREATED" \ + -H "Authorization: Bearer $LOGIN" -H 'Content-Type: application/json' \ + -d '{"deactivated": true}' | python3 -c 'import json,sys +d=json.load(sys.stdin) +print(" désactivé:", d.get("name") or d.get("error") or d)' 2>/dev/null + +echo "== vérification (lecture seule) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +c = sqlite3.connect('file:$DB?mode=ro', uri=True).cursor() +print(' comptes total/admin/deactivated:', c.execute('select count(*), sum(admin), sum(deactivated) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated from users order by admin desc'): + print(' ', r) +" +unset LOGIN PW +echo "identifiants : $CRED (600) — à changer à la première connexion, puis 2FA (C-MX3)" diff --git a/g9-migration/scripts/23-synapse-admin-fix.sh b/g9-migration/scripts/23-synapse-admin-fix.sh new file mode 100644 index 0000000..c151d4b --- /dev/null +++ b/g9-migration/scripts/23-synapse-admin-fix.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# 23-synapse-admin-fix.sh — répare ce que la voie « register » a abîmé. +# +# Erreur mesurée : dans `21-…`, les arguments du bloc python étaient décalés -> le compte a été +# créé avec **password = **, et Synapse a ignoré le localpart demandé (mxid aléatoire). +# Le compte créé porte donc admin=1 (mis à jour) et un mot de passe faible. +# +# Ce script, avec le mot de passe faible passé en argument (jamais affiché) : +# 1. login -> token admin ; 2. remplace le mot de passe par celui du fichier de credential ; +# 3. crée le compte propre @: admin=true ; 4. désactive le compte aléatoire ; +# 5. vérifie en lecture seule. +# Usage : bash 23-synapse-admin-fix.sh +set -euo pipefail + +CREATED="${1:?mxid aléatoire créé}" +WANT="${2:?localpart souhaité}" +WEAK="${3:?mot de passe actuellement en base sur ce compte}" +CTN="${CTN:-synapse}" +DB="${DB:-/data/homeserver.db}" +URL="${URL:-http://192.168.1.24:8008}" +CRED="${CRED:-/home/folzo/synapse-admin-$WANT.cred}" +DOMAIN="${DOMAIN:-192.168.1.24}" + +PW="$(python3 -c "print(open('$CRED').read().split('password=')[1].strip())")" + +TOKEN="$(python3 - "$CREATED" "$WEAK" <<'PY' +import json, subprocess, sys +body = json.dumps({"type": "m.login.password", + "identifier": {"type": "m.id.user", "user": sys.argv[1]}, + "password": sys.argv[2]}) +out = subprocess.run(["curl", "-s", "-m", "15", "-X", "POST", + "http://192.168.1.24:8008/_matrix/client/v3/login", + "-H", "Content-Type: application/json", "-d", body], + capture_output=True, text=True).stdout +print(json.loads(out).get("access_token", "")) +PY +)" +if [[ -z "$TOKEN" ]]; then echo "KO login admin — abandon (rien n'est modifié)"; exit 1; fi +echo "login admin OK (token ${#TOKEN} car., jamais affiché)" + +api() { # methode url corps + curl -s -m 20 -X "$1" "$2" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "$3" +} + +echo "== 1. mot de passe fort sur le compte admin existant ==" +api POST "$URL/_synapse/admin/v2/users/$CREATED" \ + "$(python3 - "$PW" <<'PY' +import json, sys +print(json.dumps({"password": sys.argv[1]})) +PY +)" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ", d.get("name") or d.get("error") or d)' 2>/dev/null + +echo "== 2. créer @${WANT}:${DOMAIN} (admin=true) ==" +api POST "$URL/_synapse/admin/v2/users/@${WANT}:${DOMAIN}" \ + "$(python3 - "$PW" <<'PY' +import json, sys +print(json.dumps({"password": sys.argv[1], "admin": True, "deactivated": False})) +PY +)" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ", d.get("name") or d.get("error") or d)' 2>/dev/null + +echo "== 3. désactiver le compte aléatoire ==" +api PUT "$URL/_synapse/admin/v2/users/$CREATED" '{"deactivated": true}' \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ", d.get("name") or d.get("error") or d)' 2>/dev/null + +echo "== vérification (lecture seule) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +c = sqlite3.connect('file:$DB?mode=ro', uri=True).cursor() +print(' total/admin/deactivated:', c.execute('select count(*), sum(admin), sum(deactivated) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated from users order by admin desc'): + print(' ', r) +" +echo "credential : $CRED (600) — user=@${WANT}:${DOMAIN} ; à changer à la première connexion, puis 2FA (C-MX3)" +unset TOKEN PW diff --git a/g9-migration/scripts/24-synapse-admin-put.sh b/g9-migration/scripts/24-synapse-admin-put.sh new file mode 100644 index 0000000..dd133ba --- /dev/null +++ b/g9-migration/scripts/24-synapse-admin-put.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# 24-synapse-admin-put.sh — créer @ via **PUT** (l'API admin v2 crée par PUT, pas POST). +# Séquence, avec réversibilité à chaque étape : +# 1. sqlite : rendre le compte admin aléatoire **utilisable** (admin=1, deactivated=0) +# 2. login -> token (jamais affiché) +# 3. PUT /_synapse/admin/v2/users/@: {password, admin:true} +# 4. sqlite : retirer admin au compte aléatoire et le redésactiver +# 5. vérification lecture seule +# Usage : bash 24-synapse-admin-put.sh +set -euo pipefail + +TMP_ADMIN="${1:?mxid admin temporaire}" +WANT="${2:?localpart souhaité}" +CTN="${CTN:-synapse}" +DB="${DB:-/data/homeserver.db}" +URL="${URL:-http://192.168.1.24:8008}" +CRED="${CRED:-/home/folzo/synapse-admin-$WANT.cred}" +DOMAIN="${DOMAIN:-192.168.1.24}" +PW="$(python3 -c "print(open('$CRED').read().split('password=')[1].strip())")" +# Le compte admin temporaire a été créé avec un mot de passe faible (erreur mesurée 21-…) : +# LOGIN_PW permet de se connecter avec celui qui est réellement en base, sans jamais l'afficher. +LOGIN_PW="${LOGIN_PW:-$PW}" + +echo "== 1. admin temporaire rendu utilisable ==" +docker exec "$CTN" python3 -c " +import sqlite3 +con = sqlite3.connect('$DB'); cur = con.cursor() +cur.execute('update users set admin=1, deactivated=0 where name=?', ('$TMP_ADMIN',)); con.commit() +print(' lignes:', cur.rowcount) +" + +echo "== 2. login (mot de passe lu depuis $CRED) ==" +TOKEN="$(python3 - "$TMP_ADMIN" "$LOGIN_PW" <<'PY' +import json, subprocess, sys +body = json.dumps({"type": "m.login.password", + "identifier": {"type": "m.id.user", "user": sys.argv[1]}, + "password": sys.argv[2]}) +out = subprocess.run(["curl", "-s", "-m", "15", "-X", "POST", + "http://192.168.1.24:8008/_matrix/client/v3/login", + "-H", "Content-Type: application/json", "-d", body], + capture_output=True, text=True).stdout +print(json.loads(out).get("access_token", "")) +PY +)" +if [[ -z "$TOKEN" ]]; then echo " KO login — le mot de passe en base n'est pas celui du fichier"; exit 1; fi +echo " OK (token ${#TOKEN} car., jamais affiché)" + +echo "== 3. PUT @${WANT}:${DOMAIN} (admin=true) ==" +BODY="$(python3 - "$WANT" "$PW" "$DOMAIN" <<'PY' +import json, sys +print(json.dumps({"password": sys.argv[2], "admin": True, "deactivated": False})) +PY +)" +curl -s -m 20 -X PUT "$URL/_synapse/admin/v2/users/@${WANT}:${DOMAIN}" \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "$BODY" \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ", d.get("name") or d.get("error") or d)' 2>/dev/null +unset BODY + +echo "== 4. admin temporaire retiré + redésactivé ==" +docker exec "$CTN" python3 -c " +import sqlite3 +con = sqlite3.connect('$DB'); cur = con.cursor() +cur.execute('update users set admin=0, deactivated=1 where name=?', ('$TMP_ADMIN',)); con.commit() +print(' lignes:', cur.rowcount) +" + +echo "== 5. vérification (lecture seule) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +c = sqlite3.connect('file:$DB?mode=ro', uri=True).cursor() +print(' total/admin/deactivated:', c.execute('select count(*), sum(admin), sum(deactivated) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated from users order by admin desc'): + print(' ', r) +" +echo "credential : $CRED (600) — user=@${WANT}:${DOMAIN} ; à changer à la première connexion, puis 2FA (C-MX3)" +unset TOKEN PW diff --git a/g9-migration/scripts/25-synapse-state.sh b/g9-migration/scripts/25-synapse-state.sh new file mode 100644 index 0000000..e30856b --- /dev/null +++ b/g9-migration/scripts/25-synapse-state.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# 25-synapse-state.sh — etat Synapse en LECTURE SEULE (0 ecriture, 0 creation). +set -euo pipefail +CTN="${CTN:-synapse}" +DB="${DB:-/data/homeserver.db}" + +echo "== users (lecture seule) ==" +docker exec "$CTN" python3 -c " +import sqlite3 +c = sqlite3.connect('file:$DB?mode=ro', uri=True).cursor() +print(' total/admin/deactivated/locked:', c.execute('select count(*), sum(admin), sum(deactivated), sum(locked) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated, substr(password_hash,1,7) from users order by admin desc'): + print(' ', r) +" + +echo "== bcrypt disponible dans l'image ==" +docker exec "$CTN" python3 -c "import bcrypt; print(' bcrypt', bcrypt.__version__ if hasattr(bcrypt,'__version__') else 'OK'); print(' hash', bcrypt.hashpw(b'x', bcrypt.gensalt(12)).decode()[:7])" + +echo "== credentials locaux ==" +for f in /home/folzo/synapse-admin-*.cred /home/folzo/beszel-admin.cred; do + if [ -f "$f" ]; then echo " $(stat -c '%a %U %n' "$f") ($(wc -c < "$f") o)"; else echo " absent: $f"; fi +done + +echo "== config inscription ==" +docker exec "$CTN" python3 -c " +import re +t = open('/data/homeserver.yaml').read() +for k in ('enable_registration','registration_without_token','registration_without_verification','enable_registration_tokens'): + m = re.search(r'^%s:\s*(.*)$' % k, t, re.M) + print(' ', k, '=', m.group(1) if m else 'ABSENT') +print(' shared_secret:', 'FOUND' if 'registration_shared_secret' in t else 'ABSENT') +" +echo "== synapse version ==" +curl -s -m 10 http://192.168.1.24:8008/_matrix/client/versions >/dev/null && echo " endpoint versions: OK" diff --git a/g9-migration/scripts/26-synapse-admin-repair.sh b/g9-migration/scripts/26-synapse-admin-repair.sh new file mode 100644 index 0000000..c3e0d09 --- /dev/null +++ b/g9-migration/scripts/26-synapse-admin-repair.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# 26-synapse-admin-repair.sh — repare le 1er compte admin Synapse, proprement. +# +# Cause mesuree (12/10) : le compte cree par `21-…` (`@mpijdqsyryjrrfmjdoraqcxf`) a `password_hash` +# = NULL en base -> admin=1 mais **inutilisable** (login = « Invalid username or password »). +# +# Voie : hasher le mot de passe du credential en bcrypt 12 (module `bcrypt` present dans l'image +# synapse, verifie) et l'ecrire sur le compte admin temporaire -> login -> token -> +# **PUT** /_synapse/admin/v2/users/@ (l'API admin v2 cree par PUT, pas POST) -> +# retirer admin au compte temporaire, le desactiver, remettre son hash a NULL. +# +# Bug elimine au passage : `python3 - args < **plus +# rien sur stdin** -> mot de passe lu = 0 caractere. Les programmes sont donc ecrits dans des +# fichiers (heredoc vers fichier) et l'appel se fait par chemin, ce qui laisse stdin au secret. +# +# Gardes : mot de passe et token **jamais en argv** (stdin + `curl -K` fichier 600), tmpdir 700 +# detruit a la sortie, `docker restart` pour vider les caches apres chaque ecriture sqlite directe, +# 1 seule tentative de login, verification finale en lecture seule. +# Snapshot prealable : `docker exec synapse python3 -c "sqlite3 backup"` -> /data/homeserver.db.bak-* +# +# Usage : bash 26-synapse-admin-repair.sh [--no-restart] +# bash 26-synapse-admin-repair.sh --verify (1 login, prouve le compte) +# bash 26-synapse-admin-repair.sh --admin-check (GET /_synapse/admin/v2/users) +set -euo pipefail + +CTN="${CTN:-synapse}" +DB="${DB:-/data/homeserver.db}" +URL="${URL:-http://192.168.1.24:8008}" +DOMAIN="${DOMAIN:-192.168.1.24}" + +TMPD="$(mktemp -d)"; chmod 700 "$TMPD"; trap 'rm -rf "$TMPD"' EXIT + +cat > "$TMPD/loginbody.py" <<'PY' +import json, sys +pw = sys.stdin.buffer.read().strip().decode() +print(json.dumps({"type": "m.login.password", + "identifier": {"type": "m.id.user", "user": sys.argv[1]}, + "password": pw})) +PY + +cat > "$TMPD/putbody.py" <<'PY' +import json, sys +print(json.dumps({"password": sys.stdin.buffer.read().strip().decode(), + "admin": True, "deactivated": False})) +PY + +cat > "$TMPD/token.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +tok = d.get("access_token", "") +open(sys.argv[2], "w").write(tok) +print(" " + ("login OK (token %d car., jamais affiche)" % len(tok) if tok + else "KO login: " + str(d.get("error") or d))) +sys.exit(0 if tok else 1) +PY + +cat > "$TMPD/show.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +print(" ", d.get("name") or d.get("error") or d) +PY + +cat > "$TMPD/admincheck.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +us = d.get("users", []) +print(" API admin OK — %s utilisateurs, %s admin, %s desactives" % ( + d.get("total", "?"), sum(1 for u in us if u.get("admin")), + sum(1 for u in us if u.get("deactivated")))) +for u in us: + print(" ", u.get("name"), "| admin", bool(u.get("admin")), + "| deactivated", bool(u.get("deactivated")), "| locked", bool(u.get("locked"))) +PY + +if [[ "${1:-}" == "--verify" ]]; then + WANT="${2:?localpart}" + CRED="${CRED:-/home/folzo/synapse-admin-$WANT.cred}" + sed -n 's/^password=//p' "$CRED" | python3 "$TMPD/loginbody.py" "@${WANT}:${DOMAIN}" \ + | curl -s -m 15 -X POST "$URL/_matrix/client/v3/login" -H 'Content-Type: application/json' -d @- > "$TMPD/login.json" + python3 "$TMPD/token.py" "$TMPD/login.json" "$TMPD/token" + exit 0 +fi + +if [[ "${1:-}" == "--admin-check" ]]; then + WANT="${2:?localpart}" + CRED="${CRED:-/home/folzo/synapse-admin-$WANT.cred}" + sed -n 's/^password=//p' "$CRED" | python3 "$TMPD/loginbody.py" "@${WANT}:${DOMAIN}" \ + | curl -s -m 15 -X POST "$URL/_matrix/client/v3/login" -H 'Content-Type: application/json' -d @- > "$TMPD/login.json" + python3 "$TMPD/token.py" "$TMPD/login.json" "$TMPD/token" + { printf 'header = "Authorization: Bearer %s"\n' "$(cat "$TMPD/token")"; } > "$TMPD/curlrc" + chmod 600 "$TMPD/curlrc" + curl -s -m 20 -K "$TMPD/curlrc" "$URL/_synapse/admin/v2/users?limit=50" > "$TMPD/users.json" + python3 "$TMPD/admincheck.py" "$TMPD/users.json" + exit 0 +fi + +TMP_ADMIN="${1:?mxid admin temporaire}" +WANT="${2:?localpart souhaite}" +CRED="${CRED:-/home/folzo/synapse-admin-$WANT.cred}" +RESTART=1 +[[ "${3:-}" == "--no-restart" ]] && RESTART=0 +[[ -f "$CRED" ]] || { echo "KO: credential absent ($CRED)"; exit 1; } +TARGET="@${WANT}:${DOMAIN}" + +cat > "$TMPD/hashpw.py" <<'PY' +import sqlite3, sys, bcrypt +pw = sys.stdin.buffer.read().strip() +con = sqlite3.connect(sys.argv[2]); cur = con.cursor() +if pw: + h = bcrypt.hashpw(pw, bcrypt.gensalt(12)).decode() + cur.execute('update users set password_hash=?, admin=1, deactivated=0, locked=0 where name=?', + (h, sys.argv[1])) +else: + cur.execute('update users set password_hash=NULL, admin=1, deactivated=0, locked=0 where name=?', + (sys.argv[1],)) +con.commit() +print(' lignes:', cur.rowcount, '| pw lu:', len(pw), 'car. | hash', ('bcrypt12' if pw else 'NULL')) +PY + +cat > "$TMPD/strip.py" <<'PY' +import sqlite3, sys +con = sqlite3.connect(sys.argv[2]); cur = con.cursor() +cur.execute('update users set admin=0, deactivated=1, password_hash=NULL where name=?', (sys.argv[1],)) +con.commit(); print(' lignes:', cur.rowcount) +PY + +cat > "$TMPD/report.py" <<'PY' +import sqlite3, sys +c = sqlite3.connect('file:%s?mode=ro' % sys.argv[1], uri=True).cursor() +print(' total/admin/deactivated:', c.execute('select count(*), sum(admin), sum(deactivated) from users').fetchone()) +for r in c.execute('select name, admin, locked, deactivated, case when password_hash is null then 0 else 1 end from users order by admin desc'): + print(' ', r) +PY + +up() { + for i in $(seq 1 40); do + if curl -s -m 3 -o /dev/null "$URL/_matrix/client/versions"; then echo " synapse repond apres ${i}s"; return 0; fi + sleep 1 + done + echo " KO: synapse ne repond pas"; return 1 +} + +echo "== 1. mot de passe du credential hashe (bcrypt 12) sur le compte admin temporaire ==" +docker cp "$TMPD/hashpw.py" "$CTN:/tmp/g9-hashpw.py" >/dev/null +sed -n 's/^password=//p' "$CRED" | docker exec -i "$CTN" python3 /tmp/g9-hashpw.py "$TMP_ADMIN" "$DB" +docker exec "$CTN" rm -f /tmp/g9-hashpw.py + +if [[ "$RESTART" == "1" ]]; then + echo "== 2. docker restart $CTN (caches sqlite videes apres ecriture directe) ==" + docker restart "$CTN" >/dev/null; up +fi + +echo "== 3. login du compte admin temporaire ==" +sed -n 's/^password=//p' "$CRED" | python3 "$TMPD/loginbody.py" "$TMP_ADMIN" \ + | curl -s -m 15 -X POST "$URL/_matrix/client/v3/login" -H 'Content-Type: application/json' -d @- > "$TMPD/login.json" +python3 "$TMPD/token.py" "$TMPD/login.json" "$TMPD/token" + +echo "== 4. PUT $TARGET (admin=true) ==" +{ printf 'header = "Authorization: Bearer %s"\n' "$(cat "$TMPD/token")" + printf 'header = "Content-Type: application/json"\n'; } > "$TMPD/curlrc" +chmod 600 "$TMPD/curlrc" +sed -n 's/^password=//p' "$CRED" | python3 "$TMPD/putbody.py" \ + | curl -s -m 20 -X PUT "$URL/_synapse/admin/v2/users/$TARGET" -K "$TMPD/curlrc" -d @- > "$TMPD/put.json" +python3 "$TMPD/show.py" "$TMPD/put.json" + +echo "== 5. compte temporaire : admin retire, desactive, hash a NULL ==" +docker cp "$TMPD/strip.py" "$CTN:/tmp/g9-strip.py" >/dev/null +docker exec "$CTN" python3 /tmp/g9-strip.py "$TMP_ADMIN" "$DB" +docker exec "$CTN" rm -f /tmp/g9-strip.py +if [[ "$RESTART" == "1" ]]; then + echo "== 6. docker restart $CTN ==" + docker restart "$CTN" >/dev/null; up +fi + +echo "== 7. verification (lecture seule) (nom, admin, locked, deactivated, hash_present) ==" +docker cp "$TMPD/report.py" "$CTN:/tmp/g9-report.py" >/dev/null +docker exec "$CTN" python3 /tmp/g9-report.py "$DB" +docker exec "$CTN" rm -f /tmp/g9-report.py +echo "credential : $CRED (600) — user=$TARGET ; a changer a la 1re connexion, puis 2FA (C-MX3)" +echo "verifier le login : bash $0 --verify $WANT" diff --git a/g9-migration/scripts/27-synapse-family-users.sh b/g9-migration/scripts/27-synapse-family-users.sh new file mode 100644 index 0000000..5a11604 --- /dev/null +++ b/g9-migration/scripts/27-synapse-family-users.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# 27-synapse-family-users.sh — cree les 6 comptes famille Matrix via l'API admin (admin=false). +# +# Pre-requis : un compte admin utilisable = `26-synapse-admin-repair.sh` (verifie par --admin-check). +# Cible C-MX1 : 2 adultes + 4 enfants, **admin=false**, mots de passe generes sur la machine et +# ecrits dans un fichier 600 (jamais affiches, jamais commitables), a recopier dans le coffre. +# +# Garde-fous : mode `--dry` par defaut de lecture, **confirmation `CREER` exigee** (TTY ou +# CONFIRM=CREER), 1 ecriture par compte, verification lecture seule apres, rollback = desactivation. +# Usage : bash 27-synapse-family-users.sh --dry +# CONFIRM=CREER bash 27-synapse-family-users.sh (ou saisie interactive) +# bash 27-synapse-family-users.sh --revoke (desactive les 6) +set -euo pipefail + +URL="${URL:-http://192.168.1.24:8008}" +DOMAIN="${DOMAIN:-192.168.1.24}" +ADMIN="${ADMIN:-folzo}" +CRED="${CRED:-/home/folzo/synapse-admin-$ADMIN.cred}" +OUT="${OUT:-/home/folzo/synapse-famille.cred}" +ADULTS="${ADULTS:-effi audrey}" +KIDS="${KIDS:-carla ewan lola noah}" + +TMPD="$(mktemp -d)"; chmod 700 "$TMPD"; trap 'rm -rf "$TMPD"' EXIT + +cat > "$TMPD/loginbody.py" <<'PY' +import json, sys +print(json.dumps({"type": "m.login.password", + "identifier": {"type": "m.id.user", "user": sys.argv[1]}, + "password": sys.stdin.buffer.read().strip().decode()})) +PY +cat > "$TMPD/token.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])); tok = d.get("access_token", "") +open(sys.argv[2], "w").write(tok) +print(" " + ("login admin OK (token %d car.)" % len(tok) if tok else "KO login: " + str(d.get("error") or d))) +sys.exit(0 if tok else 1) +PY +cat > "$TMPD/putbody.py" <<'PY' +import json, sys +pw = sys.stdin.buffer.read().strip().decode() +print(json.dumps({"password": pw, "admin": False, "deactivated": False, + "display_name": sys.argv[1]})) +PY +cat > "$TMPD/list.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +us = d.get("users", []) +print(" total API:", d.get("total")) +for u in us: + print(" ", u.get("name"), "| admin", bool(u.get("admin")), + "| deactivated", bool(u.get("deactivated")), "| locked", bool(u.get("locked"))) +PY + +MODE="create"; [[ "${1:-}" == "--dry" ]] && MODE="dry"; [[ "${1:-}" == "--revoke" ]] && MODE="revoke" +[[ -f "$CRED" ]] || { echo "KO: credential admin absent ($CRED) — voir 26-synapse-admin-repair.sh"; exit 1; } + +read -r -a ALL <<< "$ADULTS $KIDS" +echo "comptes vus : ${#ALL[@]} (${ALL[*]}) — 2 adultes + 4 enfants = 6 attendus" + +if [[ "$MODE" == "dry" ]]; then + echo "== PLAN (0 ecriture) ==" + for lp in "${ALL[@]}"; do + echo " PUT $URL/_synapse/admin/v2/users/@${lp}:${DOMAIN} {password: genere, admin: false}" + done + echo " mots de passe -> $OUT (600, folzo) ; rollback = --revoke (desactivation, jamais de purge)" + exit 0 +fi + +if [[ "$MODE" == "revoke" ]]; then + sed -n 's/^password=//p' "$CRED" | head -1 | python3 "$TMPD/loginbody.py" "@${ADMIN}:${DOMAIN}" \ + | curl -s -m 15 -X POST "$URL/_matrix/client/v3/login" -H 'Content-Type: application/json' -d @- > "$TMPD/login.json" + python3 "$TMPD/token.py" "$TMPD/login.json" "$TMPD/token" + { printf 'header = "Authorization: Bearer %s"\n' "$(cat "$TMPD/token")" + printf 'header = "Content-Type: application/json"\n'; } > "$TMPD/curlrc" + chmod 600 "$TMPD/curlrc" + for lp in "${ALL[@]}"; do + curl -s -m 20 -X PUT "$URL/_synapse/admin/v2/users/@${lp}:${DOMAIN}" -K "$TMPD/curlrc" \ + -d '{"deactivated": true}' | python3 -c 'import json,sys;d=json.load(sys.stdin);print(" ", d.get("name") or d.get("error") or d)' + done + exit 0 +fi + +if [[ "${CONFIRM:-}" != "CREER" ]]; then + echo "confirmation exigee : tape CREER pour creer ${#ALL[@]} comptes (admin=false), mots de passe dans $OUT" + read -r ANSWER + [[ "$ANSWER" == "CREER" ]] || { echo "abandon (aucune ecriture)"; exit 1; } +fi +[[ -e "$OUT" ]] && { echo "KO: $OUT existe deja (ne pas ecraser des mots de passe existants)"; exit 1; } + +sed -n 's/^password=//p' "$CRED" | head -1 | python3 "$TMPD/loginbody.py" "@${ADMIN}:${DOMAIN}" \ + | curl -s -m 15 -X POST "$URL/_matrix/client/v3/login" -H 'Content-Type: application/json' -d @- > "$TMPD/login.json" +python3 "$TMPD/token.py" "$TMPD/login.json" "$TMPD/token" +{ printf 'header = "Authorization: Bearer %s"\n' "$(cat "$TMPD/token")" + printf 'header = "Content-Type: application/json"\n'; } > "$TMPD/curlrc" +chmod 600 "$TMPD/curlrc" + +umask 077 +: > "$OUT" +echo "== creation (${#ALL[@]} comptes, admin=false) ==" +for lp in "${ALL[@]}"; do + PW="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)" + printf 'user=@%s:%s password=%s\n' "$lp" "$DOMAIN" "$PW" >> "$OUT" + printf '%s' "$PW" | python3 "$TMPD/putbody.py" "$lp" \ + | curl -s -m 20 -X PUT "$URL/_synapse/admin/v2/users/@${lp}:${DOMAIN}" -K "$TMPD/curlrc" -d @- \ + | python3 -c 'import json,sys;d=json.load(sys.stdin);print(" ", d.get("name") or d.get("error") or d)' + unset PW +done +chmod 600 "$OUT" +echo "mots de passe : $OUT ($(wc -l < "$OUT") lignes, 600) — a recopier dans le coffre puis detruire" + +echo "== verification (lecture seule) ==" +curl -s -m 20 -K "$TMPD/curlrc" "$URL/_synapse/admin/v2/users?limit=50" > "$TMPD/users.json" +python3 "$TMPD/list.py" "$TMPD/users.json"