feat: add Tailscale Funnel config for Claude AI connector testing
Add docker compose services (tailscale-mcp + nginx-claude-filter) behind a claude-funnel profile that expose the login-flow MCP server via Tailscale Funnel with IP-based access control: - /mcp endpoint restricted to Claude AI outbound IPs (160.79.104.0/21) - /oauth/*, /.well-known/*, /app paths open to all IPs (user login flow) - All other paths return 404 Also add favicon.png served at /favicon.ico for connector directory discovery (Google favicon service). Usage: docker compose --profile login-flow --profile claude-funnel up -d Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3cf4c777ed
commit
b8dc1d7f52
@@ -0,0 +1,88 @@
|
|||||||
|
# Claude AI IP Filter for Tailscale Funnel
|
||||||
|
#
|
||||||
|
# Routes MCP transport to Claude AI IPs only, while allowing
|
||||||
|
# OAuth/auth endpoints from any IP (needed for user login flow).
|
||||||
|
#
|
||||||
|
# Pattern: homelab-argocd/atlantis/templates/nginx-webhook-config.yaml
|
||||||
|
|
||||||
|
worker_processes auto;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# Map Claude AI outbound IPs (must be at http level)
|
||||||
|
geo $remote_addr $is_claude_ai {
|
||||||
|
default 0;
|
||||||
|
|
||||||
|
# Anthropic Claude AI Outbound IP Range
|
||||||
|
# Source: https://docs.claude.com/en/api/ip-addresses
|
||||||
|
# Last updated: 2026-03-29
|
||||||
|
|
||||||
|
# IPv4 range
|
||||||
|
160.79.104.0/21 1; # Claude AI
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Trust Tailscale proxy for real IP extraction
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
set_real_ip_from 100.64.0.0/10; # Tailscale CGNAT range
|
||||||
|
set_real_ip_from 10.0.0.0/8; # Docker internal networks
|
||||||
|
set_real_ip_from 172.16.0.0/12; # Docker bridge networks
|
||||||
|
real_ip_recursive on;
|
||||||
|
|
||||||
|
# OAuth/auth endpoints + favicon - allow ALL IPs (user browser needs access for login flow)
|
||||||
|
location ~ ^/(oauth|\.well-known|app|favicon\.ico)(/|$) {
|
||||||
|
proxy_pass http://mcp-login-flow:8004;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
}
|
||||||
|
|
||||||
|
# MCP transport endpoint - Claude AI IPs ONLY
|
||||||
|
# login-flow uses streamable-http transport (no /sse needed)
|
||||||
|
location /mcp {
|
||||||
|
if ($is_claude_ai = 0) {
|
||||||
|
return 403 '{"error": "Access denied - IP not in Claude AI range", "source_ip": "$remote_addr"}\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass http://mcp-login-flow:8004;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
|
# Streamable HTTP support (long-lived connections)
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default - deny everything else
|
||||||
|
location / {
|
||||||
|
return 404 '{"error": "Not found"}\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"TCP": {
|
||||||
|
"443": {
|
||||||
|
"HTTPS": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Web": {
|
||||||
|
"${TS_CERT_DOMAIN}:443": {
|
||||||
|
"Handlers": {
|
||||||
|
"/": {
|
||||||
|
"Proxy": "http://nginx-claude-filter:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowFunnel": {
|
||||||
|
"${TS_CERT_DOMAIN}:443": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -303,6 +303,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NEXTCLOUD_HOST=http://app:80
|
- NEXTCLOUD_HOST=http://app:80
|
||||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
|
||||||
|
#- NEXTCLOUD_MCP_SERVER_URL=https://nextcloud-mcp-dev.tail148d5.ts.net
|
||||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||||
|
|
||||||
# Login Flow v2 (ADR-022)
|
# Login Flow v2 (ADR-022)
|
||||||
@@ -324,6 +325,38 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- login-flow
|
- login-flow
|
||||||
|
|
||||||
|
# Tailscale Funnel for Claude AI connector testing
|
||||||
|
# Usage: docker compose --profile login-flow --profile claude-funnel up --build -d
|
||||||
|
# Requires: TS_AUTHKEY in .env file (see env.sample.claude-funnel)
|
||||||
|
tailscale-mcp:
|
||||||
|
image: tailscale/tailscale:latest
|
||||||
|
hostname: nextcloud-mcp-dev
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TS_AUTHKEY=${TS_AUTHKEY}
|
||||||
|
- TS_STATE_DIR=/var/lib/tailscale
|
||||||
|
- TS_SERVE_CONFIG=/config/serve-config.json
|
||||||
|
- TS_HOSTNAME=nextcloud-mcp-dev
|
||||||
|
- TS_EXTRA_ARGS=--advertise-tags=tag:container
|
||||||
|
volumes:
|
||||||
|
- tailscale-state:/var/lib/tailscale
|
||||||
|
- ./claude-funnel/serve-config.json:/config/serve-config.json:ro
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_MODULE
|
||||||
|
profiles:
|
||||||
|
- claude-funnel
|
||||||
|
|
||||||
|
nginx-claude-filter:
|
||||||
|
image: docker.io/library/nginx:alpine
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- mcp-login-flow
|
||||||
|
volumes:
|
||||||
|
- ./claude-funnel/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
profiles:
|
||||||
|
- claude-funnel
|
||||||
|
|
||||||
qdrant:
|
qdrant:
|
||||||
image: docker.io/qdrant/qdrant:v1.17.1@sha256:94728574965d17c6485dd361aa3c0818b325b9016dac5ea6afec7b4b2700865f
|
image: docker.io/qdrant/qdrant:v1.17.1@sha256:94728574965d17c6485dd361aa3c0818b325b9016dac5ea6afec7b4b2700865f
|
||||||
restart: always
|
restart: always
|
||||||
@@ -354,3 +387,4 @@ volumes:
|
|||||||
qdrant-data:
|
qdrant-data:
|
||||||
mcp-data:
|
mcp-data:
|
||||||
multi-user-basic-data:
|
multi-user-basic-data:
|
||||||
|
tailscale-state:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Claude AI Connector - Tailscale Funnel Configuration
|
||||||
|
#
|
||||||
|
# This profile exposes the MCP server (login-flow mode) via Tailscale Funnel
|
||||||
|
# for testing Claude AI connector integration.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# 1. Copy this file to .env and fill in TS_AUTHKEY
|
||||||
|
# 2. Run: docker compose --profile login-flow --profile claude-funnel up --build -d
|
||||||
|
# 3. Configure Claude.ai connector with: https://nextcloud-mcp-dev.<tailnet>.ts.net/mcp
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Generate auth key at https://login.tailscale.com/admin/settings/keys
|
||||||
|
# with tag:container and reusable=yes
|
||||||
|
# - Ensure Tailscale ACL allows funnel for tag:container:
|
||||||
|
# "nodeAttrs": [{ "target": ["tag:container"], "attr": ["funnel"] }]
|
||||||
|
#
|
||||||
|
# Security:
|
||||||
|
# - /mcp endpoint restricted to Claude AI IPs (160.79.104.0/21)
|
||||||
|
# - /oauth/* and /.well-known/* open to all IPs (needed for user login flow)
|
||||||
|
# - All other paths return 404
|
||||||
|
|
||||||
|
TS_AUTHKEY=tskey-auth-REPLACE_ME
|
||||||
@@ -24,7 +24,7 @@ from pydantic import AnyHttpUrl
|
|||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import JSONResponse, RedirectResponse
|
from starlette.responses import FileResponse, JSONResponse, RedirectResponse
|
||||||
from starlette.routing import Mount, Route
|
from starlette.routing import Mount, Route
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
from starlette.types import ASGIApp, Receive, Send
|
from starlette.types import ASGIApp, Receive, Send
|
||||||
@@ -2342,6 +2342,18 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
routes.append(Mount("/app", app=browser_app))
|
routes.append(Mount("/app", app=browser_app))
|
||||||
logger.info("App routes with session auth: /app, /app/webhooks, /app/revoke")
|
logger.info("App routes with session auth: /app, /app/webhooks, /app/revoke")
|
||||||
|
|
||||||
|
# Favicon for connector directory discovery (Google favicon service)
|
||||||
|
favicon_path = os.path.join(
|
||||||
|
os.path.dirname(__file__), "auth", "static", "favicon.png"
|
||||||
|
)
|
||||||
|
if os.path.isfile(favicon_path):
|
||||||
|
routes.append(
|
||||||
|
Route(
|
||||||
|
"/favicon.ico",
|
||||||
|
lambda request: FileResponse(favicon_path, media_type="image/png"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Mount FastMCP at root last (catch-all, handles OAuth via token_verifier)
|
# Mount FastMCP at root last (catch-all, handles OAuth via token_verifier)
|
||||||
routes.append(Mount("/", app=mcp_app))
|
routes.append(Mount("/", app=mcp_app))
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Vendored
+1
-1
Submodule third_party/astrolabe updated: d245ded7f8...829db07a8c
Reference in New Issue
Block a user