log in
consulting hosting industries the daily tools about contact

Tailscale Changed How I Think About Network Security

After running traditional VPNs for years, Tailscale quietly shifted my entire threat model. Here's what that actually means in practice.

I've been running VPNs for distributed teams since the OpenVPN days, and I thought I understood the threat model pretty well. Then I spent a weekend migrating NWOS infrastructure to Tailscale and realized I'd been solving the wrong problem for fifteen years.

What You Think You're Doing With a VPN

The classic VPN story goes like this: you have a private network, your servers live there, your team needs access, so you punch a hole through the perimeter with a VPN. Authenticated users get inside, and once inside, things are relatively trusted. That's the castle-and-moat model.

The problem is the moat metaphor breaks down the moment you have more than one office, more than one cloud region, or more than one person working from a coffee shop in Belltown. You end up managing routing rules, split tunneling configs, firewall exceptions for each subnet, and a single exit-node VPN server that becomes both a bottleneck and a single point of failure. I've maintained setups like this for clients for years. They work, but they're brittle, and the operational overhead is real.

More importantly, that model assumes the perimeter is the security boundary. Get inside, and you're trusted. That assumption is what Tailscale quietly dismantles.

What Tailscale Actually Is

Tailscale is a mesh VPN built on WireGuard. Every node — your laptop, a DigitalOcean droplet, an EC2 instance, a Raspberry Pi in a client's server room — gets a stable IP on the 100.x.x.x Tailscale network (called a tailnet) and talks directly to every other node via encrypted WireGuard tunnels. No hub. No central exit node that everything routes through.

The coordination layer — key exchange, identity, ACLs — runs through Tailscale's control plane. The actual traffic goes peer-to-peer whenever possible. When direct routing isn't available (NAT traversal fails, etc.), it falls back to DERP relay servers. In practice I've seen direct connections establish the vast majority of the time.

But the part that actually changes your threat model isn't the mesh topology. It's the ACL system and the identity binding.

The Threat Model Shift

With a traditional VPN, the question is: is this user on the network? Once the answer is yes, you typically trust them to reach most things inside.

With Tailscale, the question becomes: is this specific identity allowed to reach this specific resource? Every device is cryptographically tied to an identity (your SSO provider — Google, Okta, Azure AD, whatever). Access is defined in a policy file, version-controlled, and enforced at the network layer before a connection even completes.

This is the zero-trust shift. You're not trusting the network. You're trusting verified identities with explicit grants.

I noticed this concretely when I set up a tailnet for a small healthcare client last year. They had a mix of on-prem machines (an aging Windows Server doing billing software, a NAS) and some Linux boxes in AWS running our Laravel apps. Historically this would have meant a site-to-site VPN between the office and AWS, plus individual remote-access VPN for the staff, plus firewall rules on top of firewall rules.

With Tailscale:

  • Every machine gets a node, regardless of where it physically lives
  • I defined ACLs so clinical staff can reach the billing server and NAS but not the app servers
  • Developers (me, contractors) can reach the app servers but not the clinical systems
  • Nobody can reach anything they're not explicitly granted

The policy file for that looks roughly like this:

{
  "acls": [
    {
      "action": "accept",
      "src": ["group:clinical"],
      "dst": ["tag:billing:*", "tag:nas:*"]
    },
    {
      "action": "accept",
      "src": ["group:developers"],
      "dst": ["tag:appserver:*"]
    },
    {
      "action": "accept",
      "src": ["group:developers"],
      "dst": ["tag:appserver:22"]
    }
  ],
  "groups": {
    "group:clinical": ["user@client.com", "user2@client.com"],
    "group:developers": ["jason@nwos.com"]
  },
  "tagOwners": {
    "tag:billing": ["autogroup:admin"],
    "tag:nas": ["autogroup:admin"],
    "tag:appserver": ["autogroup:admin"]
  }
}

That file lives in a git repo. Changes get reviewed. There's an audit trail. Compare that to "SSH into the firewall and add an iptables rule" which is how it used to work.

Connecting From PHP/Laravel Apps

For web apps, Tailscale mostly lives at the infrastructure layer, but there are cases where you want an app to be Tailscale-aware. The most common one I hit is internal service-to-service calls — say, a Laravel app on one node needs to hit a private API or a local Postgres on another node.

You don't need a special SDK for this. Since every node has a stable 100.x.x.x IP (or a stable MagicDNS hostname like billing-server.your-tailnet.ts.net), you just configure your .env to point at that address:

