Overview

Tailscale provides automatic HTTPS certificates for devices on your tailnet. This solves a common problem: you have a local service (like a web app on your home server) that you want to access securely, but getting TLS certificates for private IPs or local hostnames is normally impossible.

Why HTTPS matters: Many modern web APIs (like WebAuthn/passkeys, geolocation, camera access, notifications) require a "secure context" - meaning HTTPS. Without it, these features are disabled by browsers.

How It Works

The Problem

Normally, to get an HTTPS certificate you need:

  • A public domain name (like example.com)
  • Proof that you own that domain (DNS or HTTP challenge)
  • A Certificate Authority to issue the cert (Let's Encrypt, etc.)

This doesn't work for private services because:

  • Private IPs (192.168.x.x, 10.x.x.x) can't get certificates
  • Local hostnames aren't globally unique
  • Your home server isn't reachable from the internet for validation

Tailscale's Solution

Tailscale gives every device on your tailnet a unique DNS name under *.ts.net:

your-machine.your-tailnet.ts.net

Since Tailscale controls the ts.net domain, they can issue valid certificates for your devices through Let's Encrypt.

Your Device
amd-miniserver
Tailscale MagicDNS
amd-miniserver.tail0489.ts.net
Let's Encrypt Certificate
Valid HTTPS for your domain

Tailscale Serve

tailscale serve is a built-in reverse proxy that:

  • Automatically provisions HTTPS certificates
  • Handles TLS termination
  • Proxies requests to your local HTTP service
  • Only accessible from your tailnet (private by default)

Basic Usage

# Serve a local HTTP app over HTTPS
sudo tailscale serve --bg https / http://localhost:3004

# Now accessible at:
# https://your-machine.your-tailnet.ts.net/

What Happens

┌─────────────────────────────────────────────────────────────────┐ │ Your Tailnet │ │ │ │ ┌──────────────┐ ┌──────────────────────────────────┐ │ │ │ │ HTTPS │ amd-miniserver │ │ │ │ iPhone │────────▶│ ┌─────────────────────────────┐ │ │ │ │ (Safari) │ │ │ tailscale serve │ │ │ │ │ │ │ │ (reverse proxy) │ │ │ │ └──────────────┘ │ │ │ │ │ │ │ │ - TLS termination │ │ │ │ ┌──────────────┐ │ │ - Certificate management │ │ │ │ │ │ HTTPS │ │ - Proxies to localhost │ │ │ │ │ MacBook │────────▶│ └──────────────┬──────────────┘ │ │ │ │ │ │ │ HTTP │ │ │ └──────────────┘ │ ▼ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │ Your App (port 3004) │ │ │ │ │ │ biodex, calorie-tracker │ │ │ │ │ └─────────────────────────────┘ │ │ │ └──────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘

The --bg Flag

The --bg flag runs the serve configuration in the background and persists it across reboots. Without it, the serve would stop when you close the terminal.

Common Commands

Command Description
tailscale serve status Show current serve configuration
tailscale serve --bg https / http://localhost:PORT Serve local app over HTTPS
tailscale serve reset Remove all serve configurations
tailscale cert DOMAIN Manually download certificate files
tailscale status Show your tailnet devices and IPs

Multiple Apps

If you have multiple apps on the same machine, you have several options:

Option 1: Path-based Routing

sudo tailscale serve --bg --https=443 /biodex http://localhost:3004
sudo tailscale serve --bg --https=443 /calories http://localhost:3000

# Access at:
# https://machine.tailnet.ts.net/biodex
# https://machine.tailnet.ts.net/calories
Caveat: Apps need to handle being served from a subpath. SvelteKit, for example, needs base configuration.

Option 2: Different Ports

sudo tailscale serve --bg --https=443 / http://localhost:3004
sudo tailscale serve --bg --https=8443 / http://localhost:3000

# Access at:
# https://machine.tailnet.ts.net/ (port 443)
# https://machine.tailnet.ts.net:8443/

Option 3: Caddy Reverse Proxy

For more complex setups, use Caddy with Tailscale certificate integration:

# /etc/caddy/Caddyfile
machine.tailnet.ts.net {
    handle /biodex/* {
        uri strip_prefix /biodex
        reverse_proxy localhost:3004
    }
    handle /calories/* {
        uri strip_prefix /calories
        reverse_proxy localhost:3000
    }

    tls {
        get_certificate tailscale
    }
}

Serve vs Funnel

Tailscale has two ways to expose services:

Feature tailscale serve tailscale funnel
Accessibility Tailnet only (private) Public internet
Who can access Only your devices Anyone with the URL
Use case Personal apps, dev servers Webhooks, sharing with others
Security Protected by Tailscale auth Exposed to internet
For personal apps: Use tailscale serve. Your services stay private and are only accessible from devices signed into your Tailscale account.

Certificate Details

Automatic Management

When using tailscale serve, certificates are managed automatically:

  • Provisioned on first request
  • Renewed automatically (Let's Encrypt certs are valid 90 days; Tailscale renews ~30 days before expiry)
  • Stored securely by Tailscale daemon

Manual Certificates

If you need the actual certificate files (for custom server configuration):

sudo tailscale cert your-machine.tailnet.ts.net

# Creates:
# your-machine.tailnet.ts.net.crt (certificate)
# your-machine.tailnet.ts.net.key (private key)

You can then configure your web server (nginx, Node.js, etc.) to use these files.

Why HTTPS Matters for Web Apps

Modern browsers restrict powerful APIs to "secure contexts" (HTTPS or localhost):

Feature Requires HTTPS?
WebAuthn / Passkeys Yes
Geolocation API Yes
Camera / Microphone Yes
Push Notifications Yes
Service Workers (PWA) Yes
Clipboard API Yes
Web Bluetooth Yes

This is why accessing http://amd-miniserver:3004 over plain HTTP breaks features like passkey authentication - the browser refuses to expose the WebAuthn API.

Troubleshooting

Certificate not working

# Check serve status
tailscale serve status

# Reset and try again
sudo tailscale serve reset
sudo tailscale serve --bg https / http://localhost:3004

Can't access from other device

  • Make sure both devices are on your tailnet
  • Check tailscale status shows both devices
  • Verify MagicDNS is enabled in Tailscale admin console

App works on localhost but not via Tailscale

  • Verify the port number in your serve command matches the port your app is listening on
  • tailscale serve connects locally, so your app only needs to listen on 127.0.0.1
  • Check firewall allows the port if you also need direct LAN access