# mcp-vps — MCP Linux sur le VPS memory-node (12/07/2026) **Statut** : production. **Rôle** : organe d'action de Claude pendant un blackout NAS (complète le nœud mémoire — voir `common/vps-memory-node.md`). Zéro dépendance NAS au runtime. ## Accès - **URL publique** : `https://mcp.yesminedor.tn//mcp` — Cloudflare Tunnel, aucun port ouvert sur l'IP Contabo. Le secret (64 hex) est dans `/opt/memory-node/mcp-vps/.env` (600) et chez Nabil (Bitwarden/Telegram). **`/mcp` sans secret → 404.** - **Connecteur claude.ai « vps »** : URL complète avec secret, champs OAuth vides (même pattern que mcp-nas : le connecteur ne sait pas envoyer de header Authorization — Cloudflare Access écarté pour cette raison). - **Alerte Telegram** à chaque nouvelle session MCP (POST /mcp sans header `mcp-session-id`), anti-spam 60 s, non bloquante. Même bot que les alertes SSH claude-oversight. Audit : `NEW_SESSION | ip=` dans audit.log. ## Architecture - **Code** : `/opt/memory-node/mcp-vps` (git local = source de vérité en blackout ; miroir Gitea `bolbol/mcp-vps`). Dérivé de `bolbol/linux-mcp-nas` (run_command v2, _redact audit, monkey-patch output_schema, fix lifespan — voir runbooks mcp-nas). - **Stack compose** (2 services, réseau bridge dédié `mcp-vps`, aucun port publié) : - `linux-mcp-vps` : build local, `init`, `restart: always`, `cap_drop: ALL`, `no-new-privileges`, healthcheck GET /health /30s, `dns: 1.1.1.1`. - `cloudflared-mcp-vps` : `tunnel --no-autoupdate run`, `TUNNEL_TOKEN` via `.env`. - **Volumes** : `/opt/memory-node/git` et `/opt/memory-node/data` en **ro** → `/mnt/memory-node/{git,data}` (miroir bare nas-runbooks + répliques nyora-notes/context-hub ; subdirs seulement : n'expose ni `.restic-pass` ni les `.env` hôte) ; `./data` RW (audit.log, root:root 700) ; `./ssh-claude` → `/root/.ssh` (clé persistée au recreate). - **Outils MCP** : run_command, send_keys, capture_pane, list/new/kill/reset_session. PAS d'outils NyoraNotes (service NAS) : lire les répliques via run_command. - **Actions host** : `ssh vps.internal ''` (= claude-oversight@hôte via host-gateway, clé ed25519 dédiée `mcp-vps-claude`, authorized_keys restreinte `from="172.16.0.0/12"`, sudoers scopé inchangé — jamais NOPASSWD:ALL). - **Firewall** : ufw `allow proto tcp from 172.16.0.0/12 to any port 22` (containers→host ssh). ## Cloudflare (zone + tunnel) - Zone **yesminedor.tn** (plan Free) migrée OVH→Cloudflare le 11/07/2026 (NS `kallie`/`valentin.ns.cloudflare.com`). Compte CF `f47839fbf54ef0a5ed89db20e9a02902`. - Tunnel **`mcp-vps`** id `700c0fe6-2b2c-4e1a-abfa-3f94f2f7c909`, config gérée côté Cloudflare (`config_src: cloudflare`), ingress `mcp.yesminedor.tn → http://linux-mcp-vps:8000`, fallback 404. DNS : CNAME proxié `mcp → .cfargotunnel.com` (créé au dashboard, le token API n'a pas la permission zone). - **Token API CF** (`cfat_…`, account-owned, chez Nabil) — opérations : ``` # créer : POST /accounts//cfd_tunnel {"name":"…","config_src":"cloudflare"} # ingress : PUT /accounts//cfd_tunnel//configurations # token : GET /accounts//cfd_tunnel//token → TUNNEL_TOKEN .env # état : GET /accounts//cfd_tunnel/ → status: healthy ``` ⚠️ un token **account-owned** (`cfat_`) se vérifie sur `/accounts//tokens/verify`, PAS `/user/tokens/verify` (qui renvoie « Invalid API Token »). ## Rotations - **Chemin secret** : `openssl rand -hex 32` → `.env` → `docker compose up -d` → MAJ connecteur claude.ai. - **TUNNEL_TOKEN** : révocable/regénérable au dashboard Zero Trust ou par API (GET …/token) → `.env` → up. - **Clé SSH container** : regénérer dans `./ssh-claude` (via container éphémère, ownership root), remplacer la ligne dans `~claude-oversight/.ssh/authorized_keys` (garder `from="172.16.0.0/12"`). ## Gotchas (coûteux à redécouvrir) - **DNS docker cassé sur ce VPS** : le resolv.conf hôte pointe `10.100.0.1` (injoignable depuis les bridges docker) → build : `network: host` dans compose ; runtime : `dns: [1.1.1.1, 8.8.8.8]`. - **`cap_drop: ALL` ⇒ root conteneur SANS `CAP_DAC_OVERRIDE`** : tout volume doit être possédé par root avec modes cohérents (`data/` 700 root, `ssh-claude/` 700 root + clés 600), sinon Permission denied silencieux (audit.log, known_hosts). Créer/chown via container éphémère (`docker run --rm -v … chown 0:0 …`) — claude-oversight n'a pas le droit de chown vers root. - **Miroir bare lu par root conteneur** : `chmod -R o+rX` sur `nas-runbooks.git` (700 à la création) + `git config --system --add safe.directory "*"` dans l'image (dubious ownership). - **`umask 177` casse `git add`** (répertoires objets créés sans bit x → « insufficient permission for adding an object ») : garder umask 022 pour les opérations git ; réparer par `find .git/objects -type d ! -perm -u+x -exec chmod u+rwx {} +`. - **Jamais de recreate depuis mcp-vps lui-même** (suicide) : opérer via Tailscale SSH (`claude-oversight@100.94.90.119`). - Test local du MCP depuis le réseau docker : forcer `-H "Host: mcp.yesminedor.tn"` (anti DNS-rebinding du SDK, sinon 421). ## Test blackout (validé 12/07/2026) Via l'URL publique uniquement, zéro appel NAS : 1. `run_command`: `git -C /mnt/memory-node/git/nas-runbooks.git show main:common/vps-memory-node.md` ✓ 2. `run_command`: `ls /mnt/memory-node/data/{nyora-notes,context-hub}` ✓ 3. `run_command`: `ssh vps.internal 'hostname && id'` → claude-oversight sur l'hôte ✓ 4. Recreate `compose down/up` : audit.log + clé SSH persistés, tunnel reconnecte ✓