From 9be001c4fceed877292689b68008067e0563343a18faed52f981f477e56b2073 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 25 Jun 2026 09:05:33 +0000 Subject: [PATCH] Update saiop-mcp-servers skill: docker-mcp widened to all tools (operator-approved), proxmox-mcp added (custom server + sshd ForceCommand hardening) --- .../saiop-mcp-servers/SKILL.md | 94 ++++++++++++++----- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/skills/saiop-infrastructure/saiop-mcp-servers/SKILL.md b/skills/saiop-infrastructure/saiop-mcp-servers/SKILL.md index daf9d22..3355908 100644 --- a/skills/saiop-infrastructure/saiop-mcp-servers/SKILL.md +++ b/skills/saiop-infrastructure/saiop-mcp-servers/SKILL.md @@ -1,16 +1,16 @@ --- name: saiop-mcp-servers -description: "MCP server setup for SAIOP (git, postgres read-only, docker read-only) — least-privilege patterns and what was deliberately NOT wired up." -version: 1.0.0 +description: "MCP server setup for SAIOP (git, postgres read-only, docker, proxmox snapshot access) — least-privilege patterns, and what was operator-approved to widen beyond the initial conservative defaults." +version: 2.0.0 author: Claude Code (SAIOP ops session) license: MIT platforms: [linux] prerequisites: env_vars: [MCP_POSTGRES_READONLY_PASSWORD] - commands: [uvx] + commands: [uvx, uv, ssh] metadata: hermes: - tags: [SAIOP, MCP, security, least-privilege, postgres, docker, git] + tags: [SAIOP, MCP, security, least-privilege, postgres, docker, git, proxmox, ssh] --- # MCP servers on SAIOP (Phase 6.3) @@ -32,27 +32,68 @@ Adding or auditing MCP tool access for Hermes (Deployment Guide §6.3). blocks non-SELECT statements at the app layer too. Connection string goes through the `DATABASE_URI` env var (checked the package source — `os.environ.get("DATABASE_URI", args.database_url)`), not a CLI arg, - since CLI args are visible to any local user via `ps aux`. -- **docker-mcp** (`uvx docker-mcp`) — **the npm/pip package itself has no - read-only mode.** It exposes `create-container` and `deploy-compose` - (full write/deploy) alongside `get-logs`/`list-containers`. Restricted via - Hermes's own `tools: {include: [...]}` allow-list in `config.yaml` to - just the two read-only tools — same enforcement mechanism already - field-verified for the Telegram platform-toolset restriction (a real - destructive command was confirmed blocked at the tool layer, not just - configured). **Do not change this to "all tools" without a deliberate - decision** — full Docker access would directly undermine the project's - core containment design (the `hermes` Linux user is deliberately denied - `docker compose`/restart-docker capability via scoped sudoers; an - unrestricted docker-mcp would hand that back through a side door). + since CLI args are visible to any local user via `ps aux`. `postgresql`'s + port is published to `127.0.0.1:5432` only (loopback) since Hermes runs + on the host, not in Docker, and can't resolve container names. +- **docker-mcp** (`uvx docker-mcp`) — **operator-approved widening:** + originally restricted to `get-logs`/`list-containers` only (the package + has no native read-only mode; `create-container`/`deploy-compose` are + full write/deploy). After an explicit flag-and-confirm round, the + operator chose to enable all 4 tools. This is a real, deliberate widening + of what Hermes can do to the host via MCP — if revisiting this decision, + remember the restriction mechanism is still available and field-proven + (`tools: {include: [...]}` in `config.yaml`, same mechanism verified for + the Telegram platform-toolset restriction) if you want to dial it back. +- **proxmox-mcp** — a **custom-written, single-tool MCP server** + (`/opt/ai-stack/hermes/scripts/proxmox_mcp.py`, run via + `uv run --with mcp python3 ...`), not an off-the-shelf package. Exposes + exactly one tool, `proxmox_qm(command)`, which SSHes to the Proxmox host + as the existing scoped `claude` account using a **dedicated keypair** + generated specifically for Hermes (not reusing the operator's desktop + key — separate blast radius per principal, same pattern as every other + dedicated credential in this project). + - **All three off-the-shelf SSH MCP packages checked (`mcp-server-ssh`, + `ssh-mcp-server`, `ssh-mcp`) were rejected before writing a custom one:** + `mcp-server-ssh` exposes a sprawling toolkit (`execute_command`, + `file_operations`, `process_manager`, `sftp_upload`/`download`, + `system_monitor`) — far more surface than needed. `ssh-mcp` (the + `mcpssh` package) was the narrowest (2 tools) but depends on an + external "Passman" credential-management service to resolve + connection details — an unwanted new dependency just to satisfy one + fixed host/user/key. `ssh-mcp-server` didn't expose a usable interface + for this case either. Writing ~60 lines directly against the `mcp` + Python SDK (`FastMCP`) was less work than adapting any of them, and + means the *only* security-relevant logic in the whole path is the + server-side sshd restriction (see below) — the MCP server itself is a + deliberately thin, unopinionated pipe. + - **The real enforcement is server-side, not in the MCP tool.** An sshd + `ForceCommand` drop-in on the Proxmox host (`Match User claude` in + `/etc/ssh/sshd_config.d/claude-restricted.conf`) routes every SSH + session for that account through a wrapper script + (`/usr/local/bin/claude-ssh-wrapper.sh`) that only permits + `qm snapshot/listsnapshot/status` patterns — regardless of what the + SSH client (the MCP tool, or anything else) requests. Verified directly: + allowed commands work, arbitrary commands (`whoami`, `cat /etc/passwd`) + are rejected, and a command-injection attempt via the snapshot-name + argument (`qm snapshot 141 test; whoami`) is rejected too (the wrapper + splits on whitespace and validates each token, not just regex-matching + the whole string). `AllowAgentForwarding`/`AllowTcpForwarding`/ + `PermitTTY` are all disabled in the same `Match` block. + - **Wrapper-script gotcha hit while building this:** a long `elif [[ ... =~ + ... ]]` line got an actual newline inserted into it during copy/paste + into `nano`, breaking the regex syntax (manifested as a confusing + `unexpected argument 'newline' to conditional binary operator` error — + not obviously a paste problem from the error text alone). Rewritten to + avoid any long lines at all (`read -ra parts <<< "$cmd"` + per-token + validation instead of one big regex) specifically so there's nothing + left for a wrap/paste to corrupt. + - I don't have write access to `/etc/ssh` or reload `sshd` myself — only + the scoped `claude` account, which can't escalate to root. The operator + applied the sshd-side changes directly; I verified them afterward over + SSH from the VM side. ## What was deliberately NOT wired up -- **SSH MCP** — no second system exists in this environment that needs SSH - access beyond the VM Hermes already runs on. The one genuinely sensitive - SSH target (the Proxmox hypervisor) is the project's hardest boundary — - don't wire SSH MCP access to it without the operator directly involved in - scoping exactly what it can reach. - **pfSense/MikroTik MCP** — no real device credentials exist in this environment to configure them safely. Needs the operator to provide scoped, read-only-by-default credentials first. @@ -68,3 +109,12 @@ hermes mcp test # verify a server connects and discover its tool non-interactive path — for a scripted/repeatable setup, hand-edit `mcp_servers:` in `config.yaml` directly instead (schema: `{command, args, env, enabled, tools: {include: [...]}}`). + +## If revisiting the Docker or Proxmox scope later + +- To re-restrict docker-mcp: add back + `tools: {include: ["get-logs", "list-containers"]}` under its entry. +- To extend proxmox-mcp's reach: the *only* place that matters is the sshd + wrapper on the Proxmox host, not this MCP server or Hermes's config — the + MCP tool will happily send any command, the wrapper decides what actually + runs.