From b9ffe7c06a93fc9e70b98459713b092ada5ec0e1 Mon Sep 17 00:00:00 2001 From: Micah Hausler Date: Mon, 16 Feb 2026 10:27:20 -0600 Subject: [PATCH] kubeadm: use dedicated ClusterRole for apiserver kubelet client Signed-off-by: Micah Hausler --- .../app/cmd/phases/init/bootstraptoken.go | 5 ++ .../phases/upgrade/apply/bootstraptoken.go | 5 ++ cmd/kubeadm/app/constants/constants.go | 5 ++ .../bootstraptoken/node/tlsbootstrap.go | 22 +++++++ .../bootstraptoken/node/tlsbootstrap_test.go | 57 +++++++++++++++++++ cmd/kubeadm/app/phases/certs/certlist.go | 5 +- 6 files changed, 96 insertions(+), 3 deletions(-) diff --git a/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go b/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go index a0cf5e55eaa..2a103b9aeb3 100644 --- a/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go +++ b/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go @@ -108,6 +108,11 @@ func runBootstrapToken(c workflow.RunData) error { return err } + // Create RBAC rules that allow the API server kubelet client to access the kubelet API + if err := nodebootstraptokenphase.AllowAPIServerToAccessKubeletAPI(client); err != nil { + return errors.Wrap(err, "error allowing API server to access kubelet API") + } + // Create the cluster-info ConfigMap with the associated RBAC rules if err := clusterinfophase.CreateBootstrapConfigMapIfNotExists(client, kubeconfig); err != nil { return errors.Wrap(err, "error creating bootstrap ConfigMap") diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/apply/bootstraptoken.go b/cmd/kubeadm/app/cmd/phases/upgrade/apply/bootstraptoken.go index e0c40c09e4a..58aec1ddc2e 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/apply/bootstraptoken.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/apply/bootstraptoken.go @@ -79,6 +79,11 @@ func runBootstrapToken(c workflow.RunData) error { errs = append(errs, err) } + // Create/update RBAC rules that allow the API server kubelet client to access the kubelet API + if err := nodebootstraptoken.AllowAPIServerToAccessKubeletAPI(client); err != nil { + errs = append(errs, err) + } + // Create/update RBAC rules that makes the cluster-info ConfigMap reachable if err := clusterinfophase.CreateClusterInfoRBACRules(client); err != nil { errs = append(errs, err) diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index fee98ece17c..e17e14331a2 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -210,6 +210,11 @@ const ( // built-in ClusterRole. ClusterAdminsGroupAndClusterRoleBinding = "kubeadm:cluster-admins" + // KubeletAPIAdminClusterRoleBindingName is the name of the ClusterRoleBinding for the apiserver kubelet client + KubeletAPIAdminClusterRoleBindingName = "kubeadm:apiserver-kubelet-client" + // KubeletAPIAdminClusterRoleName is the name of the built-in ClusterRole for kubelet API access + KubeletAPIAdminClusterRoleName = "system:kubelet-api-admin" + // KubernetesAPICallTimeout specifies how long kubeadm should wait for API calls KubernetesAPICallTimeout = 1 * time.Minute // KubernetesAPICallRetryInterval defines how long kubeadm should wait before retrying a failed API operation diff --git a/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap.go b/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap.go index 72154c9ecde..8485e3779a7 100644 --- a/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap.go +++ b/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap.go @@ -130,3 +130,25 @@ func AutoApproveNodeCertificateRotation(client clientset.Interface) error { }, }) } + +// AllowAPIServerToAccessKubeletAPI creates RBAC rules that allow the API server kubelet client to access the kubelet API +func AllowAPIServerToAccessKubeletAPI(client clientset.Interface) error { + fmt.Println("[bootstrap-token] Configured RBAC rules to allow the API server kubelet client certificate to access the kubelet API") + + return apiclient.CreateOrUpdate(client.RbacV1().ClusterRoleBindings(), &rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.KubeletAPIAdminClusterRoleBindingName, + }, + RoleRef: rbac.RoleRef{ + APIGroup: rbac.GroupName, + Kind: "ClusterRole", + Name: constants.KubeletAPIAdminClusterRoleName, + }, + Subjects: []rbac.Subject{ + { + Kind: rbac.UserKind, + Name: constants.APIServerKubeletClientCertCommonName, + }, + }, + }) +} diff --git a/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap_test.go b/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap_test.go index 16114105692..7a75fdeb1c6 100644 --- a/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap_test.go +++ b/cmd/kubeadm/app/phases/bootstraptoken/node/tlsbootstrap_test.go @@ -278,6 +278,63 @@ func TestAllowBootstrapTokensToGetNodes(t *testing.T) { } } +func TestAllowAPIServerToAccessKubeletAPI(t *testing.T) { + tests := []struct { + name string + client clientset.Interface + }{ + { + name: "ClusterRoleBindings is empty", + client: clientsetfake.NewSimpleClientset(), + }, + { + name: "ClusterRoleBindings already exists", + client: newMockClusterRoleBinddingClientForTest(t, &rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.KubeletAPIAdminClusterRoleBindingName, + }, + RoleRef: rbac.RoleRef{ + APIGroup: rbac.GroupName, + Kind: "ClusterRole", + Name: constants.KubeletAPIAdminClusterRoleName, + }, + Subjects: []rbac.Subject{ + { + Kind: rbac.UserKind, + Name: constants.APIServerKubeletClientCertCommonName, + }, + }, + }), + }, + { + name: "Create new ClusterRoleBindings", + client: newMockClusterRoleBinddingClientForTest(t, &rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.KubeletAPIAdminClusterRoleBindingName, + }, + RoleRef: rbac.RoleRef{ + APIGroup: rbac.GroupName, + Kind: "ClusterRole", + Name: constants.KubeletAPIAdminClusterRoleName, + }, + Subjects: []rbac.Subject{ + { + Kind: rbac.GroupKind, + Name: constants.NodesGroup, + }, + }, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := AllowAPIServerToAccessKubeletAPI(tt.client); err != nil { + t.Errorf("AllowAPIServerToAccessKubeletAPI() return error = %v", err) + } + }) + } +} + func newMockClusterRoleBinddingClientForTest(t *testing.T, clusterRoleBinding *rbac.ClusterRoleBinding) *clientsetfake.Clientset { client := clientsetfake.NewSimpleClientset() _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding, metav1.CreateOptions{}) diff --git a/cmd/kubeadm/app/phases/certs/certlist.go b/cmd/kubeadm/app/phases/certs/certlist.go index 36caff1d920..11f773e3597 100644 --- a/cmd/kubeadm/app/phases/certs/certlist.go +++ b/cmd/kubeadm/app/phases/certs/certlist.go @@ -317,9 +317,8 @@ func KubeadmCertKubeletClient() *KubeadmCert { CAName: "ca", config: pkiutil.CertConfig{ Config: certutil.Config{ - CommonName: kubeadmconstants.APIServerKubeletClientCertCommonName, - Organization: []string{kubeadmconstants.ClusterAdminsGroupAndClusterRoleBinding}, - Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + CommonName: kubeadmconstants.APIServerKubeletClientCertCommonName, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }, }, }