Scope
Full end-to-end review of the live system: Terraform infra (main.tf), Ansible
(install.yml, claudebot.yml), the Firney-MCP-Broker + all connectors, the
Claude Telegram bot, secrets handling, network exposure,
git hygiene, and the bobswiki publish pipeline. Findings verified
against the running box, not from memory.
Summary
The architecture is sound and most controls are working as designed — no open inbound ports (verified), least-privilege IAM, a well-isolated bot, secrets in Secret Manager, encrypted connector tokens. The main action item is secret rotation: several credentials were pasted in plaintext during interactive setup and must be treated as compromised. The rest are hardening / blast-radius items.
Positive controls (verified live 2026-07-03)
- No public ingress. Deny-all firewall priority 100 on tag
mcp-brokerbeats GCP's default permissive rules; ports 22 and 8002 confirmed closed from the public IP. - Broker localhost-only — binds
127.0.0.1:8002; invisible to the public IP and the tailnet (gcp-mcp-standalone/adr/0006-localhost-only-broker-bind). - Non-root container — broker runs as
appuser(uid 1000). - Least-privilege IAM —
mcp-broker-saholdssecretAccessoron exactly the three named secrets (1:1 bindings), nothing project-wide. - Secrets in GCP-Secret-Manager — Tailscale key, GitHub + Notion client secrets;
.envmode 600; no secret values committed in any git repo or history (checked all three: gcp-mcp-standalone, mcp-broker fork, Obsidian-Docs). - Bot isolation —
claudebotruns unprivileged, no sudo, metadata-blocked (confirmed it cannot fetch the VM's SA token), read-only allow-list (21 rules, zero write tools), and a sender allowlist limited to one Telegram ID. - Encrypted at rest / gated — connector tokens Fernet-encrypted;
/admin/*and/proxy/*return 401 without credentials. - Supply chain — Tailscale from its signed apt repo; cloudflared pinned + sha256-verified.
- Publish pipeline — GitHub Actions jobs gate on secret presence (skip-not-fail); secrets passed via env, never echoed.
Findings
HIGH
H1 — Credentials exposed in plaintext during setup. Over the setup sessions, these were
pasted into the assistant conversation (which is persisted) and must be treated as
compromised: the Telegram bot token, the Notion OAuth client secret, and earlier the
GitHub OAuth client secret. They work until rotated.
Fix: Rotate each at source — Telegram @BotFather → Revoke current token; Notion
integration → regenerate secret; GitHub OAuth app → regenerate client secret — then bump
the corresponding GCP-Secret-Manager version (and the bot's telegram.env for the bot
token) and redeploy. See gcp-mcp-standalone/runbooks/rotate-tailscale-key for the
version-bump pattern.
MEDIUM
M1 — Broker /admin/* reachable from the public internet (key-gated, returns 401). The
admin API can mint API keys and connect-tokens; if BROKER_ADMIN_KEY ever leaks, that's full
connector takeover. It should not be internet-facing.
Fix: A Cloudflare Access policy on /admin/*, or scope the tunnel ingress to only
/proxy, /oauth, /.well-known. Defense-in-depth on top of the admin key.
M2 — No data/ durability. Connector tokens, broker API keys and inbound registrations
live only on the boot disk. A VM rebuild wipes them — it has happened twice, each time
breaking every integration until re-authorized
(gcp-mcp-standalone/runbooks/reconnect-connectors).
Fix: Put data/ on a persistent disk, or a scheduled GCS backup + restore.
M3 — GitHub connector uses the broad repo scope — full read/write to all private repos.
Fix: Narrow to read-only / a fine-grained token if the usage doesn't need write.
M4 — Telegram DM policy is pairing, not allowlist. A stranger who DMs the bot still
gets a pairing-code reply (not access, but a response and a minor info leak).
Fix: /telegram:access policy allowlist — only known IDs get any response.
LOW
L1 — Broker token DBs world-readable (data/*.db mode 644). Fernet-encrypted and the
parent dir limits reach, but tighten anyway. Fix: chmod 600 data/*.db.
L2 — Long-lived Claude token on the VM. claudebot's CLAUDE_CODE_OAUTH_TOKEN (a Claude
subscription token) sits in telegram.env (600). If the box is compromised the subscription
is usable. Contained by the box's isolation; rotate periodically via claude setup-token.
L3 — Cloudflare API token has no expiration; Cloudflare connector scope unverified. The Pages token (a GitHub secret) never expires; and confirm the broker's Cloudflare connector was granted read-only (this account also owns the tunnel + DNS the broker depends on). Fix: set a token expiry + rotate; verify/limit the connector's Cloudflare scopes.
L4 — Project-wide default-allow rules remain. default-allow-ssh/-rdp/-icmp (priority
65534, 0.0.0.0/0) still exist; only the tagged mcp-broker VM is shielded by the deny.
Harmless with a single VM, but delete them for defense-in-depth before adding others.
Recommended remediation order
- H1 — rotate the three exposed secrets (Telegram, Notion, GitHub) + Secret Manager bump.
- M1 — Cloudflare Access policy on
/admin/*. - M2 — persist / back up
data/. - M4 — switch Telegram to
allowlistpolicy. - M3 — narrow GitHub scope.
- L1 —
chmod 600 data/*.db.
Related
wiki/projects/gcp-mcp-standalone/security-review.md · git is the source of truth