RBAC/Privilege Escalation
Description
This commit appears to be a real security hardening fix rather than a mere dependency bump. It introduces a dedicated RBAC path for the API server to access the kubelet API: adding a specific ClusterRole (system:kubelet-api-admin) and a ClusterRoleBinding (kubeadm:apiserver-kubelet-client) bound to the API server's kubelet client certificate. This replaces a broader/unrestricted RBAC setup, tightening least-privilege access for the apiserver-to-kubelet communication. The patch also includes tests and constants to ensure the binding exists and is correctly named. Additionally, there is a small code cleanup that removes an Organization field from the kubelet client certificate config, aligning certificate attributes with the new RBAC controls and avoiding potential over-granting via certificate attributes.
Commit Details
Author: Kubernetes Prow Robot
Date: 2026-05-11 11:45 UTC
Message:
Merge pull request #138957 from neolit123/1.37-adjust-kubeapiserver-kubelet-permissions
kubeadm: use dedicated ClusterRole for apiserver kubelet client
Triage Assessment
Vulnerability Type: RBAC/Privilege escalation
Confidence: MEDIUM
Reasoning:
The commit introduces a dedicated RBAC rule (ClusterRoleBinding) and a dedicated ClusterRole for the API server to access the kubelet API, replacing a broader or unspecified permission setup. This tightens access control and reduces privilege exposure, addressing RBAC-related security concerns. The changes are part of a security-hardening effort (least privilege for apiserver-kubelet client).
Verification Assessment
Vulnerability Type: RBAC/Privilege Escalation
Confidence: MEDIUM
Affected Versions: <= v1.36.0-beta.0 (prior to this change); targeted at 1.36.x and earlier releases
Code Diff
diff --git a/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go b/cmd/kubeadm/app/cmd/phases/init/bootstraptoken.go
index a0cf5e55eaa38..2a103b9aeb30d 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 e0c40c09e4adc..58aec1ddc2e43 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 87c54f5619657..4a0aed99eca38 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 72154c9ecde91..8485e3779a7b5 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 1611410569293..7a75fdeb1c6b5 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 36caff1d9203e..11f773e359738 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},
},
},
}