nas-runbooks/common/mcp-nas-security-audit.md

5.4 KiB
Raw Blame History

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/mcpinitialize 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 (/registerclient_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)

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