Tailscale Authentication

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:

  1. Configure Agent Gateway to use Tailscale for authentication
  2. Query the Tailscale daemon to identify connecting users
  3. Extract node name and user email from Tailscale identity
  4. 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 status

You should see your machine listed with a 100.x.x.x IP address.

Note your Tailscale IP:

tailscale ip -4

Step 2: Create the configuration

Create a working directory:

mkdir tailscale-auth-test && cd tailscale-auth-test

Create 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
EOF

macOS 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
EOF

Configuration 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.yaml

You 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/mcp

Expected response:

HTTP/1.1 403 Forbidden
external authorization failed

This 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/mcp

Expected response:

HTTP/1.1 406 Not Acceptable
Not Acceptable: Client must accept text/event-stream

The 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        │                      │
       │◀───────────────────│                      │
  1. Client connects from their Tailscale IP (100.x.x.x)
  2. Agent Gateway calls Tailscale’s local whois API with the source IP
  3. Tailscale returns the node and user information
  4. 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-test

Troubleshooting

“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.

Learn more