Drill · platform · difficulty ●●● · 7 min

The macOS tunnel dies 150 seconds after every tailscale up

"Every time I bring Tailscale up on my Mac it works for about two and a half minutes, then the menu bar says Not Connected. I have reconnected nine times today."

Which failure areas the drills cover connectivity 3 drills identity 3 drills policy 2 drills dns 2 drills routing 3 drills platform 2 drills bar length is the count, not a difficulty or importance score

The ticket

The Customer is a developer on a MacBook, node-a, who depends on the tailnet for SSH into build machines. Since a laptop migration last week, the tunnel dies roughly two and a half minutes after every manual tailscale up. The menu bar flips to Not Connected, SSH sessions freeze, and the cycle repeats identically every time. Reboots do not help. The Customer is on the standalone client and has already reinstalled it once, which changed nothing. Urgency is high: they have been reconnecting by hand all day.

“It connects fine, works for exactly a couple of minutes, then just turns itself off. It is like something is switching it off behind my back.”

“Exactly a couple of minutes, every time” is the gift in this ticket. Random failures suggest network weather. A repeating interval suggests a scheduled or triggered mechanism on the machine itself.

Evidence provided

$ tailscale up
$ tailscale status | head -2
100.64.0.11   node-a    ops@   macOS   -
100.64.0.30   node-b    ops@   linux   idle; offers exit node

# 150 seconds later, unprompted:
$ tailscale status
Tailscale is stopped.

The unified log around one failure, collected with log show:

$ log show --last 10m --style compact \
    --predicate 'process CONTAINS "nesessionmanager" OR process CONTAINS "sysextd"'
14:31:40.112 Df sysextd[912]: activation request for io.tailscale.ipn.macos.network-extension
14:31:41.007 Df sysextd[912]: code signature validation failed for extension io.tailscale.ipn.macos.network-extension
14:31:41.020 Df nesessionmanager[734]: NESMVPNSession[Tailscale]: status changed to disconnected, last stop reason Plugin was disabled
14:31:41.031 Df nesessionmanager[734]: removing VPN configuration: The VPN app used by the VPN configuration is not installed

Note the bundle identifier: the failing extension is io.tailscale.ipn.macos.network-extension, which is the App Store network extension, but the installed standalone client uses io.tailscale.ipn.macsys.network-extension, the macsys system extension (src-bundle-ids). Two different identifiers means two different app variants are alive on this machine (kb-macos-variants).

Hypothesis tree

A tunnel that dies on a timer has a few plausible killers, and they leave different fingerprints in the unified log.

Hypothesis tree: macOS tunnel dies 150 seconds after upTunnel dies ~150 s after tailscale upsame interval every cycleA. Control plane or keyexpiry drops the nodeB. System extension iscrashing or unhealthyC. A second app instancetears down the live VPN configDiscriminator: log show showsa LOCAL teardown, no controlplane disconnect or auth errorDiscriminator: sysext statushealthy, no crash reports,extension stays activatedDiscriminator: two GUI processeswith different bundle IDs;scutil shows the config vanish

