Files
hermes-skills/skills/.archive/tailscale-internal-services/SKILL.md
T

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