fix(ci): M1 split tailscale RBAC out of app manifests, drop deployer rolebinding grant

The dashboard/dagster manifests bundled the tailscale-sidecar SA+Role+RoleBinding,
which forced the webhook-triggered fxhnt-ci-deployer SA to hold create/update/patch
on roles/rolebindings in its own namespace — a privilege-escalation primitive. Move
those 6 objects to infra/k8s/rbac/tailscale-sidecars.yaml, applied once out-of-band
(not by CI); trim the deployer Role to drop rbac.authorization.k8s.io entirely, and
drop serviceaccounts too since neither CI-applied manifest creates one anymore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 11:32:51 +00:00
parent 40c0b187a7
commit 05f6d624c0
4 changed files with 80 additions and 70 deletions

View File

@@ -53,22 +53,20 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# the fxhnt-dashboard + dagster manifests also bundle ConfigMaps + ServiceAccounts (the tailscale sidecar
# serve-config + its identity) — kubectl apply of the whole file must be able to read/apply them.
# the fxhnt-dashboard + dagster manifests also bundle ConfigMaps (the tailscale sidecar serve-config) —
# kubectl apply of the whole file must be able to read/apply them. NOTE (CI review M1): the tailscale
# ServiceAccount + Role + RoleBinding objects were MOVED out of these app manifests into
# infra/k8s/rbac/tailscale-sidecars.yaml (applied once, out-of-band, by a human) specifically so this
# deployer SA does not need roles/rolebindings/serviceaccounts create-update-patch — that combination on a
# webhook-triggered SA is a privilege-escalation primitive. Neither manifest applied by CI creates a
# ServiceAccount anymore, so `serviceaccounts` is dropped from this grant too.
- apiGroups: [""]
resources: ["configmaps", "serviceaccounts"]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# the manifests apply NetworkPolicies (fxhnt-cockpit.yaml + dagster's).
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# the dashboard + dagster manifests bundle the tailscale-sidecar Role/RoleBinding. NOTE: granting a deployer
# SA rbac-management in its own namespace is privilege-escalation-adjacent — it is deliberately scoped to the
# foxhunt namespace only (no cluster-wide roles) and exists solely because the app manifests co-locate this
# RBAC with the Deployment. Kept minimal (no escalate/bind verbs beyond what apply needs).
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["roles", "rolebindings"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding

View File

@@ -115,36 +115,6 @@ data:
}
}
---
apiVersion: v1
kind: ServiceAccount
metadata: { name: tailscale-dagster, namespace: foxhunt }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: tailscale-dagster, namespace: foxhunt }
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "update", "patch", "list", "watch"]
# K8sRunLauncher: the daemon launches each Dagster run as its OWN K8s Job (own memory) so the heavy
# bybit_book_precompute asset (6-8Gi) never OOMs the 4Gi daemon. Needs to manage Jobs + read their pods/logs.
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: tailscale-dagster, namespace: foxhunt }
subjects:
- { kind: ServiceAccount, name: tailscale-dagster, namespace: foxhunt }
roleRef: { kind: Role, name: tailscale-dagster, apiGroup: rbac.authorization.k8s.io }
---
apiVersion: apps/v1
kind: Deployment
metadata:

View File

@@ -0,0 +1,72 @@
# Tailscale sidecar RBAC (ServiceAccount + Role + RoleBinding) for the fxhnt-dashboard and dagster
# Deployments. Moved out of infra/k8s/services/fxhnt-dashboard.yaml and infra/k8s/orchestration/dagster.yaml
# (CI review M1): those app manifests are applied every deploy by the fxhnt-ci-deployer ServiceAccount via
# the kubectl-deploy Argo template, and having RBAC objects bundled in there meant the deployer SA needed
# create/update/patch on roles/rolebindings in its own namespace — a privilege-escalation primitive on a
# webhook-triggered SA. This file is STATIC and applied ONCE, out-of-band, by a human:
#
# kubectl apply -f infra/k8s/rbac/tailscale-sidecars.yaml
#
# It is NOT applied by the CI pipeline. The Deployments still reference these ServiceAccounts via
# `serviceAccountName: tailscale-dashboard` / `tailscale-dagster` — that reference resolves because the
# SAs are managed here, separately, not because the app manifests create them anymore.
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: tailscale-dashboard
namespace: foxhunt
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tailscale-dashboard
namespace: foxhunt
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "update", "patch", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tailscale-dashboard
namespace: foxhunt
subjects:
- kind: ServiceAccount
name: tailscale-dashboard
namespace: foxhunt
roleRef:
kind: Role
name: tailscale-dashboard
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata: { name: tailscale-dagster, namespace: foxhunt }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: tailscale-dagster, namespace: foxhunt }
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "update", "patch", "list", "watch"]
# K8sRunLauncher: the daemon launches each Dagster run as its OWN K8s Job (own memory) so the heavy
# bybit_book_precompute asset (6-8Gi) never OOMs the 4Gi daemon. Needs to manage Jobs + read their pods/logs.
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: tailscale-dagster, namespace: foxhunt }
subjects:
- { kind: ServiceAccount, name: tailscale-dagster, namespace: foxhunt }
roleRef: { kind: Role, name: tailscale-dagster, apiGroup: rbac.authorization.k8s.io }

View File

@@ -112,33 +112,3 @@ metadata:
spec:
selector: { app.kubernetes.io/name: fxhnt-dashboard }
ports: [{ port: 80, targetPort: 8080, name: http }]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: tailscale-dashboard
namespace: foxhunt
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tailscale-dashboard
namespace: foxhunt
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "update", "patch", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tailscale-dashboard
namespace: foxhunt
subjects:
- kind: ServiceAccount
name: tailscale-dashboard
namespace: foxhunt
roleRef:
kind: Role
name: tailscale-dashboard
apiGroup: rbac.authorization.k8s.io