Tailscale Tunnel Setup Guide
Purpose: Public internet access with automatic .ts.net domains Audience: Users wanting secure, public internet connectivity Time Required: 10-15 minutes Prerequisites: Working cluster with Traefik ingress
⚠️ Critical Limitation: No Wildcard DNS Support
Tailscale Funnel does not support wildcard DNS routing. This means patterns like *.k8s.dog-pence.ts.net will NOT work for public internet access.
Reference: Tailscale GitHub Issue #15434 (Funnel wildcard support — still open as of Feb 2026)
Throughout this document we use the tailscale domain dog-pence.ts.net as an example. You get your own domain in the form <something>.ts.net when signing up to Tailscale.
What This Means:
- ❌ Does NOT work:
https://whoami.k8s.dog-pence.ts.net(subdomain pattern) - ❌ Does NOT work:
https://*.k8s.dog-pence.ts.net(wildcard routing) - ✅ DOES work:
https://whoami.dog-pence.ts.net(individual service via 803 script) - ✅ DOES work:
https://authentik.dog-pence.ts.net(each service gets its own URL)
The Solution: Individual Service Ingresses
We use the ./uis tailscale expose <service> command to create individual Tailscale ingresses for each service. Each service gets its own public URL directly on your tailscale domain.
🚀 Quick Summary
Transform your local cluster from http://service.localhost to public URLs like https://whoami.dog-pence.ts.net with automatic HTTPS. Each service gets its own public URL via individual Tailscale ingresses.
🏗️ How Tailscale Tunnel Works
Architecture Overview
Due to Tailscale's lack of wildcard DNS support, each service requires its own Tailscale ingress:
External User → https://whoami.dog-pence.ts.net
↓
Tailscale MagicDNS → whoami-ingress (dedicated Tailscale pod)
↓
Kubernetes Service → whoami pod
Key Components:
- Tailscale MagicDNS - Provides automatic DNS for each service (e.g.,
whoami.dog-pence.ts.net) - Individual Ingresses - Each service gets its own Tailscale pod/device
- Direct Service Routing - Traffic goes directly to each service
- Your Services - whoami, openwebui, authentik, etc.
Security Benefits:
- ✅ End-to-end encryption through Tailscale network
- ✅ No public IP exposure - services remain private
- ✅ Invite-based access - only your tailnet members can access
- ✅ Zero-trust networking - device authentication required
✅ Prerequisites
Before starting, ensure you have:
- Kubernetes cluster running (Rancher Desktop or similar)
- Traefik ingress controller deployed
- Services accessible locally (e.g.,
http://whoami.localhost) - Access to provision-host container
- Valid Tailscale account and credentials
📋 Commands Overview
| Command | Purpose | When to Use |
|---|---|---|
./uis deploy tailscale-tunnel | Deploy Tailscale operator to cluster | After secrets are configured |
./uis undeploy tailscale-tunnel | Remove Tailscale operator and ingresses | Clean up / start over |
./uis tailscale expose <service> | Expose a service via Tailscale Funnel | After operator deployed |
./uis tailscale unexpose <service> | Remove a service from Tailscale Funnel | When removing a service |
./uis tailscale verify | Check Tailscale secrets, API, devices, and operator | Diagnostics / pre-deploy checks |
🚀 Quick Start Guide
Step 1: Create Tailscale Account
- Visit tailscale.com and sign up
- Your tailnet will be created (e.g.,
yourusername.github) → Note this asTAILSCALE_TAILNET
Step 2: Configure Access Control Tags (Prepare for auth key)
- Go to Tailscale Access Controls
- Click "JSON editor" (top right of the policy editor)
- Replace the entire content with this clean configuration:
{
"tagOwners": {
"tag:k8s-operator": ["autogroup:admin"]
},
"nodeAttrs": [
{
"target": ["tag:k8s-operator"],
"attr": ["funnel"]
}
],
"acls": [
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
]
} - Click "Save"
What this does:
tagOwners: Allows admins to assigntag:k8s-operatortagsnodeAttrs: Enables Funnel capability for devices withtag:k8s-operator(public internet access)acls: Allows all devices to communicate with each other (simple setup)
Step 3: Create Auth Key (for provision-host authentication with Funnel)
- Go to Auth Keys page
- Click "Generate auth key"
- Description:
urbalurba-k8s-funnel - Reusable: ✅ Check this box (allows multiple devices)
- Expiration:
90days - Ephemeral: ❌ Leave unchecked (permanent infrastructure)
- Tags: Type
tag:k8s-operatorand click "Add tags"- The
tag:k8s-operatoris required for Funnel capability (public internet access)
- The
- Click "Generate key"
- Copy the auth key → This becomes
TAILSCALE_SECRET
Why tag:k8s-operator?
- The ACL policy grants Funnel capability only to devices with
tag:k8s-operator - This allows the device to expose services to the public internet
- Without this tag, you'll only get internal tailnet connectivity
Step 4: Create OAuth Client (for cluster operations)
-
Go to Trust credentials page
-
Click "Generate OAuth client" (this opens a 2-step wizard)
-
Step 1 - Settings: Select "OAuth client", add description
urbalurba-k8s-oauth -
Step 2 - Select required scopes:
- General → DNS: Select Write (enable MagicDNS features if needed)
- Devices → Core: Select Write (create/delete cluster devices)
- Tags (required for write scope): Click "Add tags" and add
tag:k8s-operator - This allows the OAuth client to create devices with the k8s-operator tag
- Tags (required for write scope): Click "Add tags" and add
- Keys → Auth Keys: Select Write ← REQUIRED (allows operator to create internal auth keys)
- Tags (required for write scope): Click "Add tags" and add
tag:k8s-operator
- Tags (required for write scope): Click "Add tags" and add
- Settings → Feature Settings: Select Write (enable HTTPS/Funnel features)
- Leave all other scopes unselected (principle of least privilege)
-
Click "Generate client"
-
Copy the Client ID → This becomes
TAILSCALE_CLIENTID -
Copy the Client Secret → This becomes
TAILSCALE_CLIENTSECRET⚠️ Important: Save these values immediately - you can't view the secret again!
Why these scopes?
- Keys → Auth Keys (Write): CRITICAL - Allows Tailscale operator to create internal auth keys (without this you get 403 errors)
- Devices → Core (Write): Allows Tailscale operator to create/delete cluster ingress devices
- General → DNS (Write): Enables MagicDNS configuration
- Settings → Feature Settings (Write): Allows enabling HTTPS/Funnel for internet access
Step 5: Configure MagicDNS Domain
- Go to Tailscale Admin Console → DNS
- Enable MagicDNS
- Note your MagicDNS domain (e.g.,
dog-pence.ts.net) → This becomesTAILSCALE_DOMAIN
Step 6: Configure Tailscale Secrets
Edit the secrets source file with your Tailscale values from Steps 1-5:
nano .uis.secrets/config/00-common-values.env
Update these variables:
TAILSCALE_SECRET=tskey-auth-YOUR-AUTH-KEY # From Step 3: Auth Key
TAILSCALE_TAILNET=your-tailnet-name # From Step 1: Your tailnet name
TAILSCALE_DOMAIN=your-magic-dns-domain # From Step 5: MagicDNS domain
TAILSCALE_PUBLIC_HOSTNAME=k8s # Becomes: k8s.[your-domain].ts.net (cluster ingress only)
TAILSCALE_CLIENTID=YOUR-OAUTH-CLIENT-ID # From Step 4: OAuth Client ID
TAILSCALE_CLIENTSECRET=tskey-client-YOUR-SECRET # From Step 4: OAuth Client Secret
Then regenerate the Kubernetes secrets:
./uis secrets generate
Important: TAILSCALE_PUBLIC_HOSTNAME:
- This is used for the cluster-wide ingress only (when no service parameter is provided)
- Example: If set to
k8sand your domain isdog-pence.ts.net:k8s.dog-pence.ts.net→ Routes to Traefik's default backend (nginx catch-all)- Individual services get their own URLs:
whoami.dog-pence.ts.net,grafana.dog-pence.ts.net - Note: Tailscale does NOT support wildcard DNS, so
*.k8s.dog-pence.ts.netpatterns won't work
Step 7: Verify Tailscale Configuration
Verify your Tailscale secrets and API connectivity before deploying:
./uis tailscale verify
This checks:
- Secrets are present and not placeholder values
- API connectivity (OAuth authentication test)
- Stale device report (flags devices with
-Nsuffixes) - Operator status (running/not deployed)
Step 8: Deploy Tailscale Operator to Cluster
# Deploy the Tailscale operator (secrets are applied automatically)
./uis deploy tailscale-tunnel
Step 9: Expose Services via Tailscale Funnel
Since Tailscale doesn't support wildcard DNS, expose each service individually:
# Expose whoami (uses service name as hostname)
./uis tailscale expose whoami
# Result: https://whoami.dog-pence.ts.net
# Expose other services
./uis tailscale expose open-webui
# Result: https://open-webui.dog-pence.ts.net
./uis tailscale expose authentik-server
# Result: https://authentik-server.dog-pence.ts.net
./uis tailscale expose grafana
# Result: https://grafana.dog-pence.ts.net
What happens:
- Creates a Tailscale ingress pod for that specific service
- Configures public internet access via Funnel
- Sets up DNS entry at
[service].[your-domain].ts.net - Verifies connectivity and reports the public URL
- Detects hostname mismatches (warns if a stale device caused a
-Nsuffix)
To remove a service from Funnel:
./uis tailscale unexpose whoami
This removes the Tailscale ingress and cleans up the device from your Tailnet via API.
Important notes:
- Each service requires its own Tailscale pod (slight resource overhead)
- Services are directly accessible from the public internet
- No authentication by default - add Authentik protection if needed
- DNS propagation takes 1-5 minutes globally after adding a service
Step 10: Test Public Internet Access
# Test your exposed services (replace with your actual domain):
curl https://whoami.dog-pence.ts.net
curl https://openwebui.dog-pence.ts.net
curl https://authentik.dog-pence.ts.net
# These URLs work from:
# - Any browser on any computer
# - No Tailscale client needed for visitors
# - Full public internet exposure via Funnel
# To see all your active Tailscale ingresses:
kubectl get pods -n tailscale -l app.kubernetes.io/name=tailscale-ingress
Step 11: DNS Troubleshooting
If services are not immediately accessible, use these commands to check DNS resolution:
# Check basic DNS resolution
nslookup whoami.dog-pence.ts.net
# Get detailed DNS information
dig whoami.dog-pence.ts.net
# Test connectivity with verbose output
curl -v https://whoami.dog-pence.ts.net
Expected Results:
nslookupshould return a Tailscale Funnel IP (e.g.,185.40.234.37)digshould show the A record with TTL informationcurl -vshould show successful TLS handshake and HTTP response
Common Issues:
- "Could not resolve host" - DNS propagation still in progress (wait 1-5 minutes)
- "Connection timeout" - Check if service is running in cluster
- "404 Not Found" - Service exists but Traefik routing needs adjustment
🗑️ Complete Cleanup
To completely remove Tailscale and start over:
# Remove Tailscale operator, ingresses, and all cluster devices from Tailnet
./uis undeploy tailscale-tunnel
What gets deleted:
- All Tailscale ingresses (cluster and per-service)
- Tailscale operator Helm release
- Tailscale namespace and pods
- All cluster devices from your Tailnet (via API, enabled by default)
🔧 Troubleshooting
Error: "requested tags [tag:k8s-operator] are invalid or not permitted"
This error means your OAuth client doesn't have permission for tag:k8s-operator. To fix:
- Go to Trust credentials page
- Edit your
urbalurba-k8s-oauthclient - In Devices → Core scope, ensure
tag:k8s-operatoris added - In Keys → Auth Keys scope, ensure
tag:k8s-operatoris added - Generate a new client secret (required after scope changes)
- Update
TAILSCALE_CLIENTSECRETin.uis.secrets/config/00-common-values.env - Regenerate secrets:
./uis secrets generate - Redeploy:
./uis deploy tailscale-tunnel
Key Point: The operator uses tag:k8s-operator for all devices, including itself and cluster ingress devices with Funnel capability.
Expired Tailscale Keys
If you get authentication errors, create new keys at Tailscale Admin Console:
Create OAuth Client:
- Go to Trust credentials page
- Click "Generate OAuth client", name:
urbalurba-k8s - Scopes: Devices → Core (Write), Keys → Auth Keys (Write) — both with
tag:k8s-operator - Copy Client ID and Client Secret
Create Auth Key:
- Click "Generate auth key" → "Auth key"
- Tags:
tag:provision-host(optional) - Expiry: 90 days
- Copy the auth key
Update secrets file:
# Edit .uis.secrets/config/00-common-values.env
TAILSCALE_SECRET=tskey-auth-YOUR-NEW-AUTH-KEY
TAILSCALE_CLIENTID=YOUR-NEW-CLIENT-ID
TAILSCALE_CLIENTSECRET=tskey-client-YOUR-NEW-CLIENT-SECRET
# Regenerate and redeploy
./uis secrets generate
./uis deploy tailscale-tunnel
TLS Handshake Timeout (Let's Encrypt Rate Limiting)
If ./uis deploy tailscale-tunnel or ./uis tailscale expose <service> reports a TLS handshake timeout, check the Tailscale proxy pod logs:
./uis shell
kubectl logs -n tailscale -l tailscale.com/parent-resource=traefik-ingress --tail=50
If you see an error like:
cert("k8s.dog-pence.ts.net"): getCertPEM: 429 urn:ietf:params:acme:error:rateLimited:
too many certificates (5) already issued for this exact set of identifiers
This means Let's Encrypt ACME rate limiting is blocking TLS certificate issuance. The limit is 5 certificates per exact hostname per 7 days. This typically happens when you repeatedly deploy/undeploy the same hostname during testing.
Solutions:
- Wait for the rate limit to reset (the error message includes the retry-after timestamp)
- Use a different hostname — change
TAILSCALE_PUBLIC_HOSTNAMEin.uis.secrets/config/00-common-values.env(e.g.,k8s-2instead ofk8s), then./uis secrets generateand redeploy - Avoid repeated deploy/undeploy cycles with the same hostname during testing
Script Execution Issues
Check Tailscale status in provision-host:
# Access provision-host container
./uis shell
# Check cluster operator
kubectl get pods -n tailscale
kubectl logs -n tailscale -l app=operator
Check cluster connectivity:
# From provision-host container
kubectl get ingressroute -A
kubectl describe ingress -n kube-system
Tailscale Installation Issues
Tailscale is pre-installed in the provision-host container. If missing:
# From provision-host container
curl -fsSL https://tailscale.com/install.sh | sh
📚 Architecture Details
Per-Service Routing Flow
1. External request: https://whoami.dog-pence.ts.net
2. Tailscale MagicDNS resolves to specific whoami-ingress device
3. whoami-ingress pod forwards directly to whoami service
4. No Traefik involvement - direct service connection
Setup Flow
- Configure secrets →
./uis deploy tailscale-tunnel→./uis tailscale expose <service>(sequential) - Run
./uis tailscale exposefor each service you want to make public ./uis tailscale unexpose <service>removes a single service from Funnel./uis undeploy tailscale-tunnelremoves operator and all ingresses
Integration with Other Systems
- Works alongside Cloudflare tunnels (different domains)
- Each service gets independent public URL
- Can add Authentik protection per service if needed
✅ Verification
After setup, verify your services are accessible:
# Run Tailscale diagnostics
./uis tailscale verify
# Test individual service URLs
curl https://whoami.dog-pence.ts.net
curl https://openwebui.dog-pence.ts.net
curl https://authentik.dog-pence.ts.net
🎉 Benefits Achieved
✅ Public Internet Access: Each service accessible via its own .ts.net URL from anywhere
✅ Automatic HTTPS: Zero-configuration SSL certificates
✅ No Port Forwarding: Works behind NAT/firewalls via Tailscale Funnel
✅ Flexible Service Exposure: Choose exactly which services to make public
✅ Simple Management: Add/remove services with single command
📝 Summary
While Tailscale doesn't support wildcard DNS (limiting us from using patterns like *.k8s.dog-pence.ts.net), the ./uis tailscale expose command provides a practical workaround. Each service gets its own public URL like https://whoami.dog-pence.ts.net, giving you full control over which services are exposed to the internet.
⚠️ Authentication Note: Services exposed via Tailscale are publicly accessible by default. If you need authentication, consider adding Authentik protection. See docs/rules-ingress-traefik.md for authentication setup details.