Skip to content

k3s 上的 Tailscale operator:Ingress、Connector 與 ProxyGroup

用 Tailscale Kubernetes operator 把 k3s 叢集接進 tailnet:OAuth client 與 tag 設定、Helm 安裝、三種暴露服務的方式(expose 註解、LoadBalancer、Ingress)、Connector 當 subnet router/exit node、ProxyGroup 高可用,以及與 k3s 內建 Traefik/ServiceLB 並存的注意事項

Updated View as Markdown

Tailscale 官方的 Kubernetes operator 把 tailnet 接進 Kubernetes 叢集:在叢集內跑一個 operator,它會依你建立的資源自動開關 Tailscale 節點,把叢集裡的服務暴露給 tailnet(ingress)、讓叢集內的 Pod 連到 tailnet 上的裝置(egress)、或把 Kubernetes API server 接到 tailnet。所有暴露都用 Tailscale 的身分與存取規則控管,不必開 NodePort、不必設定防火牆。

這篇以 homelab 常見的 k3s 為例,走一遍安裝與三種暴露方式,再介紹 Connector 與 ProxyGroup。

本文依 2026 年 10 月的 Tailscale 官方文件整理,以 Tailscale v1.104 為準,未實際驗證。欄位名稱與註解以 v1.104 原始碼的 cmd/k8s-operator/ 與 cmd/k8s-operator/deploy/(Helm chart、CRD)核對;chart 的 version/appVersion 在發行時會替換成 Tailscale 版本號,原始碼內是佔位值(version: 0.1.0、appVersion: stable)。範例叢集以 k3s 為對象,寫作時最新穩定版為 v1.37.1+k3s1(Kubernetes v1.37)。

運作方式

operator 本身是一組 Deployment,裝在 tailscale namespace。你只要建立標準的 Kubernetes 資源(Service、Ingress,或 Tailscale 的 CRD),operator 就為它建立一個代理 Pod,代理 Pod 內跑 tailscaled,以 tag 身分加入 tailnet,再把流量轉給叢集內的 Service。

  • Ingress(L7):對外以 *.ts.net 的 MagicDNS 名稱與 HTTPS 呈現,代理 Pod 用 Tailscale Serve 轉發 HTTP/HTTPS。
  • Service(L3):代理 Pod 用 iptables/nftables 把連到 tailnet IP 的流量 DNAT 到 Service 的 ClusterIP,適用資料庫、訊息佇列這類非 HTTP 服務。
  • Connector:在叢集內跑一個 subnet router、exit node 或 app connector。
  • ProxyGroup:一組多副本的代理,用來做高可用與共用代理。

安裝

1. 設定 tag

先到 Admin console 的 Access controls 加入兩個 tag:tag:k8s-operator 給 operator 自己,tag:k8s 給 operator 建立的代理節點。重點是讓 tag:k8s-operator 成為 tag:k8s 的 owner,operator 才有權建立帶 tag:k8s 的節點:

{
  "tagOwners": {
    "tag:k8s-operator": ["autogroup:admin"],
    "tag:k8s": ["tag:k8s-operator"]
  }
}

tag 的寫法見 Tags 與 auth key。

2. 建立 OAuth client

operator 需要 OAuth 憑證來呼叫 Tailscale API、替自己與代理節點建立 auth key。到 Admin console 的 Trust credentials 建立一個 OAuth client,加上以下 scope(Read 與 Write):

  • General > Services
  • Devices > Core
  • Keys > Auth Keys

並在 Tags 選擇 Add tags → tag:k8s-operator。產生後記下 Client ID 與 Client secret。OAuth client 的運作見 OAuth client。

3. 用 Helm 安裝

加入 Tailscale 的 Helm repo 並安裝(版本號跟隨 Tailscale 版本):

helm repo add tailscale https://pkgs.tailscale.com/helmcharts
helm repo update
helm upgrade --install tailscale-operator tailscale/tailscale-operator \
  --namespace=tailscale \
  --create-namespace \
  --set-string oauth.clientId="<OAuth client ID>" \
  --set-string oauth.clientSecret="<OAuth client secret>" \
  --wait

裝完後 operator 會建立 tailscale namespace、operator Deployment、RBAC,以及 tailscale IngressClass 和 Connector、ProxyGroup 等 CRD。到 Admin console 的 Machines 頁面,應能看到一台名為 tailscale-operator、帶 tag:k8s-operator 的節點。官方文件另註明:新 stable 版的 Helm chart 與容器映像會在 release 後幾天才上架,升級到最新版後若一時還裝不到對應 chart 版本是正常的。

