generated from atu/saiop-infrastructure
Update saiop-mcp-servers skill: docker-mcp widened to all tools (operator-approved), proxmox-mcp added (custom server + sshd ForceCommand hardening)
This commit is contained in:
@@ -1,16 +1,16 @@
|
|||||||
---
|
---
|
||||||
name: saiop-mcp-servers
|
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."
|
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: 1.0.0
|
version: 2.0.0
|
||||||
author: Claude Code (SAIOP ops session)
|
author: Claude Code (SAIOP ops session)
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
prerequisites:
|
prerequisites:
|
||||||
env_vars: [MCP_POSTGRES_READONLY_PASSWORD]
|
env_vars: [MCP_POSTGRES_READONLY_PASSWORD]
|
||||||
commands: [uvx]
|
commands: [uvx, uv, ssh]
|
||||||
metadata:
|
metadata:
|
||||||
hermes:
|
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)
|
# 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
|
blocks non-SELECT statements at the app layer too. Connection string goes
|
||||||
through the `DATABASE_URI` env var (checked the package source —
|
through the `DATABASE_URI` env var (checked the package source —
|
||||||
`os.environ.get("DATABASE_URI", args.database_url)`), not a CLI arg,
|
`os.environ.get("DATABASE_URI", args.database_url)`), not a CLI arg,
|
||||||
since CLI args are visible to any local user via `ps aux`.
|
since CLI args are visible to any local user via `ps aux`. `postgresql`'s
|
||||||
- **docker-mcp** (`uvx docker-mcp`) — **the npm/pip package itself has no
|
port is published to `127.0.0.1:5432` only (loopback) since Hermes runs
|
||||||
read-only mode.** It exposes `create-container` and `deploy-compose`
|
on the host, not in Docker, and can't resolve container names.
|
||||||
(full write/deploy) alongside `get-logs`/`list-containers`. Restricted via
|
- **docker-mcp** (`uvx docker-mcp`) — **operator-approved widening:**
|
||||||
Hermes's own `tools: {include: [...]}` allow-list in `config.yaml` to
|
originally restricted to `get-logs`/`list-containers` only (the package
|
||||||
just the two read-only tools — same enforcement mechanism already
|
has no native read-only mode; `create-container`/`deploy-compose` are
|
||||||
field-verified for the Telegram platform-toolset restriction (a real
|
full write/deploy). After an explicit flag-and-confirm round, the
|
||||||
destructive command was confirmed blocked at the tool layer, not just
|
operator chose to enable all 4 tools. This is a real, deliberate widening
|
||||||
configured). **Do not change this to "all tools" without a deliberate
|
of what Hermes can do to the host via MCP — if revisiting this decision,
|
||||||
decision** — full Docker access would directly undermine the project's
|
remember the restriction mechanism is still available and field-proven
|
||||||
core containment design (the `hermes` Linux user is deliberately denied
|
(`tools: {include: [...]}` in `config.yaml`, same mechanism verified for
|
||||||
`docker compose`/restart-docker capability via scoped sudoers; an
|
the Telegram platform-toolset restriction) if you want to dial it back.
|
||||||
unrestricted docker-mcp would hand that back through a side door).
|
- **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
|
## 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
|
- **pfSense/MikroTik MCP** — no real device credentials exist in this
|
||||||
environment to configure them safely. Needs the operator to provide
|
environment to configure them safely. Needs the operator to provide
|
||||||
scoped, read-only-by-default credentials first.
|
scoped, read-only-by-default credentials first.
|
||||||
@@ -68,3 +109,12 @@ hermes mcp test <name> # verify a server connects and discover its tool
|
|||||||
non-interactive path — for a scripted/repeatable setup, hand-edit
|
non-interactive path — for a scripted/repeatable setup, hand-edit
|
||||||
`mcp_servers:` in `config.yaml` directly instead (schema:
|
`mcp_servers:` in `config.yaml` directly instead (schema:
|
||||||
`{command, args, env, enabled, tools: {include: [...]}}`).
|
`{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.
|
||||||
|
|||||||
Reference in New Issue
Block a user