diff --git a/cmd/main.go b/cmd/main.go index 72befd16..61bb7c40 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -136,6 +136,11 @@ func main() { } operatorNamespace := strings.TrimSpace(string(nsBytes)) + operatorSA := os.Getenv("OPERATOR_SERVICE_ACCOUNT") + if operatorSA == "" { + operatorSA = "controller-manager" + } + // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and @@ -251,10 +256,11 @@ func main() { os.Exit(1) } if err := (&controller.StorageClusterReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("storagecluster-controller"), - Namespace: operatorNamespace, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("storagecluster-controller"), + Namespace: operatorNamespace, + ServiceAccountName: operatorSA, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "StorageCluster") os.Exit(1) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 7055eae7..df16188a 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -63,6 +63,11 @@ spec: args: - --leader-elect - --health-probe-bind-address=:8081 + env: + - name: OPERATOR_SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName image: controller:latest name: manager securityContext: diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index d1536fdf..5d1aec03 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -7,6 +7,8 @@ resources: - service_account.yaml - role.yaml - role_binding.yaml +- manager_secrets_role.yaml +- manager_secrets_role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect diff --git a/config/rbac/manager_secrets_role.yaml b/config/rbac/manager_secrets_role.yaml new file mode 100644 index 00000000..8f6cc6f6 --- /dev/null +++ b/config/rbac/manager_secrets_role.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: simplyblock-operator + app.kubernetes.io/managed-by: kustomize + name: manager-secrets-role + namespace: system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - patch + - update diff --git a/config/rbac/manager_secrets_role_binding.yaml b/config/rbac/manager_secrets_role_binding.yaml new file mode 100644 index 00000000..5ce99ebc --- /dev/null +++ b/config/rbac/manager_secrets_role_binding.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: simplyblock-operator + app.kubernetes.io/managed-by: kustomize + name: manager-secrets-rolebinding + namespace: system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: manager-secrets-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1c6caefc..e91f9a78 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -46,6 +46,13 @@ rules: - "" resources: - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: - serviceaccounts - services verbs: @@ -109,6 +116,8 @@ rules: resources: - clusterrolebindings - clusterroles + - rolebindings + - roles verbs: - create - delete diff --git a/dist/install.yaml b/dist/install.yaml index ea3bc494..60e20711 100644 --- a/dist/install.yaml +++ b/dist/install.yaml @@ -2075,6 +2075,13 @@ rules: - "" resources: - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: - serviceaccounts - services verbs: @@ -2138,6 +2145,8 @@ rules: resources: - clusterrolebindings - clusterroles + - rolebindings + - roles verbs: - create - delete @@ -2593,6 +2602,43 @@ rules: - get --- apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: simplyblock-operator + name: simplyblock-operator-manager-secrets-role + namespace: simplyblock-operator-system +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: simplyblock-operator + name: simplyblock-operator-manager-secrets-rolebinding + namespace: simplyblock-operator-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simplyblock-operator-manager-secrets-role +subjects: +- kind: ServiceAccount + name: simplyblock-operator-controller-manager + namespace: simplyblock-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: @@ -2687,6 +2733,11 @@ spec: - --health-probe-bind-address=:8081 command: - /manager + env: + - name: OPERATOR_SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName image: quay.io/simplyblock-io/simplyblock-operator:0.1.0 livenessProbe: httpGet: diff --git a/internal/controller/simplyblockstoragecluster_controller.go b/internal/controller/simplyblockstoragecluster_controller.go index 2aa375d6..8cbe2883 100644 --- a/internal/controller/simplyblockstoragecluster_controller.go +++ b/internal/controller/simplyblockstoragecluster_controller.go @@ -24,6 +24,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -72,9 +73,10 @@ const ( // StorageClusterReconciler reconciles a StorageCluster object type StorageClusterReconciler struct { client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder - Namespace string // operator namespace + Scheme *runtime.Scheme + Recorder record.EventRecorder + Namespace string // operator namespace + ServiceAccountName string // operator service account name } type CSICredentials struct { @@ -90,11 +92,12 @@ type CSIClusterEntry struct { // +kubebuilder:rbac:groups=storage.simplyblock.io,resources=storageclusters,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=storage.simplyblock.io,resources=storageclusters/status,verbs=get;update;patch // +kubebuilder:rbac:groups=storage.simplyblock.io,resources=storageclusters/finalizers,verbs=update -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;delete // +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;list;watch;create;update;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -123,6 +126,13 @@ func (r *StorageClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } + /* -------------------- Secret RBAC -------------------- */ + if clusterCR.Namespace != r.Namespace { + if err := r.ensureNamespaceSecretAccess(ctx, clusterCR.Namespace); err != nil { + return ctrl.Result{}, err + } + } + switch clusterCR.Spec.Action { case utils.ClusterActionActivate: return r.reconcileActivate(ctx, clusterCR) @@ -589,6 +599,57 @@ func (r *StorageClusterReconciler) deleteClusterSecret( return nil } +// ensureNamespaceSecretAccess creates a namespace-scoped Role and RoleBinding in +// the given namespace so the operator can write secrets there. Called during +// StorageCluster reconciliation for clusters deployed outside the operator's own +// namespace (e.g. simplyblock-cluster-* credentials secrets). +func (r *StorageClusterReconciler) ensureNamespaceSecretAccess(ctx context.Context, namespace string) error { + const roleName = "simplyblock-manager-secrets" + + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: namespace, + }, + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, role, func() error { + role.Rules = []rbacv1.PolicyRule{{ + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"create", "delete", "get", "patch", "update"}, + }} + return nil + }); err != nil { + return fmt.Errorf("ensure secret role in namespace %s: %w", namespace, err) + } + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: namespace, + }, + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, rb, func() error { + rb.Subjects = []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: r.ServiceAccountName, + Namespace: r.Namespace, + }} + if rb.RoleRef.Name == "" { + rb.RoleRef = rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: roleName, + } + } + return nil + }); err != nil { + return fmt.Errorf("ensure secret rolebinding in namespace %s: %w", namespace, err) + } + + return nil +} + func (r *StorageClusterReconciler) reconcileActivate( ctx context.Context, clusterCR *simplyblockv1alpha1.StorageCluster,