Repository: gardener/gardener
Pull request #11749: [GEP-28] Bootstrap `kubelet` and make `gardenadm init` idempotent
Author: login: rfranzke
Labels: name: kind/enhancement
description: Enhancement, improvement, extension
name: cla: yes
description: Indicates the PR's author has signed the cla-assistant.io CLA.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: size/XXL
description: Denotes a PR that changes 1000+ lines, ignoring generated files.
name: area/ipcei
description: IPCEI (Important Project of Common European Interest)
Created: 2025-03-26T10:54:32Z | Merged: 2025-03-28T11:23:17Z

**How to categorize this PR?**
<!--
Please select area, kind, and priority for this pull request. This helps the community categorizing it.
Replace below TODOs or exchange the existing identifiers with those that fit best in your opinion.
If multiple identifiers make sense you can also state the commands multiple times, e.g.
  /area control-plane
  /area auto-scaling
  ...

"/area" identifiers:     audit-logging|auto-scaling|backup|compliance|control-plane-migration|control-plane|cost|delivery|dev-productivity|disaster-recovery|documentation|high-availability|logging|metering|monitoring|networking|open-source|ops-productivity|os|performance|quality|robustness|scalability|security|storage|testing|usability|user-management
"/kind" identifiers:     api-change|bug|cleanup|discussion|enhancement|epic|flake|impediment|poc|post-mortem|question|regression|task|technical-debt|test

For Gardener Enhancement Proposals (GEPs), please check the following [documentation](https://github.com/gardener/gardener/tree/master/docs/proposals/README.md) before submitting this pull request.
-->
/area ipcei
/kind enhancement

**What this PR does / why we need it**:
This PR is the next increment for `gardenadm init`. It bootstraps the `kubelet` and makes the `gardenadm init` invocations idempotent (under the assumption that the initial control plane deployment worked - otherwise, you have to start from scratch).

**Which issue(s) this PR fixes**:
Part of https://github.com/gardener/gardener/issues/2906

**Special notes for your reviewer**:
~~Still in draft since it's based on #11701 which must be merged first.~~

/cc @ScheererJ 

**Release note**:
<!--
Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|noteworthy|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```other operator
NONE
```



Diff:
diff --git a/pkg/client/kubernetes/pods.go b/pkg/client/kubernetes/pods.go
index 571fd88419b..244bac1e74d 100644
--- a/pkg/client/kubernetes/pods.go
+++ b/pkg/client/kubernetes/pods.go
@@ -130,7 +130,6 @@ func SetupPortForwarder(ctx context.Context, config *rest.Config, namespace, nam
 	var (
 		readyChan = make(chan struct{}, 1)
 		out       = io.Discard
-		localPort int
 	)
 
 	client, err := corev1client.NewForConfig(config)
@@ -147,13 +146,13 @@ func SetupPortForwarder(ctx context.Context, config *rest.Config, namespace, nam
 	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", u)
 
 	if local == 0 {
-		localPort, err = utils.FindFreePort()
+		local, err = utils.FindFreePort()
 		if err != nil {
 			return nil, err
 		}
 	}
 
-	fw, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", localPort, remote)}, ctx.Done(), readyChan, out, out)
+	fw, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", local, remote)}, ctx.Done(), readyChan, out, out)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/component/etcd/bootstrap/etcd.go b/pkg/component/etcd/bootstrap/etcd.go
index f9166bd636c..d09f25331a1 100644
--- a/pkg/component/etcd/bootstrap/etcd.go
+++ b/pkg/component/etcd/bootstrap/etcd.go
@@ -135,6 +135,7 @@ func (e *etcdDeployer) Deploy(ctx context.Context) error {
 				LivenessProbe: &corev1.Probe{
 					ProbeHandler: corev1.ProbeHandler{
 						HTTPGet: &corev1.HTTPGetAction{
+							Host:   "localhost",
 							Path:   "/livez",
 							Scheme: corev1.URISchemeHTTP,
 							Port:   intstr.FromInt32(e.values.PortMetrics),
@@ -156,6 +157,7 @@ func (e *etcdDeployer) Deploy(ctx context.Context) error {
 				StartupProbe: &corev1.Probe{
 					ProbeHandler: corev1.ProbeHandler{
 						HTTPGet: &corev1.HTTPGetAction{
+							Host:   "localhost",
 							Path:   "/health",
 							Scheme: corev1.URISchemeHTTP,
 							Port:   intstr.FromInt32(e.values.PortMetrics),
diff --git a/pkg/component/extensions/operatingsystemconfig/original/components/nodeagent/rbac.go b/pkg/component/extensions/operatingsystemconfig/original/components/nodeagent/rbac.go
index f1efa7aeca6..96c5b58773c 100644
--- a/pkg/component/extensions/operatingsystemconfig/original/components/nodeagent/rbac.go
+++ b/pkg/component/extensions/operatingsystemconfig/original/components/nodeagent/rbac.go
@@ -10,6 +10,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apiserver/pkg/authentication/user"
 	bootstraptokenapi "k8s.io/cluster-bootstrap/token/api"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
 	"github.com/gardener/gardener/pkg/client/kubernetes"
@@ -104,8 +105,50 @@ func RBACResourcesData(secretNames []string) (map[string][]byte, error) {
 				},
 			},
 		}
+	)
 
-		clusterRoleBindingNodeBootstrapper = &rbacv1.ClusterRoleBinding{
+	if features.DefaultFeatureGate.Enabled(features.NodeAgentAuthorizer) {
+		// When the feature gate is enabled, gardener-node-agent service account needs permissions to create an CSR on
+		// existing nodes for migration.
+		clusterRole.Rules = append(clusterRole.Rules, rbacv1.PolicyRule{
+			APIGroups: []string{"certificates.k8s.io"},
+			Resources: []string{"certificatesigningrequests"},
+			Verbs:     []string{"create", "get"},
+		})
+	} else {
+		// For the case that NodeAgentAuthorizer feature gate is disabled again node-agents group has to be added to
+		// (cluster) role binding so that node-agents which are already migrated do not lose access.
+		clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{
+			APIGroup: rbacv1.SchemeGroupVersion.Group,
+			Kind:     rbacv1.GroupKind,
+			Name:     v1beta1constants.NodeAgentsGroup,
+		})
+		roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{
+			APIGroup: rbacv1.SchemeGroupVersion.Group,
+			Kind:     rbacv1.GroupKind,
+			Name:     v1beta1constants.NodeAgentsGroup,
+		})
+	}
+
+	return managedresources.
+		NewRegistry(kubernetes.ShootScheme, kubernetes.ShootCodec, kubernetes.ShootSerializer).
+		AddAllAndSerialize(append([]client.Object{
+			clusterRole,
+			clusterRoleBinding,
+			role,
+			roleBinding,
+		}, GetCertificateSigningRequestClusterRoleBindings()...)...)
+}
+
+// GetCertificateSigningRequestClusterRoleBindings returns the ClusterRoleBindings that allows bootstrap tokens to
+// create CertificateSigningRequests.
+func GetCertificateSigningRequestClusterRoleBindings() []client.Object {
+	return []client.Object{
+		&rbacv1.ClusterRoleBinding{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: rbacv1.SchemeGroupVersion.String(),
+				Kind:       "ClusterRoleBinding",
+			},
 			ObjectMeta: metav1.ObjectMeta{
 				Name: "system:node-bootstrapper",
 			},
@@ -119,9 +162,12 @@ func RBACResourcesData(secretNames []string) (map[string][]byte, error) {
 				Kind:     rbacv1.GroupKind,
 				Name:     bootstraptokenapi.BootstrapDefaultGroup,
 			}},
-		}
-
-		clusterRoleBindingNodeClient = &rbacv1.ClusterRoleBinding{
+		},
+		&rbacv1.ClusterRoleBinding{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: rbacv1.SchemeGroupVersion.String(),
+				Kind:       "ClusterRoleBinding",
+			},
 			ObjectMeta: metav1.ObjectMeta{
 				Name: "system:certificates.k8s.io:certificatesigningrequests:nodeclient",
 			},
@@ -135,9 +181,12 @@ func RBACResourcesData(secretNames []string) (map[string][]byte, error) {
 				Kind:     rbacv1.GroupKind,
 				Name:     bootstraptokenapi.BootstrapDefaultGroup,
 			}},
-		}
-
-		clusterRoleBindingSelfNodeClient = &rbacv1.ClusterRoleBinding{
+		},
+		&rbacv1.ClusterRoleBinding{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: rbacv1.SchemeGroupVersion.String(),
+				Kind:       "ClusterRoleBinding",
+			},
 			ObjectMeta: metav1.ObjectMeta{
 				Name: "system:certificates.k8s.io:certificatesigningrequests:selfnodeclient",
 			},
@@ -151,41 +200,6 @@ func RBACResourcesData(secretNames []string) (map[string][]byte, error) {
 				Kind:     rbacv1.GroupKind,
 				Name:     user.NodesGroup,
 			}},
-		}
-	)
-
-	if features.DefaultFeatureGate.Enabled(features.NodeAgentAuthorizer) {
-		// When the feature gate is enabled, gardener-node-agent service account needs permissions to create an CSR on
-		// existing nodes for migration.
-		clusterRole.Rules = append(clusterRole.Rules, rbacv1.PolicyRule{
-			APIGroups: []string{"certificates.k8s.io"},
-			Resources: []string{"certificatesigningrequests"},
-			Verbs:     []string{"create", "get"},
-		})
-	} else {
-		// For the case that NodeAgentAuthorizer feature gate is disabled again node-agents group have to be added to
-		// (cluster) role binding that node-agents which are already migrated do not lose access.
-		clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{
-			APIGroup: rbacv1.SchemeGroupVersion.Group,
-			Kind:     rbacv1.GroupKind,
-			Name:     v1beta1constants.NodeAgentsGroup,
-		})
-		roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{
-			APIGroup: rbacv1.SchemeGroupVersion.Group,
-			Kind:     rbacv1.GroupKind,
-			Name:     v1beta1constants.NodeAgentsGroup,
-		})
+		},
 	}
-
-	return managedresources.
-		NewRegistry(kubernetes.ShootScheme, kubernetes.ShootCodec, kubernetes.ShootSerializer).
-		AddAllAndSerialize(
-			clusterRole,
-			clusterRoleBinding,
-			role,
-			roleBinding,
-			clusterRoleBindingNodeBootstrapper,
-			clusterRoleBindingNodeClient,
-			clusterRoleBindingSelfNodeClient,
-		)
 }
