Files

4.0 KiB

name, description, version, author, platforms, metadata
name description version author platforms metadata
tailscale-internal-services Deploy internal-only services behind Tailscale for zero public exposure — Tailscale Serve for HTTPS, UFW firewall rules, and service configuration patterns. 1.0.0 ShoNuff
linux
hermes
tags
tailscale
vpn
networking
security
internal-services
docker

Tailscale Internal Services

Standard for deploying services that should not be publicly accessible. All internal services live behind Tailscale — no DNS records, no public ports, no Let's Encrypt.

Architecture

Device (iPhone/Mac)                     app1 (netcup)
┌──────────────┐   Tailscale tunnel     ┌─────────────────────┐
│  Tailscale   │◄──────────────────────►│  tailscale serve     │
│  connected    │    encrypted mesh       │  ↓                   │
│              │                        │  Docker container    │
│  localhost    │                        │  port 8080           │
└──────────────┘                        └─────────────────────┘
         │
         ▼
  https://<hostname>.tailXXXXX.ts.net

Key principle: Tailscale traffic is already encrypted end-to-end. The Tailscale Serve HTTPS layer is only needed for (a) browser security warnings and (b) apps (like Bitwarden) that reject plain HTTP self-hosted URLs. For API-only services, plain HTTP over the tailnet is fine.

Setup

1. Install Tailscale

curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
# Auth URL printed — open in browser to log in

2. Start a service with Tailscale Serve

# Expose a local Docker port via Tailscale HTTPS
tailscale serve --bg --https 443 --set-path / http://127.0.0.1:<port>

# Verify
tailscale serve status
# → https://<hostname>.tail<random>.ts.net/ → proxy http://127.0.0.1:<port>

3. Configure the service's DOMAIN

Update the container's DOMAIN environment variable to match the Tailscale URL so the service's internal redirects and API URLs work correctly.

4. Close the public port

ufw deny <port>/tcp
ufw reload

5. Rename the hostname if the random tailnet name has confusing characters

tailscale set --hostname <service-name>
# Then re-create serve config (it picks up the new hostname)

Application-Specific Notes

Vaultwarden / Bitwarden

  • Use "Self-hosted environment" in the Bitwarden app
  • URL: https://<hostname>.tail<random>.ts.net
  • If the Tailscale hostname has zeroes or letter-O confusables, rename the node: tailscale set --hostname vaultwarden
  • The Bitwarden mobile app may have issues with certain SSL implementations from Tailscale Serve. If the app can't connect, try:
    • Use the Custom Environment option in the app and enter api/identity/web vault URLs separately
    • Access via pure HTTP on the Tailscale IP (http://100.x.x.x:8080) instead of the HTTPS hostname — works fine since tailnet traffic is encrypted

UFW Rules

# Default: ALLOW SSH, HTTP, HTTPS for management
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp

# Per service: start with port open for testing, then close when Tailscale is verified
ufw allow <port>/tcp comment 'Service name (temp)'
ufw delete allow <port>/tcp  # After verifying Tailscale access

Pitfalls

  • Tailscale Serve requires HTTPS — you can't serve a port on both HTTP and HTTPS via Tailscale Serve simultaneously
  • Hostname changes after serve is active — you need to restart the serve daemon (turn off, then re-enable with the new hostname)
  • Safari on iOS shows "Not Secure" for plain HTTP over tailnet — this is cosmetic, the tunnel is still encrypted
  • tailscale serve status shows a tailnet-only badge — verify with -o json for automated checks
  • Renaming the tailscale node (e.g. app1→vaultwarden) changes the serve URL immediately without downtime