synapse: 1er compte admin cree, repare (bcrypt+PUT v2) et verifie (login OK, API admin OK) + 27-family-users outille

This commit is contained in:
Effi
2026-10-12 21:13:43 +02:00
parent 4a3c460b57
commit 4f1c267404
12 changed files with 721 additions and 19 deletions
+35 -15
View File
@@ -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`) ## 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é, Ce qui suit sort de commandes, pas d'hypothèses. Balayage **100 % lecture seule** (aucun compte
aucune valeur secrète affichée. 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 | | 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é | | **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) | | **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) | | **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 | | **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 | | **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) | | **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…"` Mesure initiale : `database.name: sqlite3` → 2 comptes, **aucun admin** → aucun moyen officiel de
donne 2 comptes, **aucun admin**. Conséquence mesurée : **aucun moyen officiel de créer un compte créer un compte famille, de verrouiller, ni de réinitialiser un mot de passe.
famille, de verrouiller un compte, 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) : **Voie tentée et échouée** (`scripts/17-synapse-admin.sh`, inscription signée par
`homeserver.yaml` contient `registration_shared_secret` → l'endpoint `registration_shared_secret` sur `POST /_synapse/admin/v1/register`) : **`HMAC incorrect`**, testé
`POST /_synapse/admin/v1/register` accepte une inscription signée avec ce secret, ce qui permet de avec `username` en localpart **et** en mxid complet, secret relevé identique entre `sed` et python
créer **le premier compte admin** (puis `admin: true` en base). C'est un geste **humain** (sha256). Le secret n'a **jamais** été recopié dans le kit ni affiché.
(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.
Rollback : `docker exec synapse python3 -c "update users set admin=0 where name='…'"` (à faire **Voie qui a marché** (`scripts/26-synapse-admin-repair.sh`, GO utilisateur) :
avec `mode=ro` d'abord pour vérifier, puis écriture assumée). 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`.
+2 -2
View File
@@ -18,8 +18,8 @@ geste physique, usage humain (G11 : interdits permanents + L3).
| Critère | Geste | Qui | Vérification | | 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-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 | TOTP sur les comptes admin + codes de récupération consignés | toi | login admin exige l'OTP (mesuré) | | **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-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é) | | **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é) |
+2 -2
View File
@@ -12,8 +12,8 @@
| Symptôme | Diagnostic (lecture seule) | Action | Rollback | | Symptôme | Diagnostic (lecture seule) | Action | Rollback |
|---|---|---|---| |---|---|---|---|
| Pas de compte admin | login admin → 403 | ajouter `registration_shared_secret` (sudo, `homeserver.yaml`) → restart → `docker exec <synapse> register_new_matrix_user -a -u <admin>` → **retirer** le secret → restart | retirer la ligne + restart | | 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/@<uid>`** `{password, admin:true}` → retirer l'admin temporaire. Voie officielle à réessayer : `docker exec synapse register_new_matrix_user -a -u <admin> -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='<temporaire>'` |
| Création de compte refusée | `PUT /_synapse/admin/v2/users/@<uid>:192.168.1.24` (token admin) | créer avec `admin:false` ; inscription publique reste **coupée** (voulu) | `POST /_synapse/admin/v1/deactivate/<uid>` (conserve la trace) | | Création de compte refusée | `PUT /_synapse/admin/v2/users/@<uid>: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é) | | 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) | | 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 | | Fused de logs / disque | `du -sh /data/*` + logs Synapse | purge via `synapse_stitch_schemas`/`purge` documenté **après** backup | restauration restic |
+1
View File
@@ -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='<compte>'"` (désactivation propre, jamais de purge) | | 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='<compte>'"` (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/<brique>/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: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/<brique>/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 <n° 8090>` ; `rm -f /home/folzo/beszel-admin.cred` ; `rm -rf /home/folzo/beszel-hub` | | 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 <n° 8090>` ; `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 |
@@ -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 <localpart>
set -euo pipefail
USER_LOCAL="${1:?usage: $0 <localpart>}"
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)"
@@ -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 <localpart>
set -euo pipefail
LOCAL="${1:?usage: $0 <localpart>}"
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',))\""
@@ -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 <mxid_aleatoire_cree> <localpart_souhaite>
set -euo pipefail
CREATED="${1:?usage: $0 <mxid_cree> <localpart_souhaite>}"
WANT="${2:?usage: $0 <mxid_cree> <localpart_souhaite>}"
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)"
@@ -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 = <localpart>**, 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 @<localpart>:<domain> admin=true ; 4. désactive le compte aléatoire ;
# 5. vérifie en lecture seule.
# Usage : bash 23-synapse-admin-fix.sh <mxid_aleatoire> <localpart_souhaite> <mot_de_passe_faible>
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
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# 24-synapse-admin-put.sh — créer @<localpart> 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/@<localpart>:<domain> {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 <mxid_admin_temporaire> <localpart_souhaite>
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
+34
View File
@@ -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"
@@ -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/@<localpart> (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 <<PY` consomme le heredoc comme programme -> **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 <mxid_admin_temporaire> <localpart> [--no-restart]
# bash 26-synapse-admin-repair.sh --verify <localpart> (1 login, prouve le compte)
# bash 26-synapse-admin-repair.sh --admin-check <localpart> (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"
@@ -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"