diff --git a/pkg/component/kubernetes/apiserver/secrets.go b/pkg/component/kubernetes/apiserver/secrets.go
index b20aa82ed64..6c2a6edd85a 100644
--- a/pkg/component/kubernetes/apiserver/secrets.go
+++ b/pkg/component/kubernetes/apiserver/secrets.go
@@ -148,7 +148,7 @@ func (k *kubeAPIServer) reconcileSecretETCDEncryptionConfiguration(ctx context.C
 
 func (k *kubeAPIServer) reconcileSecretServer(ctx context.Context) (*corev1.Secret, error) {
 	var (
-		ipAddresses    = append([]net.IP{}, k.values.ServerCertificate.ExtraIPAddresses...)
+		ipAddresses    []net.IP
 		deploymentName = k.values.NamePrefix + v1beta1constants.DeploymentNameKubeAPIServer
 		dnsNames       = kubernetesutils.DNSNamesForService(deploymentName, k.namespace)
 	)
diff --git a/pkg/component/nodemanagement/machinecontrollermanager/machine_controller_manager.go b/pkg/component/nodemanagement/machinecontrollermanager/machine_controller_manager.go
index 1f13f080f14..8c1a2c10849 100644
--- a/pkg/component/nodemanagement/machinecontrollermanager/machine_controller_manager.go
+++ b/pkg/component/nodemanagement/machinecontrollermanager/machine_controller_manager.go
@@ -56,8 +56,10 @@ const (
 // Interface contains functions for a machine-controller-manager deployer.
 type Interface interface {
 	component.DeployWaiter
-	// SetNamespaceUID sets the UID of the namespace into which the cluster-autoscaler shall be deployed.
+	// SetNamespaceUID sets the UID of the namespace into which the machine-controller-manager shall be deployed.
 	SetNamespaceUID(types.UID)
+	// SetReplicas sets the replicas.
+	SetReplicas(int32)
 }
 
 // New creates a new instance of DeployWaiter for the machine-controller-manager.
@@ -485,6 +487,7 @@ func (m *machineControllerManager) WaitCleanup(ctx context.Context) error {
 }
 
 func (m *machineControllerManager) SetNamespaceUID(uid types.UID) { m.values.namespaceUID = uid }
+func (m *machineControllerManager) SetReplicas(replicas int32)    { m.values.Replicas = replicas }
 
 func (m *machineControllerManager) computeShootResourcesData(serviceAccountName string) (map[string][]byte, error) {
 	var (
diff --git a/pkg/component/nodemanagement/machinecontrollermanager/mock/mocks.go b/pkg/component/nodemanagement/machinecontrollermanager/mock/mocks.go
index 5395798ad4c..a356f049f66 100644
--- a/pkg/component/nodemanagement/machinecontrollermanager/mock/mocks.go
+++ b/pkg/component/nodemanagement/machinecontrollermanager/mock/mocks.go
@@ -81,6 +81,18 @@ func (mr *MockInterfaceMockRecorder) SetNamespaceUID(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamespaceUID", reflect.TypeOf((*MockInterface)(nil).SetNamespaceUID), arg0)
 }
 
+// SetReplicas mocks base method.
+func (m *MockInterface) SetReplicas(arg0 int32) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetReplicas", arg0)
+}
+
+// SetReplicas indicates an expected call of SetReplicas.
+func (mr *MockInterfaceMockRecorder) SetReplicas(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReplicas", reflect.TypeOf((*MockInterface)(nil).SetReplicas), arg0)
+}
+
 // Wait mocks base method.
 func (m *MockInterface) Wait(ctx context.Context) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/extensions/cluster.go b/pkg/extensions/cluster.go
index 2a0adcdce25..8d88340fabb 100644
--- a/pkg/extensions/cluster.go
+++ b/pkg/extensions/cluster.go
@@ -29,10 +29,6 @@ func SyncClusterResourceToSeed(
 	cloudProfile *gardencorev1beta1.CloudProfile,
 	seed *gardencorev1beta1.Seed,
 ) error {
-	if shoot.Spec.SeedName == nil {
-		return nil
-	}
-
 	var (
 		cluster = &extensionsv1alpha1.Cluster{
 			ObjectMeta: metav1.ObjectMeta{
diff --git a/pkg/extensions/cluster_test.go b/pkg/extensions/cluster_test.go
index 821dc482674..103d909c09b 100644
--- a/pkg/extensions/cluster_test.go
+++ b/pkg/extensions/cluster_test.go
@@ -12,7 +12,6 @@ import (
 	. "github.com/onsi/gomega"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
@@ -103,14 +102,7 @@ var _ = Describe("Cluster", func() {
 			}
 		})
 
-		It("should not sync cloudprofile, shoot and seed to cluster if seed is not assigned to shoot", func() {
-			Expect(SyncClusterResourceToSeed(ctx, fakeSeedClient, cluster.Name, expectedShoot, expectedCloudProfile, expectedSeed)).To(Succeed())
-			Expect(fakeSeedClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).To(BeNotFoundError())
-		})
-
 		It("should sync cloudprofile, shoot and seed to cluster", func() {
-			expectedShoot.Spec.SeedName = ptr.To(expectedSeed.Name)
-
 			Expect(SyncClusterResourceToSeed(ctx, fakeSeedClient, cluster.Name, expectedShoot, expectedCloudProfile, expectedSeed)).To(Succeed())
 			Expect(fakeSeedClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).To(Succeed())
 
diff --git a/pkg/gardenadm/botanist/botanist.go b/pkg/gardenadm/botanist/botanist.go
index bb3853cb2d2..931daa1c797 100644
--- a/pkg/gardenadm/botanist/botanist.go
+++ b/pkg/gardenadm/botanist/botanist.go
@@ -46,6 +46,7 @@ type AutonomousBotanist struct {
 func NewAutonomousBotanist(
 	ctx context.Context,
 	log logr.Logger,
+	seedClientSet kubernetes.Interface,
 	project *gardencorev1beta1.Project,
 	cloudProfile *gardencorev1beta1.CloudProfile,
 	shoot *gardencorev1beta1.Shoot,
@@ -73,15 +74,18 @@ func NewAutonomousBotanist(
 		return nil, fmt.Errorf("failed creating seed object: %w", err)
 	}
 
-	log.Info("Initializing autonomous botanist",
-		"cloudProfile", cloudProfile,
-		"project", project,
-		"shoot", shoot)
+	keysAndValues := []any{"cloudProfile", cloudProfile, "project", project, "shoot", shoot}
+	if seedClientSet == nil {
+		seedClientSet = newFakeSeedClientSet(seedObj.KubernetesVersion.String())
+		log.Info("Initializing autonomous botanist with fake client set", keysAndValues...) //nolint:logcheck
+	} else {
+		log.Info("Initializing autonomous botanist with control plane client set", keysAndValues...) //nolint:logcheck
+	}
 
 	b, err := botanistpkg.New(ctx, &operation.Operation{
 		Logger:        log,
 		GardenClient:  newFakeGardenClient(),
-		SeedClientSet: newFakeSeedClientSet(seedObj.KubernetesVersion.String()),
+		SeedClientSet: seedClientSet,
 		Garden:        gardenObj,
 		Seed:          seedObj,
 		Shoot:         shootObj,
diff --git a/pkg/gardenadm/botanist/controlplane.go b/pkg/gardenadm/botanist/controlplane.go
index 5347a5aad95..9ae6d806c4c 100644
--- a/pkg/gardenadm/botanist/controlplane.go
+++ b/pkg/gardenadm/botanist/controlplane.go
@@ -7,17 +7,21 @@ package botanist
 import (
 	"context"
 	"fmt"
+	"net/http"
 	"path/filepath"
+	"time"
 
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	componentbaseconfigv1alpha1 "k8s.io/component-base/config/v1alpha1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/gardener/gardener/imagevector"
 	v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
 	extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
+	"github.com/gardener/gardener/pkg/client/kubernetes"
 	bootstrapetcd "github.com/gardener/gardener/pkg/component/etcd/bootstrap"
 	etcdconstants "github.com/gardener/gardener/pkg/component/etcd/etcd/constants"
 	kubeapiserver "github.com/gardener/gardener/pkg/component/kubernetes/apiserver"
@@ -130,3 +134,37 @@ func (b *AutonomousBotanist) populateStaticAdminTokenToAccessTokenSecret(ctx con
 
 	return b.SeedClientSet.Client().Update(ctx, secret)
 }
+
+// CreateClientSet creates a client set for the control plane.
+func (b *AutonomousBotanist) CreateClientSet(ctx context.Context) (kubernetes.Interface, error) {
+	clientSet, err := kubernetes.NewClientFromFile("", PathKubeconfig,
+		kubernetes.WithClientOptions(client.Options{Scheme: kubernetes.SeedScheme}),
+		kubernetes.WithClientConnectionOptions(componentbaseconfigv1alpha1.ClientConnectionConfiguration{QPS: 100, Burst: 130}),
+	)
+	if err != nil {
+		b.Logger.Info("Waiting for kube-apiserver to start", "error", err.Error())
+		return nil, fmt.Errorf("failed creating client set: %w", err)
+	}
+
+	clientSet.Start(ctx)
+
+	waitContext, cancel := context.WithTimeout(ctx, 10*time.Second)
+	defer cancel()
+	if !clientSet.WaitForCacheSync(waitContext) {
+		return nil, fmt.Errorf("timed out waiting for caches")
+	}
+
+	result := clientSet.RESTClient().Get().AbsPath("/readyz").Do(ctx)
+	if result.Error() != nil {
+		return nil, fmt.Errorf("failed to GET /readyz endpoint of kube-apiserver: %w", result.Error())
+	}
+
+	var statusCode int
+	result.StatusCode(&statusCode)
+	if statusCode != http.StatusOK {
+		b.Logger.Info("The kube-apiserver does not report readiness yet", "statusCode", statusCode)
+		return nil, fmt.Errorf("kube-apiserver does not report readiness yet")
+	}
+
+	return clientSet, nil
+}
diff --git a/pkg/gardenadm/botanist/customresourcedefinitions.go b/pkg/gardenadm/botanist/customresourcedefinitions.go
new file mode 100644
index 00000000000..45bd275363f
--- /dev/null
+++ b/pkg/gardenadm/botanist/customresourcedefinitions.go
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+	"fmt"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+
+	"github.com/gardener/gardener/pkg/component/autoscaling/vpa"
+	extensioncrds "github.com/gardener/gardener/pkg/component/extensions/crds"
+	"github.com/gardener/gardener/pkg/component/observability/logging/fluentoperator"
+	"github.com/gardener/gardener/pkg/component/observability/monitoring/prometheusoperator"
+	"github.com/gardener/gardener/pkg/utils/kubernetes/health"
+)
+
+// ReconcileCustomResourceDefinitions reconciles the custom resource definitions.
+func (b *AutonomousBotanist) ReconcileCustomResourceDefinitions(ctx context.Context) error {
+	vpaCRDDeployer := vpa.NewCRD(b.SeedClientSet.Applier(), nil)
+
+	prometheusCRDDeployer, err := prometheusoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	fluentCRDDeployer, err := fluentoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating fluent CRD deployer: %w", err)
+	}
+
+	extensionCRDDeployer, err := extensioncrds.NewCRD(b.SeedClientSet.Client(), b.SeedClientSet.Applier(), true, true)
+	if err != nil {
+		return fmt.Errorf("failed creating extension CRD deployer: %w", err)
+	}
+
+	for description, deploy := range map[string]func(context.Context) error{
+		"VPA":        vpaCRDDeployer.Deploy,
+		"Prometheus": prometheusCRDDeployer.Deploy,
+		"Fluent":     fluentCRDDeployer.Deploy,
+		"Extension":  extensionCRDDeployer.Deploy,
+	} {
+		if err := deploy(ctx); err != nil {
+			return fmt.Errorf("failed to deploy CustomResourceDefinition related to %s: %w", description, err)
+		}
+	}
+
+	return nil
+}
+
+// EnsureCustomResourceDefinitionsReady ensures that the custom resource definitions are ready.
+func (b *AutonomousBotanist) EnsureCustomResourceDefinitionsReady(ctx context.Context) error {
+	crdList := &apiextensionsv1.CustomResourceDefinitionList{}
+	if err := b.SeedClientSet.Client().List(ctx, crdList); err != nil {
+		return fmt.Errorf("failed to list CustomResourceDefinitions: %w", err)
+	}
+
+	for _, crd := range crdList.Items {
+		if err := health.CheckCustomResourceDefinition(&crd); err != nil {
+			return fmt.Errorf("CustomResourceDefinition %s is not ready yet: %w", crd.Name, err)
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/gardenadm/botanist/customresourcedefinitions_test.go b/pkg/gardenadm/botanist/customresourcedefinitions_test.go
new file mode 100644
index 00000000000..3e89cf80077
--- /dev/null
+++ b/pkg/gardenadm/botanist/customresourcedefinitions_test.go
@@ -0,0 +1,132 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist_test
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/gstruct"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/gardener/gardener/pkg/client/kubernetes"
+	fakekubernetes "github.com/gardener/gardener/pkg/client/kubernetes/fake"
+	. "github.com/gardener/gardener/pkg/gardenadm/botanist"
+	"github.com/gardener/gardener/pkg/gardenlet/operation"
+	botanistpkg "github.com/gardener/gardener/pkg/gardenlet/operation/botanist"
+)
+
+var _ = Describe("CustomResourceDefinitions", func() {
+	var (
+		ctx context.Context
+
+		fakeClient client.Client
+
+		b *AutonomousBotanist
+	)
+
+	BeforeEach(func() {
+		ctx = context.Background()
+
+		fakeClient = fakeclient.NewClientBuilder().WithScheme(kubernetes.SeedScheme).Build()
+
+		mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{apiextensionsv1.SchemeGroupVersion})
+		mapper.Add(apiextensionsv1.SchemeGroupVersion.WithKind("CustomResourceDefinition"), meta.RESTScopeRoot)
+		applier := kubernetes.NewApplier(fakeClient, mapper)
+
+		b = &AutonomousBotanist{
+			Botanist: &botanistpkg.Botanist{
+				Operation: &operation.Operation{
+					SeedClientSet: fakekubernetes.
+						NewClientSetBuilder().
+						WithClient(fakeClient).
+						WithApplier(applier).
+						WithRESTConfig(&rest.Config{}).
+						Build(),
+				},
+			},
+		}
+	})
+
+	Describe("#ReconcileCustomResourceDefinitions", func() {
+		It("should reconcile all expected CRDs", func() {
+			crdList := &apiextensionsv1.CustomResourceDefinitionList{}
+			Expect(fakeClient.List(ctx, crdList)).To(Succeed())
+
+			Expect(b.ReconcileCustomResourceDefinitions(ctx)).To(Succeed())
+
+			Expect(fakeClient.List(ctx, crdList)).To(Succeed())
+			Expect(crdList.Items).To(ConsistOf(
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("alertmanagerconfigs.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("alertmanagers.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("backupbuckets.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("backupentries.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("bastions.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clusterfilters.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clusterfluentbitconfigs.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clusterinputs.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clustermultilineparsers.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clusteroutputs.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clusterparsers.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("clusters.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("collectors.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("containerruntimes.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("controlplanes.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("dnsrecords.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("extensions.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("filters.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("fluentbitconfigs.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("fluentbits.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("infrastructures.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("multilineparsers.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("networks.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("operatingsystemconfigs.extensions.gardener.cloud")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("outputs.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("parsers.fluentbit.fluent.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("podmonitors.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("probes.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("prometheusagents.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("prometheuses.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("prometheusrules.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("scrapeconfigs.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("servicemonitors.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("thanosrulers.monitoring.coreos.com")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("verticalpodautoscalercheckpoints.autoscaling.k8s.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("verticalpodautoscalers.autoscaling.k8s.io")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("workers.extensions.gardener.cloud")})}),
+			))
+		})
+	})
+
+	Describe("#EnsureCustomResourceDefinitionsReady", func() {
+		var crd *apiextensionsv1.CustomResourceDefinition
+
+		BeforeEach(func() {
+			crd = &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "some.crd"}}
+			Expect(fakeClient.Create(ctx, crd)).To(Succeed())
+		})
+
+		It("should fail because some CRD is not ready", func() {
+			Expect(b.EnsureCustomResourceDefinitionsReady(ctx)).To(MatchError(ContainSubstring(crd.Name + " is not ready yet")))
+		})
+
+		It("should succeed because all CRDs are ready", func() {
+			crd.Status.Conditions = []apiextensionsv1.CustomResourceDefinitionCondition{
+				{Type: apiextensionsv1.NamesAccepted, Status: apiextensionsv1.ConditionTrue},
+				{Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue},
+			}
+			Expect(fakeClient.Status().Update(ctx, crd)).To(Succeed())
+
+			Expect(b.EnsureCustomResourceDefinitionsReady(ctx)).To(Succeed())
+		})
+	})
+})
diff --git a/pkg/gardenadm/botanist/kubelet.go b/pkg/gardenadm/botanist/kubelet.go
index b6b9504ea84..36d7bd04287 100644
--- a/pkg/gardenadm/botanist/kubelet.go
+++ b/pkg/gardenadm/botanist/kubelet.go
@@ -8,15 +8,23 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"path/filepath"
+	"slices"
 	"time"
 
 	"github.com/spf13/afero"
+	certificatesv1 "k8s.io/api/certificates/v1"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	bootstraptokenapi "k8s.io/cluster-bootstrap/token/api"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	"github.com/gardener/gardener/cmd/gardener-node-agent/app/bootstrappers"
 	v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
 	"github.com/gardener/gardener/pkg/component/extensions/operatingsystemconfig/original/components/kubelet"
+	nodeagentcomponent "github.com/gardener/gardener/pkg/component/extensions/operatingsystemconfig/original/components/nodeagent"
+	"github.com/gardener/gardener/pkg/nodeagent"
 	nodeagentconfigv1alpha1 "github.com/gardener/gardener/pkg/nodeagent/apis/config/v1alpha1"
 	"github.com/gardener/gardener/pkg/utils/kubernetes/bootstraptoken"
 	secretsutils "github.com/gardener/gardener/pkg/utils/secrets"
@@ -24,8 +32,8 @@ import (
 
 const kubeletTokenFilePermission = 0o600
 
-// CreateBootstrapToken creates a bootstrap token for the kubelet and writes it to the file system.
-func (b *AutonomousBotanist) CreateBootstrapToken(ctx context.Context) error {
+// WriteBootstrapToken creates a bootstrap token for the kubelet and writes it to the file system.
+func (b *AutonomousBotanist) WriteBootstrapToken(ctx context.Context) error {
 	bootstrapTokenSecret, err := bootstraptoken.ComputeBootstrapToken(
 		ctx,
 		b.SeedClientSet.Client(),
@@ -80,3 +88,75 @@ func (b *AutonomousBotanist) WriteKubeletBootstrapKubeconfig(ctx context.Context
 
 	return kubeletBootstrapKubeconfigCreator.Start(ctx)
 }
+
+// BootstrapKubelet bootstraps the kubelet.
+func (b *AutonomousBotanist) BootstrapKubelet(ctx context.Context) error {
+	node, err := nodeagent.FetchNodeByHostName(ctx, b.SeedClientSet.Client(), b.HostName)
+	if err != nil {
+		return fmt.Errorf("failed fetching node object via hostname: %w", err)
+	}
+	if node != nil {
+		b.Logger.Info("Found node for hostname, skipping kubelet bootstrap", "hostName", b.HostName, "nodeName", node.Name)
+		return nil
+	}
+
+	b.Logger.Info("No node found for hostname, bootstrapping kubelet", "hostName", b.HostName)
+
+	for _, obj := range nodeagentcomponent.GetCertificateSigningRequestClusterRoleBindings() {
+		desired := obj.DeepCopyObject()
+		if _, err := controllerutil.CreateOrUpdate(ctx, b.SeedClientSet.Client(), obj, func() error {
+			obj.(*rbacv1.ClusterRoleBinding).Subjects = desired.(*rbacv1.ClusterRoleBinding).Subjects
+			obj.(*rbacv1.ClusterRoleBinding).RoleRef = desired.(*rbacv1.ClusterRoleBinding).RoleRef
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed reconciling ClusterRoleBinding %q: %w", obj.GetName(), err)
+		}
+	}
+
+	if err := b.WriteBootstrapToken(ctx); err != nil {
+		return fmt.Errorf("failed writing bootstrap token: %w", err)
+	}
+
+	if err := b.WriteKubeletBootstrapKubeconfig(ctx); err != nil {
+		return fmt.Errorf("failed writing kubelet bootstrap kubeconfig: %w", err)
+	}
+
+	return b.DBus.Restart(ctx, nil, nil, kubelet.UnitName)
+}
+
+// ApproveKubeletServerCertificateSigningRequest approves the kubelet server certificate signing request.
+func (b *AutonomousBotanist) ApproveKubeletServerCertificateSigningRequest(ctx context.Context) error {
+	serverCertificateExists, err := b.FS.Exists(filepath.Join(kubelet.PathKubeletDirectory, "pki", "kubelet-server-current.pem"))
+	if err != nil {
+		return fmt.Errorf("failed checking if kubelet server certificate exists: %w", err)
+	}
+	if serverCertificateExists {
+		return nil
+	}
+
+	csrList := &certificatesv1.CertificateSigningRequestList{}
+	if err := b.SeedClientSet.Client().List(ctx, csrList); err != nil {
+		return fmt.Errorf("failed listing certificate signing requests: %w", err)
+	}
+
+	for _, csr := range csrList.Items {
+		if csr.Spec.Username == "system:node:"+b.HostName {
+			if !slices.ContainsFunc(csr.Status.Conditions, func(condition certificatesv1.CertificateSigningRequestCondition) bool {
+				return condition.Type == certificatesv1.CertificateApproved && condition.Status == corev1.ConditionTrue
+			}) {
+				b.Logger.Info("Approving kubelet server certificate signing request", "csrName", csr.Name, "hostName", b.HostName)
+				csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "RequestApproved",
+					Message: "Approving kubelet server certificate signing request via gardenadm",
+				})
+				return b.SeedClientSet.Client().SubResource("approval").Update(ctx, &csr)
+			}
+
+			return nil
+		}
+	}
+
+	return fmt.Errorf("no certificate signing request found for node %q", b.HostName)
+}
diff --git a/pkg/gardenadm/botanist/kubelet_test.go b/pkg/gardenadm/botanist/kubelet_test.go
index 5bafb709bf4..e1dc9fea1c0 100644
--- a/pkg/gardenadm/botanist/kubelet_test.go
+++ b/pkg/gardenadm/botanist/kubelet_test.go
@@ -11,7 +11,9 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/spf13/afero"
+	certificatesv1 "k8s.io/api/certificates/v1"
 	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -24,17 +26,21 @@ import (
 	"github.com/gardener/gardener/pkg/gardenlet/operation"
 	botanistpkg "github.com/gardener/gardener/pkg/gardenlet/operation/botanist"
 	"github.com/gardener/gardener/pkg/gardenlet/operation/shoot"
+	fakedbus "github.com/gardener/gardener/pkg/nodeagent/dbus/fake"
 	secretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager"
 	fakesecretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager/fake"
+	. "github.com/gardener/gardener/pkg/utils/test/matchers"
 )
 
 var _ = Describe("Kubelet", func() {
 	var (
 		ctx       context.Context
 		namespace = "kube-system"
+		hostName  = "test"
 
 		fakeSeedClient    client.Client
 		fakeSecretManager secretsmanager.Interface
+		fakeDBus          *fakedbus.DBus
 
 		b *AutonomousBotanist
 	)
@@ -44,6 +50,7 @@ var _ = Describe("Kubelet", func() {
 
 		fakeSeedClient = fakeclient.NewClientBuilder().WithScheme(kubernetes.SeedScheme).Build()
 		fakeSecretManager = fakesecretsmanager.New(fakeSeedClient, namespace)
+		fakeDBus = fakedbus.New()
 
 		b = &AutonomousBotanist{
 			Botanist: &botanistpkg.Botanist{
@@ -53,16 +60,14 @@ var _ = Describe("Kubelet", func() {
 					SecretsManager: fakeSecretManager,
 					SeedClientSet: fakekubernetes.
 						NewClientSetBuilder().
-						WithClient(fakeclient.
-							NewClientBuilder().
-							WithScheme(kubernetes.SeedScheme).
-							Build(),
-						).
+						WithClient(fakeSeedClient).
 						WithRESTConfig(&rest.Config{}).
 						Build(),
 				},
 			},
-			FS: afero.Afero{Fs: afero.NewMemMapFs()},
+			FS:       afero.Afero{Fs: afero.NewMemMapFs()},
+			DBus:     fakeDBus,
+			HostName: hostName,
 		}
 		b.Shoot.SetInfo(&gardencorev1beta1.Shoot{
 			ObjectMeta: metav1.ObjectMeta{
@@ -74,10 +79,10 @@ var _ = Describe("Kubelet", func() {
 		Expect(fakeSeedClient.Create(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "ca", Namespace: namespace}})).To(Succeed())
 	})
 
-	Describe("#CreateBootstrapToken", func() {
+	Describe("#WriteBootstrapToken", func() {
 		It("should create a bootstrap token", func() {
 			Expect(b.FS.Exists("/var/lib/gardener-node-agent/credentials/bootstrap-token")).To(BeFalse())
-			Expect(b.CreateBootstrapToken(ctx)).To(Succeed())
+			Expect(b.WriteBootstrapToken(ctx)).To(Succeed())
 			Expect(b.FS.Exists("/var/lib/gardener-node-agent/credentials/bootstrap-token")).To(BeTrue())
 		})
 	})
@@ -107,4 +112,61 @@ var _ = Describe("Kubelet", func() {
 			Entry("with existing token file", false),
 		)
 	})
+
+	Describe("#BootstrapKubelet", func() {
+		It("should do nothing when the node was already found", func() {
+			Expect(fakeSeedClient.Create(ctx, &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{"kubernetes.io/hostname": hostName}}})).To(Succeed())
+
+			Expect(b.BootstrapKubelet(ctx)).To(Succeed())
+
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKey{Name: "system:node-bootstrapper"}, &rbacv1.ClusterRoleBinding{})).To(BeNotFoundError())
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKey{Name: "system:certificates.k8s.io:certificatesigningrequests:nodeclient"}, &rbacv1.ClusterRoleBinding{})).To(BeNotFoundError())
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKey{Name: "system:certificates.k8s.io:certificatesigningrequests:selfnodeclient"}, &rbacv1.ClusterRoleBinding{})).To(BeNotFoundError())
+
+			Expect(b.FS.Exists("/var/lib/gardener-node-agent/credentials/bootstrap-token")).To(BeFalse())
+		})
+
+		It("should write a bootstrap token and restart the kubelet unit", func() {
+			Expect(b.BootstrapKubelet(ctx)).To(Succeed())
+
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKey{Name: "system:node-bootstrapper"}, &rbacv1.ClusterRoleBinding{})).To(Succeed())
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKey{Name: "system:certificates.k8s.io:certificatesigningrequests:nodeclient"}, &rbacv1.ClusterRoleBinding{})).To(Succeed())
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKey{Name: "system:certificates.k8s.io:certificatesigningrequests:selfnodeclient"}, &rbacv1.ClusterRoleBinding{})).To(Succeed())
+
+			Expect(b.FS.Exists("/var/lib/gardener-node-agent/credentials/bootstrap-token")).To(BeTrue())
+			Expect(fakeDBus.Actions).To(HaveExactElements(fakedbus.SystemdAction{Action: fakedbus.ActionRestart, UnitNames: []string{"kubelet.service"}}))
+		})
+	})
+
+	Describe("#ApproveKubeletServerCertificateSigningRequest", func() {
+		It("should do nothing because the server certificate file already exists", func() {
+			file, err := b.FS.Create("/var/lib/kubelet/pki/kubelet-server-current.pem")
+			Expect(err).NotTo(HaveOccurred())
+			Expect(file).NotTo(BeNil())
+
+			Expect(b.ApproveKubeletServerCertificateSigningRequest(ctx)).To(Succeed())
+		})
+
+		It("should return an error when no CSR was found for the node", func() {
+			Expect(b.ApproveKubeletServerCertificateSigningRequest(ctx)).To(MatchError(ContainSubstring("no certificate signing request found for node")))
+		})
+
+		It("should approve the CSR when not already approved", func() {
+			csr := &certificatesv1.CertificateSigningRequest{
+				ObjectMeta: metav1.ObjectMeta{Name: "csr"},
+				Spec:       certificatesv1.CertificateSigningRequestSpec{Username: "system:node:" + hostName},
+			}
+			Expect(fakeSeedClient.Create(ctx, csr)).To(Succeed())
+
+			Expect(b.ApproveKubeletServerCertificateSigningRequest(ctx)).To(Succeed())
+
+			Expect(fakeSeedClient.Get(ctx, client.ObjectKeyFromObject(csr), csr)).To(Succeed())
+			Expect(csr.Status.Conditions).To(HaveExactElements(certificatesv1.CertificateSigningRequestCondition{
+				Type:    certificatesv1.CertificateApproved,
+				Status:  corev1.ConditionTrue,
+				Reason:  "RequestApproved",
+				Message: "Approving kubelet server certificate signing request via gardenadm",
+			}))
+		})
+	})
 })
diff --git a/pkg/gardenadm/botanist/secrets.go b/pkg/gardenadm/botanist/secrets.go
new file mode 100644
index 00000000000..ea03e7360d3
--- /dev/null
+++ b/pkg/gardenadm/botanist/secrets.go
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/gardener/gardener/pkg/utils/flow"
+)
+
+// MigrateSecrets exports the secrets generated with the fake client and imports them with the real client.
+func (b *AutonomousBotanist) MigrateSecrets(ctx context.Context, fakeClient, realClient client.Client) error {
+	secretList := &corev1.SecretList{}
+	if err := fakeClient.List(ctx, secretList, client.InNamespace(b.Shoot.ControlPlaneNamespace)); err != nil {
+		return fmt.Errorf("failed listing secrets with fake client: %w", err)
+	}
+
+	var taskFns []flow.TaskFn
+
+	for _, secret := range secretList.Items {
+		taskFns = append(taskFns, func(ctx context.Context) error {
+			return realClient.Create(ctx, &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        secret.Name,
+					Namespace:   secret.Namespace,
+					Labels:      secret.Labels,
+					Annotations: secret.Annotations,
+				},
+				Type:      secret.Type,
+				Immutable: secret.Immutable,
+				Data:      secret.Data,
+			})
+		})
+	}
+
+	return flow.Parallel(taskFns...)(ctx)
+}
diff --git a/pkg/gardenadm/botanist/secrets_test.go b/pkg/gardenadm/botanist/secrets_test.go
new file mode 100644
index 00000000000..2e9b7a8c534
--- /dev/null
+++ b/pkg/gardenadm/botanist/secrets_test.go
@@ -0,0 +1,104 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist_test
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/gardener/gardener/pkg/client/kubernetes"
+	fakekubernetes "github.com/gardener/gardener/pkg/client/kubernetes/fake"
+	. "github.com/gardener/gardener/pkg/gardenadm/botanist"
+	"github.com/gardener/gardener/pkg/gardenlet/operation"
+	botanistpkg "github.com/gardener/gardener/pkg/gardenlet/operation/botanist"
+	shootpkg "github.com/gardener/gardener/pkg/gardenlet/operation/shoot"
+)
+
+var _ = Describe("Secrets", func() {
+	var (
+		ctx context.Context
+
+		fakeClient1 client.Client
+		fakeClient2 client.Client
+
+		b *AutonomousBotanist
+	)
+
+	BeforeEach(func() {
+		ctx = context.Background()
+
+		fakeClient1 = fakeclient.NewClientBuilder().WithScheme(kubernetes.SeedScheme).Build()
+		fakeClient2 = fakeclient.NewClientBuilder().WithScheme(kubernetes.SeedScheme).Build()
+
+		b = &AutonomousBotanist{
+			Botanist: &botanistpkg.Botanist{
+				Operation: &operation.Operation{
+					SeedClientSet: fakekubernetes.
+						NewClientSetBuilder().
+						WithClient(fakeClient1).
+						WithRESTConfig(&rest.Config{}).
+						Build(),
+					Shoot: &shootpkg.Shoot{
+						ControlPlaneNamespace: "kube-system",
+					},
+				},
+			},
+		}
+	})
+
+	Describe("#MigrateSecrets", func() {
+		It("should copy all secrets from kube-system", func() {
+			var (
+				secret1 = &corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "kube-system", Finalizers: []string{"hugo"}},
+					Type:       corev1.SecretTypeOpaque,
+					Data:       map[string][]byte{"foo": []byte("bar")},
+				}
+				secret2 = &corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "default"},
+					Type:       corev1.SecretTypeOpaque,
+					Data:       map[string][]byte{"bar": []byte("foo")},
+				}
+				secret3 = &corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{Name: "s2", Namespace: "kube-system", OwnerReferences: []metav1.OwnerReference{{}}},
+					Type:       corev1.SecretTypeOpaque,
+					Data:       map[string][]byte{"baz": []byte("bar")},
+				}
+
+				secretList = &corev1.SecretList{}
+			)
+
+			Expect(fakeClient1.Create(ctx, secret1)).To(Succeed())
+			Expect(fakeClient1.Create(ctx, secret2)).To(Succeed())
+			Expect(fakeClient1.Create(ctx, secret3)).To(Succeed())
+
+			Expect(fakeClient2.List(ctx, secretList)).To(Succeed())
+			Expect(secretList.Items).To(BeEmpty())
+
+			Expect(b.MigrateSecrets(ctx, fakeClient1, fakeClient2)).To(Succeed())
+
+			Expect(fakeClient2.List(ctx, secretList)).To(Succeed())
+			Expect(secretList.Items).To(HaveExactElements(
+				corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "kube-system", ResourceVersion: "1"},
+					Type:       corev1.SecretTypeOpaque,
+					Data:       map[string][]byte{"foo": []byte("bar")},
+				},
+				corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{Name: "s2", Namespace: "kube-system", ResourceVersion: "1"},
+					Type:       corev1.SecretTypeOpaque,
+					Data:       map[string][]byte{"baz": []byte("bar")},
+				},
+			))
+		})
+	})
+})
diff --git a/pkg/gardenadm/cmd/init/init.go b/pkg/gardenadm/cmd/init/init.go
index 8cf0eeeac71..25b1e6b18b8 100644
--- a/pkg/gardenadm/cmd/init/init.go
+++ b/pkg/gardenadm/cmd/init/init.go
@@ -8,9 +8,12 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"time"
 
 	"github.com/spf13/cobra"
 
+	"github.com/gardener/gardener/pkg/client/kubernetes"
+	gardenerextensions "github.com/gardener/gardener/pkg/extensions"
 	"github.com/gardener/gardener/pkg/gardenadm"
 	"github.com/gardener/gardener/pkg/gardenadm/botanist"
 	"github.com/gardener/gardener/pkg/gardenadm/cmd"
@@ -52,37 +55,44 @@ gardenadm init`,
 }
 
 func run(ctx context.Context, opts *Options) error {
-	cloudProfile, project, shoot, err := gardenadm.ReadManifests(opts.Log, os.DirFS(opts.ConfigDir))
-	if err != nil {
-		return fmt.Errorf("failed reading Kubernetes resources from config directory %s: %w", opts.ConfigDir, err)
-	}
-
-	b, err := botanist.NewAutonomousBotanist(ctx, opts.Log, project, cloudProfile, shoot)
+	b, err := bootstrapControlPlane(ctx, opts)
 	if err != nil {
-		return fmt.Errorf("failed constructing botanist: %w", err)
+		return fmt.Errorf("failed bootstrapping control plane: %w", err)
 	}
 
 	var (
 		g = flow.NewGraph("init")
 
-		initializeSecretsManagement = g.Add(flow.Task{
-			Name: "Initializing secrets management",
-			Fn:   b.InitializeSecretsManagement,
+		reconcileCustomResourceDefinitions = g.Add(flow.Task{
+			Name: "Reconciling CustomResourceDefinitions",
+			Fn:   b.ReconcileCustomResourceDefinitions,
 		})
-		writeKubeletBootstrapKubeconfig = g.Add(flow.Task{
-			Name:         "Writing kubelet bootstrap kubeconfig with a fake token to disk to make kubelet start",
-			Fn:           b.WriteKubeletBootstrapKubeconfig,
-			Dependencies: flow.NewTaskIDs(initializeSecretsManagement),
+		ensureCustomResourceDefinitionsReady = g.Add(flow.Task{
+			Name:         "Ensuring CustomResourceDefinitions are ready",
+			Fn:           flow.TaskFn(b.EnsureCustomResourceDefinitionsReady).RetryUntilTimeout(time.Second, time.Minute),
+			Dependencies: flow.NewTaskIDs(reconcileCustomResourceDefinitions),
 		})
-		deployOperatingSystemConfigSecretForNodeAgent = g.Add(flow.Task{
-			Name:         "Generating OperatingSystemConfig and deploying Secret for gardener-node-agent",
-			Fn:           b.DeployOperatingSystemConfigSecretForNodeAgent,
+		reconcileClusterResource = g.Add(flow.Task{
+			Name: "Reconciling extensions.gardener.cloud/v1alpha1.Cluster resource",
+			Fn: func(ctx context.Context) error {
+				return gardenerextensions.SyncClusterResourceToSeed(ctx, b.SeedClientSet.Client(), b.Shoot.ControlPlaneNamespace, b.Shoot.GetInfo(), b.Shoot.CloudProfile, b.Seed.GetInfo())
+			},
+			Dependencies: flow.NewTaskIDs(ensureCustomResourceDefinitionsReady),
+		})
+		initializeSecretsManagement = g.Add(flow.Task{
+			Name:         "Initializing internal state of Gardener secrets manager",
+			Fn:           b.InitializeSecretsManagement,
+			Dependencies: flow.NewTaskIDs(reconcileClusterResource),
+		})
+		bootstrapKubelet = g.Add(flow.Task{
+			Name:         "Creating real bootstrap token for kubelet and restart unit",
+			Fn:           b.BootstrapKubelet,
 			Dependencies: flow.NewTaskIDs(initializeSecretsManagement),
 		})
 		_ = g.Add(flow.Task{
-			Name:         "Applying OperatingSystemConfig using gardener-node-agent's reconciliation logic",
-			Fn:           b.ApplyOperatingSystemConfig,
-			Dependencies: flow.NewTaskIDs(writeKubeletBootstrapKubeconfig, deployOperatingSystemConfigSecretForNodeAgent),
+			Name:         "Approving kubelet server certificate signing request if necessary",
+			Fn:           flow.TaskFn(b.ApproveKubeletServerCertificateSigningRequest).RetryUntilTimeout(2*time.Second, time.Minute),
+			Dependencies: flow.NewTaskIDs(bootstrapKubelet),
 		})
 	)
 
@@ -119,3 +129,77 @@ see https://gardener.cloud/docs/gardener/shoot/shoot_access/.
 
 	return nil
 }
+
+func bootstrapControlPlane(ctx context.Context, opts *Options) (*botanist.AutonomousBotanist, error) {
+	cloudProfile, project, shoot, err := gardenadm.ReadManifests(opts.Log, os.DirFS(opts.ConfigDir))
+	if err != nil {
+		return nil, fmt.Errorf("failed reading Kubernetes resources from config directory %s: %w", opts.ConfigDir, err)
+	}
+
+	b, err := botanist.NewAutonomousBotanist(ctx, opts.Log, nil, project, cloudProfile, shoot)
+	if err != nil {
+		return nil, fmt.Errorf("failed constructing botanist: %w", err)
+	}
+
+	kubeconfigFileExists, err := b.FS.Exists(botanist.PathKubeconfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed checking whether kubeconfig file %s exists: %w", botanist.PathKubeconfig, err)
+	}
+
+	if kubeconfigFileExists {
+		b.Logger.Info("Found existing kubeconfig file, skipping initialization of control plane", "path", botanist.PathKubeconfig)
+	}
+
+	var (
+		clientSet kubernetes.Interface
+		g         = flow.NewGraph("bootstrap")
+
+		initializeSecretsManagement = g.Add(flow.Task{
+			Name:   "Initializing secrets management",
+			Fn:     b.InitializeSecretsManagement,
+			SkipIf: kubeconfigFileExists,
+		})
+		writeKubeletBootstrapKubeconfig = g.Add(flow.Task{
+			Name:         "Writing kubelet bootstrap kubeconfig with a fake token to disk to make kubelet start",
+			Fn:           b.WriteKubeletBootstrapKubeconfig,
+			SkipIf:       kubeconfigFileExists,
+			Dependencies: flow.NewTaskIDs(initializeSecretsManagement),
+		})
+		deployOperatingSystemConfigSecretForNodeAgent = g.Add(flow.Task{
+			Name:         "Generating OperatingSystemConfig and deploying Secret for gardener-node-agent",
+			Fn:           b.DeployOperatingSystemConfigSecretForNodeAgent,
+			SkipIf:       kubeconfigFileExists,
+			Dependencies: flow.NewTaskIDs(initializeSecretsManagement),
+		})
+		applyOperatingSystemConfig = g.Add(flow.Task{
+			Name:         "Applying OperatingSystemConfig using gardener-node-agent's reconciliation logic",
+			Fn:           b.ApplyOperatingSystemConfig,
+			SkipIf:       kubeconfigFileExists,
+			Dependencies: flow.NewTaskIDs(writeKubeletBootstrapKubeconfig, deployOperatingSystemConfigSecretForNodeAgent),
+		})
+		initializeClientSet = g.Add(flow.Task{
+			Name: "Initializing connection to Kubernetes control plane",
+			Fn: flow.TaskFn(func(_ context.Context) error {
+				clientSet, err = b.CreateClientSet(ctx)
+				return err
+			}).RetryUntilTimeout(2*time.Second, time.Minute),
+			Dependencies: flow.NewTaskIDs(applyOperatingSystemConfig),
+		})
+		_ = g.Add(flow.Task{
+			Name: "Importing secrets into control plane",
+			Fn: func(ctx context.Context) error {
+				return b.MigrateSecrets(ctx, b.SeedClientSet.Client(), clientSet.Client())
+			},
+			SkipIf:       kubeconfigFileExists,
+			Dependencies: flow.NewTaskIDs(initializeClientSet),
+		})
+	)
+
+	if err := g.Compile().Run(ctx, flow.Opts{
+		Log: b.Logger,
+	}); err != nil {
+		return nil, flow.Errors(err)
+	}
+
+	return botanist.NewAutonomousBotanist(ctx, opts.Log, clientSet, project, cloudProfile, shoot)
+}
diff --git a/pkg/gardenadm/staticpod/translator.go b/pkg/gardenadm/staticpod/translator.go
index 7e7f1eb0075..9ce80c156f5 100644
--- a/pkg/gardenadm/staticpod/translator.go
+++ b/pkg/gardenadm/staticpod/translator.go
@@ -29,7 +29,7 @@ func Translate(ctx context.Context, c client.Client, o client.Object) ([]extensi
 	case *appsv1.Deployment:
 		return translatePodTemplate(ctx, c, obj.ObjectMeta, obj.Spec.Template)
 	case *corev1.Pod:
-		return translatePodTemplate(ctx, c, obj.ObjectMeta, corev1.PodTemplateSpec{ObjectMeta: obj.ObjectMeta, Spec: obj.Spec})
+		return translatePodTemplate(ctx, c, obj.ObjectMeta, corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: obj.Labels, Annotations: obj.Annotations}, Spec: obj.Spec})
 	// TODO(rfranzke): Consider adding support for StatefulSet in the future.
 	default:
 		return nil, fmt.Errorf("unsupported object type %T", o)
diff --git a/pkg/gardenadm/staticpod/translator_test.go b/pkg/gardenadm/staticpod/translator_test.go
index 010d7c886f7..9db4a83f3bc 100644
--- a/pkg/gardenadm/staticpod/translator_test.go
+++ b/pkg/gardenadm/staticpod/translator_test.go
@@ -270,9 +270,12 @@ kind: Config
 			BeforeEach(func() {
 				pod = &corev1.Pod{
 					ObjectMeta: metav1.ObjectMeta{
-						Labels:      map[string]string{"baz": "foo"},
-						Annotations: map[string]string{"bar": "baz"},
-						Name:        "foo",
+						Labels:          map[string]string{"baz": "foo"},
+						Annotations:     map[string]string{"bar": "baz"},
+						Name:            "foo",
+						ResourceVersion: "1",
+						Finalizers:      []string{"bar"},
+						OwnerReferences: []metav1.OwnerReference{{}},
 					},
 				}
 			})
diff --git a/pkg/gardenlet/operation/botanist/botanist.go b/pkg/gardenlet/operation/botanist/botanist.go
index 40f3280d576..8c4967a0e2b 100644
--- a/pkg/gardenlet/operation/botanist/botanist.go
+++ b/pkg/gardenlet/operation/botanist/botanist.go
@@ -137,7 +137,7 @@ func New(ctx context.Context, o *operation.Operation) (*Botanist, error) {
 		if err != nil {
 			return nil, err
 		}
-		o.Shoot.Components.ControlPlane.MachineControllerManager, err = b.DefaultMachineControllerManager(ctx)
+		o.Shoot.Components.ControlPlane.MachineControllerManager, err = b.DefaultMachineControllerManager()
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/gardenlet/operation/botanist/infrastructure_test.go b/pkg/gardenlet/operation/botanist/infrastructure_test.go
index 5f5ef07ff7d..1648a895864 100644
--- a/pkg/gardenlet/operation/botanist/infrastructure_test.go
+++ b/pkg/gardenlet/operation/botanist/infrastructure_test.go
@@ -19,6 +19,7 @@ import (
 	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
+	extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
 	"github.com/gardener/gardener/pkg/client/kubernetes"
 	kubernetesmock "github.com/gardener/gardener/pkg/client/kubernetes/mock"
 	mockinfrastructure "github.com/gardener/gardener/pkg/component/extensions/infrastructure/mock"
@@ -161,6 +162,7 @@ var _ = Describe("Infrastructure", func() {
 			botanist.GardenClient = gardenClient
 			botanist.SeedClientSet = seedClientSet
 			botanist.Shoot.SetInfo(shoot)
+			botanist.Shoot.ControlPlaneNamespace = "shoot--foo--bar"
 		})
 
 		It("should successfully wait (w/ CIDRs)", func() {
@@ -184,7 +186,10 @@ var _ = Describe("Infrastructure", func() {
 			gardenClient.EXPECT().Status().Return(mockStatusWriter)
 			test.EXPECTStatusPatch(ctx, mockStatusWriter, updatedShoot2, updatedShoot, types.StrategicMergePatchType)
 
+			// cluster resource sync
 			seedClientSet.EXPECT().Client().Return(seedClient)
+			seedClient.EXPECT().Get(ctx, client.ObjectKey{Name: botanist.Shoot.ControlPlaneNamespace}, gomock.AssignableToTypeOf(&extensionsv1alpha1.Cluster{}))
+			seedClient.EXPECT().Patch(ctx, gomock.AssignableToTypeOf(&extensionsv1alpha1.Cluster{}), gomock.Any())
 
 			Expect(botanist.WaitForInfrastructure(ctx)).To(Succeed())
 			Expect(botanist.Shoot.GetInfo()).To(Equal(updatedShoot2))
@@ -205,7 +210,10 @@ var _ = Describe("Infrastructure", func() {
 			gardenClient.EXPECT().Status().Return(mockStatusWriter)
 			test.EXPECTStatusPatch(ctx, mockStatusWriter, updatedShoot2, updatedShoot, types.StrategicMergePatchType)
 
+			// cluster resource sync
 			seedClientSet.EXPECT().Client().Return(seedClient)
+			seedClient.EXPECT().Get(ctx, client.ObjectKey{Name: botanist.Shoot.ControlPlaneNamespace}, gomock.AssignableToTypeOf(&extensionsv1alpha1.Cluster{}))
+			seedClient.EXPECT().Patch(ctx, gomock.AssignableToTypeOf(&extensionsv1alpha1.Cluster{}), gomock.Any())
 
 			Expect(botanist.WaitForInfrastructure(ctx)).To(Succeed())
 			Expect(botanist.Shoot.GetInfo()).To(Equal(updatedShoot2))
diff --git a/pkg/gardenlet/operation/botanist/machinecontrollermanager.go b/pkg/gardenlet/operation/botanist/machinecontrollermanager.go
index 37814f25ca5..4721bfd5ad7 100644
--- a/pkg/gardenlet/operation/botanist/machinecontrollermanager.go
+++ b/pkg/gardenlet/operation/botanist/machinecontrollermanager.go
@@ -19,15 +19,27 @@ import (
 )
 
 // DefaultMachineControllerManager returns a deployer for the machine-controller-manager.
-func (b *Botanist) DefaultMachineControllerManager(ctx context.Context) (machinecontrollermanager.Interface, error) {
+func (b *Botanist) DefaultMachineControllerManager() (machinecontrollermanager.Interface, error) {
 	image, err := imagevector.Containers().FindImage(imagevector.ContainerImageNameMachineControllerManager, imagevectorutils.RuntimeVersion(b.SeedVersion()), imagevectorutils.TargetVersion(b.ShootVersion()))
 	if err != nil {
 		return nil, err
 	}
 
+	return machinecontrollermanager.New(
+		b.SeedClientSet.Client(),
+		b.Shoot.ControlPlaneNamespace,
+		b.SecretsManager,
+		machinecontrollermanager.Values{
+			Image: image.String(),
+		},
+	), nil
+}
+
+// DeployMachineControllerManager deploys the machine-controller-manager.
+func (b *Botanist) DeployMachineControllerManager(ctx context.Context) error {
 	machineDeploymentList := &machinev1alpha1.MachineDeploymentList{}
 	if err := b.SeedClientSet.Client().List(ctx, machineDeploymentList, client.InNamespace(b.Shoot.ControlPlaneNamespace)); err != nil {
-		return nil, err
+		return err
 	}
 
 	var replicas int32 = 1
@@ -56,20 +68,9 @@ func (b *Botanist) DefaultMachineControllerManager(ctx context.Context) (machine
 		replicas = 0
 	}
 
-	return machinecontrollermanager.New(
-		b.SeedClientSet.Client(),
-		b.Shoot.ControlPlaneNamespace,
-		b.SecretsManager,
-		machinecontrollermanager.Values{
-			Image:    image.String(),
-			Replicas: replicas,
-		},
-	), nil
-}
-
-// DeployMachineControllerManager deploys the machine-controller-manager.
-func (b *Botanist) DeployMachineControllerManager(ctx context.Context) error {
+	b.Shoot.Components.ControlPlane.MachineControllerManager.SetReplicas(replicas)
 	b.Shoot.Components.ControlPlane.MachineControllerManager.SetNamespaceUID(b.SeedNamespaceObject.UID)
+
 	return b.Shoot.Components.ControlPlane.MachineControllerManager.Deploy(ctx)
 }
 
diff --git a/pkg/gardenlet/operation/botanist/machinecontrollermanager_test.go b/pkg/gardenlet/operation/botanist/machinecontrollermanager_test.go
index 7a6ac9505ac..ef161dbbadb 100644
--- a/pkg/gardenlet/operation/botanist/machinecontrollermanager_test.go
+++ b/pkg/gardenlet/operation/botanist/machinecontrollermanager_test.go
@@ -24,7 +24,6 @@ import (
 	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
 	"github.com/gardener/gardener/pkg/client/kubernetes"
 	kubernetesmock "github.com/gardener/gardener/pkg/client/kubernetes/mock"
-	mockmachinecontrollermanager "github.com/gardener/gardener/pkg/component/nodemanagement/machinecontrollermanager/mock"
 	"github.com/gardener/gardener/pkg/gardenlet/operation"
 	. "github.com/gardener/gardener/pkg/gardenlet/operation/botanist"
 	seedpkg "github.com/gardener/gardener/pkg/gardenlet/operation/seed"
@@ -73,9 +72,21 @@ var _ = Describe("MachineControllerManager", func() {
 		})
 	})
 
-	Describe("#DefaultMachineControllerManager", func() {
+	Describe("#DeployMachineControllerManager", func() {
 		BeforeEach(func() {
 			kubernetesClient.EXPECT().Version()
+			kubernetesClient.EXPECT().Client().Return(fakeClient)
+
+			botanist.SeedNamespaceObject = &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					UID: types.UID("5678"),
+				},
+			}
+
+			machineControllerManager, err := botanist.DefaultMachineControllerManager()
+			Expect(machineControllerManager).NotTo(BeNil())
+			Expect(err).NotTo(HaveOccurred())
+			botanist.Shoot.Components = &shootpkg.Components{ControlPlane: &shootpkg.ControlPlane{MachineControllerManager: machineControllerManager}}
 
 			By("Create secrets managed outside of this package for whose secretsmanager.Get() will be called")
 			Expect(fakeClient.Create(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "generic-token-kubeconfig", Namespace: namespace}})).To(Succeed())
@@ -83,17 +94,13 @@ var _ = Describe("MachineControllerManager", func() {
 
 		DescribeTable("it should successfully create a machine-controller-manager interface",
 			func(expectedReplicas int, prepTest func()) {
-				kubernetesClient.EXPECT().Client().Return(fakeClient).Times(2)
+				kubernetesClient.EXPECT().Client().Return(fakeClient)
 
 				if prepTest != nil {
 					prepTest()
 				}
 
-				machineControllerManager, err := botanist.DefaultMachineControllerManager(ctx)
-				Expect(machineControllerManager).NotTo(BeNil())
-				Expect(err).NotTo(HaveOccurred())
-
-				Expect(machineControllerManager.Deploy(ctx)).To(Succeed())
+				Expect(botanist.DeployMachineControllerManager(ctx)).To(Succeed())
 				Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment)).To(Succeed())
 				Expect(deployment.Spec.Replicas).To(PointTo(Equal(int32(expectedReplicas))))
 			},
@@ -149,41 +156,6 @@ var _ = Describe("MachineControllerManager", func() {
 		)
 	})
 
-	Describe("#DeployMachineControllerManager", func() {
-		var (
-			machineControllerManager *mockmachinecontrollermanager.MockInterface
-			namespaceUID             = types.UID("5678")
-		)
-
-		BeforeEach(func() {
-			machineControllerManager = mockmachinecontrollermanager.NewMockInterface(ctrl)
-			machineControllerManager.EXPECT().SetNamespaceUID(namespaceUID)
-
-			botanist.SeedNamespaceObject = &corev1.Namespace{
-				ObjectMeta: metav1.ObjectMeta{
-					UID: namespaceUID,
-				},
-			}
-			botanist.Shoot = &shootpkg.Shoot{
-				Components: &shootpkg.Components{
-					ControlPlane: &shootpkg.ControlPlane{
-						MachineControllerManager: machineControllerManager,
-					},
-				},
-			}
-		})
-
-		It("should set the namespace uid and deploy", func() {
-			machineControllerManager.EXPECT().Deploy(ctx)
-			Expect(botanist.DeployMachineControllerManager(ctx)).To(Succeed())
-		})
-
-		It("should fail when the deploy function fails", func() {
-			machineControllerManager.EXPECT().Deploy(ctx).Return(fakeErr)
-			Expect(botanist.DeployMachineControllerManager(ctx)).To(Equal(fakeErr))
-		})
-	})
-
 	Describe("#ScaleMachineControllerManagerToZero", func() {
 		var (
 			mockClient            *mockclient.MockClient
diff --git a/skaffold-gardenadm.yaml b/skaffold-gardenadm.yaml
index 16b1fddd432..63038e76236 100644
--- a/skaffold-gardenadm.yaml
+++ b/skaffold-gardenadm.yaml
@@ -69,6 +69,19 @@ build:
             - pkg/component/etcd/etcd/crds/templates/crd-druid.gardener.cloud_etcds.yaml
             - pkg/component/extensions/containerruntime
             - pkg/component/extensions/controlplane
+            - pkg/component/extensions/crds
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_backupbuckets.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_backupentries.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_bastions.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_clusters.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_containerruntimes.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_controlplanes.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_dnsrecords.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_extensions.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_infrastructures.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_networks.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_operatingsystemconfigs.yaml
+            - pkg/component/extensions/crds/assets/crd-extensions.gardener.cloud_workers.yaml
             - pkg/component/extensions/dnsrecord
             - pkg/component/extensions/extension
             - pkg/component/extensions/infrastructure
diff --git a/test/e2e/gardenadm/hightouch/gardenadm.go b/test/e2e/gardenadm/hightouch/gardenadm.go
index e385e9fe1bf..dabc1e280de 100644
--- a/test/e2e/gardenadm/hightouch/gardenadm.go
+++ b/test/e2e/gardenadm/hightouch/gardenadm.go
@@ -6,18 +6,25 @@ package hightouch
 
 import (
 	"context"
+	"fmt"
 	"io"
+	"os"
+	"path/filepath"
+	"strings"
 	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/onsi/gomega/gbytes"
 	"github.com/onsi/gomega/gexec"
+	. "github.com/onsi/gomega/gstruct"
 	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/uuid"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	"github.com/gardener/gardener/pkg/client/kubernetes"
 	"github.com/gardener/gardener/pkg/utils"
 	"github.com/gardener/gardener/pkg/utils/kubernetes/health"
 	. "github.com/gardener/gardener/test/e2e/gardenadm/common"
@@ -44,6 +51,17 @@ var _ = Describe("gardenadm high-touch scenario tests", Label("gardenadm", "high
 	}, NodeTimeout(time.Minute))
 
 	Describe("Single-node control plane", Ordered, Label("single"), func() {
+		var (
+			portForwardCtx    context.Context
+			cancelPortForward context.CancelFunc
+			shootClientSet    kubernetes.Interface
+		)
+
+		BeforeAll(func() {
+			portForwardCtx, cancelPortForward = context.WithCancel(context.Background())
+			DeferCleanup(func() { cancelPortForward() })
+		})
+
 		It("should initialize as control plane node", func(ctx SpecContext) {
 			stdOut, _, err := execute(ctx, 0,
 				"gardenadm", "init", "-d", "/gardenadm/resources",
@@ -51,17 +69,65 @@ var _ = Describe("gardenadm high-touch scenario tests", Label("gardenadm", "high
 			Expect(err).NotTo(HaveOccurred())
 
 			Eventually(ctx, stdOut).Should(gbytes.Say("Your Shoot cluster control-plane has initialized successfully!"))
-		}, SpecTimeout(time.Minute))
+		}, SpecTimeout(5*time.Minute))
 
-		It("should be able to communicate with the API server", func(ctx SpecContext) {
-			Eventually(ctx, func(g Gomega) *gbytes.Buffer {
-				_, stdErr, err := execute(ctx, 0,
-					"kubectl", "--kubeconfig", "/etc/kubernetes/admin.conf", "get", "nodes",
-				)
+		It("copy admin kubeconfig and create client", func(ctx SpecContext) {
+			tempDir, err := os.MkdirTemp("", "tmp")
+			Expect(err).NotTo(HaveOccurred())
+			DeferCleanup(func() { Expect(os.RemoveAll(tempDir)).To(Succeed()) })
+			adminKubeconfigFile := filepath.Join(tempDir, "admin.conf")
+
+			By("Copy admin kubeconfig to local file")
+			localPort := 6443
+			Eventually(ctx, func(g Gomega) error {
+				stdOut, _, err := execute(ctx, 0, "cat", "/etc/kubernetes/admin.conf")
 				g.Expect(err).NotTo(HaveOccurred())
-				return stdErr
-			}).WithPolling(10 * time.Second).Should(gbytes.Say("No resources found"))
-		}, SpecTimeout(30*time.Second))
+
+				kubeconfig := strings.ReplaceAll(string(stdOut.Contents()), "localhost", fmt.Sprintf("localhost:%d", localPort))
+				return os.WriteFile(adminKubeconfigFile, []byte(kubeconfig), 0600)
+			}).Should(Succeed())
+
+			By("Forward port to control plane machine pod")
+			fw, err := kubernetes.SetupPortForwarder(portForwardCtx, RuntimeClient.RESTConfig(), namespace, machinePodName(0), localPort, 443)
+			Expect(err).NotTo(HaveOccurred())
+
+			go func() {
+				if err := fw.ForwardPorts(); err != nil {
+					Fail("Error forwarding ports: " + err.Error())
+				}
+			}()
+
+			Eventually(func() chan struct{} { return fw.Ready() }).Should(BeClosed())
+
+			By("Create client set")
+			Eventually(func() error {
+				shootClientSet, err = kubernetes.NewClientFromFile("", adminKubeconfigFile,
+					kubernetes.WithDisabledCachedClient(),
+					kubernetes.WithClientOptions(client.Options{Scheme: kubernetes.SeedScheme}),
+				)
+				return err
+			}).Should(Succeed())
+		})
+
+		It("should be able to communicate with the API server and see the node and the control plane pods", func(ctx SpecContext) {
+			Eventually(ctx, func(g Gomega) []corev1.Node {
+				nodeList := &corev1.NodeList{}
+				g.Expect(shootClientSet.Client().List(ctx, nodeList)).To(Succeed())
+				return nodeList.Items
+			}).Should(HaveLen(1))
+
+			Eventually(ctx, func(g Gomega) []corev1.Pod {
+				podList := &corev1.PodList{}
+				g.Expect(shootClientSet.Client().List(ctx, podList, client.InNamespace("kube-system"))).To(Succeed())
+				return podList.Items
+			}).Should(ConsistOf(
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("etcd-events-0-machine-0")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("etcd-main-0-machine-0")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("kube-apiserver-machine-0")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("kube-controller-manager-machine-0")})}),
+				MatchFields(IgnoreExtras, Fields{"ObjectMeta": MatchFields(IgnoreExtras, Fields{"Name": Equal("kube-scheduler-machine-0")})}),
+			))
+		}, SpecTimeout(5*time.Second))
 
 		It("should join as worker node", func(ctx SpecContext) {
 			_, stdErr, err := execute(ctx, 1,


Reviews:
user: login: ScheererJ
body: Thanks for the next big step towards working autonomous shoot clusters.

I found a few potential options for improvement.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T08:51:49Z
user: login: rfranzke
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T13:27:40Z
user: login: rfranzke
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T13:28:55Z
user: login: ScheererJ
body: Thanks for the changes. Looks good apart from the unit tests.

/lgtm
/approve
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T15:40:40Z

Review comments:
diff_hunk: @@ -104,8 +105,50 @@ func RBACResourcesData(secretNames []string) (map[string][]byte, error) {
 				},
 			},
 		}
+	)
 
-		clusterRoleBindingNodeBootstrapper = &rbacv1.ClusterRoleBinding{
+	if features.DefaultFeatureGate.Enabled(features.NodeAgentAuthorizer) {
+		// When the feature gate is enabled, gardener-node-agent service account needs permissions to create an CSR on
+		// existing nodes for migration.
+		clusterRole.Rules = append(clusterRole.Rules, rbacv1.PolicyRule{
+			APIGroups: []string{"certificates.k8s.io"},
+			Resources: []string{"certificatesigningrequests"},
+			Verbs:     []string{"create", "get"},
+		})
+	} else {
+		// For the case that NodeAgentAuthorizer feature gate is disabled again node-agents group have to be added to
+		// (cluster) role binding that node-agents which are already migrated do not lose access.
path: pkg/component/extensions/operatingsystemconfig/original/components/nodeagent/rbac.go
user: login: ScheererJ
body: ```suggestion
		// For the case that NodeAgentAuthorizer feature gate is disabled again node-agents group has to be added to
		// (cluster) role binding so that node-agents which are already migrated do not lose access.
```
created_at: 2025-03-27T08:12:45Z
author_association: MEMBER
diff_hunk: @@ -130,3 +134,37 @@ func (b *AutonomousBotanist) populateStaticAdminTokenToAccessTokenSecret(ctx con
 
 	return b.SeedClientSet.Client().Update(ctx, secret)
 }
+
+// CreateClientSet creates a client set for the control plane.
+func (b *AutonomousBotanist) CreateClientSet(ctx context.Context) (kubernetes.Interface, error) {
+	clientSet, err := kubernetes.NewClientFromFile("", PathKubeconfig,
+		kubernetes.WithClientOptions(client.Options{Scheme: kubernetes.SeedScheme}),
+		kubernetes.WithClientConnectionOptions(componentbaseconfigv1alpha1.ClientConnectionConfiguration{QPS: 100, Burst: 130}),
+	)
+	if err != nil {
+		b.Logger.Info("Waiting for kube-apiserver to start", "error", err.Error())
path: pkg/gardenadm/botanist/controlplane.go
user: login: ScheererJ
body: ```suggestion
		b.Logger.Info("Creating client set failed", "error", err.Error())
```
created_at: 2025-03-27T08:15:48Z
author_association: MEMBER
diff_hunk: @@ -80,3 +84,38 @@ func (b *AutonomousBotanist) WriteKubeletBootstrapKubeconfig(ctx context.Context
 
 	return kubeletBootstrapKubeconfigCreator.Start(ctx)
 }
+
+// BootstrapKubelet bootstraps the kubelet.
+func (b *AutonomousBotanist) BootstrapKubelet(ctx context.Context) error {
+	node, err := nodeagent.FetchNodeByHostName(ctx, b.SeedClientSet.Client(), b.HostName)
+	if err != nil {
+		return fmt.Errorf("failed fetching node object via hostname: %w", err)
+	}
+	if node != nil {
+		b.Logger.Info("Found node for hostname, skipping kubelet bootstrap", "hostName", b.HostName, "nodeName", node.Name)
+		return nil
+	}
+
+	b.Logger.Info("No node found for hostname, bootstrapping kubelet", "hostName", b.HostName)
+
+	for _, obj := range nodeagentcomponent.GetCertificateSigningRequestClusterRoleBindings() {
+		desired := obj.DeepCopyObject()
+		if _, err := controllerutil.CreateOrUpdate(ctx, b.SeedClientSet.Client(), obj, func() error {
+			obj.(*rbacv1.ClusterRoleBinding).Subjects = desired.(*rbacv1.ClusterRoleBinding).Subjects
+			obj.(*rbacv1.ClusterRoleBinding).RoleRef = desired.(*rbacv1.ClusterRoleBinding).RoleRef
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed reconciling ClusterRoleBinding %q: %w", obj.GetName(), err)
+		}
+	}
path: pkg/gardenadm/botanist/kubelet.go
user: login: ScheererJ
body: Why do we need to set `Subjects` and `RoleRef` again? It seems as if they would already be set.
What am I missing?
created_at: 2025-03-27T08:22:25Z
author_association: MEMBER
diff_hunk: @@ -80,3 +84,38 @@ func (b *AutonomousBotanist) WriteKubeletBootstrapKubeconfig(ctx context.Context
 
 	return kubeletBootstrapKubeconfigCreator.Start(ctx)
 }
+
+// BootstrapKubelet bootstraps the kubelet.
+func (b *AutonomousBotanist) BootstrapKubelet(ctx context.Context) error {
+	node, err := nodeagent.FetchNodeByHostName(ctx, b.SeedClientSet.Client(), b.HostName)
+	if err != nil {
+		return fmt.Errorf("failed fetching node object via hostname: %w", err)
+	}
+	if node != nil {
+		b.Logger.Info("Found node for hostname, skipping kubelet bootstrap", "hostName", b.HostName, "nodeName", node.Name)
+		return nil
+	}
+
+	b.Logger.Info("No node found for hostname, bootstrapping kubelet", "hostName", b.HostName)
+
+	for _, obj := range nodeagentcomponent.GetCertificateSigningRequestClusterRoleBindings() {
+		desired := obj.DeepCopyObject()
+		if _, err := controllerutil.CreateOrUpdate(ctx, b.SeedClientSet.Client(), obj, func() error {
+			obj.(*rbacv1.ClusterRoleBinding).Subjects = desired.(*rbacv1.ClusterRoleBinding).Subjects
+			obj.(*rbacv1.ClusterRoleBinding).RoleRef = desired.(*rbacv1.ClusterRoleBinding).RoleRef
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed reconciling ClusterRoleBinding %q: %w", obj.GetName(), err)
+		}
+	}
+
+	if err := b.WriteBootstrapToken(ctx); err != nil {
+		return fmt.Errorf("failed creating bootstrap token: %w", err)
path: pkg/gardenadm/botanist/kubelet.go
user: login: ScheererJ
body: ```suggestion
		return fmt.Errorf("failed writing bootstrap token: %w", err)
```
created_at: 2025-03-27T08:22:56Z
author_association: MEMBER
diff_hunk: @@ -107,4 +110,23 @@ var _ = Describe("Kubelet", func() {
 			Entry("with existing token file", false),
 		)
 	})
+
+	Describe("#BootstrapKubelet", func() {
+		It("should do nothing when the node was already found", func() {
+			Expect(fakeSeedClient.Create(ctx, &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{"kubernetes.io/hostname": hostName}}})).To(Succeed())
+
+			Expect(b.BootstrapKubelet(ctx)).To(Succeed())
+		})
path: pkg/gardenadm/botanist/kubelet_test.go
user: login: ScheererJ
body: Should we add some check to ensure that actually nothing was done, e.g. checking for one of the cluster role bindings or the bootstrap token?
created_at: 2025-03-27T08:25:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/gardener/gardener/pkg/utils/flow"
+)
+
+// MigrateSecrets exports the secrets generated with the fake client and imports them with the real client.
+func (b *AutonomousBotanist) MigrateSecrets(ctx context.Context, fakeClient, realClient client.Client) error {
+	secretList := &corev1.SecretList{}
+	if err := fakeClient.List(ctx, secretList, client.InNamespace(b.Shoot.ControlPlaneNamespace)); err != nil {
+		panic(err)
path: pkg/gardenadm/botanist/secrets.go
user: login: ScheererJ
body: ```suggestion
		return fmt.Errorf("failed listing secrets with fake client: %w", err)
```
created_at: 2025-03-27T08:29:21Z
author_association: MEMBER
diff_hunk: @@ -119,3 +123,40 @@ func (b *AutonomousBotanist) BootstrapKubelet(ctx context.Context) error {
 
 	return b.DBus.Restart(ctx, nil, nil, kubelet.UnitName)
 }
+
+// ApproveKubeletServerCertificateSigningRequest approves the kubelet server certificate signing request.
+func (b *AutonomousBotanist) ApproveKubeletServerCertificateSigningRequest(ctx context.Context) error {
+	serverCertificateExists, err := b.FS.Exists(filepath.Join(kubelet.PathKubeletDirectory, "pki", "kubelet-server-current.pem"))
+	if err != nil {
+		return fmt.Errorf("failed checking if kubelet server certificate exists: %w", err)
+	}
+	if serverCertificateExists {
+		return nil
+	}
+
+	csrList := &certificatesv1.CertificateSigningRequestList{}
+	if err := b.SeedClientSet.Client().List(ctx, csrList); err != nil {
+		return fmt.Errorf("failed listing certificate signing requests: %w", err)
+	}
+
+	for _, csr := range csrList.Items {
+		if csr.Spec.Username == "system:node:"+b.HostName {
+			if !slices.ContainsFunc(csr.Status.Conditions, func(condition certificatesv1.CertificateSigningRequestCondition) bool {
+				return condition.Type == certificatesv1.CertificateApproved && condition.Status == corev1.ConditionTrue
+			}) {
+				b.Logger.Info("Approving kubelet server certificate signing request")
path: pkg/gardenadm/botanist/kubelet.go
user: login: ScheererJ
body: Do we want to add context, e.g. name of CSR, or is this so obvious that we do not need it in case something went wrong?
created_at: 2025-03-27T08:38:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+	"fmt"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+
+	"github.com/gardener/gardener/pkg/component/autoscaling/vpa"
+	extensioncrds "github.com/gardener/gardener/pkg/component/extensions/crds"
+	"github.com/gardener/gardener/pkg/component/observability/logging/fluentoperator"
+	"github.com/gardener/gardener/pkg/component/observability/monitoring/prometheusoperator"
+	"github.com/gardener/gardener/pkg/utils/kubernetes/health"
+)
+
+// ReconcileCustomResourceDefinitions reconciles the custom resource definitions.
+func (b *AutonomousBotanist) ReconcileCustomResourceDefinitions(ctx context.Context) error {
+	vpaCRDDeployer := vpa.NewCRD(b.SeedClientSet.Applier(), nil)
+
+	prometheusCRDDeployer, err := prometheusoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	fluentCRDDeployer, err := fluentoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
path: pkg/gardenadm/botanist/customresourcedefinitions.go
user: login: ScheererJ
body: ```suggestion
		return fmt.Errorf("failed creating fluent bit CRD deployer: %w", err)
```
created_at: 2025-03-27T08:46:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+	"fmt"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+
+	"github.com/gardener/gardener/pkg/component/autoscaling/vpa"
+	extensioncrds "github.com/gardener/gardener/pkg/component/extensions/crds"
+	"github.com/gardener/gardener/pkg/component/observability/logging/fluentoperator"
+	"github.com/gardener/gardener/pkg/component/observability/monitoring/prometheusoperator"
+	"github.com/gardener/gardener/pkg/utils/kubernetes/health"
+)
+
+// ReconcileCustomResourceDefinitions reconciles the custom resource definitions.
+func (b *AutonomousBotanist) ReconcileCustomResourceDefinitions(ctx context.Context) error {
+	vpaCRDDeployer := vpa.NewCRD(b.SeedClientSet.Applier(), nil)
+
+	prometheusCRDDeployer, err := prometheusoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	fluentCRDDeployer, err := fluentoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	extensionCRDDeployer, err := extensioncrds.NewCRD(b.SeedClientSet.Client(), b.SeedClientSet.Applier(), true, true)
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
path: pkg/gardenadm/botanist/customresourcedefinitions.go
user: login: ScheererJ
body: ```suggestion
		return fmt.Errorf("failed creating extension CRD deployer: %w", err)
```
created_at: 2025-03-27T08:47:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+	"fmt"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+
+	"github.com/gardener/gardener/pkg/component/autoscaling/vpa"
+	extensioncrds "github.com/gardener/gardener/pkg/component/extensions/crds"
+	"github.com/gardener/gardener/pkg/component/observability/logging/fluentoperator"
+	"github.com/gardener/gardener/pkg/component/observability/monitoring/prometheusoperator"
+	"github.com/gardener/gardener/pkg/utils/kubernetes/health"
+)
+
+// ReconcileCustomResourceDefinitions reconciles the custom resource definitions.
+func (b *AutonomousBotanist) ReconcileCustomResourceDefinitions(ctx context.Context) error {
+	vpaCRDDeployer := vpa.NewCRD(b.SeedClientSet.Applier(), nil)
+
+	prometheusCRDDeployer, err := prometheusoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	fluentCRDDeployer, err := fluentoperator.NewCRDs(b.SeedClientSet.Client(), b.SeedClientSet.Applier())
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	extensionCRDDeployer, err := extensioncrds.NewCRD(b.SeedClientSet.Client(), b.SeedClientSet.Applier(), true, true)
+	if err != nil {
+		return fmt.Errorf("failed creating Prometheus CRD deployer: %w", err)
+	}
+
+	for _, deploy := range []func(context.Context) error{
+		vpaCRDDeployer.Deploy,
+		prometheusCRDDeployer.Deploy,
+		fluentCRDDeployer.Deploy,
+		extensionCRDDeployer.Deploy,
+	} {
+		if err := deploy(ctx); err != nil {
+			return fmt.Errorf("failed to deploy CustomResourceDefinition: %w", err)
path: pkg/gardenadm/botanist/customresourcedefinitions.go
user: login: ScheererJ
body: Should we add some context, e.g. the type of the deployer or at least the loop index so that it is clear which one failed in case the error is generic?
created_at: 2025-03-27T08:48:35Z
author_association: MEMBER
diff_hunk: @@ -130,3 +134,37 @@ func (b *AutonomousBotanist) populateStaticAdminTokenToAccessTokenSecret(ctx con
 
 	return b.SeedClientSet.Client().Update(ctx, secret)
 }
+
+// CreateClientSet creates a client set for the control plane.
+func (b *AutonomousBotanist) CreateClientSet(ctx context.Context) (kubernetes.Interface, error) {
+	clientSet, err := kubernetes.NewClientFromFile("", PathKubeconfig,
+		kubernetes.WithClientOptions(client.Options{Scheme: kubernetes.SeedScheme}),
+		kubernetes.WithClientConnectionOptions(componentbaseconfigv1alpha1.ClientConnectionConfiguration{QPS: 100, Burst: 130}),
+	)
+	if err != nil {
+		b.Logger.Info("Waiting for kube-apiserver to start", "error", err.Error())
path: pkg/gardenadm/botanist/controlplane.go
user: login: rfranzke
body: This is the technical error reason, but I think it's more user-friendly to keep what we have here. Once KAPI started, the client set can be created. While this is just a log/eye-candy, the error returned a line later contains the technical error reason. WDYT?
created_at: 2025-03-27T13:27:40Z
author_association: MEMBER
diff_hunk: @@ -80,3 +84,38 @@ func (b *AutonomousBotanist) WriteKubeletBootstrapKubeconfig(ctx context.Context
 
 	return kubeletBootstrapKubeconfigCreator.Start(ctx)
 }
+
+// BootstrapKubelet bootstraps the kubelet.
+func (b *AutonomousBotanist) BootstrapKubelet(ctx context.Context) error {
+	node, err := nodeagent.FetchNodeByHostName(ctx, b.SeedClientSet.Client(), b.HostName)
+	if err != nil {
+		return fmt.Errorf("failed fetching node object via hostname: %w", err)
+	}
+	if node != nil {
+		b.Logger.Info("Found node for hostname, skipping kubelet bootstrap", "hostName", b.HostName, "nodeName", node.Name)
+		return nil
+	}
+
+	b.Logger.Info("No node found for hostname, bootstrapping kubelet", "hostName", b.HostName)
+
+	for _, obj := range nodeagentcomponent.GetCertificateSigningRequestClusterRoleBindings() {
+		desired := obj.DeepCopyObject()
+		if _, err := controllerutil.CreateOrUpdate(ctx, b.SeedClientSet.Client(), obj, func() error {
+			obj.(*rbacv1.ClusterRoleBinding).Subjects = desired.(*rbacv1.ClusterRoleBinding).Subjects
+			obj.(*rbacv1.ClusterRoleBinding).RoleRef = desired.(*rbacv1.ClusterRoleBinding).RoleRef
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed reconciling ClusterRoleBinding %q: %w", obj.GetName(), err)
+		}
+	}
path: pkg/gardenadm/botanist/kubelet.go
user: login: rfranzke
body: `controllerutil.CreateOrUpdate` reads the `ClusterRoleBinding` from the system into `obj`. I.e, if the binding already exists, `obj` does not necessarily contain our desired `Subjects`/`RoleRef`. Hence we have to set them again.

Previously, we were using server-side apply here, but this is not supported by the fake client (for unit testing), so I decided to go back to this approach (also used commonly in g/g).
created_at: 2025-03-27T13:28:54Z
author_association: MEMBER

PR comments:
user: login: gardener-prow[bot]
created_at: 2025-03-26T10:54:35Z
body: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
author_association: CONTRIBUTOR
user: login: ScheererJ
created_at: 2025-03-26T16:06:03Z
body: /assign
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-03-27T15:40:44Z
body: LGTM label has been added.  <details>Git tree hash: 8d4df7fa448bd226c7a4a7a7fe6f5ab3c21cfb22</details>
author_association: CONTRIBUTOR
user: login: gardener-prow[bot]
created_at: 2025-03-27T15:40:48Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/gardener/gardener/pull/11749#pullrequestreview-2722250384" title="Approved">ScheererJ</a>*

The full list of commands accepted by this bot can be found [here](https://prow.gardener.cloud/command-help?repo=gardener%2Fgardener).

The pull request process is described [here](https://gardener.cloud/docs/contribute/#pull-request-checklist)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/gardener/gardener/blob/master/OWNERS)~~ [ScheererJ]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: ScheererJ
created_at: 2025-03-28T09:17:41Z
body: /lgtm
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-03-28T09:17:45Z
body: LGTM label has been added.  <details>Git tree hash: 1b2df350bd3166557d5764aa393ed5fdabed5263</details>
author_association: CONTRIBUTOR