Files
sans.pub/CLAUDE.md
T
Pierre MartinandClaude Opus 5.5 3fc9ae52c9 fix(mail): certificat TLS de poste.io synchronisé depuis CapRover
Le certificat de mail.sans.pub servi par poste.io avait expiré le
2021-02-06 : son renouvellement Let's Encrypt intégré ne peut pas
fonctionner derrière le nginx de CapRover. Le script mail/mail-cert-sync.sh
(cron quotidien) copie désormais le certificat renouvelé par CapRover
et recharge Dovecot/Haraka. L'alerte du backup n'a plus besoin de -k.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:43:22 +02:00

12 KiB

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) :

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)
    • 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) :
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']
  1. Déployer une nouvelle image :
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

# 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)