Investigation

  1. Count the GUI instances.

    $ pgrep -fl Tailscale | grep -v grep
    1043 /Applications/Tailscale.app/Contents/MacOS/Tailscale
    1187 /Applications/Tailscale-AppStore.app/Contents/MacOS/Tailscale

    Two running copies: the freshly installed standalone app, and the older Mac App Store copy, which the admin renamed rather than removed when installing the standalone build. Renaming leaves the bundle launchable and leaves its login item intact, so both start at boot. The macOS variants documentation is explicit: “Do not install the Mac App Store variant and the Standalone variant on the same machine. Having both variants running simultaneously can prevent the Tailscale extension from launching” (kb-macos-variants). This immediately rules out hypothesis A: key expiry and control plane behavior live in Module 02 and do not care how many local processes exist, and the log showed no auth error anyway.

  2. Check extension health.

    $ tailscale configure sysext status
    System extension state: OK. For more detailed information, run `systemextensionsctl list`.

    The standalone system extension itself is healthy (docs-debug-menu points at this same command from the Debug menu’s System Extension item), systemextensionsctl list shows io.tailscale.ipn.macsys.network-extension in state [activated enabled], and there are no crash reports for it. That rules out hypothesis B: nothing is crashing. Something is administratively removing the tunnel out from under a healthy extension.

  3. Watch the VPN configuration across one failure cycle.

    $ scutil --nc list
    Available network connection services in the current set (*=enabled):
    * (Connected)      8F2A11D0-51B2-4E2E-9B1C-0F44D2AA61C7 VPN (io.tailscale.ipn.macsys) "Tailscale"    [VPN:io.tailscale.ipn.macsys]
    
    # 150 seconds later
    $ scutil --nc list
    Available network connection services in the current set (*=enabled):

    The live VPN configuration does not disconnect; it is deleted. Combined with the nesessionmanager log line “removing VPN configuration,” this confirms the mechanism is at the macOS configuration layer, not inside WireGuard or the network.

  4. Line up the log timeline. The sysextd activation request for the App Store bundle ID fires at T+140 s after login item respawn, code signature validation fails one second later (the migrated app bundle is damaged), and nesessionmanager tears down the VPN configuration in the same second. The interval is not mysterious: it is the stale app’s own retry cadence.

  5. Check what the daemon believes after a flap.

    $ tailscale debug prefs | grep -i wantrunning
    	"WantRunning": false,

    This is the second half of the bug, and the reason the reinstall “changed nothing.” When the extension restarts after the teardown, it does not resume the running state the Customer created with tailscale up. It reloads its persisted preferences from disk, and the persisted record now says WantRunning false, written during the forced stop. Runtime state said connected; persisted state said stopped; persisted state wins on every restart.

Root cause

A chain, and every link is required. The laptop migration left a second Tailscale GUI variant on disk with a login item. The two variants are genuinely different programs: the App Store build is a sandboxed network extension, the standalone build is a system extension with different capabilities and different credential storage, which is the Module 09 platform matrix in action (kb-macos-variants). The stale App Store copy relaunches, registers its own VPN configuration, and fails macOS code signature validation because the migrated bundle is damaged. macOS responds by removing the VPN configuration, and that removal kills the LIVE tunnel, not just the stale one. The extension then restarts and loads persisted preferences in which WantRunning is false, so the machine settles into Not Connected instead of self-healing. Every manual tailscale up only flips runtime state; it never removes the process that keeps rewriting the outcome.

Fix and prevention

Immediate. Quit both GUI instances. Delete the stale App Store copy and its login item so exactly one variant remains, as the variants documentation requires (kb-macos-variants). Then hold down Option, select the Tailscale icon in the menu bar to reveal the Debug menu, and use Reset, which “Deletes and reinstalls the macOS VPN configuration” (docs-debug-menu), clearing the damaged registration. Note the documented limit: Reset “won’t work if your organization is deploying a VPN configuration profile on your Mac using an MDM solution” (docs-debug-menu). Run tailscale up.

Verification, and this is the part that matters: soak it. The failure signature was T+150 s, so a point check proves nothing. Watch for at least two full failure windows:

$ for i in $(seq 1 20); do date +%T; tailscale status --peers=false | head -1; sleep 15; done

Five minutes of Running, plus scutil --nc list still showing the configuration Connected, is a fix. Anything shorter is a guess. This is the Module 11 discipline: verify against the failure’s own period, not against the moment of repair.

Durable. Add a post-migration check to laptop provisioning: exactly one Tailscale.app on disk, one bundle ID in login items. If a Customer reports repeat flapping, collect tailscale bugreport and the unified log first (kb-troubleshooting); the timeline is the diagnosis.

The handoff package

Had this needed engineering:

The trap

The weak investigation fixes the symptom it can see: run tailscale up, watch the menu bar turn Connected, close the ticket. It passes the point check and dies 150 seconds later, nine times, burning the Customer’s trust a little more each round. The second trap is reinstalling the visible app while never counting processes, so the invisible second instance survives every reinstall. The discipline this drill teaches: when a failure has a period, your verification must be longer than the period, and when state keeps reverting, ask who persists that state and when it is reloaded. Point checks validate moments. Soak checks validate mechanisms.

Sources

  1. Three ways to run Tailscale on macOS checked 2026-08-10
  2. Troubleshooting guide checked 2026-08-10
  3. Debug menu and options checked 2026-08-10
  4. tailscale/tailscale version/prop.go (macOS bundle identifiers) checked 2026-08-10

All drills