Tailscale Authentication
Agent Gateway can integrate with Tailscale to authenticate users based on their Tailscale identity, enabling zero-trust access to your MCP servers.
What you’ll build
In this tutorial, you’ll:
- Configure Agent Gateway to use Tailscale for authentication
- Query the Tailscale daemon to identify connecting users
- Extract node name and user email from Tailscale identity
- Enable zero-trust access to your MCP servers
Prerequisites
- Agent Gateway installed
- Tailscale installed and connected to your tailnet
- Another device on your tailnet to test from (or use the same machine via its Tailscale IP)
Step 1: Verify Tailscale is running
Check that Tailscale is connected:
tailscale statusYou should see your machine listed with a 100.x.x.x IP address.
Note your Tailscale IP:
tailscale ip -4Step 2: Create the configuration
Create a working directory:
mkdir tailscale-auth-test && cd tailscale-auth-testCreate a config.yaml file:
Linux configuration:
cat > config.yaml << 'EOF'
frontendPolicies:
accessLog:
add:
tailscale.node: extauthz.tailscaleNode
tailscale.email: extauthz.tailscaleEmail
binds:
- port: 3000
listeners:
- name: default
protocol: HTTP
routes:
- name: application
backends:
- mcp:
targets:
- name: everything
stdio:
cmd: npx
args: ["@modelcontextprotocol/server-everything"]
policies:
cors:
allowOrigins: ["*"]
allowHeaders: ["*"]
exposeHeaders: ["Mcp-Session-Id"]
extAuthz:
# Linux: Tailscale socket location
host: unix:/run/tailscale/tailscaled.sock
protocol:
http:
path: |
"/localapi/v0/whois?addr=" + source.address
addRequestHeaders:
:authority: '"local-tailscaled.sock"'
metadata:
tailscaleNode: json(response.body).Node.Name
tailscaleEmail: json(response.body).UserProfile.LoginName
EOFmacOS configuration:
cat > config.yaml << 'EOF'
frontendPolicies:
accessLog:
add:
tailscale.node: extauthz.tailscaleNode
tailscale.email: extauthz.tailscaleEmail
binds:
- port: 3000
listeners:
- name: default
protocol: HTTP
routes:
- name: application
backends:
- mcp:
targets:
- name: everything
stdio:
cmd: npx
args: ["@modelcontextprotocol/server-everything"]
policies:
cors:
allowOrigins: ["*"]
allowHeaders: ["*"]
exposeHeaders: ["Mcp-Session-Id"]
extAuthz:
# macOS: Tailscale socket location
host: unix:/var/run/tailscale/tailscaled.sock
protocol:
http:
path: |
"/localapi/v0/whois?addr=" + source.address
addRequestHeaders:
:authority: '"local-tailscaled.sock"'
metadata:
tailscaleNode: json(response.body).Node.Name
tailscaleEmail: json(response.body).UserProfile.LoginName
EOFConfiguration explained
| Setting | Description |
|---|---|
frontendPolicies.accessLog.add |
Adds Tailscale identity to access logs |
extAuthz.host |
Unix socket path to Tailscale daemon |
extAuthz.protocol.http.path |
CEL expression calling Tailscale’s whois API with client IP |
addRequestHeaders.:authority |
Required hostname for Tailscale local API |
metadata.tailscaleNode |
Extracts machine name from Tailscale response |
metadata.tailscaleEmail |
Extracts user email from Tailscale response |
Step 3: Start Agent Gateway
agentgateway -f config.yamlYou should see:
info proxy::gateway started bind bind="bind/3000"Step 4: Test the authentication
Test from localhost (should fail)
Requests from localhost won’t have a Tailscale identity:
curl -i http://localhost:3000/mcpExpected response:
HTTP/1.1 403 Forbidden
external authorization failedThis is expected - localhost isn’t a Tailscale IP.
Test via Tailscale IP (should succeed)
Use your Tailscale IP address:
# Get your Tailscale IP
TAILSCALE_IP=$(tailscale ip -4)
# Make request via Tailscale IP
curl -i http://$TAILSCALE_IP:3000/mcpExpected response:
HTTP/1.1 406 Not Acceptable
Not Acceptable: Client must accept text/event-streamThe 406 response means authentication passed and the request reached the MCP server (which requires SSE headers).
Test with proper MCP headers
TAILSCALE_IP=$(tailscale ip -4)
curl -X POST "http://$TAILSCALE_IP:3000/mcp" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'Check the logs
After a successful request, the Agent Gateway logs will show Tailscale identity:
info request ... tailscale.node=your-machine-name [email protected]How it works
┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │────▶│Agent Gateway │────▶│ Tailscale Daemon│
│(100.x.x.x) │ │ │ │ │
└──────────────┘ └──────────────┘ └─────────────────┘
│ │ │
│ 1. Request │ │
│───────────────────▶│ │
│ │ 2. whois?addr= │
│ │ 100.x.x.x │
│ │─────────────────────▶│
│ │ 3. {Node, User} │
│ │◀─────────────────────│
│ 4. Response │ │
│◀───────────────────│ │- Client connects from their Tailscale IP (100.x.x.x)
- Agent Gateway calls Tailscale’s local
whoisAPI with the source IP - Tailscale returns the node and user information
- Agent Gateway allows/denies the request and logs the identity
Adding authorization rules
Restrict access based on Tailscale identity:
policies:
extAuthz:
host: unix:/var/run/tailscale/tailscaled.sock
protocol:
http:
path: |
"/localapi/v0/whois?addr=" + source.address
addRequestHeaders:
:authority: '"local-tailscaled.sock"'
metadata:
tailscaleNode: json(response.body).Node.Name
tailscaleEmail: json(response.body).UserProfile.LoginName
authorization:
rules:
# Only allow specific users
- if: 'extauthz.tailscaleEmail == "[email protected]"'
# Or check node name patterns
- if: 'extauthz.tailscaleNode.startsWith("prod-")'Tailscale socket locations
| Platform | Socket Path |
|---|---|
| Linux | /run/tailscale/tailscaled.sock |
| macOS | /var/run/tailscale/tailscaled.sock |
| Windows | Named pipe (not supported via unix socket) |
Cleanup
Stop the Agent Gateway with Ctrl+C and remove the test directory:
cd .. && rm -rf tailscale-auth-testTroubleshooting
“external authorization failed” for Tailscale IPs
Check that the Tailscale socket exists and is accessible:
# Linux
ls -la /run/tailscale/tailscaled.sock
# macOS
ls -la /var/run/tailscale/tailscaled.sock“no match for IP:port” in Tailscale response
The connecting IP isn’t recognized by Tailscale. Ensure you’re connecting via a Tailscale IP address, not localhost or a LAN IP.