From 8b961332ca2fabfe25e231c65df7a57c30849980 Mon Sep 17 00:00:00 2001 From: bolbol Date: Sat, 11 Jul 2026 22:50:01 +0000 Subject: [PATCH] =?UTF-8?q?runbook:=20mcp-vps=20(MCP=20VPS=20memory-node,?= =?UTF-8?q?=20Cloudflare=20Tunnel)=20=E2=80=94=2012/07/2026?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/mcp-vps.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 common/mcp-vps.md diff --git a/common/mcp-vps.md b/common/mcp-vps.md new file mode 100644 index 0000000..490fd07 --- /dev/null +++ b/common/mcp-vps.md @@ -0,0 +1,87 @@ +# 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 ✓