# .env
DB_HOST=db-primary.your-tailnet.ts.net
DB_PORT=5432
DB_DATABASE=production
DB_USERNAME=app
DB_PASSWORD=secret

Laravel's database config picks that up like any hostname. The WireGuard tunnel handles encryption in transit. You get mTLS-level security without touching your application code.

For a client with a more complex microservice setup, I wrote a quick artisan command that uses the Tailscale local API to introspect the current node's identity and log it on startup — useful for debugging which tailnet node the app thinks it's running on:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class TailscaleStatus extends Command
{
    protected $signature = 'tailscale:status';
    protected $description = 'Show current Tailscale node identity';

    public function handle(): int
    {
        // Tailscale exposes a local HTTP API on a Unix socket.
        // On Linux you can curl it via --unix-socket /run/tailscale/tailscaled.sock
        // From PHP, shell out since Guzzle doesn't support Unix sockets natively.
        $output = shell_exec('curl -s --unix-socket /run/tailscale/tailscaled.sock http://local-tailscaled.sock/localapi/v0/status');

        if (! $output) {
            $this->error('Could not reach Tailscale local API. Is tailscaled running?');
            return self::FAILURE;
        }

        $status = json_decode($output, true);

        $this->info('Node: ' . $status['Self']['DNSName']);
        $this->info('IP:   ' . implode(', ', $status['Self']['TailscaleIPs']));
        $this->info('Tags: ' . implode(', ', $status['Self']['Tags'] ?? ['none']));

        return self::SUCCESS;
    }
}

Not glamorous, but useful. The local API is underdocumented but functional.

Gotchas That Will Bite You

MagicDNS split-brain. Tailscale's MagicDNS is convenient until it conflicts with your existing internal DNS. If you're already running a Pi-hole or a local resolver with custom zones, you'll need to sort out the override order carefully. I spent a couple of hours chasing a resolution issue on a client machine where the Tailscale DNS was winning over the local resolver for an internal domain. The fix is straightforward (configure split DNS in the admin panel) but not obvious.

The control plane is Tailscale's. The coordination server is SaaS. Your WireGuard traffic is peer-to-peer, but key distribution and identity go through Tailscale's infrastructure. For most of my clients this is fine — the actual data never touches their servers — but I have one client in a more regulated space who pushed back on this. Headscale (the open-source self-hosted control plane) exists and works, but it lags behind Tailscale features and requires you to maintain it. Factor that in.

Subnet routing adds latency. If you're using Tailscale subnet routers to expose a whole network rather than installing the client on each device, you're routing through that subnet router node. For latency-sensitive stuff, install the client directly on the machine.

Key expiry will lock you out of servers. By default, device keys expire every 180 days and require reauthentication. On a server you're not logging into regularly, that means suddenly losing access at the worst possible time. Disable key expiry on server nodes explicitly in the admin panel or via the API. I learned this one live.

# Disable key expiry via CLI (run on the node or from admin)
tailscale set --key-expiry=off

Ephemeral auth keys for CI/CD. If you're spinning up containers or EC2 instances in automation, use ephemeral auth keys with tags. They auto-deregister when the node exits. Don't use your personal auth key in a GitHub Actions secret.

When I'd Reach for Tailscale

Almost any time I need to connect nodes across networks now. Specifically:

  • Distributed teams accessing internal services without a VPN server to babysit
  • Connecting cloud VMs to on-prem machines for a client without site-to-site VPN budget or expertise
  • Zero-trust ACLs for teams where I want explicit grants rather than implicit trust
  • Dev environments — my local machine as a node, production database read replica accessible without exposing it publicly
  • Replacing bastion hosts for SSH access

When I wouldn't reach for it: if the client has a hard requirement for self-hosted control plane and doesn't want to run Headscale, or if they're in an environment where installing software on every node isn't feasible (legacy Windows machines managed by someone else's IT, for instance). Also, if you just need to expose one port on one server publicly, a properly configured security group or ufw rule is simpler. Tailscale is network infrastructure; don't use it where a firewall rule is sufficient.

The Bottom Line

The real value isn't the mesh topology or even the WireGuard performance. It's that Tailscale forces you to make access explicit and version-controlled, which is a better default posture than "inside the VPN, trust everything." That shift is what makes it worth the operational change.

If you're still running an OpenVPN or WireGuard setup you hand-tuned three years ago, spend an afternoon with Tailscale. The migration is faster than you expect, and you'll probably end up deleting firewall rules instead of adding them.

Need help shipping something like this? Get in touch.