RBAC/Privilege Escalation

MEDIUM
kubernetes/kubernetes
Commit: 165a309d8ba3
Affected: <= v1.36.0-beta.0 (prior to this change); targeted at 1.36.x and earlier releases
2026-05-26 18:10 UTC

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}, }, }, }
← Back to Alerts View on GitHub →