From e4187aeb5d68d6a885527a22f3c308fec13b28bc Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 8 Apr 2026 15:36:19 +0000 Subject: [PATCH 1/4] fix: separate ServiceAccount for router workloads (zero RBAC) The controller and router previously shared the same `controller-manager` ServiceAccount, giving the router unnecessary cluster-wide secrets CRUD access. This creates a dedicated `router-sa` ServiceAccount with no RBAC bindings and `automountServiceAccountToken: false`, following the principle of least privilege. Fixes #351 Co-Authored-By: Claude Opus 4.6 --- .../additional-router-deployment.yaml | 3 +- .../templates/rbac/leader_election_role.yaml | 2 +- .../rbac/leader_election_role_binding.yaml | 2 +- .../templates/rbac/role_binding.yaml | 2 +- .../templates/rbac/service_account.yaml | 13 ++++- .../templates/router-deployment.yaml | 3 +- .../jumpstarter/jumpstarter_controller.go | 5 +- .../internal/controller/jumpstarter/rbac.go | 57 +++++++++++++++++++ 8 files changed, 79 insertions(+), 8 deletions(-) diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml index 49cc02726..562457085 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml @@ -106,6 +106,7 @@ spec: requests: cpu: 1000m memory: 256Mi - serviceAccountName: controller-manager + serviceAccountName: router-sa + automountServiceAccountToken: false terminationGracePeriodSeconds: 10 {{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml index b0390bd15..0dd9975b5 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml @@ -3,7 +3,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: - app.kubernetes.io/name: jumpstarter-router + app.kubernetes.io/name: jumpstarter-controller name: leader-election-role namespace: {{ default .Release.Namespace .Values.namespace }} rules: diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml index d60dc3c9f..cb24e1fab 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml @@ -2,7 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: - app.kubernetes.io/name: jumpstarter-router + app.kubernetes.io/name: jumpstarter-controller namespace: {{ default .Release.Namespace .Values.namespace }} name: leader-election-rolebinding roleRef: diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml index 71d864b05..2cec5d38d 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml @@ -2,7 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: - app.kubernetes.io/name: jumpstarter-router + app.kubernetes.io/name: jumpstarter-controller annotations: argocd.argoproj.io/sync-wave: "-1" name: jumpstarter-manager-rolebinding diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml index 5359726af..86ec35683 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml @@ -2,8 +2,19 @@ apiVersion: v1 kind: ServiceAccount metadata: labels: - app.kubernetes.io/name: jumpstarter-router + app.kubernetes.io/name: jumpstarter-controller annotations: argocd.argoproj.io/sync-wave: "-1" name: controller-manager namespace: {{ default .Release.Namespace .Values.namespace }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: jumpstarter-router + annotations: + argocd.argoproj.io/sync-wave: "-1" + name: router-sa + namespace: {{ default .Release.Namespace .Values.namespace }} +automountServiceAccountToken: false diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml index fa9978bf3..323243724 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml @@ -105,5 +105,6 @@ spec: secret: secretName: {{ .Values.grpc.tls.routerCertSecret }} {{- end }} - serviceAccountName: controller-manager + serviceAccountName: router-sa + automountServiceAccountToken: false terminationGracePeriodSeconds: 10 diff --git a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index dcf512b7e..1db16f373 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -1017,8 +1017,9 @@ func (r *JumpstarterReconciler) createRouterDeployment(jumpstarter *operatorv1al Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, - ServiceAccountName: fmt.Sprintf("%s-controller-manager", jumpstarter.Name), - TopologySpreadConstraints: jumpstarter.Spec.Routers.TopologySpreadConstraints, + ServiceAccountName: fmt.Sprintf("%s-router-sa", jumpstarter.Name), + AutomountServiceAccountToken: boolPtr(false), + TopologySpreadConstraints: jumpstarter.Spec.Routers.TopologySpreadConstraints, }, }, }, diff --git a/controller/deploy/operator/internal/controller/jumpstarter/rbac.go b/controller/deploy/operator/internal/controller/jumpstarter/rbac.go index e03336181..52c2352a6 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/rbac.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/rbac.go @@ -61,6 +61,46 @@ func (r *JumpstarterReconciler) reconcileRBAC(ctx context.Context, jumpstarter * "namespace", existingSA.Namespace, "operation", op) + // Router ServiceAccount (zero RBAC, no token automount) + desiredRouterSA := r.createRouterServiceAccount(jumpstarter) + + existingRouterSA := &corev1.ServiceAccount{} + existingRouterSA.Name = desiredRouterSA.Name + existingRouterSA.Namespace = desiredRouterSA.Namespace + + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, existingRouterSA, func() error { + if existingRouterSA.CreationTimestamp.IsZero() { + existingRouterSA.Labels = desiredRouterSA.Labels + existingRouterSA.Annotations = desiredRouterSA.Annotations + existingRouterSA.AutomountServiceAccountToken = desiredRouterSA.AutomountServiceAccountToken + return nil + } + + if !serviceAccountNeedsUpdate(existingRouterSA, desiredRouterSA) { + log.V(1).Info("Router ServiceAccount is up to date, skipping update", + "name", existingRouterSA.Name, + "namespace", existingRouterSA.Namespace) + return nil + } + + existingRouterSA.Labels = desiredRouterSA.Labels + existingRouterSA.Annotations = desiredRouterSA.Annotations + existingRouterSA.AutomountServiceAccountToken = desiredRouterSA.AutomountServiceAccountToken + return nil + }) + + if err != nil { + log.Error(err, "Failed to reconcile Router ServiceAccount", + "name", desiredRouterSA.Name, + "namespace", desiredRouterSA.Namespace) + return err + } + + log.Info("Router ServiceAccount reconciled", + "name", existingRouterSA.Name, + "namespace", existingRouterSA.Namespace, + "operation", op) + // Role desiredRole := r.createRole(jumpstarter) @@ -169,6 +209,23 @@ func (r *JumpstarterReconciler) createServiceAccount(jumpstarter *operatorv1alph } } +// createRouterServiceAccount creates a service account for the router with no RBAC permissions +func (r *JumpstarterReconciler) createRouterServiceAccount(jumpstarter *operatorv1alpha1.Jumpstarter) *corev1.ServiceAccount { + automount := false + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-router-sa", jumpstarter.Name), + Namespace: jumpstarter.Namespace, + Labels: map[string]string{ + "app": "jumpstarter-router", + "app.kubernetes.io/name": "jumpstarter-router", + "app.kubernetes.io/managed-by": "jumpstarter-operator", + }, + }, + AutomountServiceAccountToken: &automount, + } +} + // createRole creates a role with necessary permissions for the controller func (r *JumpstarterReconciler) createRole(jumpstarter *operatorv1alpha1.Jumpstarter) *rbacv1.Role { return &rbacv1.Role{ From 8bc4c6d48dc8333cf11589415aa58994395ae1b3 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 8 Apr 2026 16:38:02 +0000 Subject: [PATCH 2/4] fix: revert Helm chart changes per maintainer feedback Helm charts are deprecated; only the operator deployment path should be modified for the separate router ServiceAccount. Co-Authored-By: Claude Opus 4.6 --- .../templates/additional-router-deployment.yaml | 3 +-- .../templates/rbac/leader_election_role.yaml | 2 +- .../rbac/leader_election_role_binding.yaml | 2 +- .../templates/rbac/role_binding.yaml | 2 +- .../templates/rbac/service_account.yaml | 13 +------------ .../templates/router-deployment.yaml | 3 +-- 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml index 562457085..49cc02726 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/additional-router-deployment.yaml @@ -106,7 +106,6 @@ spec: requests: cpu: 1000m memory: 256Mi - serviceAccountName: router-sa - automountServiceAccountToken: false + serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 {{ end }} diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml index 0dd9975b5..b0390bd15 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role.yaml @@ -3,7 +3,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: - app.kubernetes.io/name: jumpstarter-controller + app.kubernetes.io/name: jumpstarter-router name: leader-election-role namespace: {{ default .Release.Namespace .Values.namespace }} rules: diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml index cb24e1fab..d60dc3c9f 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/leader_election_role_binding.yaml @@ -2,7 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: - app.kubernetes.io/name: jumpstarter-controller + app.kubernetes.io/name: jumpstarter-router namespace: {{ default .Release.Namespace .Values.namespace }} name: leader-election-rolebinding roleRef: diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml index 2cec5d38d..71d864b05 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/role_binding.yaml @@ -2,7 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: - app.kubernetes.io/name: jumpstarter-controller + app.kubernetes.io/name: jumpstarter-router annotations: argocd.argoproj.io/sync-wave: "-1" name: jumpstarter-manager-rolebinding diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml index 86ec35683..5359726af 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/service_account.yaml @@ -1,20 +1,9 @@ apiVersion: v1 kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/name: jumpstarter-controller - annotations: - argocd.argoproj.io/sync-wave: "-1" - name: controller-manager - namespace: {{ default .Release.Namespace .Values.namespace }} ---- -apiVersion: v1 -kind: ServiceAccount metadata: labels: app.kubernetes.io/name: jumpstarter-router annotations: argocd.argoproj.io/sync-wave: "-1" - name: router-sa + name: controller-manager namespace: {{ default .Release.Namespace .Values.namespace }} -automountServiceAccountToken: false diff --git a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml index 323243724..fa9978bf3 100644 --- a/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml +++ b/controller/deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/router-deployment.yaml @@ -105,6 +105,5 @@ spec: secret: secretName: {{ .Values.grpc.tls.routerCertSecret }} {{- end }} - serviceAccountName: router-sa - automountServiceAccountToken: false + serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 From 93ce2723ce227c25435489f295855c26372e3515 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 8 Apr 2026 17:18:12 +0000 Subject: [PATCH 3/4] fix: allow router SA to access K8s API for config loading The router process needs Kubernetes API access at startup to load its configuration from a ConfigMap (via ctrl.GetConfigOrDie() and LoadRouterConfiguration). Setting AutomountServiceAccountToken: false on both the ServiceAccount and pod spec prevented the router from authenticating, causing the pod to crash and never become ready (180s timeout in CI). Changes: - Remove AutomountServiceAccountToken: false from router ServiceAccount and pod spec so the token is mounted - Add a minimal router Role granting read-only access to configmaps and secrets (the only resources the router needs) - Add a RoleBinding to bind the router Role to the router ServiceAccount This maintains the security goal of separating the router SA from the controller SA while granting only the minimum permissions needed. Co-Authored-By: Claude Opus 4.6 --- .../jumpstarter/jumpstarter_controller.go | 5 +- .../internal/controller/jumpstarter/rbac.go | 142 +++++++++++++++++- 2 files changed, 139 insertions(+), 8 deletions(-) diff --git a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go index 1db16f373..c5c2f57f6 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go @@ -1017,9 +1017,8 @@ func (r *JumpstarterReconciler) createRouterDeployment(jumpstarter *operatorv1al Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, - ServiceAccountName: fmt.Sprintf("%s-router-sa", jumpstarter.Name), - AutomountServiceAccountToken: boolPtr(false), - TopologySpreadConstraints: jumpstarter.Spec.Routers.TopologySpreadConstraints, + ServiceAccountName: fmt.Sprintf("%s-router-sa", jumpstarter.Name), + TopologySpreadConstraints: jumpstarter.Spec.Routers.TopologySpreadConstraints, }, }, }, diff --git a/controller/deploy/operator/internal/controller/jumpstarter/rbac.go b/controller/deploy/operator/internal/controller/jumpstarter/rbac.go index 52c2352a6..0d37d9846 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/rbac.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/rbac.go @@ -72,7 +72,6 @@ func (r *JumpstarterReconciler) reconcileRBAC(ctx context.Context, jumpstarter * if existingRouterSA.CreationTimestamp.IsZero() { existingRouterSA.Labels = desiredRouterSA.Labels existingRouterSA.Annotations = desiredRouterSA.Annotations - existingRouterSA.AutomountServiceAccountToken = desiredRouterSA.AutomountServiceAccountToken return nil } @@ -85,7 +84,6 @@ func (r *JumpstarterReconciler) reconcileRBAC(ctx context.Context, jumpstarter * existingRouterSA.Labels = desiredRouterSA.Labels existingRouterSA.Annotations = desiredRouterSA.Annotations - existingRouterSA.AutomountServiceAccountToken = desiredRouterSA.AutomountServiceAccountToken return nil }) @@ -191,6 +189,88 @@ func (r *JumpstarterReconciler) reconcileRBAC(ctx context.Context, jumpstarter * "namespace", existingRoleBinding.Namespace, "operation", op) + // Router Role (minimal permissions: read configmaps) + desiredRouterRole := r.createRouterRole(jumpstarter) + + existingRouterRole := &rbacv1.Role{} + existingRouterRole.Name = desiredRouterRole.Name + existingRouterRole.Namespace = desiredRouterRole.Namespace + + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, existingRouterRole, func() error { + if existingRouterRole.CreationTimestamp.IsZero() { + existingRouterRole.Labels = desiredRouterRole.Labels + existingRouterRole.Annotations = desiredRouterRole.Annotations + existingRouterRole.Rules = desiredRouterRole.Rules + return controllerutil.SetControllerReference(jumpstarter, existingRouterRole, r.Scheme) + } + + if !roleNeedsUpdate(existingRouterRole, desiredRouterRole) { + log.V(1).Info("Router Role is up to date, skipping update", + "name", existingRouterRole.Name, + "namespace", existingRouterRole.Namespace) + return nil + } + + existingRouterRole.Labels = desiredRouterRole.Labels + existingRouterRole.Annotations = desiredRouterRole.Annotations + existingRouterRole.Rules = desiredRouterRole.Rules + return controllerutil.SetControllerReference(jumpstarter, existingRouterRole, r.Scheme) + }) + + if err != nil { + log.Error(err, "Failed to reconcile Router Role", + "name", desiredRouterRole.Name, + "namespace", desiredRouterRole.Namespace) + return err + } + + log.Info("Router Role reconciled", + "name", existingRouterRole.Name, + "namespace", existingRouterRole.Namespace, + "operation", op) + + // Router RoleBinding + desiredRouterRoleBinding := r.createRouterRoleBinding(jumpstarter) + + existingRouterRoleBinding := &rbacv1.RoleBinding{} + existingRouterRoleBinding.Name = desiredRouterRoleBinding.Name + existingRouterRoleBinding.Namespace = desiredRouterRoleBinding.Namespace + + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, existingRouterRoleBinding, func() error { + if existingRouterRoleBinding.CreationTimestamp.IsZero() { + existingRouterRoleBinding.Labels = desiredRouterRoleBinding.Labels + existingRouterRoleBinding.Annotations = desiredRouterRoleBinding.Annotations + existingRouterRoleBinding.Subjects = desiredRouterRoleBinding.Subjects + existingRouterRoleBinding.RoleRef = desiredRouterRoleBinding.RoleRef + return controllerutil.SetControllerReference(jumpstarter, existingRouterRoleBinding, r.Scheme) + } + + if !roleBindingNeedsUpdate(existingRouterRoleBinding, desiredRouterRoleBinding) { + log.V(1).Info("Router RoleBinding is up to date, skipping update", + "name", existingRouterRoleBinding.Name, + "namespace", existingRouterRoleBinding.Namespace) + return nil + } + + existingRouterRoleBinding.Labels = desiredRouterRoleBinding.Labels + existingRouterRoleBinding.Annotations = desiredRouterRoleBinding.Annotations + existingRouterRoleBinding.Subjects = desiredRouterRoleBinding.Subjects + existingRouterRoleBinding.RoleRef = desiredRouterRoleBinding.RoleRef + return controllerutil.SetControllerReference(jumpstarter, existingRouterRoleBinding, r.Scheme) + }) + + if err != nil { + log.Error(err, "Failed to reconcile Router RoleBinding", + "name", desiredRouterRoleBinding.Name, + "namespace", desiredRouterRoleBinding.Namespace) + return err + } + + log.Info("Router RoleBinding reconciled", + "name", existingRouterRoleBinding.Name, + "namespace", existingRouterRoleBinding.Namespace, + "operation", op) + return nil } @@ -209,9 +289,8 @@ func (r *JumpstarterReconciler) createServiceAccount(jumpstarter *operatorv1alph } } -// createRouterServiceAccount creates a service account for the router with no RBAC permissions +// createRouterServiceAccount creates a service account for the router with minimal RBAC permissions func (r *JumpstarterReconciler) createRouterServiceAccount(jumpstarter *operatorv1alpha1.Jumpstarter) *corev1.ServiceAccount { - automount := false return &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-router-sa", jumpstarter.Name), @@ -222,7 +301,6 @@ func (r *JumpstarterReconciler) createRouterServiceAccount(jumpstarter *operator "app.kubernetes.io/managed-by": "jumpstarter-operator", }, }, - AutomountServiceAccountToken: &automount, } } @@ -278,6 +356,60 @@ func (r *JumpstarterReconciler) createRole(jumpstarter *operatorv1alpha1.Jumpsta } } +// createRouterRole creates a role with minimal permissions for the router (read configmaps and secrets) +func (r *JumpstarterReconciler) createRouterRole(jumpstarter *operatorv1alpha1.Jumpstarter) *rbacv1.Role { + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-router-role", jumpstarter.Name), + Namespace: jumpstarter.Namespace, + Labels: map[string]string{ + "app": "jumpstarter-router", + "app.kubernetes.io/name": "jumpstarter-router", + "app.kubernetes.io/managed-by": "jumpstarter-operator", + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + } +} + +// createRouterRoleBinding creates a role binding for the router service account +func (r *JumpstarterReconciler) createRouterRoleBinding(jumpstarter *operatorv1alpha1.Jumpstarter) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-router-rolebinding", jumpstarter.Name), + Namespace: jumpstarter.Namespace, + Labels: map[string]string{ + "app": "jumpstarter-router", + "app.kubernetes.io/name": "jumpstarter-router", + "app.kubernetes.io/managed-by": "jumpstarter-operator", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: fmt.Sprintf("%s-router-role", jumpstarter.Name), + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: fmt.Sprintf("%s-router-sa", jumpstarter.Name), + Namespace: jumpstarter.Namespace, + }, + }, + } +} + // createRoleBinding creates a role binding for the controller func (r *JumpstarterReconciler) createRoleBinding(jumpstarter *operatorv1alpha1.Jumpstarter) *rbacv1.RoleBinding { return &rbacv1.RoleBinding{ From 8de38161a67329f118bd331ce01bbce9f4aced49 Mon Sep 17 00:00:00 2001 From: Ambient Code Bot Date: Wed, 8 Apr 2026 17:26:03 +0000 Subject: [PATCH 4/4] fix: remove unnecessary secrets permission from router Role The router only reads a ConfigMap via LoadRouterConfiguration() and does not access any secrets. Remove the secrets PolicyRule from the router Role per review feedback. Co-Authored-By: Claude Opus 4.6 --- .../operator/internal/controller/jumpstarter/rbac.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/controller/deploy/operator/internal/controller/jumpstarter/rbac.go b/controller/deploy/operator/internal/controller/jumpstarter/rbac.go index 0d37d9846..d31b40d44 100644 --- a/controller/deploy/operator/internal/controller/jumpstarter/rbac.go +++ b/controller/deploy/operator/internal/controller/jumpstarter/rbac.go @@ -356,7 +356,7 @@ func (r *JumpstarterReconciler) createRole(jumpstarter *operatorv1alpha1.Jumpsta } } -// createRouterRole creates a role with minimal permissions for the router (read configmaps and secrets) +// createRouterRole creates a role with minimal permissions for the router (read configmaps) func (r *JumpstarterReconciler) createRouterRole(jumpstarter *operatorv1alpha1.Jumpstarter) *rbacv1.Role { return &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ @@ -374,11 +374,6 @@ func (r *JumpstarterReconciler) createRouterRole(jumpstarter *operatorv1alpha1.J Resources: []string{"configmaps"}, Verbs: []string{"get", "list", "watch"}, }, - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"get", "list", "watch"}, - }, }, } }