github.com/argoproj/argo-cd/v3@v3.2.1/util/clusterauth/clusterauth.go (about)

     1  package clusterauth
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/golang-jwt/jwt/v5"
    10  	log "github.com/sirupsen/logrus"
    11  	corev1 "k8s.io/api/core/v1"
    12  	rbacv1 "k8s.io/api/rbac/v1"
    13  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/util/wait"
    16  	"k8s.io/client-go/kubernetes"
    17  
    18  	"github.com/argoproj/argo-cd/v3/common"
    19  )
    20  
    21  // ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
    22  const (
    23  	ArgoCDManagerServiceAccount     = "argocd-manager"
    24  	ArgoCDManagerClusterRole        = "argocd-manager-role"
    25  	ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
    26  	SATokenSecretSuffix             = "-long-lived-token"
    27  )
    28  
    29  // ArgoCDManagerPolicyRules are the policies to give argocd-manager
    30  var ArgoCDManagerClusterPolicyRules = []rbacv1.PolicyRule{
    31  	{
    32  		APIGroups: []string{"*"},
    33  		Resources: []string{"*"},
    34  		Verbs:     []string{"*"},
    35  	},
    36  	{
    37  		NonResourceURLs: []string{"*"},
    38  		Verbs:           []string{"*"},
    39  	},
    40  }
    41  
    42  // ArgoCDManagerNamespacePolicyRules are the namespace level policies to give argocd-manager
    43  var ArgoCDManagerNamespacePolicyRules = []rbacv1.PolicyRule{
    44  	{
    45  		APIGroups: []string{"*"},
    46  		Resources: []string{"*"},
    47  		Verbs:     []string{"*"},
    48  	},
    49  }
    50  
    51  // CreateServiceAccount creates a service account in a given namespace
    52  func CreateServiceAccount(
    53  	clientset kubernetes.Interface,
    54  	serviceAccountName string,
    55  	namespace string,
    56  ) error {
    57  	serviceAccount := corev1.ServiceAccount{
    58  		TypeMeta: metav1.TypeMeta{
    59  			APIVersion: "v1",
    60  			Kind:       "ServiceAccount",
    61  		},
    62  		ObjectMeta: metav1.ObjectMeta{
    63  			Name:      serviceAccountName,
    64  			Namespace: namespace,
    65  		},
    66  	}
    67  	_, err := clientset.CoreV1().ServiceAccounts(namespace).Create(context.Background(), &serviceAccount, metav1.CreateOptions{})
    68  	if err != nil {
    69  		if !apierrors.IsAlreadyExists(err) {
    70  			return fmt.Errorf("failed to create service account %q in namespace %q: %w", serviceAccountName, namespace, err)
    71  		}
    72  		log.Infof("ServiceAccount %q already exists in namespace %q", serviceAccountName, namespace)
    73  		return nil
    74  	}
    75  	log.Infof("ServiceAccount %q created in namespace %q", serviceAccountName, namespace)
    76  	return nil
    77  }
    78  
    79  func upsert(kind string, name string, create func() (any, error), update func() (any, error)) error {
    80  	_, err := create()
    81  	if err != nil {
    82  		if !apierrors.IsAlreadyExists(err) {
    83  			return fmt.Errorf("failed to create %s %q: %w", kind, name, err)
    84  		}
    85  		_, err = update()
    86  		if err != nil {
    87  			return fmt.Errorf("failed to update %s %q: %w", kind, name, err)
    88  		}
    89  		log.Infof("%s %q updated", kind, name)
    90  	} else {
    91  		log.Infof("%s %q created", kind, name)
    92  	}
    93  	return nil
    94  }
    95  
    96  func upsertClusterRole(clientset kubernetes.Interface, name string, rules []rbacv1.PolicyRule) error {
    97  	clusterRole := rbacv1.ClusterRole{
    98  		TypeMeta: metav1.TypeMeta{
    99  			APIVersion: "rbac.authorization.k8s.io/v1",
   100  			Kind:       "ClusterRole",
   101  		},
   102  		ObjectMeta: metav1.ObjectMeta{
   103  			Name: name,
   104  		},
   105  		Rules: rules,
   106  	}
   107  	return upsert("ClusterRole", name, func() (any, error) {
   108  		return clientset.RbacV1().ClusterRoles().Create(context.Background(), &clusterRole, metav1.CreateOptions{})
   109  	}, func() (any, error) {
   110  		return clientset.RbacV1().ClusterRoles().Update(context.Background(), &clusterRole, metav1.UpdateOptions{})
   111  	})
   112  }
   113  
   114  func upsertRole(clientset kubernetes.Interface, name string, namespace string, rules []rbacv1.PolicyRule) error {
   115  	role := rbacv1.Role{
   116  		TypeMeta: metav1.TypeMeta{
   117  			APIVersion: "rbac.authorization.k8s.io/v1",
   118  			Kind:       "Role",
   119  		},
   120  		ObjectMeta: metav1.ObjectMeta{
   121  			Name: name,
   122  		},
   123  		Rules: rules,
   124  	}
   125  	return upsert("Role", fmt.Sprintf("%s/%s", namespace, name), func() (any, error) {
   126  		return clientset.RbacV1().Roles(namespace).Create(context.Background(), &role, metav1.CreateOptions{})
   127  	}, func() (any, error) {
   128  		return clientset.RbacV1().Roles(namespace).Update(context.Background(), &role, metav1.UpdateOptions{})
   129  	})
   130  }
   131  
   132  func upsertClusterRoleBinding(clientset kubernetes.Interface, name string, clusterRoleName string, subject rbacv1.Subject) error {
   133  	roleBinding := rbacv1.ClusterRoleBinding{
   134  		TypeMeta: metav1.TypeMeta{
   135  			APIVersion: "rbac.authorization.k8s.io/v1",
   136  			Kind:       "ClusterRoleBinding",
   137  		},
   138  		ObjectMeta: metav1.ObjectMeta{
   139  			Name: name,
   140  		},
   141  		RoleRef: rbacv1.RoleRef{
   142  			APIGroup: "rbac.authorization.k8s.io",
   143  			Kind:     "ClusterRole",
   144  			Name:     clusterRoleName,
   145  		},
   146  		Subjects: []rbacv1.Subject{subject},
   147  	}
   148  	return upsert("ClusterRoleBinding", name, func() (any, error) {
   149  		return clientset.RbacV1().ClusterRoleBindings().Create(context.Background(), &roleBinding, metav1.CreateOptions{})
   150  	}, func() (any, error) {
   151  		return clientset.RbacV1().ClusterRoleBindings().Update(context.Background(), &roleBinding, metav1.UpdateOptions{})
   152  	})
   153  }
   154  
   155  func upsertRoleBinding(clientset kubernetes.Interface, name string, roleName string, namespace string, subject rbacv1.Subject) error {
   156  	roleBinding := rbacv1.RoleBinding{
   157  		TypeMeta: metav1.TypeMeta{
   158  			APIVersion: "rbac.authorization.k8s.io/v1",
   159  			Kind:       "RoleBinding",
   160  		},
   161  		ObjectMeta: metav1.ObjectMeta{
   162  			Name: name,
   163  		},
   164  		RoleRef: rbacv1.RoleRef{
   165  			APIGroup: "rbac.authorization.k8s.io",
   166  			Kind:     "Role",
   167  			Name:     roleName,
   168  		},
   169  		Subjects: []rbacv1.Subject{subject},
   170  	}
   171  	return upsert("RoleBinding", fmt.Sprintf("%s/%s", namespace, name), func() (any, error) {
   172  		return clientset.RbacV1().RoleBindings(namespace).Create(context.Background(), &roleBinding, metav1.CreateOptions{})
   173  	}, func() (any, error) {
   174  		return clientset.RbacV1().RoleBindings(namespace).Update(context.Background(), &roleBinding, metav1.UpdateOptions{})
   175  	})
   176  }
   177  
   178  // InstallClusterManagerRBAC installs RBAC resources for a cluster manager to operate a cluster. Returns a token
   179  func InstallClusterManagerRBAC(clientset kubernetes.Interface, ns string, namespaces []string, bearerTokenTimeout time.Duration) (string, error) {
   180  	err := CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  
   185  	if len(namespaces) == 0 {
   186  		err = upsertClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerClusterPolicyRules)
   187  		if err != nil {
   188  			return "", err
   189  		}
   190  
   191  		err = upsertClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, rbacv1.Subject{
   192  			Kind:      rbacv1.ServiceAccountKind,
   193  			Name:      ArgoCDManagerServiceAccount,
   194  			Namespace: ns,
   195  		})
   196  		if err != nil {
   197  			return "", err
   198  		}
   199  	} else {
   200  		for _, namespace := range namespaces {
   201  			err = upsertRole(clientset, ArgoCDManagerClusterRole, namespace, ArgoCDManagerNamespacePolicyRules)
   202  			if err != nil {
   203  				return "", err
   204  			}
   205  
   206  			err = upsertRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, namespace, rbacv1.Subject{
   207  				Kind:      rbacv1.ServiceAccountKind,
   208  				Name:      ArgoCDManagerServiceAccount,
   209  				Namespace: ns,
   210  			})
   211  			if err != nil {
   212  				return "", err
   213  			}
   214  		}
   215  	}
   216  
   217  	return GetServiceAccountBearerToken(clientset, ns, ArgoCDManagerServiceAccount, bearerTokenTimeout)
   218  }
   219  
   220  // GetServiceAccountBearerToken determines if a ServiceAccount has a
   221  // bearer token secret to use or if a secret should be created. It then
   222  // waits for the secret to have a bearer token if a secret needs to
   223  // be created and returns the token in encoded base64.
   224  func GetServiceAccountBearerToken(clientset kubernetes.Interface, ns string, sa string, timeout time.Duration) (string, error) {
   225  	secretName, err := getOrCreateServiceAccountTokenSecret(clientset, sa, ns)
   226  	if err != nil {
   227  		return "", err
   228  	}
   229  
   230  	var secret *corev1.Secret
   231  	err = wait.PollUntilContextTimeout(context.Background(), 500*time.Millisecond, timeout, true, func(ctx context.Context) (bool, error) {
   232  		ctx, cancel := context.WithTimeout(ctx, common.ClusterAuthRequestTimeout)
   233  		defer cancel()
   234  		secret, err = clientset.CoreV1().Secrets(ns).Get(ctx, secretName, metav1.GetOptions{})
   235  		if err != nil {
   236  			return false, fmt.Errorf("failed to get secret %q for serviceaccount %q: %w", secretName, sa, err)
   237  		}
   238  
   239  		_, ok := secret.Data[corev1.ServiceAccountTokenKey]
   240  		if !ok {
   241  			return false, nil
   242  		}
   243  
   244  		return true, nil
   245  	})
   246  	if err != nil {
   247  		return "", fmt.Errorf("failed to get token for serviceaccount %q: %w", sa, err)
   248  	}
   249  
   250  	return string(secret.Data[corev1.ServiceAccountTokenKey]), nil
   251  }
   252  
   253  // getOrCreateServiceAccountTokenSecret will create a
   254  // kubernetes.io/service-account-token secret associated with a
   255  // ServiceAccount named '<service account name>-long-lived-token', or
   256  // use the existing one with that name.
   257  // This was added to help add k8s v1.24+ clusters.
   258  func getOrCreateServiceAccountTokenSecret(clientset kubernetes.Interface, serviceaccount, namespace string) (string, error) {
   259  	secret := &corev1.Secret{
   260  		ObjectMeta: metav1.ObjectMeta{
   261  			Name:      serviceaccount + SATokenSecretSuffix,
   262  			Namespace: namespace,
   263  			Annotations: map[string]string{
   264  				corev1.ServiceAccountNameKey: serviceaccount,
   265  			},
   266  		},
   267  		Type: corev1.SecretTypeServiceAccountToken,
   268  	}
   269  
   270  	ctx, cancel := context.WithTimeout(context.Background(), common.ClusterAuthRequestTimeout)
   271  	defer cancel()
   272  	_, err := clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
   273  
   274  	switch {
   275  	case apierrors.IsAlreadyExists(err):
   276  		log.Infof("Using existing bearer token secret %q for ServiceAccount %q", secret.Name, serviceaccount)
   277  	case err != nil:
   278  		return "", fmt.Errorf("failed to create secret %q for serviceaccount %q: %w", secret.Name, serviceaccount, err)
   279  	default:
   280  		log.Infof("Created bearer token secret %q for ServiceAccount %q", secret.Name, serviceaccount)
   281  	}
   282  
   283  	return secret.Name, nil
   284  }
   285  
   286  // UninstallClusterManagerRBAC removes RBAC resources for a cluster manager to operate a cluster
   287  func UninstallClusterManagerRBAC(clientset kubernetes.Interface) error {
   288  	return UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
   289  }
   290  
   291  // UninstallRBAC uninstalls RBAC related resources  for a binding, role, and service account
   292  func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) error {
   293  	if err := clientset.RbacV1().ClusterRoleBindings().Delete(context.Background(), bindingName, metav1.DeleteOptions{}); err != nil {
   294  		if !apierrors.IsNotFound(err) {
   295  			return fmt.Errorf("failed to delete ClusterRoleBinding: %w", err)
   296  		}
   297  		log.Infof("ClusterRoleBinding %q not found", bindingName)
   298  	} else {
   299  		log.Infof("ClusterRoleBinding %q deleted", bindingName)
   300  	}
   301  
   302  	if err := clientset.RbacV1().ClusterRoles().Delete(context.Background(), roleName, metav1.DeleteOptions{}); err != nil {
   303  		if !apierrors.IsNotFound(err) {
   304  			return fmt.Errorf("failed to delete ClusterRole: %w", err)
   305  		}
   306  		log.Infof("ClusterRole %q not found", roleName)
   307  	} else {
   308  		log.Infof("ClusterRole %q deleted", roleName)
   309  	}
   310  
   311  	if err := clientset.CoreV1().ServiceAccounts(namespace).Delete(context.Background(), serviceAccount, metav1.DeleteOptions{}); err != nil {
   312  		if !apierrors.IsNotFound(err) {
   313  			return fmt.Errorf("failed to delete ServiceAccount: %w", err)
   314  		}
   315  		log.Infof("ServiceAccount %q in namespace %q not found", serviceAccount, namespace)
   316  	} else {
   317  		log.Infof("ServiceAccount %q deleted", serviceAccount)
   318  	}
   319  	return nil
   320  }
   321  
   322  type ServiceAccountClaims struct {
   323  	Namespace          string `json:"kubernetes.io/serviceaccount/namespace"`
   324  	SecretName         string `json:"kubernetes.io/serviceaccount/secret.name"`
   325  	ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"`
   326  	ServiceAccountUID  string `json:"kubernetes.io/serviceaccount/service-account.uid"`
   327  	jwt.RegisteredClaims
   328  }
   329  
   330  // ParseServiceAccountToken parses a Kubernetes service account token
   331  func ParseServiceAccountToken(token string) (*ServiceAccountClaims, error) {
   332  	parser := jwt.NewParser(jwt.WithoutClaimsValidation())
   333  	var claims ServiceAccountClaims
   334  	_, _, err := parser.ParseUnverified(token, &claims)
   335  	if err != nil {
   336  		return nil, fmt.Errorf("failed to parse service account token: %w", err)
   337  	}
   338  	return &claims, nil
   339  }
   340  
   341  // GenerateNewClusterManagerSecret creates a new secret derived with same metadata as existing one
   342  // and waits until the secret is populated with a bearer token
   343  func GenerateNewClusterManagerSecret(clientset kubernetes.Interface, claims *ServiceAccountClaims) (*corev1.Secret, error) {
   344  	secretsClient := clientset.CoreV1().Secrets(claims.Namespace)
   345  	existingSecret, err := secretsClient.Get(context.Background(), claims.SecretName, metav1.GetOptions{})
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	var newSecret corev1.Secret
   350  	secretNameSplit := strings.Split(claims.SecretName, "-")
   351  	if len(secretNameSplit) > 0 {
   352  		secretNameSplit = secretNameSplit[:len(secretNameSplit)-1]
   353  	}
   354  	newSecret.Type = corev1.SecretTypeServiceAccountToken
   355  	newSecret.GenerateName = strings.Join(secretNameSplit, "-") + "-"
   356  	newSecret.Annotations = existingSecret.Annotations
   357  	// We will create an empty secret and let kubernetes populate the data
   358  	newSecret.Data = nil
   359  
   360  	created, err := secretsClient.Create(context.Background(), &newSecret, metav1.CreateOptions{})
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	err = wait.PollUntilContextTimeout(context.Background(), 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (bool, error) {
   366  		created, err = secretsClient.Get(ctx, created.Name, metav1.GetOptions{})
   367  		if err != nil {
   368  			return false, err
   369  		}
   370  		if len(created.Data) == 0 {
   371  			return false, nil
   372  		}
   373  		return true, nil
   374  	})
   375  	if err != nil {
   376  		return nil, fmt.Errorf("timed out waiting for secret to generate new token: %w", err)
   377  	}
   378  	return created, nil
   379  }
   380  
   381  // RotateServiceAccountSecrets rotates the entries in the service accounts secrets list
   382  func RotateServiceAccountSecrets(clientset kubernetes.Interface, claims *ServiceAccountClaims, newSecret *corev1.Secret) error {
   383  	// 1. update service account secrets list with new secret name while also removing the old name
   384  	saClient := clientset.CoreV1().ServiceAccounts(claims.Namespace)
   385  	sa, err := saClient.Get(context.Background(), claims.ServiceAccountName, metav1.GetOptions{})
   386  	if err != nil {
   387  		return err
   388  	}
   389  	var newSecretsList []corev1.ObjectReference
   390  	alreadyPresent := false
   391  	for _, objRef := range sa.Secrets {
   392  		if objRef.Name == claims.SecretName {
   393  			continue
   394  		}
   395  		if objRef.Name == newSecret.Name {
   396  			alreadyPresent = true
   397  		}
   398  		newSecretsList = append(newSecretsList, objRef)
   399  	}
   400  	if !alreadyPresent {
   401  		sa.Secrets = append(newSecretsList, corev1.ObjectReference{Name: newSecret.Name})
   402  	}
   403  	_, err = saClient.Update(context.Background(), sa, metav1.UpdateOptions{})
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	// 2. delete existing secret object
   409  	secretsClient := clientset.CoreV1().Secrets(claims.Namespace)
   410  	err = secretsClient.Delete(context.Background(), claims.SecretName, metav1.DeleteOptions{})
   411  	if !apierrors.IsNotFound(err) {
   412  		return err
   413  	}
   414  	return nil
   415  }