Back to blog
Product

Run a private MCP Gateway on Tailnet

Tristan Cartledge

Tristan Cartledge

September 4, 2026 · 8 min read

Run a private MCP Gateway on Tailnet

The Speakeasy MCP gateway hosts your MCP servers so that agents and people across an organization make tool calls through one governed path. Until now, that gateway lived on the public internet. You could put an IP allowlist in front of them, but the listener was still public.

Security-sensitive organizations run their businesses's employee activity through VPNs like Tailscale, why should agent-based activity be any different?

If you're a tailscale customer, you can now run the Speakeasy MCP gateway directly on your tailnet at a stable private hostname. If you choose, public access is switched off entirely, and the platform host and your custom domain return a 403 to all requests on a public network.

Public endpoint with an IP allowlist
Where the server is reachable
The public internet, filtered by source address
What a request tells you
An IP address
Where access policy lives
A second allowlist in Speakeasy, maintained alongside your network
What it needs from you
Stable IPv4 egress addresses
Private MCP gateway on your tailnet
Where the server is reachable
Your tailnet only. Public hostnames return a 403
What a request tells you
Tailscale user, device, and tags, on every call
Where access policy lives
Your tailnet ACLs, where you already manage it
What it needs from you
Nothing at the network layer. IPv6 works too

What changes when MCP lives inside your VPN

No public listener

If you run your tailnet on private-only mode there's nothing on the public internet to find. The platform host and your custom domain will answer 403 to every request. There's no DNS name that resolves to your servers, no TLS handshake to complete, and nothing for a scanner to enumerate.

It also fails closed. Enforcement of the public block doesn't depend on the tailnet being up, so a Tailscale outage, a misconfiguration, or a restart on our side never reopens the public path. The worst case is that your servers are unreachable, never that they're exposed.

Identity on every request

A request arriving over the tailnet carries the Tailscale user, device, and tags it came from. Speakeasy attaches that identity to the request, logs it, and makes it available in your audit trail next to the tool that was called, so "who made this call" has an answer at the network layer as well as at the MCP authorization layer.

That identity is also the foundation for what comes next. We're building per-server authorization on top of it using Tailscale ACL capability grants, so a tailnet policy can say which people and devices may call which MCP servers.

Your tailnet ACLs govern access

Who can reach the Speakeasy MCP gateway is decided by your tailnet policy, the same file that already governs everything else on your network. There's no second allowlist on our side to keep in step with it.

That also removes the requirement the IP allowlist imposed. You don't need stable egress addresses, you don't need to route developer laptops through a fixed gateway, and IPv6 works without any special handling.

Self-serve in under ten minutes

An org admin connects the tailnet from the domain settings page in the Speakeasy dashboard. Once the node joins, your MCP endpoints, OAuth flows, and install pages are all served at one stable private hostname, so clients configured against it keep working across restarts and upgrades on our side.

How it works

Speakeasy runs on Kubernetes, and the Tailscale Kubernetes operator is what puts each organization's MCP gateway on that organization's tailnet. The four steps below run once per org, and every org's credentials, node, and workloads are isolated from every other's.

MECHANISM · PRIVATE NETWORK INGRESSA request's path onto your tailnetPUBLIC INTERNET203.0.113.7GET /mcp/ops403mcp.acme.com · no listener · fails closedYOUR TAILNETjane-mbpjane@acme.com · tag:engci-runner-02tag:ci · ephemeralJOINEDspeakeasy.acme.ts.netMCP gateway · tailnet nodeoauth client · org-scopedATTESTORuser jane@acme.comdevice jane-mbptags tag:eng/mcp/opsprivate-only/mcp/billingdual/mcp/githubprivate-only20001 CONNECT02 JOIN03 ATTEST04 ENFORCESPEAKEASY AUDIT LOGtool.call user=jane@acme.com device=jane-mbp tags=tag:eng server=/mcp/ops status=200
01 · CONNECT
You authorize Speakeasy on your tailnet
An org admin authorizes Speakeasy with a scoped Tailscale OAuth client. The credential is stored per org and used only for that org's node.
02 · JOIN
Speakeasy becomes a node on your tailnet
The Tailscale Kubernetes operator provisions a highly available ProxyGroup and Ingress for your org. Your MCP endpoints appear as a service on the tailnet at a stable hostname, with TLS handled by the platform.
03 · ATTEST
Each request is verified and tagged with identity
A per-org attestor sits between the tailnet and your servers. It forwards only the allowlisted MCP and OAuth paths, verifies the workload with a Kubernetes TokenReview, and attaches the Tailscale user, device, and tags to the request.
04 · ENFORCE
Each server picks its exposure
Every MCP server is public-only, dual, or private-only. Private-only returns a 403 on all public routes and never reopens on failure.

The attestor forwards a fixed set of MCP and OAuth paths and nothing else, and it checks that the workload it's talking to is the one it expects before any request goes through. The tailnet gives you a private path in. The attestor makes sure that path only leads to your MCP servers.

Choosing an exposure mode per server

Connecting your tailnet doesn't change how any server is reached until you say so. Each MCP server has its own mode, so you can move servers over one at a time.

ModePublic hostnamesTailnet hostnameUse it for
Public-onlyServed, with the IP allowlist if you set oneNot servedServers that external partners or hosted agents need to reach
DualServedServedMigrating clients from the public URL to the private one
Private-only403 on every routeServedAnything that should never be reachable off your network

Dual mode is the migration path. Turn it on, repoint clients at the tailnet hostname, watch the activity view until the public URL goes quiet, then switch the server to private-only.

Set it up

Private network ingress is configured per organization from the same domain settings page that holds your custom domain and IP allowlist.

  1. In Tailscale, create an OAuth client scoped for Speakeasy and give it the tag your policy will use for the Speakeasy node.
  2. In Speakeasy, open the domain settings page under Organization settings, connect your tailnet, and provide the OAuth client credentials. The node joins your tailnet and the page shows its hostname.
  3. Add the node's tag to your tailnet ACLs alongside the users and devices that should reach it.
  4. On each MCP server, set the exposure mode. Start with dual, then move to private-only once clients are on the tailnet hostname.

From that point the MCP endpoint, its OAuth authorization and token endpoints, and its install page are all served at the private hostname, and every call through it lands in your audit log with the Tailscale identity that made it.

Rolling out

Private network ingress is rolling out per organization. Reach out to have it enabled for yours.

Why Tailscale first

Tailscale is where our security-sensitive customers already are, and its Kubernetes operator gives us the cleanest way to put a per-org service on a customer's network: isolation, high availability, and TLS are handled by the platform rather than rebuilt by us. That made it the right launch partner and the reference implementation for private network ingress. Other overlay networks are on the roadmap, and they'll follow the same shape.

If you've been running Speakeasy behind an IP allowlist, this is the deeper integration that post promised. Together with MCP tunnels for servers inside your network, it means an agent can reach a private MCP server without either end ever touching the public internet.


Need an MCP gateway on a network that can't have a public listener? Book time with our team and we'll connect your tailnet with you.

Last updated on

AI everywhere.

Control here.