k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/gencred/pkg/serviceaccount/serviceaccount.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package serviceaccount
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  
    24  	authenticationv1 "k8s.io/api/authentication/v1"
    25  	authorizationv1 "k8s.io/api/authorization/v1"
    26  	corev1 "k8s.io/api/core/v1"
    27  	rbacv1 "k8s.io/api/rbac/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/client-go/kubernetes"
    30  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api/v1"
    31  	"k8s.io/test-infra/gencred/pkg/kubeconfig"
    32  )
    33  
    34  const (
    35  	// clusterRole is the ClusterRole role reference for created ClusterRoleBinding.
    36  	clusterRole = "cluster-admin"
    37  	// clusterRoleBindingName is the name for the created ClusterRoleBinding.
    38  	clusterRoleBindingName = "serviceaccount-cluster-admin-crb"
    39  	// serviceAccountName is the name for the created ServiceAccount.
    40  	serviceAccountName = "serviceaccount-cluster-admin"
    41  )
    42  
    43  // checkSAAuth checks authorization for required cluster service account (SA) resources.
    44  func checkSAAuth(clientset kubernetes.Interface) error {
    45  	client := clientset.AuthorizationV1().SelfSubjectAccessReviews()
    46  
    47  	// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#privilege-escalation-prevention-and-bootstrapping
    48  	if sar, err := client.Create(
    49  		context.TODO(),
    50  		&authorizationv1.SelfSubjectAccessReview{
    51  			Spec: authorizationv1.SelfSubjectAccessReviewSpec{
    52  				ResourceAttributes: &authorizationv1.ResourceAttributes{
    53  					Group:    "rbac.authorization.k8s.io",
    54  					Verb:     "bind",
    55  					Resource: "clusterroles",
    56  					Name:     clusterRole,
    57  				},
    58  			},
    59  		},
    60  		metav1.CreateOptions{}); err != nil {
    61  		return fmt.Errorf("bind %s: %w", clusterRole, err)
    62  	} else if !sar.Status.Allowed {
    63  		return fmt.Errorf("not authorized to bind %s: %s", clusterRole, sar.Status.Reason)
    64  	}
    65  
    66  	return nil
    67  }
    68  
    69  // getOrCreateSA gets existing or creates new service account (SA).
    70  func getOrCreateSA(clientset kubernetes.Interface, duration metav1.Duration) ([]byte, []byte, error) {
    71  	client := clientset.CoreV1().ServiceAccounts(corev1.NamespaceDefault)
    72  
    73  	// Check SelfSubjectAccessReviews are allowed.
    74  	if err := checkSAAuth(clientset); err != nil {
    75  		return nil, nil, err
    76  	}
    77  
    78  	// Create ServiceAccount if not exists.
    79  	if _, err := client.Get(context.TODO(), serviceAccountName, metav1.GetOptions{}); err != nil {
    80  		// Generate a Kubernetes ServiceAccount object.
    81  		saObj := &corev1.ServiceAccount{
    82  			ObjectMeta: metav1.ObjectMeta{
    83  				Name: serviceAccountName,
    84  			},
    85  		}
    86  
    87  		// Create ServiceAccount.
    88  		_, err := client.Create(context.TODO(), saObj, metav1.CreateOptions{})
    89  		if err != nil {
    90  			return nil, nil, fmt.Errorf("create SA: %w", err)
    91  		}
    92  	}
    93  
    94  	// Get/Create ClusterRoleBinding.
    95  	err := getOrCreateCRB(clientset)
    96  	if err != nil {
    97  		return nil, nil, fmt.Errorf("get or create CRB: %w", err)
    98  	}
    99  
   100  	// Get ServiceAccount.
   101  	saObj, err := client.Get(context.TODO(), serviceAccountName, metav1.GetOptions{})
   102  	if err != nil {
   103  		return nil, nil, fmt.Errorf("get SA: %w", err)
   104  	}
   105  
   106  	return getSASecrets(clientset, saObj, duration)
   107  }
   108  
   109  // getOrCreateCRB gets existing or creates new cluster role binding (CRB).
   110  func getOrCreateCRB(clientset kubernetes.Interface) error {
   111  	client := clientset.RbacV1().ClusterRoleBindings()
   112  
   113  	// Get ClusterRoleBinding if exists.
   114  	if _, err := client.Get(context.TODO(), clusterRoleBindingName, metav1.GetOptions{}); err == nil {
   115  		return nil
   116  	}
   117  
   118  	// Generate a Kubernetes ClusterRoleBinding object.
   119  	crbObj := &rbacv1.ClusterRoleBinding{
   120  		ObjectMeta: metav1.ObjectMeta{Name: clusterRoleBindingName},
   121  		Subjects:   []rbacv1.Subject{{Kind: "ServiceAccount", Name: serviceAccountName, Namespace: corev1.NamespaceDefault}},
   122  		RoleRef:    rbacv1.RoleRef{Kind: "ClusterRole", Name: clusterRole},
   123  	}
   124  
   125  	// Create ClusterRoleBinding.
   126  	_, err := client.Create(context.TODO(), crbObj, metav1.CreateOptions{})
   127  	if err != nil {
   128  		return fmt.Errorf("create CRB: %w", err)
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // getSASecrets gets service account token and root CA secrets.
   135  func getSASecrets(clientset kubernetes.Interface, saObj *corev1.ServiceAccount, duration metav1.Duration) ([]byte, []byte, error) {
   136  	durationInSeconds := int64(duration.Seconds())
   137  	tokenReq := &authenticationv1.TokenRequest{
   138  		Spec: authenticationv1.TokenRequestSpec{
   139  			ExpirationSeconds: &durationInSeconds,
   140  		},
   141  	}
   142  	tokenResp, err := clientset.CoreV1().ServiceAccounts(saObj.Namespace).CreateToken(context.TODO(), saObj.Name, tokenReq, metav1.CreateOptions{})
   143  	if err != nil {
   144  		return nil, nil, fmt.Errorf("creating service account token: %w", err)
   145  	}
   146  	if tokenResp.Status.Token == "" {
   147  		return nil, nil, fmt.Errorf("no service account token returned")
   148  	}
   149  
   150  	caConfigMap, err := clientset.CoreV1().ConfigMaps(corev1.NamespaceDefault).Get(context.TODO(), "kube-root-ca.crt", metav1.GetOptions{})
   151  	if err != nil {
   152  		return nil, nil, fmt.Errorf("locate root CA configmap: %w", err)
   153  	}
   154  	caPEM, ok := caConfigMap.Data["ca.crt"]
   155  	if !ok {
   156  		return nil, nil, errors.New("locate root CA data")
   157  	}
   158  
   159  	return []byte(tokenResp.Status.Token), []byte(caPEM), nil
   160  }
   161  
   162  // CreateClusterServiceAccountCredentials creates a service account to authenticate to a cluster API server.
   163  func CreateClusterServiceAccountCredentials(clientset kubernetes.Interface, duration metav1.Duration) (token []byte, caPEM []byte, err error) {
   164  	token, caPEM, err = getOrCreateSA(clientset, duration)
   165  	if err != nil {
   166  		return nil, nil, fmt.Errorf("get or create SA: %w", err)
   167  	}
   168  
   169  	return token, caPEM, nil
   170  }
   171  
   172  // CreateKubeConfigWithServiceAccountCredentials creates a kube config containing a service account token to authenticate to a Kubernetes cluster API server.
   173  func CreateKubeConfigWithServiceAccountCredentials(clientset kubernetes.Interface, name string, duration metav1.Duration) ([]byte, error) {
   174  	token, caPEM, err := CreateClusterServiceAccountCredentials(clientset, duration)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	authInfo := clientcmdapi.AuthInfo{
   180  		Token: string(token),
   181  	}
   182  
   183  	return kubeconfig.CreateKubeConfig(clientset, name, caPEM, authInfo)
   184  }