From d7f16bc8f62f86ba3734f4cf19301d554604f550 Mon Sep 17 00:00:00 2001 From: alokkumardalei-wq Date: Thu, 16 Jul 2026 06:53:39 +0530 Subject: [PATCH] Restrict konnector clusterrole and dynamically provision RBAC per binding Signed-off-by: alokkumardalei-wq --- backend/kubernetes/resources/konnector.go | 28 +++-- .../plugin/deploy_konnector.go | 21 +++- .../kubectl/bind-apiservice/plugin/rbac.go | 110 ++++++++++++++++++ .../bind-apiservice/plugin/servicebindings.go | 10 ++ 4 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 cli/pkg/kubectl/bind-apiservice/plugin/rbac.go diff --git a/backend/kubernetes/resources/konnector.go b/backend/kubernetes/resources/konnector.go index dfa5879c9..3f9071bd7 100644 --- a/backend/kubernetes/resources/konnector.go +++ b/backend/kubernetes/resources/konnector.go @@ -60,19 +60,33 @@ func NewKonnectorManifests(konnectorImage string, hostAliases []corev1.HostAlias Namespace: KonnectorNamespace, }, }, - // Broad access is required because the konnector dynamically manages CRDs - // and syncs arbitrary resource types discovered from the provider. Scoping - // down would require knowing the bound resource types in advance, which - // defeats the auto-discovery model. + // The konnector dynamically manages CRDs and syncs arbitrary resource types. + // Wildcard permissions are no longer used here; instead, the kubectl bind + // CLI dynamically creates RBAC for the bound custom resources. ClusterRole: &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: KonnectorClusterRoleName, }, Rules: []rbacv1.PolicyRule{ { - APIGroups: []string{"*"}, - Resources: []string{"*"}, - Verbs: []string{"*"}, + APIGroups: []string{""}, + Resources: []string{"namespaces", "secrets", "events", "serviceaccounts", "configmaps"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{"kube-bind.io"}, + Resources: []string{"apiservicebindings", "apiservicebindings/status", "clusterbindings", "clusterbindings/status", "servicebindings", "servicebindings/status", "serviceexports", "serviceexports/status"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{"apiextensions.k8s.io"}, + Resources: []string{"customresourcedefinitions"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{"coordination.k8s.io"}, + Resources: []string{"leases"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, }, }, }, diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/deploy_konnector.go b/cli/pkg/kubectl/bind-apiservice/plugin/deploy_konnector.go index 0707dc518..1bc00afeb 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/deploy_konnector.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/deploy_konnector.go @@ -108,9 +108,24 @@ func getKonnectorClusterRole() *rbacv1.ClusterRole { }, Rules: []rbacv1.PolicyRule{ { - APIGroups: []string{"*"}, - Resources: []string{"*"}, - Verbs: []string{"*"}, + APIGroups: []string{""}, + Resources: []string{"namespaces", "secrets", "events", "serviceaccounts", "configmaps"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{"kube-bind.io"}, + Resources: []string{"apiservicebindings", "apiservicebindings/status", "clusterbindings", "clusterbindings/status", "servicebindings", "servicebindings/status", "serviceexports", "serviceexports/status"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{"apiextensions.k8s.io"}, + Resources: []string{"customresourcedefinitions"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{"coordination.k8s.io"}, + Resources: []string{"leases"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, }, }, } diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/rbac.go b/cli/pkg/kubectl/bind-apiservice/plugin/rbac.go new file mode 100644 index 000000000..aa3aea247 --- /dev/null +++ b/cli/pkg/kubectl/bind-apiservice/plugin/rbac.go @@ -0,0 +1,110 @@ +package plugin + +import ( + "context" + "fmt" + + kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +func ensureKonnectorDynamicRBAC(ctx context.Context, config *rest.Config, binding *kubebindv1alpha2.APIServiceBinding, request *kubebindv1alpha2.APIServiceExportRequest) error { + kubeClient, err := kubernetes.NewForConfig(config) + if err != nil { + return err + } + + roleName := fmt.Sprintf("kube-bind-konnector-%s", binding.Name) + ownerRef := metav1.OwnerReference{ + APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(), + Kind: "APIServiceBinding", + Name: binding.Name, + UID: binding.UID, + } + + var rules []rbacv1.PolicyRule + for _, res := range request.Spec.Resources { + rules = append(rules, rbacv1.PolicyRule{ + APIGroups: []string{res.Group}, + Resources: []string{res.Resource, res.Resource + "/status"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }) + } + for _, claim := range request.Spec.PermissionClaims { + rules = append(rules, rbacv1.PolicyRule{ + APIGroups: []string{claim.Group}, + Resources: []string{claim.Resource, claim.Resource + "/status"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }) + } + + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + Rules: rules, + } + + _, err = kubeClient.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{}) + if err != nil { + if apierrors.IsAlreadyExists(err) { + existing, err := kubeClient.RbacV1().ClusterRoles().Get(ctx, roleName, metav1.GetOptions{}) + if err != nil { + return err + } + existing.Rules = rules + existing.OwnerReferences = []metav1.OwnerReference{ownerRef} + _, err = kubeClient.RbacV1().ClusterRoles().Update(ctx, existing, metav1.UpdateOptions{}) + if err != nil { + return err + } + } else { + return err + } + } + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: roleName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: KonnectorServiceAccount, + Namespace: KonnectorNamespace, + }, + }, + } + + _, err = kubeClient.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}) + if err != nil { + if apierrors.IsAlreadyExists(err) { + existing, err := kubeClient.RbacV1().ClusterRoleBindings().Get(ctx, roleName, metav1.GetOptions{}) + if err != nil { + return err + } + existing.RoleRef = clusterRoleBinding.RoleRef + existing.Subjects = clusterRoleBinding.Subjects + existing.OwnerReferences = []metav1.OwnerReference{ownerRef} + _, err = kubeClient.RbacV1().ClusterRoleBindings().Update(ctx, existing, metav1.UpdateOptions{}) + if err != nil { + return err + } + } else { + return err + } + } + + return nil +} diff --git a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go index b539c8b64..bbddc2088 100644 --- a/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go +++ b/cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go @@ -68,6 +68,11 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co } } + // Ensure dynamic RBAC is created for the konnector to access the bound resources + if err := ensureKonnectorDynamicRBAC(ctx, config, existing, request); err != nil { + return nil, fmt.Errorf("failed to create dynamic RBAC for konnector: %w", err) + } + return []*kubebindv1alpha2.APIServiceBinding{existing}, nil } @@ -106,6 +111,11 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co return nil, err } + // Ensure dynamic RBAC is created for the konnector to access the bound resources + if err := ensureKonnectorDynamicRBAC(ctx, config, created, request); err != nil { + return nil, fmt.Errorf("failed to create dynamic RBAC for konnector: %w", err) + } + fmt.Fprintf(b.Options.IOStreams.ErrOut, "✅ Created APIServiceBinding %s for %d resources\n", bindingName, len(request.Spec.Resources)) return []*kubebindv1alpha2.APIServiceBinding{created}, nil }