Tailscale Deployment Design
Issue: #514 Date: 2025-12-31 Status: Approved
Summary
Section titled “Summary”Deploy Tailscale to the cluster as a subnet router and exit node, enabling secure remote access to the home network (192.168.20.0/22) from any device on the existing tailnet.
- Remote admin access — SSH to nodes, kubectl from anywhere on the tailnet
- Service exposure — Access internal services directly via Tailscale (bypassing Cloudflare)
- Exit node — Route device traffic through home network when traveling
- Reliable DNS — Ensure consistent route to Firewalla DNS (
192.168.20.1) for MagicDNS
Approach
Section titled “Approach”Tailscale Operator with Connector CRD — The operator manages node lifecycle, and the Connector resource defines the subnet router with HA enabled.
Why This Approach
Section titled “Why This Approach”- Official Kubernetes-native solution
- Built-in HA with automatic failover (no manual replica management)
- OAuth-based auth eliminates key expiration issues
- Clean CRD-based config fits GitOps workflow
Prerequisites
Section titled “Prerequisites”Tailscale Admin Console
Section titled “Tailscale Admin Console”- Create OAuth client at Settings → OAuth clients with scopes:
devices:writeroutes:write
- Note the Client ID and Client Secret
Tailscale ACL (Optional but Recommended)
Section titled “Tailscale ACL (Optional but Recommended)”Add autoApprovers to avoid manual route approval:
"autoApprovers": { "routes": { "192.168.20.0/22": ["tag:k8s-operator"] }, "exitNode": ["tag:k8s-operator"]}Vault Secret
Section titled “Vault Secret”Store OAuth credentials at secret/tailscale/operator:
clientId: "k..."clientSecret: "tskey-client-..."Vault Policy
Section titled “Vault Policy”Add read access for External Secrets Operator:
path "secret/data/tailscale/*" { capabilities = ["read"]}Architecture
Section titled “Architecture”┌─────────────────────────────────────────────────────────────┐│ Tailscale Control Plane │└─────────────────────────────────────────────────────────────┘ │ │ OAuth + WireGuard ▼┌─────────────────────────────────────────────────────────────┐│ tailscale namespace ││ ┌──────────────────┐ ┌──────────────────────────────┐ ││ │ tailscale- │ │ Connector: home-network │ ││ │ operator │───▶│ - advertises 192.168.20.0/22│ ││ │ │ │ - exit node enabled │ ││ └──────────────────┘ │ - HA: 2 replicas │ ││ └──────────────────────────────────┘ ││ │ ││ ┌───────────┴───────────┐ ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │k8s-subnet- │ │k8s-subnet- │ ││ │router-0 │ │router-1 │ ││ │(primary) │ │(standby) │ ││ └─────────────────┘ └─────────────────┘ │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Home Network 192.168.20.0/22 ││ ┌────────────┐ ┌────────────┐ ┌────────────┐ ││ │ Firewalla │ │ k8s nodes │ │ Other │ ││ │ DNS .20.1 │ │ .20.101+ │ │ devices │ ││ └────────────┘ └────────────┘ └────────────┘ │└─────────────────────────────────────────────────────────────┘Implementation
Section titled “Implementation”Directory Structure
Section titled “Directory Structure”argocd/app-configs/tailscale/├── namespace.yaml├── external-secret.yaml├── connector.yaml└── values.yamlNamespace
Section titled “Namespace”apiVersion: v1kind: Namespacemetadata: name: tailscale labels: app.kubernetes.io/name: tailscaleExternalSecret
Section titled “ExternalSecret”apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: tailscale-oauth namespace: tailscalespec: refreshInterval: 1h secretStoreRef: name: vault-backend kind: ClusterSecretStore target: name: tailscale-oauth creationPolicy: Owner data: - secretKey: oauth-client-id remoteRef: key: secret/tailscale/operator property: clientId - secretKey: oauth-client-secret remoteRef: key: secret/tailscale/operator property: clientSecretHelm Values (values.yaml)
Section titled “Helm Values (values.yaml)”operatorConfig: oauthSecretName: tailscale-oauth defaultTags: - "tag:k8s-operator"
resources: limits: memory: 256Mi requests: cpu: 50m memory: 128MiConnector
Section titled “Connector”apiVersion: tailscale.com/v1alpha1kind: Connectormetadata: name: home-network namespace: tailscalespec: hostname: k8s-subnet-router subnetRouter: advertiseRoutes: - "192.168.20.0/22" highAvailability: true exitNode: true tags: - "tag:k8s-operator"ArgoCD Application
Section titled “ArgoCD Application”apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: tailscale namespace: argocdspec: project: default sources: - repoURL: https://pkgs.tailscale.com/helmcharts chart: tailscale-operator targetRevision: "1.82.0" helm: valueFiles: - $values/argocd/app-configs/tailscale/values.yaml - repoURL: https://github.com/fzymgc-house/selfhosted-cluster targetRevision: HEAD ref: values - repoURL: https://github.com/fzymgc-house/selfhosted-cluster targetRevision: HEAD path: argocd/app-configs/tailscale destination: server: https://kubernetes.default.svc namespace: tailscale syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=trueVerification
Section titled “Verification”Cluster Checks
Section titled “Cluster Checks”# Operator runningkubectl get pods -n tailscale -l app.kubernetes.io/name=tailscale-operator
# Connector statuskubectl get connector -n tailscale
# Proxy pods (subnet router replicas)kubectl get pods -n tailscale -l tailscale.com/parent-resource=home-networkTailscale Admin Console
Section titled “Tailscale Admin Console”- Verify
k8s-subnet-router-0andk8s-subnet-router-1appear in Machines - Confirm routes are approved (automatic if using autoApprovers)
- Confirm exit node capability is enabled
Client Testing
Section titled “Client Testing”# Check route availabilitytailscale status
# Test connectivity to cluster nodeping 192.168.20.101
# Test DNS via Firewalladig @192.168.20.1 some-local-hostname
# Test exit nodetailscale up --exit-node=k8s-subnet-router-0curl ifconfig.me # Should show home IPSecurity Considerations
Section titled “Security Considerations”- OAuth credentials stored in Vault, never in Git
- Nodes tagged
k8s-operatorfor ACL targeting - Routes restricted to home network CIDR only
- Exit node optional per-client (user chooses when to enable)
Future Enhancements
Section titled “Future Enhancements”- Add Tailscale Funnel for specific services (alternative to Cloudflare Tunnel)
- Explore app-specific ingress via Tailscale Ingress class
- Consider MagicDNS split-DNS for cluster service discovery