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,
traefikIngressClass 標記為預設 class。Tailscale operator 建立的是另一個非預設的tailscaleIngressClass,因此你建立的 Ingress 一定要寫ingressClassName: tailscale,否則會被 Traefik 搶去處理(在節點的 80/443 埠開對外服務)。反過來,也別把tailscaleIngressClass 設成預設——operator 會拒絕帶有預設標記的tailscaleIngressClass。 - 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: 80tls.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: 2exit 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: 2spec.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 入門。