diff --git a/_INDEX.md b/_INDEX.md index 968153b..3e80aad 100644 --- a/_INDEX.md +++ b/_INDEX.md @@ -11,6 +11,7 @@ |---------|------|------| | [Acces Bifrost (passerelle LLM, vision/OCR)](common/bifrost-access.md) | 2026-06-14 | infra, bifrost, llm, vision, x-bf-vk | | [Audit securite reseau NAS (surface WAN, crowdsec, firewall, wg-easy)](common/audit-securite-reseau-nas-20260703.md) | 2026-07-03 | infra, securite, reseau, crowdsec, firewall, wg-easy, wan | +| [Audit securite mcp-nas — RCE root ANONYME exposee Internet (docker.sock, secrets clairs audit.log, correctifs P0 + rotation)](common/mcp-nas-security-audit.md) | 2026-07-08 | infra, securite, mcp-nas, rce, docker-socket, secrets, rotation | | [Fragilites de dependances stack 43 containers (restart/persistance/couplages)](common/fragilites-dependances-stack-20260703.md) | 2026-07-03 | infra, stack, restart, couplage, dependances, rla-api, n8n | | [Correctifs fragilites appliques (rla-api compose, n8n/trilium restart, bifrost-proxy healthcheck)](common/correctifs-fragilites-application-20260704.md) | 2026-07-04 | infra, correctifs, rla-api, n8n, bifrost-proxy, healthcheck | | [nyora-doc-api mode template — cause racine non-restyle des runs (rPr > styles) + correctif restyle opt-in proposé](common/nyora-doc-api-template-restyle-cause-racine.md) | 2026-07-08 | infra, nyora-doc-api, docx, pptx, template, cause-racine | diff --git a/common/mcp-nas-security-audit.md b/common/mcp-nas-security-audit.md new file mode 100644 index 0000000..e5d7de9 --- /dev/null +++ b/common/mcp-nas-security-audit.md @@ -0,0 +1,49 @@ +# Audit sécurité mcp-nas — RCE root anonyme exposée Internet (2026-07-08) + +**Tags** : infra, securite, mcp-nas, rce, docker-socket, secrets, rotation +**Périmètre** : conteneur `linux-mcp-nas` (`mcp-nas.nd.i234.me`, port hôte 3042→8000, réseau Docker `n8n`). +**Complète** : `common/audit-securite-reseau-nas-20260703.md` (couche réseau/WAN). Ici = **couche applicative**. +**Statut** : audit seul, correctifs **NON appliqués** (décision Nabil requise — rotation + coupure exposition = disruptif). + +## Verdict + +**Compromission totale possible par un attaquant anonyme sur Internet.** Chaîne vérifiée depuis un poste externe, sans aucun credential : + +1. **Endpoint MCP sans authentification.** `POST https://mcp-nas.nd.i234.me/mcp` → `initialize` répond, `tools/list` renvoie `run_command`. Le bloc nginx (`deploy/nginx-reverse-proxy.conf`) fait `proxy_pass http://localhost:3042` **sans `auth_basic`, sans allowlist IP, sans vérif de token**. Les routes OAuth du serveur (`/register` → `client_id:"public"`, `/token`, `/authorize`, `/.well-known/...`) sont des **stubs décoratifs** : elles ne valident aucun bearer. FastMCP n'a aucun auth provider. → **shell arbitraire pour tout le monde**. +2. **Évasion vers root hôte via `docker.sock`.** Le conteneur tourne en **root** (uid=0) avec `/var/run/docker.sock` monté (RW). L'API Docker est joignable (`curl --unix-socket /var/run/docker.sock http://localhost/version` OK). → créer un conteneur privilégié montant `/` = **root sur le NAS**. Le binaire `docker` n'est pas installé mais le socket suffit (curl). Le serveur MCP **n'utilise jamais** ce socket (aucune référence dans le code) → mount inutile. +3. **Pivot latéral.** Clés SSH privées `/root/.ssh/claude_oversight_contabo{,_v2,_v3}` **dans le conteneur** → accès VPS Contabo. +4. **2e chemin root hôte.** Le conteneur ssh vers `Best0f@172.17.0.1:22222` (host-gateway) avec le mot de passe sudo passé en clair → sudo hôte. + +## audit.log = coffre à secrets en clair + +`/mnt/docker/linux-mcp-nas/audit.log` (volume hôte RW, **lisible par la RCE**) journalise chaque commande entière. Relevé au 2026-07-08 : mot de passe sudo NAS en clair **266×**, **102** URLs `http://bolbol:MDP@.../repo.git`, mots de passe n8n / Portainer, **127** clés Bifrost (`x-bf-vk`), **19** Bearer. Le mot de passe maître est **réutilisé** (même préfixe pour sudo / n8n / Portainer). → un attaquant `cat` le log et récupère tout le trousseau. + +## Autres surfaces + +- **Volumes RW** : `/volume1/docker` monté RW → tous les `.env` de l'infra (43 conteneurs) lisibles ET modifiables par la RCE. Doit être `:ro`. +- **Denylist** (`DENY_ANCHORED`/`DENY_GLOBAL` dans `linux_mcp_server.py`) **contournable trivialement** : segment après `&&`/`;`, `find -delete`, `python -c`, docker.sock, ssh+sudo. C'est de la défense en profondeur, **pas une frontière de sécurité** — ne jamais s'y fier. +- **Contournement Gitea-API-HTTP** (quand git absent du NAS) : utilise le **mot de passe du compte `bolbol` complet** dans l'URL de clone (→ audit.log + `argv` visible en `ps`), au lieu d'un token scopé. Sur-privilège (accès à tous les repos) + fuite. +- **Gemini : aucune fuite.** Le serveur ne référence ni Gemini ni Bifrost (grep code = 0 résultat) ; la sortie texte ne part que vers NyoraNotes. La frontière « exclusif Claude » tient au niveau code : aucune commande mcp-nas n'alimente un prompt Gemini (Gemini n'est câblé que pour la vision dans Bifrost, et mcp-nas ne produit pas d'image). + +## Correctifs priorisés + +**P0 — urgence (RCE root exposée) :** +1. **Couper l'exposition publique** : retirer la règle nginx `mcp-nas.nd.i234.me` OU la restreindre au réseau WireGuard uniquement (pas de `proxy_pass` depuis le WAN). Si un accès distant reste nécessaire : `auth_request` nginx ou un secret partagé obligatoire (header) + implémenter une vraie validation bearer côté serveur. +2. **Supprimer le mount `docker.sock`** du `docker-compose.yml` (jamais utilisé par le code). +3. **Run non-root** : `user: "1026:100"` + `cap_drop: [ALL]` + `security_opt: no-new-privileges`. +4. Ne plus embarquer le mot de passe sudo dans les commandes ; **sortir les clés SSH** du conteneur (ou les monter en lecture seule depuis un secret) ; monter `/mnt/docker` en **`:ro`** (garder RW uniquement le sous-chemin strictement nécessaire). +5. **Rédiger les secrets** avant écriture dans audit.log (regex password/token/bearer/x-bf-vk → `***`) + `chmod 600` + rotation du fichier. +6. **ROTATION complète** des credentials fuités : mot de passe sudo NAS `Best0f`, n8n, Portainer `bestof`, compte Gitea `bolbol`, **toutes** les clés virtuelles Bifrost, clés SSH Contabo. + +**P1 :** token Gitea repo-scopé (au lieu du mot de passe bolbol) via credential helper, jamais dans l'URL ; rate-limiting/fail2ban sur l'endpoint. + +**P2 :** healthcheck ne teste que `/health` pas `/mcp` (connu) ; dérive doc `MCP_MAX_OUTPUT_CHARS` (README dit 2000, défaut code 8000). + +## Vérif rapide (depuis un poste externe, doit ÉCHOUER après P0) +```sh +curl -s https://mcp-nas.nd.i234.me/health # ne doit plus répondre sans auth +curl -s -X POST https://mcp-nas.nd.i234.me/mcp \ + -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"x","version":"0"}}}' +# tant que ça renvoie un result MCP sans token → RCE ouverte +```