From a99fd423813e460a7407beee53b6fc97efb6f1a7 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Sun, 28 Jun 2026 10:51:47 -0400 Subject: [PATCH] feat(caddy): expose MCP gateway at /mcp/* (bearer-gated via SOPS) for tailnet clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remote MCP clients (Cline/Cursor on other tailnet PCs) had no way to reach the aggregated tool gateway: it's 127.0.0.1-only, on a network Caddy couldn't reach, and has no auth of its own. One endpoint already aggregates all 8 servers (n8n, comfyui, orchestration, searxng, blog-mcp, playwright, qdrant-rag, codebase-memory), so a single bearer-gated route exposes everything. - secrets/.env.sops: add MCP_GATEWAY_TOKEN (env-form, decrypts to runtime/.env). - docker-compose.yml: add model... mcp-gateway to proxy-net so Caddy can reach it; pass MCP_GATEWAY_TOKEN into caddy's env. - auth/caddy/Caddyfile: /mcp /mcp/* bypass route — 401 unless the request carries Authorization: Bearer {$MCP_GATEWAY_TOKEN}; passed through unstripped (gateway serves /mcp) with flush_interval -1 for SSE/streamable-HTTP. Tailnet-only. Validated live: POST /mcp without the token -> 401; with it -> 200 MCP initialize from the Docker MCP Gateway. No secret value is committed (only the encrypted blob and an env-var reference). Co-Authored-By: Claude Opus 4.8 (1M context) --- auth/caddy/Caddyfile | 20 ++++++++++++++++++++ docker-compose.yml | 5 +++++ secrets/.env.sops | 29 +++++++++++++++-------------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/auth/caddy/Caddyfile b/auth/caddy/Caddyfile index 9944d6f..7166f32 100644 --- a/auth/caddy/Caddyfile +++ b/auth/caddy/Caddyfile @@ -55,6 +55,26 @@ reverse_proxy model-gateway:11435 } + # ---- MCP gateway (aggregated tools) for tailnet MCP clients (Cline/Cursor) ---- + # Bypasses Google SSO (programmatic clients can't do an interactive login) but + # requires a static Bearer token (MCP_GATEWAY_TOKEN, from SOPS) because the + # gateway has NO auth of its own. /mcp is passed through UNSTRIPPED — the + # gateway serves the MCP endpoint at /mcp. flush_interval -1 keeps the + # streamable-HTTP / SSE responses unbuffered. Reachable only over the tailnet. + @mcp_unauthed { + path /mcp /mcp/* + not header Authorization "Bearer {$MCP_GATEWAY_TOKEN}" + } + @mcp path /mcp /mcp/* + handle @mcp_unauthed { + respond "Unauthorized" 401 + } + handle @mcp { + reverse_proxy mcp-gateway:8811 { + flush_interval -1 + } + } + # ---- Open WebUI at ROOT (no auth needed for the redirect) ---- # Open WebUI's upstream v0.9.2 image is a prebuilt SvelteKit SPA with # base="" (assets are root-absolute: /_app, /static, /api, /ws). It MUST be diff --git a/docker-compose.yml b/docker-compose.yml index a6dbd50..e246fe6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -810,6 +810,8 @@ services: max-file: "3" networks: - backend + # proxy-net: lets Caddy (front door) reach the gateway for the /mcp/* route. + - proxy-net oauth2-proxy: # alpine variant ships with wget for the in-container healthcheck below. @@ -867,6 +869,9 @@ services: environment: - CADDY_TAILNET_HOSTNAME=${CADDY_TAILNET_HOSTNAME} - CADDY_TAILNET_DOMAIN=${CADDY_TAILNET_DOMAIN} + # Bearer token (SOPS) gating the /mcp/* route for remote MCP clients + # (Cline/Cursor). The gateway has no auth of its own; Caddy enforces this. + - MCP_GATEWAY_TOKEN=${MCP_GATEWAY_TOKEN:-} volumes: - ./auth/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ${TAILSCALE_CERT_DIR:-./auth/caddy/certs}:/etc/caddy/certs:ro diff --git a/secrets/.env.sops b/secrets/.env.sops index 181a925..0fcc522 100644 --- a/secrets/.env.sops +++ b/secrets/.env.sops @@ -1,15 +1,16 @@ -LITELLM_MASTER_KEY=ENC[AES256_GCM,data:feKV13k=,iv:1a7rNMmiXKej2/QDORkEpyPIH6ZBpUbRaCc6BnHFcek=,tag:MBU6A38mAK6dSQ1l0dUVBg==,type:str] -DASHBOARD_AUTH_TOKEN=ENC[AES256_GCM,data:CHgZvEUpVcRcI2Ca+fjiKNOEWkMV30A0WhNdu1twzqGRBSiCMy43jg==,iv:TEq9UBIp85HB7Bh31STPLvGgHMEipAImNkRrcYyuJxg=,tag:7J/JjnmH6zWv0W5l21OWyA==,type:str] -OPS_CONTROLLER_TOKEN=ENC[AES256_GCM,data:dw7/dZYqj5DLbrPw7TUD6pllfr2HsBt5y/oJ5vApkRKBgSxQpdiKZw==,iv:ekKQ49ySZiJSnrVbc1vYousxiGRQaygvZYKpiDsJbfY=,tag:Q0i4j1gb847GHOnDJ5O4+w==,type:str] -OAUTH2_PROXY_CLIENT_ID=ENC[AES256_GCM,data:XjdJ+tAXazMMh2QErT7GlxdvlAxBCJfLByB4MyCsCSzU+WVXU3nA2TrmXysXh2cKcaLzxzXtMWM7Y0i/uf+ez5kPq9OifQsd,iv:uL1aZFCqg6fucgQtchdj5XOymiX1vZdBoksfZF2M1OU=,tag:siWN+v1HXTmkNHY772p3/A==,type:str] -OAUTH2_PROXY_CLIENT_SECRET=ENC[AES256_GCM,data:MXCCSE//4WwvBtthFswKJ7V/7CjlbHJsjLhxx5247Gu4M3g=,iv:GRT2xmyzhOGO1mi8QbPo4FaYCuQWUteFe//4KbJSzoo=,tag:xm/ikAWTdntfq3MDTDKkWA==,type:str] -OAUTH2_PROXY_COOKIE_SECRET=ENC[AES256_GCM,data:p0zIp8R/4sPQH4pmmh27wIDaquxKPbUZqHjnWWJ+Fjw=,iv:qFw5JHqJGTJzA5m1Tm0PPCAVdS7vK7UyK1CTdvLq7wA=,tag:rZBlyVXK5gk/HHbfNQkMig==,type:str] -SEARXNG_SECRET=ENC[AES256_GCM,data:J0mX8XTVRWtcc/BUpy0/g4nRewAeAp0J6Id4QcJ4VXxnslC5g63qukOkQOYPLNEr+n6Q8UydypYK7j5I6+hILA==,iv:2g08spN7I9QXCOIPZnoA6DTqNJ9un/CHU3pyFYCkaRk=,tag:pVIYOdw4ztkbTGJBDOKE1g==,type:str] -N8N_OWNER_EMAIL=ENC[AES256_GCM,data:IOfRaiHu9O28FBh60tCtDjX6,iv:nKGk8dOKHmqq+sXfoQWk4ze/OsOPYb552zXQV2HKW4g=,tag:6E83ddOhjhZOIX1ZCipAgg==,type:str] -N8N_OWNER_PASSWORD=ENC[AES256_GCM,data:RQcLp6uNfEMtgBk0JMmPvNuH7hr8JAWA6jCvU4HcoWjVKzs=,iv:/2CKVPn3YshdTt2sDKNAqtDQc6gAi5e1MdTjJ2wgyI4=,tag:jG45DJTxCzPVezqeJB/D+Q==,type:str] -sops_age__list_0__map_recipient=age1egt7028wtwpf3g9fqe5xvf80ptmhze25tltw0ff6nwv5hf6v6grqpl2qar -sops_mac=ENC[AES256_GCM,data:ouvchsNJW421UGx9dsXbAibL5GJ3FG+sdcyeoiDJ1ZeskK9sMsE8Pd9O/o3Q4elyfN0TXv6A6c7vwbf67XRRqBOtfLbBgrpOig1CJikm4l24qSIFDu4mBFD0UAukLAtv0FjhJcSTmuScQ+MOw/KRe6W5Wd1mpykBlPsMg8v+940=,iv:JzE/n7PE59kerDqKWuzi2Qk12fWhHoWINHJCDgSydOI=,tag:bki9K1lvDEBXZW+s+olxWg==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoaXhlMVBnUnkrQ1lzTXVk\nUlR3Vi9IM2JZNmRRaXFQcnFmdk5pTGhUKzN3CjM5Z3JiY1pYZHltQzA4MVJSVElQ\nR3A1OUxXaHJVN3M5VXNjTXEvTlNodzAKLS0tIENpR1dxUzIzSlFKOHkvWHNEblZH\nK2tiRGpRL3hCLy9YbjY5RlM4OUZiK1UKWWOF5VU4+lK+2CdfVsy+DojCI9+VF+71\nTHEJ9+xlHoJ/Mno1u3s52ioSoJg40kqw7TtC/iiqDbYplVQD6PLgRw==\n-----END AGE ENCRYPTED FILE-----\n -sops_version=3.7.3 -sops_lastmodified=2026-05-14T14:32:59Z +LITELLM_MASTER_KEY=ENC[AES256_GCM,data:WV+db9k=,iv:J6zwV1wXHlMBxsLx1D0TiBx5BbKt+d6pIEjF799vwY0=,tag:eoaVmrYXlERGPwwQuK8mrw==,type:str] +DASHBOARD_AUTH_TOKEN=ENC[AES256_GCM,data:YK4xerrMr0Hj1SkaBhgUiMnqQU6Y45/v42zMG7RR0tOticQEaEXyPQ==,iv:lB6Wtx+xFuGxxkLlo22ZEQ8xUoHA/chKEmTlR4Mosyg=,tag:r0trp/4GEDJqlTWNptgoKg==,type:str] +OPS_CONTROLLER_TOKEN=ENC[AES256_GCM,data:ypFbfl5jPfnjlhFx1P0VKqB4Qvb+o/suXiLuErAvpX16XtPK5ZvOXQ==,iv:yf1/SxTF1GF/UE1TtD3b+CP/KTeKH/R0V2NV/v3kyaI=,tag:l6aVtyoadn7C2qWGVmLyEg==,type:str] +OAUTH2_PROXY_CLIENT_ID=ENC[AES256_GCM,data:G3P9E6UWrcwR2sO3qYYbdLGTm96G6S952Otf6KAA9uoxhuyxDvM93f7UjIAL1EiON+McnZsNsfuomzgEipPgkByRHsTWjnGS,iv:M9PJmauBlsq9bH91Lua2MmDy0KQ0uk7yyvtFZl6xYH4=,tag:IuB+UkDYp4l+uAydK8H7KQ==,type:str] +OAUTH2_PROXY_CLIENT_SECRET=ENC[AES256_GCM,data:RZE/3aIzDm6wUtVitogEGE1KWn6C34wqb3vazseEdijT6LY=,iv:TYLfftNVOkF0cCOGN4aCemPI+z/0+nbw3E9qikZxvLg=,tag:Vo29nS+8R0FlcZPcchbd6g==,type:str] +OAUTH2_PROXY_COOKIE_SECRET=ENC[AES256_GCM,data:pIsoEiaNdm6nblaGJVA7cMNeatyn7x8zASKpS4r0TPM=,iv:rqUeo5fiCRqY7vPKvWP2p3sHeVSDEyZRsaFy0SJQG0s=,tag:kgH2RrKBJJvsihWyHy2RgA==,type:str] +SEARXNG_SECRET=ENC[AES256_GCM,data:/MHNl1/dNO4WqJIdMRLvyOnhB2VBCI2XAomWjkb03z6+oIyoVOHyqPoaVC/vsh1PQax9epNNAxJrzqBMKkQuDw==,iv:p0O8fZBE8HNITm5H/PmKbAA9JlwondHU+XkMUv6aof8=,tag:aIaHyMYc5VYDqag2MRCNPg==,type:str] +N8N_OWNER_EMAIL=ENC[AES256_GCM,data:5fIVQRn8kcV1svlVPjOMjl3G,iv:41qfQEZepl6tT6dE839pmHmEB+OLde+FRMS8iA3DKZM=,tag:jzb0aQuWRZHA1acrFpAjGw==,type:str] +N8N_OWNER_PASSWORD=ENC[AES256_GCM,data:qmz0QI+LfTQIKwbOxSibtRdAVqv87uXmM/TUcHOC5ML2xHQ=,iv:K8ASy2fLa0N/hCi5C5vEdrmyA8UEJBJ52ah/ckZb7B4=,tag:xtzmOlKjKsmm1eVIrNnc4g==,type:str] +MCP_GATEWAY_TOKEN=ENC[AES256_GCM,data:UM2LsE8EFZcTQb44QAfgEzNwlkdZPDLllywb4VcpBf8bEWnzt3N4A2wvNeEGcTFqp1/zZxR7ULozd5SQDF2rMA==,iv:cfRjD3ZbDajgR18w8UolcqfQgp197m8ysELMDAXG4Os=,tag:zJHE4No3RuM0EtN3Rnl8mg==,type:str] +sops_mac=ENC[AES256_GCM,data:olk2HnSJ9ueKvkwNmdRKw/0pG6XR4tIwN1FPuWQk/wfA1bT6mwIlKyRCEMswP+cY7+lZ2DPD0QP3/fdZKf8tWhPjgEPoGA3LQA8qaiJKdi3R1/z0I45WDRrBI/4R5r+L9LEBb5JOFJhVZ7FIGs1wTj2Fu4Drdjpe6uhST7CM6rw=,iv:Zmebi/W7nMJ6DjX2BCiHpnMdN4Xrw7RyghyHgTtvJNo=,tag:P/uQybXLJN+T+UJzIbR+Xw==,type:str] sops_unencrypted_suffix=_unencrypted +sops_version=3.7.3 +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmVmp4aXVRSlZpSkFvVjAy\nVU9Odmt4V1p1b1pjK0JtT0cybzlPTXBYYVRRCmFhK0NlN0s0b0R6dVZOeVNzbElO\nU0VVRGZGcTZBdHNzMnRsMG5FV3NtVHcKLS0tIHdhZyt2aTJwS1MxcWNMQ3o4MWJ2\ndlhRSFFncHp0QytMaEYxa3ZGM01WejQKG3rh9CehnTuY/oTb4fHJXXBfePSviaGP\nDRjwDY63GemECHXybFH8xpEBebS+9xj0dFlXbkRF/sldWFgqjjE1ng==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1egt7028wtwpf3g9fqe5xvf80ptmhze25tltw0ff6nwv5hf6v6grqpl2qar +sops_lastmodified=2026-06-28T14:48:14Z