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 }