dumps logiques 3 bases (gitea rejouabilité prouvée 116 tables) + hub beszel hors-G9 sur J5040 LAN-only
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Beszel hub — monitoring résilient (C-AP5).
|
||||
# Le hub vit sur le J5040, PAS sur le G9 : une surveillance qui meurt avec la machine
|
||||
# surveillée ne surveille rien (trou mesuré 12/10 : les sondes tournent depuis le G9).
|
||||
# Image épinglée par digest ; stateful (sa base est dans ./data) ; hors Watchtower.
|
||||
name: beszel-hub
|
||||
|
||||
services:
|
||||
hub:
|
||||
image: henrygd/beszel@sha256:6b39a8b64f9b15d00a25e7683e31a93c46161182a54f62713525107064e10599
|
||||
container_name: beszel-hub
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Publié sur toutes les interfaces, mais ufw n'autorise que le LAN (règle ajoutée
|
||||
# à l'installation) : le hub n'est jamais joignable depuis le WAN.
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- /home/folzo/beszel-hub/data:/pb_data
|
||||
environment:
|
||||
- "PUBLIC_URL=http://192.168.1.27:8090"
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
@@ -152,3 +152,5 @@
|
||||
| 2026-10-12 18:20 | 4.5 | **faux positif corrigé : CouchDB n'est PAS sans auth** | C-EX1 (exactitude des constats), C-AP1 (fiabilité des sondeurs) | la sonde lisait `GET :5984/` = 200 → lu « sans auth ». Mesure directe : **`GET /_all_databases` = 401** et **`GET /_active_tasks` = 401** ; `compose/g9-queue/docker-compose.yml` déclare bien `COUCHDB_USER` + `COUCHDB_PASSWORD=${COUCHDB_PASSWORD}`. `/` est public **par conception** dans CouchDB. **Décision : ne pas recréer le conteneur** pour publier `127.0.0.1:5984` — gain quasi nul (ufw LAN-only + auth déjà en place) et **casse probable de la sync Obsidian** si le client pointe sur `http://192.168.1.24:5984`. Échange mauvais → statu quo, consigné | aucun (aucun conteneur recréé, aucun fichier modifié sur le G9) |
|
||||
| 2026-10-12 18:40 | 4.5 | **balayage d'identités en lecture seule** (`15-identity-sweep.sh`, `16-identity-sweep-sqlite.sh`) → §7 du registre | C-ID2 (preuve de la moitié B), C-ID4/7/9, C-MX1/3 : l'existant est **mesuré**, plus supposé | **Gitea** : 4 comptes (`Master`, `Folzo`, `Antoine`, `g9ops`), `IsAdmin`=2, **2FA = false sur les 4** → `gitea admin user list` (CLI local, rc=0). **Nextcloud** : 8 comptes, groupes `admin`={Effi, Master} et `Famille Folzo`={Audrey, Folzo, carla, ewan, lola, noah}, providers 2FA installés (`twofactor_totp`, `backupcodes`, `nextcloud_notification`). **Synapse** : **2 comptes (`@ops`, `@ops-bot`), `admin` = 0**, locked 0, deactivated 0, 14 devices → **aucun admin : C-MX1 et C-MX3 bloqués à la racine, aucune porte de sortie**. **n8n** : 1 compte `effi@openfox.local` (disabled=0) + basic auth déclarée. **PhotoPrism** : 3 comptes. **Vaultwarden** : `ADMIN_TOKEN_FILE` (jeton en fichier, pas en env en clair). **CouchDB** : 401 sans identifiants → auth active | 0 écriture : commandes `admin user list`, `occ user:list/group:list/twofactorprovider:states`, sqlite en `mode=ro`, `mariadb -N -e select`. Aucun compte créé, aucun privilège changé |
|
||||
| 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: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` |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# 18-db-dumps.sh — dumps logiques COHÉRENTS des bases (C-P5).
|
||||
#
|
||||
# Pourquoi : le backup quotidien copie les datadir MariaDB/Postgres **conteneurs tournants**
|
||||
# (`RESTIC_STATEFUL` ne stoppe que valheim/n8n/synapse/couchdb/obsidian). La copie est complète
|
||||
# en octets, pas garantie rejouable. Un dump logique est la seule preuve de rejouabilité.
|
||||
#
|
||||
# Choix technique : `--single-transaction` (InnoDB = instantané cohérent **sans stopper la
|
||||
# brique**) et `pg_dump` en auth locale (socket) -> aucun mot de passe sur une ligne de commande,
|
||||
# donc rien de secret dans `ps`, dans le shell ni dans le journal.
|
||||
#
|
||||
# Les dumps tombent sous /data/docker/<brique>/dumps/ -> déjà dans le périmètre restic.
|
||||
# Usage : sudo ./18-db-dumps.sh [--dry]
|
||||
set -euo pipefail
|
||||
|
||||
TS="$(date -u +%Y%m%d-%H%MZ)"
|
||||
DRY=0
|
||||
[[ "${1:-}" == "--dry" ]] && DRY=1
|
||||
run() { if [[ "$DRY" == 1 ]]; then echo "DRY: $*"; else "$@"; fi; }
|
||||
|
||||
dumped=0
|
||||
|
||||
echo "== Gitea (Postgres, pg_dump auth socket) =="
|
||||
PG_USER="$(docker inspect -f '{{json .Config.Env}}' gitea-db | python3 -c 'import json,sys; print([e.split("=",1)[1] for e in json.load(sys.stdin) if e.startswith("POSTGRES_USER=")][0])')"
|
||||
PG_DB="$(docker inspect -f '{{json .Config.Env}}' gitea-db | python3 -c 'import json,sys; print([e.split("=",1)[1] for e in json.load(sys.stdin) if e.startswith("POSTGRES_DB=")][0])')"
|
||||
run mkdir -p /data/docker/gitea/dumps
|
||||
run bash -c "docker exec -u \$'$PG_USER' gitea-db pg_dump -U '$PG_USER' '$PG_DB' | gzip > /data/docker/gitea/dumps/gitea-$TS.sql.gz"
|
||||
run ls -l "/data/docker/gitea/dumps/gitea-$TS.sql.gz"
|
||||
dumped=$((dumped + 1))
|
||||
|
||||
echo "== Nextcloud (MariaDB, --single-transaction) =="
|
||||
run mkdir -p /data/docker/nextcloud/dumps
|
||||
run bash -c "docker exec nextcloud_db sh -c 'mysqldump --single-transaction --routines --events --flush-privileges -u root -p\"\$(cat \$MARIADB_ROOT_PASSWORD_FILE)\" \"\$MARIADB_DATABASE\"' | gzip > /data/docker/nextcloud/dumps/nextcloud-$TS.sql.gz"
|
||||
run ls -l "/data/docker/nextcloud/dumps/nextcloud-$TS.sql.gz"
|
||||
dumped=$((dumped + 1))
|
||||
|
||||
echo "== PhotoPrism (MariaDB, --single-transaction) =="
|
||||
run mkdir -p /data/docker/photoprism/dumps
|
||||
run bash -c "docker exec photoprism_db sh -c 'mysqldump --single-transaction --routines -u root -p\"\$(cat \$MARIADB_ROOT_PASSWORD_FILE)\" \"\$MARIADB_DATABASE\"' | gzip > /data/docker/photoprism/dumps/photoprism-$TS.sql.gz"
|
||||
run ls -l "/data/docker/photoprism/dumps/photoprism-$TS.sql.gz"
|
||||
dumped=$((dumped + 1))
|
||||
|
||||
echo "== contrôle : les dumps sont-ils dans le périmètre restic ? =="
|
||||
run bash -c "restic -r \"\$(sudo sh -c 'set -a; . /etc/restic.env; set +a; printf %s \"\$RESTIC_REPOSITORY\"')\" ls latest 2>/dev/null | grep -c 'dumps/' || echo '0 (visibles au prochain backup)'"
|
||||
|
||||
echo "DUMPS_OK $dumped dumps produits (ts=$TS)"
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Vérifie que le superuser Beszel est utilisable (auth API) — n'affiche ni mot de passe ni token.
|
||||
set -uo pipefail
|
||||
CRED="${1:-/home/folzo/beszel-admin.cred}"
|
||||
URL="${URL:-http://127.0.0.1:8090}"
|
||||
PW="$(cat "$CRED")"
|
||||
BODY="$(python3 - "$PW" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"identity": "admin@folzocorp.fr", "password": sys.argv[1]}))
|
||||
PY
|
||||
)"
|
||||
unset PW
|
||||
RESP="$(curl -s -m 10 -X POST "$URL/api/auth/authenticators/superusers/email" -H 'Content-Type: application/json' -d "$BODY")"
|
||||
unset BODY
|
||||
python3 - "$RESP" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
d = json.loads(sys.argv[1])
|
||||
except Exception:
|
||||
print("auth KO (réponse non JSON)")
|
||||
sys.exit(1)
|
||||
print("auth OK | superuser:", d.get("meta", {}).get("superuser"), "| token présent:", bool(d.get("token")))
|
||||
PY
|
||||
Reference in New Issue
Block a user