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
reposcope (read/write all private repos): STILL OPEN. Connector still requestsscopes=["repo", "read:org"]. - M4 — Telegram DM policy
pairingnotallowlist: STILL OPEN.access.jsonon the box showsdmPolicy: pairing,allowFrom= 1 (only Bob). - L1 — broker token DBs mode 644 (world-readable): STILL OPEN.
data/{broker_keys,tokens,inbound_oauth}.dball-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 sanitizedValueError-only errors that never leak URL/body/token. Adzuna deliberately keeps itsapp_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 rolemcpBrokerOps. Secrets fetched from Secret Manager withno_log; all 11 inallowed_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-sacan now mint tokens for the read-only poller SA; M2(dash) Cloudflare Access is the only gate onstatus.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 binds0.0.0.0. - Broker + dashboard localhost-only (the
0.0.0.0mistake stays fixed);/adminand/proxyboth 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;.envmode 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/adminrequest as a backstop./proxy/*and the localssh -Ladmin 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
dmPolicy→allowlist. - L1 — CLOSED.
data/*.db→0600, persisted in the deploy. - M3 — ACCEPTED (unchanged). Bob uses GitHub write through the broker, so the
reposcope stays. - H1 — STILL OPEN (Bob): rotate the three exposed secrets. M2(dash) — STILL
OPEN (Bob): confirm the
status.bobsmcp.ukAccess policy is email-restricted.
Recommended remediation order (original)
- H1 — confirm/rotate the three exposed secrets + Secret Manager bump (Bob).
- M1 — Cloudflare Access policy (or scoped tunnel ingress) on
/admin/*. ✅ done - M2 — persist / back up
data/. ✅ done - M2(dash) — confirm the
status.bobsmcp.ukAccess policy is email-restricted. - M4 — switch Telegram to
allowlist. ✅ done - M3 — narrow the GitHub scope if write isn't needed. (accepted — write in use)
- L1 —
chmod 600 data/*.db. ✅ done
Related
wiki/projects/gcp-mcp-standalone/security-review.md · git is the source of truth