k3s 特有的注意事項

k3s 預設內建兩個會跟你搶資源的元件,安裝與設定時要特別留神:

  • Traefik:k3s 預設用 Traefik 當 Ingress controller,traefik IngressClass 標記為預設 class。Tailscale operator 建立的是另一個非預設的 tailscale IngressClass,因此你建立的 Ingress 一定要寫 ingressClassName: tailscale,否則會被 Traefik 搶去處理(在節點的 80/443 埠開對外服務)。反過來,也別把 tailscale IngressClass 設成預設——operator 會拒絕帶有預設標記的 tailscale IngressClass。
  • ServiceLB:k3s 預設用 ServiceLB(前身 Klipper LoadBalancer)當 LoadBalancer 實作。type: LoadBalancer 但沒有指定 loadBalancerClass 的 Service 會被 ServiceLB 接手、在節點上以 hostPort 綁定該 service port;指定 loadBalancerClass: tailscale 後才由 Tailscale operator 接手,不會去佔用 hostPort。

簡單說:在 k3s 上,要接進 tailnet 的資源一律明確指定 ingressClassName: tailscale 或 loadBalancerClass: tailscale,別依賴預設值。

暴露服務的三種方式

方式一:Service 加 tailscale.com/expose 註解(L3)

在 Service 加一行註解,operator 就會把它暴露到 tailnet。Service 可維持 ClusterIP 型態,不必改成 LoadBalancer:

apiVersion: v1
kind: Service
metadata:
  name: postgres
  annotations:
    tailscale.com/expose: "true"
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

預設的 tailnet 主機名稱是 <namespace>-<service 名稱>,例如 default-postgres.<你的 tailnet>.ts.net。要自訂,加 tailscale.com/hostname 註解。

方式二:LoadBalancer + loadBalancerClass: tailscale(L3)

效果和方式一相同,改用標準的 LoadBalancer 欄位:

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  type: LoadBalancer
  loadBalancerClass: tailscale
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

裝完後用 kubectl get service postgres,EXTERNAL-IP 欄位會出現這個 Service 的 Tailscale IP(100.x.y.z),tailnet 上的裝置就能用 100.x.y.z:5432 或 MagicDNS 名稱連線。L3 走的是原始 TCP/UDP,不需要開啟 HTTPS,適合資料庫、訊息佇列等非 HTTP 服務。

方式三:Ingress(L7)

要暴露網頁、API 這類 HTTP/HTTPS 服務,用 Ingress,ingressClassName 設為 tailscale:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
spec:
  ingressClassName: tailscale
  tls:
    - hosts:
        - my-app
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
  • tls.hosts 的第一個值決定 tailnet 主機名稱(取第一個標籤),上例會得到 my-app.<你的 tailnet>.ts.net,透過 https://my-app.<你的 tailnet>.ts.net 存取。
  • operator 會自動向 Let’s Encrypt 申請 *.ts.net 的 TLS 憑證,你不需要建立 Secret。前提是 tailnet 要先開啟 HTTPS,否則憑證不會簽發。每個 tailnet 每週有 50 個新主機名稱的憑證額度,測試時可用 ProxyClass 的 useLetsEncryptStagingEnvironment 切到 staging 環境。
  • 預設只開 443。要讓 HTTP 自動跳 HTTPS,加 tailscale.com/http-redirect: "true" 註解(會另開 80 埠)。
  • 想公開到網際網路(不必裝 Tailscale 就能連),加 tailscale.com/funnel: "true" 註解,見 Tailscale Funnel。注意:開 Funnel 前要在 ACL 的 nodeAttrs 給 tag:k8s 加上 funnel 屬性,否則不會生效。

Connector:subnet router、exit node 與 app connector

Connector 是 cluster 層級的 CRD,讓 operator 在叢集內跑一個 subnet router、exit node 或 app connector。

當 subnet router

把叢集內的 Pod/Service CIDR(或任何從叢集可達的網段)廣告給 tailnet:

apiVersion: tailscale.com/v1alpha1
kind: Connector
metadata:
  name: ts-pod-cidrs
spec:
  hostnamePrefix: ts-pod-cidrs
  subnetRouter:
    advertiseRoutes:
      - "10.42.0.0/16"

欄位 subnetRouter.advertiseRoutes 是 CIDR 清單。建立後到 Machines 核准路由(或見下方用 autoApprovers 自動核准)。subnet router 的概念見 Subnet Router。

當 exit node

