96 lines
4.0 KiB
Markdown
96 lines
4.0 KiB
Markdown
---
|
|
name: tailscale-internal-services
|
|
description: "Deploy internal-only services behind Tailscale for zero public exposure — Tailscale Serve for HTTPS, UFW firewall rules, and service configuration patterns."
|
|
version: 1.0.0
|
|
author: ShoNuff
|
|
platforms: [linux]
|
|
metadata:
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
ufw deny <port>/tcp
|
|
ufw reload
|
|
```
|
|
|
|
### 5. Rename the hostname if the random tailnet name has confusing characters
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|