Author SHA1 Message Date
Pierre MartinandClaude Sonnet 5 7e5f15577a docs: nettoyage complet des fichiers malveillants restants + vérif elevenbook
Les scripts p0_*.sh et un fichier .s3w étaient encore sur le disque
(seule la référence dans .gitconfig avait été nettoyée le 09-22).
Repéré en vérifiant une alerte Gitea sans rapport sur elevenbook
("hooks broken", finalement bénigne). IOC désormais absents du volume,
pas de réinfection, aucun process malveillant actif.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 01:25:43 +02:00
Pierre MartinandClaude Sonnet 5 70e9476420 docs: retire la recommandation de restriction réseau /api/internal/*
INTERNAL_TOKEN est le bon périmètre d'authentification pour cet endpoint,
pas l'adresse réseau — une restriction reverse-proxy serait redondante.
Le token a de toute façon été roté, ce qui referme ce point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 01:13:02 +02:00
Pierre MartinandClaude Sonnet 5 2e0ae9a91c docs: upgrade Gitea 1.25.4 -> 1.27.3 + leçon sur appDefinitions/update
- Backup complet (dump SQL + volume) avant upgrade
- Documente l'incident secondaire (502 temporaire sur git.sans.pub) causé
  par un payload appDefinitions/update incomplet ayant réinitialisé
  containerHttpPort à sa valeur par défaut, et le correctif appliqué
- Ajoute un avertissement dans CLAUDE.md : cette API remplace la config,
  ne la fusionne pas

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 01:11:03 +02:00
Pierre MartinandClaude Sonnet 5 66c356848b docs: rotation des secrets Gitea effectuée + CLI CapRover documenté
- INTERNAL_TOKEN, LFS_JWT_SECRET et mot de passe MySQL rotés le 2026-09-23
- DB_PASSWD CapRover resynchronisé, service redémarré et vérifié
- Confirmation que les env vars CapRover de l'app gitea (DB_PASSWD etc.)
  ne sont pas au format GITEA__section__clé donc inertes pour app.ini
- Documentation de l'installation/usage du CLI caprover via fnm

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 00:51:58 +02:00
Pierre MartinandClaude Sonnet 5 8f584a8672 docs: documente l'incident cryptominer Gitea et détails infra découverts
Incident du 2026-09-14 : injection de packObjectsHook via l'API interne
Gitea pour déployer un mineur Monero (XMRig). Contenu et nettoyé le
2026-09-22. Voir incidents/2026-09-14-gitea-cryptominer.md pour le détail
complet (analyse, IOC, remédiation, reste à faire).

Ajout également des détails techniques sur le setup Gitea (containers,
volumes, réseau) découverts pendant l'investigation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 00:00:40 +02:00
2 changed files with 155 additions and 2 deletions
+32 -2
View File
@@ -4,9 +4,9 @@ Lorsque tu interroges le serveur, ne lances que des commandes en LECTURE SEULE (
## Accès
- **SSH**: `ssh cloud@sans.pub`
- **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
- **Utilisateur**: cloud (gestion Docker/Caprover)
- **Gitea**: `git.sans.pub` (port SSH 2242, `User git` dans `~/.ssh/config`)
## Infrastructure
@@ -78,6 +78,36 @@ Aucun service en erreur connu (vérifié 2026-02-19). `mail` tourne correctement
**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.
## Suivi des tâches
Voir `todo.txt` (format todo.txt standard)
+123
View File
@@ -0,0 +1,123 @@
# Incident : mineur de cryptomonnaie via Gitea (packObjectsHook)
- **Statut** : contenu, secrets serveur rotés et Gitea mis à jour vers 1.27.3 (2026-09-23). Reste : mots de passe utilisateurs/tokens/webhooks.
- **Date de compromission** : 2026-09-14 ~15:16-15:17
- **Date de détection** : 2026-09-22 (via un `git clone` cassé)
- **Impact** : exécution de code sur le serveur hébergeant Gitea (et potentiellement l'hôte partagé, ~40 services CapRover) ; minage de cryptomonnaie (Monero) actif pendant ~8 jours
## Découverte
Un `git clone` sur `git.sans.pub` échouait systématiquement (SSH et HTTPS) avec :
```
remote: wget: server returned error: HTTP/1.1 404 Not Found
remote: curl: (22) The requested URL returned error: 404
fatal: fin de fichier prématurée
fatal: fetch-pack : sortie d'index de pack invalide
```
Le problème touchait **tous les dépôts**, pas un seul — signe d'un problème serveur global plutôt qu'un souci de repo.
## Analyse
`GIT_TRACE_PACKET=1` a montré que le serveur, juste après avoir annoncé `packfile`, envoyait sur le canal d'erreur (sideband 2) le texte `wget: ... 404` / `curl: ... 404` puis coupait la connexion — donc un script serveur s'exécute pendant la génération du pack, avant même l'envoi des données.
Cause trouvée dans `/data/gitea/home/.gitconfig` (dans le volume Docker `captain--gitea-data`) : **13 lignes `[uploadpack] packObjectsHook = sh /data/gitea/home/p0_XXXXXXXX.sh` injectées**, chacune maquillée en fin de ligne par un faux commentaire imitant une entrée de log Gitea :
```
;#router: completed POST /api/internal/manager/add-logger for 10.0.1.105:XXXXX, 200 OK ...
```
Cette technique fait croire, à la lecture rapide du fichier, qu'il s'agit d'une ligne de log et non de config active.
### Vecteur d'entrée (hypothèse la plus probable)
L'endpoint interne Gitea `/api/internal/manager/add-logger` (normalement réservé à un usage interne, protégé par `INTERNAL_TOKEN`) semble avoir été atteignable depuis l'extérieur via le reverse-proxy public (l'IP source `10.0.1.105` observée dans les logs correspond au conteneur `captain-nginx`, donc toutes les requêtes publiques passent par cette IP côté Gitea — la requête malveillante est arrivée par le chemin HTTP public normal). Ceci est cohérent avec des vulnérabilités connues de Gitea permettant l'injection de config git via cette API interne mal exposée.
**Décision** : pas de restriction réseau supplémentaire sur `/api/internal/*` au niveau du reverse-proxy. L'authentification par `INTERNAL_TOKEN` est le bon périmètre de sécurité pour cet endpoint (pas l'adresse IP source) ; ajouter un blocage réseau serait redondant. Le vecteur réel reste donc **la compromission du `INTERNAL_TOKEN` lui-même** (fuite non identifiée), pas son exposition réseau — token désormais roté (voir plus bas), ce qui referme ce point sans action supplémentaire.
**À vérifier/confirmer** : version exacte de Gitea au moment de l'attaque (`gitea/gitea:1.25.4`), CVE correspondant à une éventuelle fuite/faiblesse du `INTERNAL_TOKEN`.
## Charge malveillante
Les scripts injectés (`p0_c9cd952f.sh`, `p0_9759db49.sh`) :
1. Téléchargent un binaire déguisé : `https://sunnyeye.solarvest.my/health.bin` → sauvegardé sous `/data/gitea/.sys_health_s3`
2. Écrivent une config **XMRig** (JSON encodé en base64 dans le script) pointant vers des pools Monero (`gulf.moneroocean.stream`, `pool.hashvault.pro`) avec une adresse de wallet dédiée
3. Assurent la persistance :
- via `crontab` (`*/3 * * * *` + `@reboot`) si `cron`/`crond` est disponible
- sinon via une boucle `sh -c 'while sleep 180; do <watchdog>; done'` détachée (`nohup`)
4. Se camouflent sous des noms plausibles : `.sys_health_s3`, `.gitea_cron_health`, `.wp_s2_cron`, `.gitea_watchdog.pid`
Le mineur tournait en continu depuis le 2026-09-14 (le conteneur Gitea n'avait pas redémarré depuis), consommant ~298% CPU / ~2.4 Go RAM au moment de la détection. Il a cessé de fonctionner correctement quand le domaine `sunnyeye.solarvest.my` s'est mis à répondre 404 — c'est cet échec de téléchargement qui a cassé les clones et permis la détection.
## Remédiation effectuée (2026-09-22)
- [x] Process du mineur et du watchdog tués (`kill -9` sur les PID hôte)
- [x] Fichiers de persistance supprimés : `.sys_health_s3`, `.sys_health_s3.json`, `.sys_health_s3.log`, `.gitea_cron_health`, `.gitea_watchdog.pid`
- [x] `.gitconfig` nettoyé des 13 lignes `packObjectsHook` injectées (backup conservé : `/data/gitea/home/.gitconfig.bak-20260922234958`)
- [x] Clone git re-testé et fonctionnel
- [x] Audit de la base Gitea (voir ci-dessous) : aucune autre persistance trouvée
- [x] **(2026-09-23, oubli corrigé)** Les scripts malveillants eux-mêmes (`p0_c9cd952f.sh`, `p0_9759db49.sh`) et un fichier sonde (`.s3w`) étaient toujours présents sur le disque (`/data/gitea/home/`) — seule la référence dans `.gitconfig` avait été nettoyée le 09-22, pas les fichiers. Repéré en creusant une alerte Gitea sans rapport ("hooks broken" sur `elevenbook`, finalement bénigne — hash identique à un repo sain, juste pas repoussé depuis 2023). Fichiers supprimés, recherche exhaustive des IOC sur tout le volume confirmée vide, aucun process malveillant actif, `.gitconfig` toujours propre (pas de réinfection).
## Audit Gitea (base de données) — résultat
Aucune anomalie détectée au-delà de l'injection `packObjectsHook` :
| Élément vérifié | Résultat |
|---|---|
| Comptes utilisateurs | 3 comptes (`pierre` admin, `Sans.pub`, `Duogeeks`), tous antérieurs à l'incident |
| Clés SSH publiques | 3 clés, aucune ajout suspect autour du 09-14 |
| Tokens d'accès personnels | 4 tokens, tous antérieurs et cohérents avec l'usage habituel |
| Webhooks | 4 webhooks de déploiement CapRover, tous de 2020-2021 |
| Sources de login externes | 1 seule, préexistante (2020) |
| Collaborateurs sur les repos | Rien d'ajouté récemment |
| Organisations | Aucune organisation suspecte |
**Conclusion** : l'attaque semble limitée à l'exécution de commande via le hook git (cryptojacking), sans prise de compte ni persistance au niveau applicatif Gitea. Mais l'attaquant avait un accès en lecture au système de fichiers du conteneur, donc à `app.ini` (secrets en clair) — voir rotation ci-dessous.
## Rotation des secrets — effectuée le 2026-09-23
Confirmé via `docker service inspect` puis via l'API CapRover (double vérification) : les variables d'env CapRover de l'app `gitea` sont `RUN_MODE`, `DB_TYPE`, `DB_HOST`, `DB_USER`, `DB_PASSWD` — aucune au format `GITEA__<section>__<clé>` attendu par l'image officielle pour être réinjectée dans `app.ini`. Elles sont donc **inertes** pour Gitea (c'est `app.ini` dans le volume qui fait foi). Conséquence : `INTERNAL_TOKEN`/`LFS_JWT_SECRET` n'ont pas de variable CapRover à mettre à jour ; seul `DB_PASSWD` avait une copie dormante à resynchroniser.
Effectué :
- [x] `INTERNAL_TOKEN` régénéré (`gitea generate secret INTERNAL_TOKEN`) dans `app.ini`
- [x] `LFS_JWT_SECRET` régénéré (`gitea generate secret LFS_JWT_SECRET`) dans `app.ini`
- [x] Mot de passe MySQL de l'utilisateur `gitea`@`%` changé (`ALTER USER ... IDENTIFIED BY ...`)
- [x] `app.ini` mis à jour avec les 3 nouvelles valeurs (backup : `app.ini.bak-20260923004549`)
- [x] Service Gitea redémarré (`docker service update --force srv-captain--gitea`), reconnexion DB confirmée dans les logs, aucune erreur
- [x] Clone git testé et fonctionnel après rotation
- [x] Variable CapRover `DB_PASSWD` resynchronisée via l'API (`/user/apps/appDefinitions/update`), vérifiée par comparaison de hash
Outillage utilisé : CLI `caprover` (installé localement via `fnm` + `npm install -g caprover`, authentifié avec `caprover login` — voir section CapRover CLI dans `CLAUDE.md`).
## Mise à jour Gitea 1.25.4 → 1.27.3 — effectuée le 2026-09-23
Corrige 15 advisories de sécurité publiées le 2026-08-29 (dont au moins 2 RCE en sévérité haute : bypass d'approbation sur les runners self-hosted, RCE via rendu externe de markup). Aucune ne correspond exactement au vecteur de cet incident (API interne), mais l'upgrade était largement justifiée.
Effectué :
- [x] Backup complet avant upgrade : dump SQL (`mysqldump`, 114 tables) + tarball du volume de données complet (2,1 Go), dans `/home/zuck/backups/gitea-upgrade-20260923005619/` sur le serveur
- [x] Déploiement de `gitea/gitea:1.27.3` via l'API CapRover (`POST /user/apps/appData/gitea` avec `captainDefinitionContent`)
- [x] Démarrage vérifié sans erreur de migration, `Git version: 2.54.0`
### Incident secondaire causé pendant l'upgrade (auto-résolu le jour même)
La mise à jour CapRover de `DB_PASSWD` faite plus tôt (rotation des secrets, cf section dédiée) avait été envoyée avec un payload **incomplet** — le champ `containerHttpPort` n'était pas inclus. CapRover l'a réinitialisé à sa valeur par défaut (`80`) au lieu de conserver `3000` (le port réel utilisé par l'image officielle Gitea). Conséquence : **`git.sans.pub` a répondu en 502 pendant plusieurs minutes** (visible dans les logs nginx avec du trafic réel externe), le temps de diagnostiquer (`connect() failed (111: Connection refused)` vers `10.0.1.x:80`) et de renvoyer un payload complet avec `containerHttpPort: 3000` restauré.
**Leçon** : toute mise à jour via `POST /user/apps/appDefinitions/update` doit **toujours renvoyer l'intégralité des champs de la définition actuelle** (récupérés via un `GET /user/apps/appDefinitions` juste avant), pas seulement le(s) champ(s) qu'on veut changer — l'API ne fait pas de merge partiel, elle remplace.
## Reste à faire
- [ ] Changer les mots de passe des 3 comptes Gitea (pierre, Sans.pub, Duogeeks) par précaution
- [ ] Révoquer et régénérer les 4 tokens d'accès personnels (LML, Piaire CI/Deploy, Piaire, Laptop .npmrc) — penser à mettre à jour partout où ils sont utilisés
- [ ] Régénérer les 4 tokens de webhook CapRover (déploiement tmp-fc, dechets-agglo-muretain, etherpad, domi-marche) et mettre à jour les URLs de webhook correspondantes dans Gitea
- [ ] Vérifier l'intégrité des autres services sur le même hôte (mouvement latéral) — rien trouvé à ce stade, mais l'hôte partagé fait tourner Vaultwarden, Nextcloud, mail
- [ ] Envisager un scan antirootkit / vérification d'intégrité des binaires système sur l'hôte, vu la durée de la compromission (8 jours)
- [ ] Revoir si `DOMAIN = localhost` dans `app.ini` (`[server]`) est correct ou un reliquat de config
## Indicateurs de compromission (IOC)
- Domaine : `sunnyeye.solarvest.my`
- Fichiers : `p0_*.sh`, `.sys_health_s3`, `.sys_health_s3.json`, `.sys_health_s3.log`, `.gitea_cron_health`, `.gitea_watchdog.pid`, `.wp_s2_cron`, `.wp_s2_wd.pid`, `.s3w`
- Chemins ciblés pour la persistance (dans cet ordre) : `/data/gitea`, `/data/git`, `/home/git`, `/var/tmp`, `/dev/shm`, `/tmp`, `$HOME`
- Pattern de commentaire d'obfuscation dans les configs git : `;#router: completed POST /api/internal/manager/add-logger for <ip>, 200 OK ...`