Files
sans.pub/CLAUDE.md
T
Pierre MartinandClaude Opus 5.5 c92248c821 fix(backup): PATH explicite dans les scripts lancés par cron
Les backups nocturnes du 4 et 5 octobre ont échoué ("restic : commande
introuvable") : cron ne fournit que /usr/bin:/bin alors que restic est
dans /usr/local/bin. Testé avec le PATH de cron (systemd-run -E).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:26:27 +02:00

190 lines
12 KiB
Markdown

# Serveur sans.pub
Lorsque tu interroges le serveur, ne lances que des commandes en LECTURE SEULE (non destructrices). Sauf à la demande explicite de l'utilisateur.
## Accès
- **SSH**: `ssh zuck@sans.pub` (alias `~/.ssh/config` : `Host sans.pub`) — sudo sans mot de passe. Note : ce document mentionnait auparavant un utilisateur `cloud`, à vérifier/clarifier (voir `install-serveur.md`, le compte créé est `zuck`).
- **CapRover**: https://captain.cloud.sans.pub
- **Gitea**: `git.sans.pub` (port SSH 2242, `User git` dans `~/.ssh/config`)
## Infrastructure
- **OS**: Debian 9 (kernel 4.9.0-12-amd64)
- **Docker**: 19.03.12 (mode Swarm)
- **Disque**: 914 Go total, ~193 Go utilisés (23%)
- **RAM**: 23 Go (~6 Go utilisés, swap quasi vide — vérifié 2026-02-19)
## Structure Caprover
- `/captain/data/` - données persistantes
- `/captain/generated/` - configs générées
- `/captain/temp/` - fichiers temporaires
## Services actifs (~40 services)
### Outils collaboratifs
- **etherpad** - éditeur collaboratif
- **ethercalc** - tableur collaboratif
- **cryptpad** - suite bureautique chiffrée
- **nextcloud** - cloud personnel
### Développement
- **gitea** - forge Git
- **adminer** - gestion BDD
### Communication
- **writefreely** - blog
- **peertube** - vidéos
- **rss** + **rss-bridge** - flux RSS
### Sécurité/Outils
- **bitwarden** - gestionnaire mots de passe
- **privatebin** - partage de texte chiffré
- **ots** - secrets temporaires
- **it-tools** - boîte à outils
- **cyberchef** - manipulation de données
### Sites perso
- **pierre-martin** - site perso
- **sans-pub-site** - site principal
- **une-espace** - autre projet
### Enquêtes
- **limesurvey** - sondages
## Services en erreur
Aucun service en erreur connu (vérifié 2026-02-19). `mail` tourne correctement (1/1, poste.io).
## Commandes et skills Claude
### Commande
- `/supervision` - analyse complète de la santé du serveur (invocation manuelle)
### Skill (auto-activé)
- `caprover` - gestion des apps via API CapRover
- `list` : lister les apps
- `info NOM` : détails d'une app
- `env NOM` : variables d'environnement
- `scale NOM N` : changer le nombre d'instances
- `restart NOM` : redémarrer une app
**Prérequis** : définir `CAPROVER_PASSWORD` dans `.env` (voir `.envrc`)
### CLI CapRover (alternative à l'API brute)
Installé localement via `fnm` (Node géré sans toucher au système) :
```bash
curl -fsSL https://fnm.vercel.app/install | bash # une seule fois
fnm install --lts && fnm use lts-latest
npm install -g caprover
caprover login # à faire soi-même dans un vrai terminal (TTY requis pour le mot de passe)
```
Ensuite : `caprover list` (machines connectées), `caprover api -n <nom-machine> -t /chemin/api -m GET -d '{}'` pour des appels génériques (toujours passer `-d` même vide, sinon prompt interactif qui plante sans TTY).
Endpoints utiles :
- `GET /user/apps/appDefinitions` → liste toutes les apps avec `envVars`, `volumes`, `ports`, etc.
- `POST /user/apps/appDefinitions/update` (payload : `appName` + les champs à conserver/modifier) → met à jour la config d'une app **sans redéployer l'image** (mais redémarre le service)
⚠️ **`appDefinitions/update` remplace, ne fusionne pas** : un champ absent du payload est réinitialisé à sa valeur par défaut (vécu en prod le 2026-09-23 : `containerHttpPort` oublié → réinitialisé à `80` au lieu de `3000` → 502 sur git.sans.pub pendant plusieurs minutes, cf `incidents/2026-09-14-gitea-cryptominer.md`). **Toujours faire un `GET /user/apps/appDefinitions` juste avant et renvoyer l'objet complet**, en ne modifiant que le(s) champ(s) voulu(s).
## Gitea — détails techniques
- **Containers Docker** (Swarm) : `srv-captain--gitea.1.*` (gitea/gitea:1.27.3, mis à jour le 2026-09-23) + `srv-captain--gitea-db.1.*` (mysql:5.7)
- **Volume de données** : `captain--gitea-data` → monté sur `/data` dans le container
- Dépôts bare : `/data/git/repositories/<owner>/<repo>.git`
- Config app : `/data/gitea/conf/app.ini` (contient des secrets en clair : `INTERNAL_TOKEN`, `LFS_JWT_SECRET`, mot de passe MySQL — jamais versionner ce fichier)
- `.gitconfig` global (compte système utilisé par Gitea pour exécuter git) : `/data/gitea/home/.gitconfig`
- Logs applicatifs : `/data/gitea/log/gitea.log` (rotation quotidienne en `.gz`)
- **Réseau** : container Gitea sur `captain-overlay-network`, IP interne observée `10.0.1.75`. Le reverse-proxy CapRover (`captain-nginx`) apparaît côté Gitea sous l'IP `10.0.1.105` pour toutes les requêtes publiques (pas d'IP client réelle sans config `X-Forwarded-For`/trusted proxies).
- **Connexion à la base MySQL depuis le host/container** : passer par `-h 127.0.0.1` (TCP), pas par défaut socket local (`localhost`) — le grant MySQL de l'utilisateur `gitea` ne couvre que `%`/TCP, pas `localhost`.
- **Variables d'env CapRover de l'app `gitea`** (vérifié via `docker service inspect srv-captain--gitea`) : `DB_HOST`, `DB_PASSWD`, `DB_TYPE`, `DB_USER`, `RUN_MODE`. ⚠️ Aucune n'est au format `GITEA__<section>__<clé>` attendu par l'image officielle pour réinjecter dans `app.ini` au démarrage — elles sont donc **inertes** pour Gitea (c'est `app.ini` dans le volume qui fait foi). En cas de rotation de secret (mot de passe DB notamment), mettre à jour `app.ini` + redémarrer suffit fonctionnellement ; penser quand même à resynchroniser `DB_PASSWD` côté CapRover pour ne pas laisser une copie obsolète du secret.
- **⚠️ Voir `incidents/2026-09-14-gitea-cryptominer.md`** : l'API interne Gitea (`/api/internal/manager/add-logger`) a été exploitée pour injecter des `packObjectsHook` malveillants dans `.gitconfig`. Toujours vérifier ce fichier ne contient pas d'entrées `[uploadpack] packObjectsHook` inattendues.
## Backup
- **Système actuel (depuis le 2026-10-03)** : restic 0.19.1 (`/usr/local/bin/restic`) → OVH Swift, conteneur `sans-pub-restic` (même projet OVH que l'ancien backup, région GRA)
- Script : `/root/restic/backup.sh` (source versionnée : `backup/restic-backup.sh` dans ce dépôt), cron `/etc/cron.d/restic-backup` à 03:30
- Config : `/root/restic/env` (identifiants OVH) + `/root/restic/password` (mot de passe du dépôt) — `600`, **jamais afficher**. Le mot de passe doit aussi être dans Vaultwarden, sinon le backup est inutilisable si le serveur disparaît
- Contenu : dumps SQL de toutes les bases (`/var/backups/sans-pub-db/`, détection auto MySQL/MariaDB/PostgreSQL), volumes Docker (hors registres, previews et logs Nextcloud), `/captain/data` (hors registry), `/home/cloud/apps_data`, `/root/backup`, `/etc`. Les fichiers bruts d'une base sont exclus **seulement si son dump a réussi** (sinon sauvegardés en repli)
- Particularités des bases : `limesurvey-mariadb-db` n'accepte root qu'en TCP (`-h127.0.0.1`, le mot de passe de `root@localhost` est différent) ; `writefreely-db` a une table `mysql.proc` incompatible (dump sans `--routines`). Le script essaie automatiquement ces variantes
- Premier snapshot complet : 2026-10-03 (~53 Gio, ~45 Gio stockés) ; restauration testée (`restic dump` + comparaison SHA256)
- ⚠️ Les scripts lancés par `/etc/cron.d/` n'ont que `PATH=/usr/bin:/bin` : d'où le `export PATH=...` en tête des scripts (restic dans `/usr/local/bin` → backups du 4 et 5 octobre 2026 en échec). Tester un script cron avec `systemd-run -E PATH=/usr/bin:/bin <script>`
- Rétention : 7 quotidiens, 4 hebdos, 6 mensuels ; `prune` + `check` (5 % des données) le dimanche
- Log : `/var/log/restic-backup.log` ; mail à `pierre@sans.pub` en cas d'échec (via SMTP de poste.io `mail.sans.pub:465`, donc **pas d'alerte si le serveur entier tombe**)
- Consulter : `ssh sans.pub 'sudo bash -c ". /root/restic/env && restic snapshots"'`
- **Ancien système (arrêté)** : Duplicity (`tecnativa/duplicity`) configuré dans `/root/backup/` sur le serveur (`docker-compose.yaml` + `config.json`, nov. 2020) → OVH Public Cloud Archive `backup-principal` (données) + Swift `backup-principal-metadata` (manifestes), région GRA. Conteneur et image absents du serveur au 2026-10-03 : plus aucun backup automatique. Ces deux fichiers contiennent des secrets (identifiants OVH, passphrase de chiffrement) — ne jamais les afficher ni les versionner.
- Suites (Vaultwarden, alerte externe, rotation des identifiants OVH) : voir `plan-migration.md`, phase 0.
## Mail (poste.io) — certificat TLS
- poste.io (`analogic/poste.io:2.4.11`) est derrière le nginx de CapRover : son renouvellement Let's Encrypt intégré ne fonctionne pas (certificat resté expiré du 2021-02-06 au 2026-10-03)
- Solution (2026-10-03) : `/root/mail-cert-sync.sh` (source : `mail/mail-cert-sync.sh`), cron `/etc/cron.d/mail-cert-sync` à 04:15. Si le certificat `mail.sans.pub` renouvelé par CapRover (`/captain/data/letencrypt/etc/live/mail.sans.pub/`) diffère de `/data/ssl/server.crt` de poste.io : copie + `21-certificate.sh` + rechargement Dovecot (SIGHUP) + redémarrage Haraka (quelques secondes). Log : `/var/log/mail-cert-sync.log`
- Le certificat ne couvre que **`mail.sans.pub`** : configurer les clients mail avec ce nom (pas `smtp.sans.pub`, qui donnerait une erreur de nom)
- Vérifier : `echo | openssl s_client -connect mail.sans.pub:993 2>/dev/null | openssl x509 -noout -enddate`
## Nextcloud — détails techniques
- **Cron** : service CapRover `nextcloud-cron` (image perso `img-captain-nextcloud-cron:4`, PHP 8.1, mai 2023 — Nextcloud lui-même tourne en PHP 8.3) qui monte le même volume `nextcloud-data` et lance `cron.php` toutes les 5 min. Mode `backgroundjobs_mode = cron`.
- Vérifier : `occ config:app:get core lastcron` (timestamp Unix, doit dater de < 10 min)
- ⚠️ Panne du 2026-08-09 au 2026-10-03 : le conteneur, non redémarré lors du redémarrage Docker du 2026-02-16, avait perdu la résolution DNS du réseau overlay (`getaddrinfo for srv-captain--nextcloud-db failed`). Corrigé par redémarrage via `appDefinitions/update` (définition complète renvoyée). Si ça se reproduit : même remède.
- **Rétention** (depuis le 2026-10-03) : `trashbin_retention_obligation` et `versions_retention_obligation` = `auto, 30` (suppression au plus tard après 30 jours ; avant, `auto` sans quota = jamais supprimé → corbeille de 28 Go)
- `occ` : `docker exec -u www-data <conteneur srv-captain--nextcloud.1> php occ ...`
## Suivi des tâches
Voir `todo.txt` (format todo.txt standard)
## Mettre à jour une app CapRover
**TOUJOURS passer par l'API CapRover** — jamais via `docker service update` directement (CapRover ne le verrait pas).
### Procédure
1. **Login** (obtenir un token) :
```python
import urllib.request, json
data = json.dumps({'password': 'CAPROVER_PASSWORD'}).encode()
req = urllib.request.Request('https://captain.cloud.sans.pub/api/v2/login', data=data, headers={'Content-Type': 'application/json'}, method='POST')
with urllib.request.urlopen(req) as r:
token = json.loads(r.read().decode())['data']['token']
```
2. **Déployer une nouvelle image** :
```python
headers = {'Content-Type': 'application/json', 'x-captain-auth': token, 'x-namespace': 'captain'}
captain_def = json.dumps({'schemaVersion': 2, 'imageName': 'IMAGE:TAG'})
payload = json.dumps({'appName': 'NOM_APP', 'captainDefinitionContent': captain_def, 'gitHash': ''}).encode()
req = urllib.request.Request('https://captain.cloud.sans.pub/api/v2/user/apps/appData/NOM_APP', data=payload, headers=headers, method='POST')
with urllib.request.urlopen(req) as r:
print(r.read().decode()) # {"status":100,"description":"Deploy is done"}
```
> Note : `curl` a un problème d'encodage dans cet environnement — utiliser `python3` à la place.
## Commandes utiles
```bash
# Voir les logs d'un service
ssh cloud@sans.pub "docker service logs srv-captain--NOM --tail 100"
# Espace disque des volumes
ssh cloud@sans.pub "docker system df -v"
# Nettoyer les images inutilisées
ssh cloud@sans.pub "docker image prune -a"
```
## Attention
- Debian 9 est obsolète (fin de support LTS)
- RAM : surveiller si ajout de nouveaux services (mail = 1,78 Go, scenari = 862 Mo)