Skip to content

Tailscale Deployment Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Deploy Tailscale Operator with HA subnet router advertising 192.168.20.0/22 and exit node capability.

Architecture: Tailscale Operator manages a Connector CRD that creates 2 HA replica pods joining the tailnet as subnet routers. OAuth credentials stored in Vault, pulled via ExternalSecret.

Tech Stack: Tailscale Operator Helm chart, ExternalSecrets, Vault KV, ArgoCD GitOps


Before starting implementation, complete these manual steps:

  1. Go to Tailscale Admin Console → Settings → OAuth clients
  2. Create new OAuth client with scopes: devices:write, routes:write
  3. Note the Client ID and Client Secret
Terminal window
vault kv put secret/fzymgc-house/cluster/tailscale/oauth \
clientId="<your-client-id>" \
clientSecret="<your-client-secret>"

In Tailscale Admin Console → Access Controls, add:

"autoApprovers": {
"routes": {
"192.168.20.0/22": ["tag:k8s-operator"]
},
"exitNode": ["tag:k8s-operator"]
}

Files:

  • Create: argocd/app-configs/tailscale/kustomization.yaml

Step 1: Create the directory and kustomization file

argocd/app-configs/tailscale/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- external-secret.yaml
- connector.yaml

Step 2: Validate YAML syntax

Run: yamllint argocd/app-configs/tailscale/kustomization.yaml Expected: No errors

Step 3: Commit

Terminal window
git add argocd/app-configs/tailscale/kustomization.yaml
git commit -m "feat(tailscale): add kustomization for tailscale app"

Files:

  • Create: argocd/app-configs/tailscale/namespace.yaml

Step 1: Create the namespace manifest

argocd/app-configs/tailscale/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: tailscale
labels:
app.kubernetes.io/name: tailscale

Step 2: Validate YAML syntax

Run: yamllint argocd/app-configs/tailscale/namespace.yaml Expected: No errors

Step 3: Commit

Terminal window
git add argocd/app-configs/tailscale/namespace.yaml
git commit -m "feat(tailscale): add namespace manifest"

Task 3: Create ExternalSecret for OAuth Credentials

Section titled “Task 3: Create ExternalSecret for OAuth Credentials”

Files:

  • Create: argocd/app-configs/tailscale/external-secret.yaml

Step 1: Create the ExternalSecret manifest

argocd/app-configs/tailscale/external-secret.yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: tailscale-oauth
namespace: tailscale
spec:
refreshPolicy: Periodic
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: tailscale-oauth
creationPolicy: Owner
deletionPolicy: Delete
data:
- secretKey: oauth-client-id
remoteRef:
key: fzymgc-house/cluster/tailscale/oauth
property: clientId
- secretKey: oauth-client-secret
remoteRef:
key: fzymgc-house/cluster/tailscale/oauth
property: clientSecret

Step 2: Validate YAML syntax

Run: yamllint argocd/app-configs/tailscale/external-secret.yaml Expected: No errors

Step 3: Commit

Terminal window
git add argocd/app-configs/tailscale/external-secret.yaml
git commit -m "feat(tailscale): add ExternalSecret for OAuth credentials"

Files:

  • Create: argocd/app-configs/tailscale/connector.yaml

Step 1: Create the Connector manifest

argocd/app-configs/tailscale/connector.yaml
apiVersion: tailscale.com/v1alpha1
kind: Connector
metadata:
name: home-network
namespace: tailscale
spec:
hostname: k8s-subnet-router
subnetRouter:
advertiseRoutes:
- "192.168.20.0/22"
exitNode: true
tags:
- "tag:k8s-operator"

Step 2: Validate YAML syntax

Run: yamllint argocd/app-configs/tailscale/connector.yaml Expected: No errors

Step 3: Commit

Terminal window
git add argocd/app-configs/tailscale/connector.yaml
git commit -m "feat(tailscale): add Connector for subnet router and exit node"

Files:

  • Create: argocd/app-configs/tailscale/values.yaml

Step 1: Create the Helm values file

