@ -2,17 +2,200 @@ package cli
import (
"context"
"time"
"github.com/portainer/portainer/api/internal/randomstring"
"github.com/rs/zerolog/log"
authv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
authv1types "k8s.io/client-go/kubernetes/typed/authorization/v1"
corev1types "k8s.io/client-go/kubernetes/typed/core/v1"
rbacv1types "k8s.io/client-go/kubernetes/typed/rbac/v1"
)
// IsRBACEnabled checks if RBAC is enabled in the current Kubernetes cluster by listing cluster roles.
// if the cluster roles can be listed, RBAC is enabled.
// otherwise, RBAC is not enabled.
const maxRetries = 5
// IsRBACEnabled checks if RBAC is enabled in the cluster by creating a service account, then checking it's access to a resourcequota before and after setting a cluster role and cluster role binding
func ( kcl * KubeClient ) IsRBACEnabled ( ) ( bool , error ) {
_ , err := kcl . cli . RbacV1 ( ) . ClusterRoles ( ) . List ( context . TODO ( ) , metav1 . ListOptions { } )
namespace := "default"
verb := "list"
resource := "resourcequotas"
saClient := kcl . cli . CoreV1 ( ) . ServiceAccounts ( namespace )
uniqueString := randomstring . RandomString ( 4 ) // Append a unique string to resource names, in case they already exist
saName := "portainer-rbac-test-sa-" + uniqueString
if err := createServiceAccount ( saClient , saName , namespace ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error creating service account" )
return false , err
}
defer deleteServiceAccount ( saClient , saName )
accessReviewClient := kcl . cli . AuthorizationV1 ( ) . LocalSubjectAccessReviews ( namespace )
allowed , err := checkServiceAccountAccess ( accessReviewClient , saName , verb , resource , namespace )
if err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error checking service account access" )
return false , err
}
// If the service account with no authorizations is allowed, RBAC must be disabled
if allowed {
return false , nil
}
return true , nil
// Otherwise give the service account an rbac authorisation and check again
roleClient := kcl . cli . RbacV1 ( ) . Roles ( namespace )
roleName := "portainer-rbac-test-role-" + uniqueString
if err := createRole ( roleClient , roleName , verb , resource , namespace ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error creating role" )
return false , err
}
defer deleteRole ( roleClient , roleName )
roleBindingClient := kcl . cli . RbacV1 ( ) . RoleBindings ( namespace )
roleBindingName := "portainer-rbac-test-role-binding-" + uniqueString
if err := createRoleBinding ( roleBindingClient , roleBindingName , roleName , saName , namespace ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error creating role binding" )
return false , err
}
defer deleteRoleBinding ( roleBindingClient , roleBindingName )
allowed , err = checkServiceAccountAccess ( accessReviewClient , saName , verb , resource , namespace )
if err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error checking service account access with authorizations added" )
return false , err
}
// If the service account allowed to list resource quotas after given rbac role, then RBAC is enabled
return allowed , nil
}
func createServiceAccount ( saClient corev1types . ServiceAccountInterface , name string , namespace string ) error {
serviceAccount := & corev1 . ServiceAccount {
ObjectMeta : metav1 . ObjectMeta {
Name : name ,
Namespace : namespace ,
} ,
}
_ , err := saClient . Create ( context . Background ( ) , serviceAccount , metav1 . CreateOptions { } )
return err
}
func deleteServiceAccount ( saClient corev1types . ServiceAccountInterface , name string ) {
if err := saClient . Delete ( context . Background ( ) , name , metav1 . DeleteOptions { } ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error deleting service account: " + name )
}
}
func createRole ( roleClient rbacv1types . RoleInterface , name string , verb string , resource string , namespace string ) error {
role := & rbacv1 . Role {
ObjectMeta : metav1 . ObjectMeta {
Name : name ,
Namespace : namespace ,
} ,
Rules : [ ] rbacv1 . PolicyRule {
{
APIGroups : [ ] string { "" } ,
Verbs : [ ] string { verb } ,
Resources : [ ] string { resource } ,
} ,
} ,
}
_ , err := roleClient . Create ( context . Background ( ) , role , metav1 . CreateOptions { } )
return err
}
func deleteRole ( roleClient rbacv1types . RoleInterface , name string ) {
if err := roleClient . Delete ( context . Background ( ) , name , metav1 . DeleteOptions { } ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error deleting role: " + name )
}
}
func createRoleBinding ( roleBindingClient rbacv1types . RoleBindingInterface , clusterRoleBindingName string , roleName string , serviceAccountName string , namespace string ) error {
clusterRoleBinding := & rbacv1 . RoleBinding {
ObjectMeta : metav1 . ObjectMeta {
Name : clusterRoleBindingName ,
} ,
Subjects : [ ] rbacv1 . Subject {
{
Kind : "ServiceAccount" ,
Name : serviceAccountName ,
Namespace : namespace ,
} ,
} ,
RoleRef : rbacv1 . RoleRef {
Kind : "Role" ,
Name : roleName ,
APIGroup : "rbac.authorization.k8s.io" ,
} ,
}
roleBinding , err := roleBindingClient . Create ( context . Background ( ) , clusterRoleBinding , metav1 . CreateOptions { } )
if err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error creating role binding: " + clusterRoleBindingName )
return err
}
// Retry checkRoleBinding a maximum of 5 times with a 100ms wait after each attempt
for range maxRetries {
err = checkRoleBinding ( roleBindingClient , roleBinding . Name )
time . Sleep ( 100 * time . Millisecond ) // Wait for 100ms, even if the check passes
if err == nil {
break
}
}
return err
}
func checkRoleBinding ( roleBindingClient rbacv1types . RoleBindingInterface , name string ) error {
if _ , err := roleBindingClient . Get ( context . Background ( ) , name , metav1 . GetOptions { } ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error finding rolebinding: " + name )
return err
}
return nil
}
func deleteRoleBinding ( roleBindingClient rbacv1types . RoleBindingInterface , name string ) {
if err := roleBindingClient . Delete ( context . Background ( ) , name , metav1 . DeleteOptions { } ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "Error deleting role binding: " + name )
}
}
func checkServiceAccountAccess ( accessReviewClient authv1types . LocalSubjectAccessReviewInterface , serviceAccountName string , verb string , resource string , namespace string ) ( bool , error ) {
subjectAccessReview := & authv1 . LocalSubjectAccessReview {
ObjectMeta : metav1 . ObjectMeta {
Namespace : namespace ,
} ,
Spec : authv1 . SubjectAccessReviewSpec {
ResourceAttributes : & authv1 . ResourceAttributes {
Namespace : namespace ,
Verb : verb ,
Resource : resource ,
} ,
User : "system:serviceaccount:default:" + serviceAccountName , // a workaround to be able to use the service account as a user
} ,
}
result , err := accessReviewClient . Create ( context . Background ( ) , subjectAccessReview , metav1 . CreateOptions { } )
if err != nil {
return false , err
}
return result . Status . Allowed , nil
}