For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Tailscale authentication
Authenticate users using Tailscale identity for zero-trust access
Agentgateway 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 configure the following.
- Configure agentgateway 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
Before you begin
- agentgateway 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'
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
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
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 agentgateway
agentgateway -f config.yamlExample output:
info proxy::gateway started bind bind="bind/3000"Step 4: Test the authentication
Test from localhost (should fail)
Requests from localhost do not 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 agentgateway logs show Tailscale identity.
info request ... tailscale.node=your-machine-name [email protected]How it works
┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │────▶│agentgateway │────▶│ 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)
- Agentgateway calls Tailscale’s local
whoisAPI with the source IP - Tailscale returns the node and user information
- Agentgateway allows/denies the request and logs the identity
Tailscale socket locations
| Platform | Socket Path |
|---|---|
| Linux | /var/run/tailscale/tailscaled.sock |
| macOS | /var/run/tailscale/tailscaled.sock |
| Windows | Named pipe (not supported via unix socket) |
/var/run is a symlink to /run, so /var/run/tailscale/tailscaled.sock and /run/tailscale/tailscaled.sock point to the same socket.Cleanup
Stop the agentgateway 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.