argocd/app-configs/tailscale/values.yaml
operatorConfig:
defaultTags:
- "tag:k8s-operator"
oauth:
clientId: ""
clientSecret: ""
clientIdFromSecret:
name: tailscale-oauth
key: oauth-client-id
clientSecretFromSecret:
name: tailscale-oauth
key: oauth-client-secret
apiServerProxyConfig:
mode: "noauth"
resources:
limits:
memory: 256Mi
requests:
cpu: 50m
memory: 128Mi

Step 2: Validate YAML syntax

Run: yamllint argocd/app-configs/tailscale/values.yaml Expected: No errors

Step 3: Commit

Terminal window
git add argocd/app-configs/tailscale/values.yaml
git commit -m "feat(tailscale): add Helm values for operator configuration"

Files:

  • Create: argocd/cluster-app/templates/tailscale.yaml

Step 1: Create the ArgoCD Application manifest

argocd/cluster-app/templates/tailscale.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: tailscale
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
project: core-services
sources:
- chart: tailscale-operator
repoURL: https://pkgs.tailscale.com/helmcharts
targetRevision: "1.82.0"
helm:
releaseName: tailscale-operator
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:
- ServerSideApply=true
- CreateNamespace=true
- RespectIgnoreDifferences=true
ignoreDifferences:
- kind: ExternalSecret
group: external-secrets.io
jqPathExpressions:
- .spec.data[].remoteRef.conversionStrategy
- .spec.data[].remoteRef.decodingStrategy
- .spec.data[].remoteRef.metadataPolicy

Step 2: Validate YAML syntax

Run: yamllint argocd/cluster-app/templates/tailscale.yaml Expected: No errors

Step 3: Commit

Terminal window
git add argocd/cluster-app/templates/tailscale.yaml
git commit -m "feat(tailscale): add ArgoCD Application for tailscale-operator"

Files:

  • Modify: argocd/app-configs/velero/backup-schedule.yaml

Step 1: Read current backup schedule

Check the excludedNamespaces list in the backup schedule.

Step 2: Add tailscale to excluded namespaces

Add tailscale to the Operators category in excludedNamespaces.

Step 3: Commit

Terminal window
git add argocd/app-configs/velero/backup-schedule.yaml
git commit -m "chore(velero): exclude tailscale namespace from backups"

Files:

  • Modify: docs/reference/services.md (add Tailscale entry)
  • Modify: docs/reference/secrets.md (add Vault path)

Step 1: Add Tailscale to services catalog

Add entry for Tailscale with:

  • Name: Tailscale
  • Namespace: tailscale
  • Purpose: Subnet router and exit node for tailnet access
  • URL: N/A (not a web service)

Step 2: Add Vault path to secrets reference

Add entry:

  • Path: fzymgc-house/cluster/tailscale/oauth
  • Properties: clientId, clientSecret
  • Consumer: tailscale-operator

Step 3: Commit

Terminal window
git add docs/reference/services.md docs/reference/secrets.md
git commit -m "docs: add Tailscale to services and secrets reference"

Step 1: Validate all manifests with kustomize

Run: kubectl kustomize argocd/app-configs/tailscale Expected: Combined YAML output with namespace, external-secret, and connector

Step 2: Dry-run the ArgoCD app template

Run: helm template argocd/cluster-app --show-only templates/tailscale.yaml Expected: Valid Application manifest

Step 3: Run pre-commit checks

Run: pre-commit run --all-files Expected: All checks pass


After PR is merged and ArgoCD syncs:

Step 1: Check operator deployment

Terminal window
kubectl get pods -n tailscale -l app.kubernetes.io/name=tailscale-operator

Expected: 1/1 Running

Step 2: Check ExternalSecret sync

Terminal window
kubectl get externalsecret -n tailscale

Expected: SecretSynced status

Step 3: Check Connector status

Terminal window
kubectl get connector -n tailscale

Expected: home-network connector listed

Step 4: Check subnet router pods

Terminal window
kubectl get pods -n tailscale -l tailscale.com/parent-resource=home-network

Expected: 2 pods Running (HA replicas)

Step 5: Verify in Tailscale Admin Console

  • Machines: k8s-subnet-router-0 and k8s-subnet-router-1 connected
  • Routes: 192.168.20.0/22 approved (or pending if no autoApprovers)
  • Exit node: Enabled

Step 6: Test from tailnet client

Terminal window
tailscale status
ping 192.168.20.101

Expected: Route available, ping succeeds