diff --git a/argoproj/tailscale-operator/application.yaml b/argoproj/tailscale-operator/application.yaml index f53fe51e2..a548e61e6 100644 --- a/argoproj/tailscale-operator/application.yaml +++ b/argoproj/tailscale-operator/application.yaml @@ -10,7 +10,7 @@ spec: targetRevision: main ref: lolice - repoURL: https://pkgs.tailscale.com/helmcharts - targetRevision: 1.80.3 + targetRevision: 1.94.2 chart: tailscale-operator helm: valueFiles: diff --git a/argoproj/tailscale-operator/external-secret.yaml b/argoproj/tailscale-operator/external-secret.yaml index 4d198657e..894fe4bd4 100644 --- a/argoproj/tailscale-operator/external-secret.yaml +++ b/argoproj/tailscale-operator/external-secret.yaml @@ -1,3 +1,7 @@ +# Retained for rollback to OAuth Secret mode. +# With Workload Identity Federation (WIF), this Secret is NOT mounted +# by the operator. It only exists so that reverting values.yaml to the +# oauthSecretVolume configuration works without reprovisioning SSM. apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: diff --git a/argoproj/tailscale-operator/helm/values.yaml b/argoproj/tailscale-operator/helm/values.yaml index cf1c66fb3..c730af5b4 100644 --- a/argoproj/tailscale-operator/helm/values.yaml +++ b/argoproj/tailscale-operator/helm/values.yaml @@ -1,20 +1,28 @@ # Tailscale Kubernetes Operator - Helm values -# Ref: https://tailscale.com/kb/1236/kubernetes-operator +# Ref: https://tailscale.com/docs/features/kubernetes-operator -# OAuth credentials are provided via ExternalSecret (tailscale-operator-oauth). -# Do not set oauth.clientId/clientSecret here; the chart would create a -# duplicate "operator-oauth" Secret that conflicts with the ExternalSecret. +# ── Authentication ────────────────────────────────────────────────── +# Current mode: OAuth Secret (via ExternalSecret). +# After Terraform (boxp/arch) applies the WIF credential, switch to +# Workload Identity Federation by replacing this block with: +# +# oauth: +# clientId: "" +# audience: "" +# +# and removing the oauthSecretVolume section below. +# See docs/runbook/tailscale-operator-workload-identity.md for details. oauth: {} -# Override the default volume source (secretName: operator-oauth) so the -# operator deployment mounts the ExternalSecret-managed Secret instead. +# Mount the ExternalSecret-managed Secret (OAuth client credentials). +# Remove this section when switching to Workload Identity Federation. oauthSecretVolume: secret: secretName: tailscale-operator-oauth # Operator image tag (pinned for reproducibility) image: - tag: v1.80.3 + tag: v1.94.2 # Operator runs in userspace mode (no NET_ADMIN required) proxyConfig: diff --git a/argoproj/tailscale-operator/kustomization.yaml b/argoproj/tailscale-operator/kustomization.yaml index 646daa820..32a2bc0ef 100644 --- a/argoproj/tailscale-operator/kustomization.yaml +++ b/argoproj/tailscale-operator/kustomization.yaml @@ -4,3 +4,4 @@ resources: - namespace.yaml - external-secret.yaml - networkpolicy.yaml + - oidc-discovery-clusterrolebinding.yaml diff --git a/argoproj/tailscale-operator/oidc-discovery-clusterrolebinding.yaml b/argoproj/tailscale-operator/oidc-discovery-clusterrolebinding.yaml new file mode 100644 index 000000000..b1d5612f3 --- /dev/null +++ b/argoproj/tailscale-operator/oidc-discovery-clusterrolebinding.yaml @@ -0,0 +1,16 @@ +# Allow unauthenticated access to the OIDC discovery endpoint. +# Required by Tailscale Workload Identity Federation so that the +# tailnet coordination server can validate ServiceAccount tokens +# issued by the Kubernetes API server. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: oidc-discovery +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:service-account-issuer-discovery +subjects: + - kind: Group + apiGroup: rbac.authorization.k8s.io + name: system:unauthenticated diff --git a/docs/project_docs/T-20260302-009/plan.md b/docs/project_docs/T-20260302-009/plan.md new file mode 100644 index 000000000..0c4cc27a0 --- /dev/null +++ b/docs/project_docs/T-20260302-009/plan.md @@ -0,0 +1,143 @@ +# T-20260302-009: tailscale-operator Workload Identity(beta) 移行計画 + +## 背景 + +tailscale-operator の認証方式を現行の OAuth Secret ベースから Workload Identity Federation (WIF, beta) へ移行する。 +Phase 1 (PR #494, #497, #500) で導入された OAuth Secret 方式は ExternalSecret + SSM Parameter Store で管理されているが、 +WIF 化により長寿命の Secret をクラスタ内に保持する必要がなくなり、セキュリティと保守性が向上する。 + +## 前提条件の調査結果 + +### Tailscale WIF の要件 +1. **OIDC issuer がパブリックアクセス可能** であること +2. Helm chart **v1.92.0 以上** が必要(v1.80.3 は WIF 未対応) +3. `oauth.clientId` + `oauth.audience` の2値で認証(Secret 不要) +4. Kubernetes の Projected ServiceAccount Token を自動マウント + +### lolice クラスタの現状 +- **kubeadm ベース**のオンプレミスクラスタ(EKS ではない) +- `--service-account-issuer` 未設定(デフォルト: `https://kubernetes.default.svc.cluster.local`) +- OIDC discovery endpoint **未公開**(API Server がプライベートネットワーク上) +- → **S3 での OIDC discovery ドキュメントホスティングが必要** + +## 実装方針 + +### 変更対象と責務分離 + +| リポジトリ | 変更内容 | 優先度 | +|-----------|---------|--------| +| **boxp/arch** | S3 OIDC ホスティング (Terraform)、kubeadm config 変更 (Ansible)、WIF credential 作成 (Terraform) | 先行 | +| **boxp/lolice** | Helm chart v1.94.2 アップグレード、values.yaml WIF 対応、ドキュメント | arch 適用後 | + +### Phase 1: S3 OIDC Discovery ホスティング (arch) + +S3 バケットに OIDC discovery ドキュメントを公開ホスティングする。 + +**新規ファイル:** `terraform/aws/lolice-k8s-oidc/` +- `backend.tf` - S3 バックエンド + provider 宣言 +- `provider.tf` - AWS provider (ap-northeast-1) +- `main.tf` - S3 バケット + パブリック読み取りポリシー +- `variables.tf` - バケット名等の変数 +- `outputs.tf` - バケット URL をエクスポート + +**ホスティングするドキュメント:** +- `.well-known/openid-configuration` - OIDC discovery メタデータ +- `openid/v1/jwks` - SA 署名用公開鍵 (JWKS) + +### Phase 2: kubeadm config 変更 (arch) + +**変更ファイル:** `ansible/roles/kubernetes_components/templates/kubeadm-config.yaml.j2` + +API Server に以下の extraArgs を追加: +```yaml +apiServer: + extraArgs: + service-account-issuer: "{{ k8s_oidc_issuer_url }}" + service-account-jwks-uri: "{{ k8s_oidc_issuer_url }}/openid/v1/jwks" + api-audiences: "{{ k8s_oidc_audience }}" +``` + +**注意:** 複数の `--service-account-issuer` をサポート(K8s 1.22+)するため、 +既存トークンの互換性は維持される。 + +### Phase 3: Tailscale WIF Credential 作成 (arch) + +**変更ファイル:** `terraform/tailscale/lolice/wif.tf` + +```hcl +resource "tailscale_federated_identity" "k8s_operator" { + issuer = var.k8s_oidc_issuer_url + subject = "system:serviceaccount:tailscale-operator:operator" + scopes = ["auth_keys", "devices:core", "services"] + tags = ["tag:k8s-operator"] + depends_on = [tailscale_acl.this] +} +``` + +エクスポート: `id` (= client_id), `audience` +→ SSM Parameter Store に格納して lolice 側で参照 + +### Phase 4: Helm Chart アップグレード + WIF 切り替え (lolice) + +**変更ファイル:** +- `argoproj/tailscale-operator/application.yaml` - chart version 1.80.3 → 1.94.2 +- `argoproj/tailscale-operator/helm/values.yaml` - WIF 認証に切り替え +- `argoproj/tailscale-operator/external-secret.yaml` - WIF 用パラメータに変更 +- `argoproj/tailscale-operator/kustomization.yaml` - OIDC discovery ClusterRoleBinding 追加 + +**values.yaml 変更:** +```yaml +# Before (OAuth Secret) +oauth: {} +oauthSecretVolume: + secret: + secretName: tailscale-operator-oauth + +# After (Workload Identity) +oauth: + clientId: "" + audience: "" +# oauthSecretVolume は削除 +``` + +### Phase 5: ドキュメント + +- `docs/project_docs/T-20260302-009/plan.md` - 本計画書 +- `docs/runbook/tailscale-operator-workload-identity.md` - 導入手順 + ロールバック手順 + +## ロールバック手順 + +### 条件 +- WIF 認証で operator が起動しない場合 +- Proxy Pod が作成されない場合 +- OIDC discovery endpoint に到達できない場合 + +### 手順 +1. `argoproj/tailscale-operator/helm/values.yaml` を OAuth Secret 版に revert +2. `argoproj/tailscale-operator/application.yaml` の chart version を 1.80.3 に revert(任意、1.94.2 は OAuth Secret もサポート) +3. ExternalSecret が SSM から client_id/client_secret を引き続き取得可能(SSM パラメータは削除しない) +4. ArgoCD が自動 Sync で適用 + +### ロールバック後の確認 +- `kubectl -n tailscale-operator get pods` で operator Pod が Running +- `kubectl -n tailscale-operator logs deploy/operator` でエラーなし +- Tailscale admin console で operator ノードが表示される + +## 既知リスク + +1. **beta 機能**: WIF は Tailscale 側でベータ版。API/動作が変更される可能性 +2. **kubeadm config 変更**: API Server 再起動が必要。ダウンタイムはないが注意 +3. **OIDC discovery の可用性**: S3 が利用不可の場合、トークン検証が失敗する +4. **SA signing key のローテーション**: key をローテーションした場合、S3 の JWKS も更新が必要 + +## 段階移行手順 (Canary → 本番) + +1. **Step 1**: arch PR をマージし Terraform apply(S3 バケット + WIF credential 作成) +2. **Step 2**: kubeadm config を適用し API Server を再起動 +3. **Step 3**: S3 に OIDC discovery ドキュメントをアップロード +4. **Step 4**: lolice PR をマージ(ArgoCD が自動 Sync) +5. **Step 5**: 検証 + - `kubectl -n tailscale-operator get pods` で operator 正常起動 + - `kubectl -n tailscale-operator logs deploy/operator` でエラーなし + - argocd-diff で Tailscale 経路確認 +6. **Step 6**: 問題があればロールバック手順を実行 diff --git a/docs/runbook/tailscale-operator-workload-identity.md b/docs/runbook/tailscale-operator-workload-identity.md new file mode 100644 index 000000000..f86e2b573 --- /dev/null +++ b/docs/runbook/tailscale-operator-workload-identity.md @@ -0,0 +1,225 @@ +# Runbook: tailscale-operator Workload Identity Federation (beta) 移行 + +## 概要 + +tailscale-operator の認証を OAuth Secret → Workload Identity Federation (WIF) へ移行する手順書。 +WIF により、長寿命の `client_secret` をクラスタ内に保持する必要がなくなる。 + +## 前提条件 + +- boxp/arch リポジトリの Terraform が適用済み(S3 OIDC バケット + WIF credential) +- kubeadm control plane ノードへの SSH アクセス +- `kubectl` で lolice クラスタに接続可能 +- AWS CLI (`aws`) が設定済み + +## ステータス + +- **フェーズ**: beta +- **Helm chart**: v1.94.2 (WIF サポート: v1.92.0+) +- **Tailscale provider**: v0.28.0 + +--- + +## 導入手順 + +### Step 1: arch Terraform を apply (S3 + WIF credential) + +```bash +cd terraform/tailscale/lolice +terraform plan +terraform apply +``` + +出力を控える: +```bash +terraform output k8s_operator_wif_client_id +terraform output k8s_operator_wif_audience +terraform output k8s_oidc_issuer_url +``` + +### Step 2: kubeadm API Server に OIDC issuer を設定 + +1. Ansible の group_vars で `k8s_oidc_issuer_url` を設定: + +```yaml +# group_vars/control_plane.yml (または host_vars) +k8s_oidc_issuer_url: "https://lolice-k8s-oidc.s3.ap-northeast-1.amazonaws.com" +``` + +2. Ansible playbook を実行: + +```bash +ansible-playbook -i inventory site.yml --tags kubeadm +``` + +3. kubeadm upgrade を実行(各 control plane ノードで): + +```bash +sudo kubeadm upgrade apply --config /etc/kubernetes/kubeadm-config.yaml +``` + +### Step 3: OIDC Discovery ドキュメントを S3 にアップロード + +control plane ノードで JWKS を取得: +```bash +kubectl get --raw /openid/v1/jwks > /tmp/jwks.json +``` + +S3 にアップロード: +```bash +aws s3 cp /tmp/jwks.json s3://lolice-k8s-oidc/openid/v1/jwks \ + --content-type application/json +``` + +動作確認: +```bash +curl -s https://lolice-k8s-oidc.s3.ap-northeast-1.amazonaws.com/.well-known/openid-configuration | jq . +curl -s https://lolice-k8s-oidc.s3.ap-northeast-1.amazonaws.com/openid/v1/jwks | jq .keys[0].kid +``` + +### Step 4: lolice values.yaml のプレースホルダを更新 + +`argoproj/tailscale-operator/helm/values.yaml` の以下を更新: + +```yaml +oauth: + clientId: "" + audience: "" +``` + +コミットして push: +```bash +git add argoproj/tailscale-operator/helm/values.yaml +git commit -m "feat: set real WIF credentials for tailscale-operator" +git push origin main +``` + +### Step 5: 検証 + +ArgoCD が自動 sync するのを待つか、手動 sync: +```bash +argocd app sync tailscale-operator +``` + +確認事項: +```bash +# Operator Pod が Running +kubectl -n tailscale-operator get pods + +# ログにエラーなし +kubectl -n tailscale-operator logs deploy/operator --tail=50 + +# Projected volume がマウントされている +kubectl -n tailscale-operator get deploy/operator -o jsonpath='{.spec.template.spec.volumes}' | jq . + +# CLIENT_ID 環境変数が設定されている(CLIENT_SECRET_FILE は存在しないこと) +kubectl -n tailscale-operator exec deploy/operator -- env | grep CLIENT + +# Tailscale admin console で operator ノードが tag:k8s-operator で表示 +``` + +argocd-diff で Tailscale 経路確認: +```bash +# GitHub Actions workflow で argocd-diff を実行 +# ログに "Auth path: tailscale" が表示されること +``` + +--- + +## ロールバック手順 + +### 条件 + +以下のいずれかに該当する場合はロールバックを検討: +- WIF 認証で operator が起動しない (CrashLoopBackOff) +- Tailscale admin console に operator ノードが表示されない +- Proxy Pod が作成されない +- OIDC discovery endpoint に到達できない + +### 手順 + +1. **values.yaml を OAuth Secret 版に revert**: + +```yaml +# oauth.clientId/audience を削除し、以下に置き換え: +oauth: {} + +oauthSecretVolume: + secret: + secretName: tailscale-operator-oauth +``` + +2. **コミットして push** (ArgoCD が自動 sync): + +```bash +git add argoproj/tailscale-operator/helm/values.yaml +git commit -m "revert: rollback tailscale-operator to OAuth Secret mode" +git push origin main +``` + +3. **確認**: + +```bash +kubectl -n tailscale-operator get pods -w +kubectl -n tailscale-operator logs deploy/operator --tail=50 +``` + +### 注意事項 + +- Helm chart version (1.94.2) は revert 不要。1.94.2 は OAuth Secret モードもサポート +- ExternalSecret は削除していないため、SSM から client_id/client_secret を引き続き取得可能 +- SSM パラメータ (`operator-oauth-client-id`, `operator-oauth-client-secret`) は削除していない +- kubeadm の `--service-account-issuer` 変更は revert 不要(WIF 不使用時は無害) + +--- + +## トラブルシューティング + +### Operator が起動しない + +```bash +kubectl -n tailscale-operator describe pod -l app=operator +kubectl -n tailscale-operator logs deploy/operator --previous +``` + +よくある原因: +- `oauth.clientId` / `oauth.audience` のプレースホルダが更新されていない +- S3 OIDC バケットが存在しない +- JWKS が S3 にアップロードされていない + +### トークン交換エラー + +Tailscale admin console → Trust credentials → 該当の federated identity → View/Edit でエラー詳細を確認。 + +API レスポンス: `{ "message": "Unauthorized. Visit [link] for details" }` +→ admin console でエラーの詳細を確認できる。 + +### OIDC discovery に到達できない + +```bash +curl -v https://lolice-k8s-oidc.s3.ap-northeast-1.amazonaws.com/.well-known/openid-configuration +``` + +確認事項: +- S3 バケットの public access block 設定 +- S3 バケットポリシー +- OIDC ドキュメントの Content-Type (`application/json`) + +--- + +## SA signing key のローテーション + +kubeadm の SA signing key をローテーションした場合、JWKS も更新が必要: + +```bash +# 新しい JWKS を取得 +kubectl get --raw /openid/v1/jwks > /tmp/jwks.json + +# S3 にアップロード +aws s3 cp /tmp/jwks.json s3://lolice-k8s-oidc/openid/v1/jwks \ + --content-type application/json + +# Terraform の変数も更新(次回 apply でドリフトしないよう) +# ※ oidc.tf の lifecycle { ignore_changes = [content] } により +# JWKS は Terraform で管理しない設計 +```