Project · gcp-mcp-standalone

gcp-mcp-standalone — Security Review (2026-07-12, full system)

type referencestatus activegcp · mcp · security · review

Scope

Full end-to-end review of the live system as it stands today: Terraform infra (main.tf), Ansible (install.yml, claudebot.yml, status-dashboard.yml), the Firney-MCP-Broker + all 11 connectors (including the four added since the last full review — GCP, Tailscale, GitLab, Adzuna), the Claude Telegram bot, the status dashboard, secrets handling, and network exposure. Supersedes the 2026-07-03 review as the current source of truth. Findings verified against the running box (Tailscale SSH + the broker's own read-only MCP tools) on 2026-07-12, not from memory.

Summary

The architecture remains sound and has scaled cleanly: every connector added since 2026-07-03 is read-only, credential-isolated, and follows the same hardened pattern; the new dashboard was built keyless and locked down as hard as the rest of the box. No new HIGH or MEDIUM finding is introduced by any of the new code. The open items are the same carry-forward hardening/hygiene findings from the 2026-07-03 review that still need Bob-actions — none regressed by the new work. The one genuinely-new capability is the dashboard's impersonation grant, reviewed and accepted.

Closure status of the 2026-07-03 findings (verified live 2026-07-12)

  • H1 — rotate exposed secrets (Telegram / Notion / GitHub): OPEN, needs Bob. Rotation can't be verified externally — confirm each was regenerated + the Secret Manager version bumped. Highest priority.
  • M1 — broker /admin/* internet-reachable: STILL OPEN. POST bobsmcp.uk/admin/keys → 401 (reachable, key-gated only); no edge Access policy.
  • M2 — no data/ durability: STILL OPEN. Tokens/keys still only on the boot disk; a rebuild wipes them (has bitten twice).
  • M3 — GitHub connector repo scope (read/write all private repos): STILL OPEN. Connector still requests scopes=["repo", "read:org"].
  • M4 — Telegram DM policy pairing not allowlist: STILL OPEN. access.json on the box shows dmPolicy: pairing, allowFrom = 1 (only Bob).
  • L1 — broker token DBs mode 644 (world-readable): STILL OPEN. data/{broker_keys,tokens,inbound_oauth}.db all -rw-r--r-- (Fernet-encrypted).

New surface since 2026-07-03 — audited clean

  • Four new connectors (GCP, Tailscale, GitLab, Adzuna) — all Native auth_mode=none, read-only, with input validation that blocks path/query injection (regex-guarded, URL-encoded refs; allow-listed enums; clamped pagination), targets pinned server-side (never tool params), and sanitized ValueError-only errors that never leak URL/body/token. Adzuna deliberately keeps its app_key (which rides in the request URL) out of every error message. GCP is the one connector with any write power (instance stop/start/reset) — deliberate, least-privilege via custom role mcpBrokerOps. Secrets fetched from Secret Manager with no_log; all 11 in allowed_connectors. Live-verified: the connector tools return only infra metadata, no keys/tokens.
  • Status dashboard — has its own live-verified review (docs/security-review-2026-07-10-status-dashboard.md), folded in here. Carry- forward: M1(dash) the impersonation grant is a new (accepted) capability — mcp-broker-sa can now mint tokens for the read-only poller SA; M2(dash) Cloudflare Access is the only gate on status.bobsmcp.uk (verified live: unauth → 302 to the Access login; Bob must confirm the policy is email-restricted, not "Everyone"). Poller/site code re-reviewed: keyless impersonation, atomic snapshot writes, per-source failure isolation, the schema leakage guard, and an XSS-safe frontend + strict CSP all confirmed.

Positive controls (verified live 2026-07-12)

  • No public inbound app ports. Only listeners: 127.0.0.1:8002 (broker), 127.0.0.1:8080 (dashboard nginx), cloudflared-local, tailscaled, and sshd (firewall-shielded → tailnet-only). Nothing binds 0.0.0.0.
  • Broker + dashboard localhost-only (the 0.0.0.0 mistake stays fixed); /admin and /proxy both 401 without credentials.
  • Least-privilege, keyless IAM — no static GCP credential anywhere (metadata token + impersonation, both short-lived); the poller SA has zero write verbs.
  • Secrets in Secret Manager, fetched with no_log; .env mode 600; no secret values committed in any of the three repos.
  • Bot isolation unchanged — unprivileged, metadata-blocked, read-only allow-list, single-sender allowlist.

Remediation applied 2026-07-12 (same day)

  • M1 — CLOSED (verified live). Cloudflare WAF rule blocks bobsmcp.uk/admin/* at the edge (403, server: cloudflare); a new origin middleware (AdminTunnelGuardMiddleware) 404s any tunnelled /admin request as a backstop. /proxy/* and the local ssh -L admin path are unaffected.
  • M2 — CLOSED (verified end-to-end). Private versioned GCS bucket + hourly systemd backup timer + restore-on-fresh-VM step; a backup/restore round-trip reconstructs all three token DBs intact.
  • M4 — CLOSED. Telegram dmPolicyallowlist.
  • L1 — CLOSED. data/*.db0600, persisted in the deploy.
  • M3 — ACCEPTED (unchanged). Bob uses GitHub write through the broker, so the repo scope stays.
  • H1 — STILL OPEN (Bob): rotate the three exposed secrets. M2(dash) — STILL OPEN (Bob): confirm the status.bobsmcp.uk Access policy is email-restricted.
  1. H1 — confirm/rotate the three exposed secrets + Secret Manager bump (Bob).
  2. M1 — Cloudflare Access policy (or scoped tunnel ingress) on /admin/*. ✅ done
  3. M2 — persist / back up data/. ✅ done
  4. M2(dash) — confirm the status.bobsmcp.uk Access policy is email-restricted.
  5. M4 — switch Telegram to allowlist. ✅ done
  6. M3 — narrow the GitHub scope if write isn't needed. (accepted — write in use)
  7. L1chmod 600 data/*.db. ✅ done
Compiled from wiki/projects/gcp-mcp-standalone/security-review.md · git is the source of truth