# 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). ## Correctifs APPLIQUÉS le 2026-07-08 - **P0 #1 — exposition fermée (nginx).** `/etc/nginx/conf.d/http.mcp-nas.conf` (backup `.bak-audit-20260708`). - **Piège connecteur claude.ai : il n'accepte PAS de header `Authorization` brut, uniquement OAuth (Client ID/Secret).** Le gate Bearer ne marche donc pas avec le connecteur. → auth par **CHEMIN SECRET** : `location // { proxy_pass http://localhost:3042/; }` (le `/` final strippe le préfixe). Le connecteur utilise l'URL `https://mcp-nas.nd.i234.me//mcp`, champs OAuth **vides**. - `/mcp` direct reste gaté par `Authorization: Bearer ` (pour scripts/curl). `/health` + stubs OAuth ouverts. - Le `` (64 hex) N'EST PAS dans ce runbook — il est chez Nabil (mémoire Claude Code + fichier NAS `linux-mcp-nas/` scratch). Rotation = regénérer + MAJ nginx + MAJ URL connecteur. - **P0 #2 — évasion hôte fermée (compose).** Mount `docker.sock` retiré + `security_opt: no-new-privileges` + `cap_drop: [ALL]` dans `docker-compose.yml` (backup `.bak-audit-20260708`). Recréé, `healthy`, `run_command` OK. **Reste (non fait) :** P0 #3 (clés SSH Contabo hors conteneur + pwd sudo), P1 #4 (rédaction audit.log + **rotation credentials**), run non-root (`user:1026:100`), token Gitea repo-scopé. ## Vérif rapide (depuis un poste externe) ```sh # Doit ÉCHOUER (401) — plus de RCE anonyme : curl -s -o /dev/null -w "%{http_code}\n" -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"}}}' # Doit MARCHER (200) via le chemin secret (URL du connecteur) : curl -s -X POST https://mcp-nas.nd.i234.me//mcp -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' -d '{...initialize...}' ```