2026-06-21 Tailscale Serve for Open WebUI
What I set out to do
Reach my self-hosted Open WebUI instance from my phone when off the home network. Started as a survey of iOS clients, drifted into “what’s the right way to expose the instance,” and landed on Tailscale Serve. (Sibling to the 2026-06-21 Remote Obsidian MCP Connector work and a continuation of 2026-06-21 Open WebUI Deployment and Auth Cascade.)
What I actually did
Worked it end to end with Claude over one session.
Framing the clients first. Compared the iOS Open WebUI clients: Conduit (9.99, iOS 18+, on-device MLX voice, RAG, automations), and the free PWA (Add to Home Screen). Concluded the PWA is the best free path, which set up the real question: how to reach the instance remotely.
LAN first, then the real fix. The container published only on 127.0.0.1, so the phone couldn’t reach it even on Wi-Fi. Briefly flipped the Docker publish to 0.0.0.0 for LAN access (atlas.local:7878), then reverted it once Tailscale Serve made loopback the correct binding again.
Transport decision: Tailscale over Cloudflare. For a single user hitting their own instance, Tailscale Serve beats a Cloudflare Tunnel + Cloudflare Access setup: no public endpoint exists at all (vs a public hostname gated by an app-layer policy), WireGuard is end-to-end so no third party terminates TLS, and WEBUI_AUTH=False stays acceptable because the network is the auth. Kept Cloudflare for the Obsidian MCP (that one must be public, since Anthropic’s cloud dials in); Tailscale for OWUI (private). The classic “Tailscale for private, Cloudflare for public” split.
Why the daemon isn’t in Nix. macOS transparent tunneling needs Apple’s NetworkExtension entitlement, which requires Apple code-signing a /nix/store path can’t carry. nixpkgs tailscaled only does userspace-networking (a proxy), not a real tunnel. So Tailscale is a Homebrew cask (Tier 1.5), same as BetterTouchTool/Zotero. Nix owns only the declarative serve mapping.
Build (declarative pieces in home-manager).
- New module
tailscale-serve.nix: a Darwin-gatedprograms.tailscaleServethat appliestailscale serve --bg <port>as a one-shot activation (not a supervised launchd agent like the obsidian bridge/tunnel) because Serve persists its config in the daemon and restores it across reboots. Tolerant of the daemon being logged out sohm switchnever fails on it. - Reverted the OWUI publish bind
0.0.0.0→127.0.0.1; Serve proxies tailnet→loopback, so no LAN/public exposure. - Allowlisted
Bash(tailscale *); enabledprograms.tailscaleServeinhome.nixwired toconfig.programs.open-webui.port. brew install --cask tailscale, logged in on Mac + iPhone (both on tailnettail0cef90.ts.net).
HTTPS certs and the public-ledger nuance. Enabled HTTPS Certificates in the admin console (needed because iOS gates the secure-context features behind a valid cert). Verified the gates: microphone/getUserMedia → navigator.mediaDevices is undefined on plain HTTP non-localhost; service workers + push → HTTPS without exception; but “Add to Home Screen” itself works on HTTP (just a hollow shell). The catch: Tailscale’s Let’s Encrypt cert publishes atlas.tail0cef90.ts.net to public Certificate Transparency logs permanently. Only names leak (machine + the already-randomized tailnet name), not reachability. atlas is non-sensitive, so accepted it.
Verified end to end. tailscale serve status shows https://atlas.tail0cef90.ts.net → 127.0.0.1:7878; cert is Let’s Encrypt CN atlas.tail0cef90.ts.net; HTTPS health returns 200 with tls_verify=0. First request timed out on on-demand cert provisioning, then cached. Phone PWA confirmed working.
What was striking
- “Wouldn’t a port have to be exposed?” — no. Both Tailscale and Cloudflare are outbound-dial; neither opens a router port. The real difference is whether a publicly reachable endpoint exists. WireGuard silently drops unauthenticated packets, so the socket is invisible to scanners.
- HTTPS adds nothing to security here — WireGuard already encrypts the wire. The certs are purely to unlock iOS secure-context features (voice, push, service workers). Easy to conflate “HTTPS” with “secure transport” when the actual driver was browser feature-gating.
- CT logs are permanent and unrevocable. Disabling HTTPS later doesn’t remove the entry. Decide before provisioning; the randomized tailnet name is Tailscale’s built-in mitigation.
- The macOS entitlement wall is why so many network tools can’t be pure Nix on Darwin: Apple funnels kernel-level networking through signed system extensions, the opposite of Nix’s unsigned relocatable store paths.
- Serve vs the obsidian agents: Serve config lives in the daemon, so it needed a one-shot apply, not a
KeepAliveagent. Right tool per persistence model.
Top 3 next
- Consider tailnet ACLs to scope which devices can reach
atlas:443(defense-in-depth beyond device enrollment). - Decide whether to enable Open WebUI’s own
WEBUI_AUTHas belt-and-suspenders, or leave it network-gated. - Optionally wrap the
serveapply in aRunAtLoadlogin agent if daemon-state wipes ever become a problem (currently a one-shot activation).
Related
- Config:
tailscale-serve.nix,open-webui.nix(commit6c5249aonmain) - Endpoint:
https://atlas.tail0cef90.ts.net(tailnet-only) - Sibling work: 2026-06-21 Remote Obsidian MCP Connector, 2026-06-21 Open WebUI Deployment and Auth Cascade
- Concepts: Cloudflare Tunnel, Cloudflare Access