Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 20 additions & 35 deletions cf-access-mcp.tf
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Gates mcp.makeitwork.cloud (ToolHive VirtualMCPServer in kustomize-cluster
# workloads/mcp-gateway). MCP clients are headless HTTP agents, so machine
# access uses a dedicated service token (CF-Access-Client-* headers) rather
# than the browser OIDC flow; the admins policy stays for interactive debug.
# MCP clients are headless HTTP agents, so every direct endpoint uses the
# existing Cloudflare Access service token (CF-Access-Client-* headers) rather
# than browser OIDC. The shared token is an owner-approved solo-developer trust
# boundary; individual ToolHive proxy Services and their backend credentials
# remain cluster-internal.
resource "cloudflare_zero_trust_access_service_token" "mcp_gateway" {
account_id = local.account_id
name = "mcp-gateway"
# Non-expiring; rotate deliberately by bumping client_secret_version.
duration = "forever"
}

# Retain this Access application until Argo CD has removed the aggregate
# TunnelBinding route. Removing edge Access before that route reconciles away
# would briefly expose the aggregate without authentication. A later scoped
# cleanup PR removes this now-unrouted application after verification.
resource "cloudflare_zero_trust_access_application" "mcp_gateway" {
account_id = local.account_id
name = "MCP Gateway"
Expand Down Expand Up @@ -43,26 +48,16 @@ resource "cloudflare_zero_trust_access_application" "mcp_gateway" {
]
}

# Per-backend MCP endpoints (mcp-<name>.makeitwork.cloud) terminate on the
# same toolhive proxyrunners behind the same tunnel. One application per FQDN:
# Access domains cannot wildcard a name prefix, and first-level names are
# required anyway because Universal SSL only covers one subdomain level.
#
# This list controls Access applications only. Tunnel DNS is exclusively owned
# by TunnelBinding resources in kustomize-cluster.
# Individual ToolHive backend endpoints use one first-level hostname each so
# Universal SSL covers them. Cloudflare Access applications are explicit per
# hostname because Access does not wildcard a name prefix. Tunnel DNS and routes
# are exclusively owned by the corresponding TunnelBinding subjects in
# kustomize-cluster.
locals {
mcp_backends = [
"makeitwork-apify",
"makeitwork-argocd",
"makeitwork-aws-docs",
"makeitwork-cloudflare",
"makeitwork-context7",
"makeitwork-github",
"makeitwork-github-xnoto",
"makeitwork-grafana",
"makeitwork-kubernetes",
"makeitwork-parallel-search",
"makeitwork-terraform-docs",
"apify", "argocd", "aws", "aws-docs", "cloudflare", "context7", "gcp",
"grafana", "kubernetes", "parallel-search", "playwright", "slidespeak",
"terraform-docs", "twilio-docs",
]
}

Expand All @@ -74,29 +69,19 @@ resource "cloudflare_zero_trust_access_application" "mcp_gateway_backend" {
domain = "mcp-${each.key}.makeitwork.cloud"
session_duration = "24h"

allowed_idps = [
cloudflare_zero_trust_access_identity_provider.github.id,
]
allowed_idps = [cloudflare_zero_trust_access_identity_provider.github.id]

policies = [
{
name = "mcp-gateway-clients"
decision = "non_identity"
include = [{
service_token = {
token_id = cloudflare_zero_trust_access_service_token.mcp_gateway.id
}
}]
include = [{ service_token = { token_id = cloudflare_zero_trust_access_service_token.mcp_gateway.id } }]
},
{
name = "makeitworkcloud-admins"
decision = "allow"
session_duration = "24h"
include = [{
group = {
id = cloudflare_zero_trust_access_group.admins.id
}
}]
include = [{ group = { id = cloudflare_zero_trust_access_group.admins.id } }]
}
]
}
Loading