同一個 CRD,改設 exitNode: true 即可。subnet router 和 exit node 可以並存在同一個 Connector 上(官方文件明言「A single Connector can act as both a subnet router and an exit node simultaneously」)。唯一的例外是 SNAT:若 subnet router 關掉 SNAT,官方建議不要把 exit node 放在同一台節點——兩者並存可能造成 upstream 封包被丟棄,應把這兩個角色分到不同節點(見 Subnet Router 的 Disable SNAT 段落)。

apiVersion: tailscale.com/v1alpha1
kind: Connector
metadata:
  name: exit-node
spec:
  exitNode: true
  hostnamePrefix: exit-node
  replicas: 2

exit node 的概念見 Exit Node。

當 app connector

appConnector 讓這台裝置扮演 app connector,提供固定的 egress IP 來連線外部 SaaS。它和 subnetRouter、exitNode 互斥,且網域與權限要另外在 Admin console 手動設定:

apiVersion: tailscale.com/v1alpha1
kind: Connector
metadata:
  name: app-connector
spec:
  appConnector: {}

Connector 節點預設帶 tag:k8s,可用 spec.tags 覆寫;replicas 大於 1 時要用 hostnamePrefix(不能用 hostname)。若想讓 subnet route 或 exit node 自動核准,在 ACL 加:

{
  "autoApprovers": {
    "routes": {
      "10.42.0.0/16": ["tag:k8s"]
    },
    "exitNode": ["tag:k8s"]
  }
}

ProxyGroup:高可用與三種類型

單一 Ingress 或 Service 只會得到一個代理 Pod,Pod 重啟時會短暫中斷。ProxyGroup 改用一組 StatefulSet 副本(預設 2 個,用 spec.replicas 調整),一個 ProxyGroup 可以服務多個 Ingress/Service,副本掛掉時自動由其他副本接手。

apiVersion: tailscale.com/v1alpha1
kind: ProxyGroup
metadata:
  name: ingress-proxies
spec:
  type: ingress
  replicas: 2

spec.type 有三種,用途對應 operator 的三種方向:

  • ingress:把叢集內的服務暴露給 tailnet。
  • egress:讓叢集內的 Pod 連到 tailnet 上的裝置。
  • kube-apiserver:把 Kubernetes API server 接到 tailnet(可選 auth/noauth 模式)。

要套用 ProxyGroup,在 Ingress 或 Service 加 tailscale.com/proxy-group 註解:

apiVersion: v1
kind: Service
metadata:
  name: my-app
  annotations:
    tailscale.com/proxy-group: ingress-proxies
    tailscale.com/hostname: my-app
spec:
  type: LoadBalancer
  loadBalancerClass: tailscale
  # ...

ProxyGroup 底層用 Tailscale Services 讓多個副本共用同一個 MagicDNS 名稱,因此需要在 ACL 加上 services 的 autoApprover(tag:k8s 自己核准自己):

{
  "autoApprovers": {
    "services": {
      "tag:k8s": ["tag:k8s"]
    }
  }
}

L3 與 L7 的差別同樣適用於 ProxyGroup:type: ingress 的 ProxyGroup 接 L7 Ingress(走 Serve、有 *.ts.net 憑證)時處理 HTTP/HTTPS,接 L3 Service(走 DNAT)時處理原始 TCP/UDP。官方文件沒有說明 ProxyGroup 對 L3/L7 另有不同行為,兩者主要差異就是前面「暴露服務」一節描述的 L3 與 L7。

最小權限的 grants

前面為了方便,ACL 若維持 tailnet 預設的「全開」規則({"action":"accept","src":["*"],"dst":["*:*"]}),tailnet 裡任何人都能連到 operator 暴露的所有服務。要收斂,刪掉預設全開規則,只放行需要的連線。例如只讓 group:dev 連到 tag:k8s 代理節點的 443 埠:

{
  "tagOwners": {
    "tag:k8s-operator": ["autogroup:admin"],
    "tag:k8s": ["tag:k8s-operator"]
  },
  "autoApprovers": {
    "services": {
      "tag:k8s": ["tag:k8s"]
    }
  },
  "grants": [
    {
      "src": ["group:dev"],
      "dst": ["tag:k8s"],
      "ip": ["tcp:443"]
    }
  ]
}
  • 代理節點都帶 tag:k8s,所以 dst 對準 tag:k8s、ip 只開必要的埠。
  • 用 L3 暴露資料庫時,把對應埠(例如 tcp:5432)加進 ip。
  • 若還用 API server proxy,要另外放行對 tag:k8s-operator 的 443 連線。

規則的寫法見 ACL / Grants 入門。

參考資料

Navigation

Type to search…

↑↓ navigate↵ selectEsc close