Repository: gardener/gardener
Pull request #12052: [GEP-28] `gardenadm bootstrap`: Deploy gardener-resource-manager and provider extension
Author: login: timebertt
Labels: name: kind/enhancement
description: Enhancement, improvement, extension
name: size/XL
description: Denotes a PR that changes 500-999 lines, ignoring generated files.
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: area/ipcei
description: IPCEI (Important Project of Common European Interest)
Created: 2025-05-12T13:49:58Z | Merged: 2025-05-13T12:39:33Z

**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 continues https://github.com/gardener/gardener/pull/12041 and deploys both gardener-resource-manager and the provider extension in `gardenadm bootstrap`.

**Which issue(s) this PR fixes**:
Part of https://github.com/gardener/gardener/issues/2906

**Special notes for your reviewer**:

/cc @rfranzke @ScheererJ 

Although gardener-resource-manager runs in the control plane namespace (`shoot--project--name`), it is configured like the runtime/seed resource manager in the garden namespace.
Furthermore, the `ManagedResources` of extensions are deployed into the control plane namespace.

This deployment mode makes cleanup very simple. Even though we probably don't want to automate this in `gardenadm`, it helps with development. Instead of recreating the kind cluster, you can reset the state using (orphans global resources like CRDs though):
```
k -n shoot--garden--root delete mr --all && k delete ns shoot--garden--root extension-provider-local
```

**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/docs/deployment/getting_started_locally_with_gardenadm.md b/docs/deployment/getting_started_locally_with_gardenadm.md
index d26d181bf99..e33e3050fa6 100644
--- a/docs/deployment/getting_started_locally_with_gardenadm.md
+++ b/docs/deployment/getting_started_locally_with_gardenadm.md
@@ -147,6 +147,7 @@ This will first build the needed images and then render the needed manifests for
 Afterward, you can use `go run` to execute `gardenadm` commands on your machine:
 
 ```shell
+$ export IMAGEVECTOR_OVERWRITE=$PWD/example/gardenadm-local/.imagevector-overwrite.yaml
 $ go run ./cmd/gardenadm bootstrap -d ./example/gardenadm-local/medium-touch
 ...
 Command is work in progress
diff --git a/pkg/apis/core/validation/controllerregistration.go b/pkg/apis/core/validation/controllerregistration.go
index 5946dd637d6..884245a71ea 100644
--- a/pkg/apis/core/validation/controllerregistration.go
+++ b/pkg/apis/core/validation/controllerregistration.go
@@ -45,21 +45,6 @@ func ValidateControllerRegistration(controllerRegistration *core.ControllerRegis
 	return allErrs
 }
 
-// SupportedExtensionKinds contains all supported extension kinds.
-var SupportedExtensionKinds = sets.New(
-	extensionsv1alpha1.BackupBucketResource,
-	extensionsv1alpha1.BackupEntryResource,
-	extensionsv1alpha1.BastionResource,
-	extensionsv1alpha1.ContainerRuntimeResource,
-	extensionsv1alpha1.ControlPlaneResource,
-	extensionsv1alpha1.DNSRecordResource,
-	extensionsv1alpha1.ExtensionResource,
-	extensionsv1alpha1.InfrastructureResource,
-	extensionsv1alpha1.NetworkResource,
-	extensionsv1alpha1.OperatingSystemConfigResource,
-	extensionsv1alpha1.WorkerResource,
-)
-
 // ValidateControllerRegistrationSpec validates the specification of a ControllerRegistration object.
 func ValidateControllerRegistrationSpec(spec *core.ControllerRegistrationSpec, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
@@ -79,8 +64,8 @@ func ValidateControllerRegistrationSpec(spec *core.ControllerRegistrationSpec, f
 			allErrs = append(allErrs, field.Required(idxPath.Child("kind"), "field is required"))
 		}
 
-		if !SupportedExtensionKinds.Has(resource.Kind) {
-			allErrs = append(allErrs, field.NotSupported(idxPath.Child("kind"), resource.Kind, SupportedExtensionKinds.UnsortedList()))
+		if !extensionsv1alpha1.AllExtensionKinds.Has(resource.Kind) {
+			allErrs = append(allErrs, field.NotSupported(idxPath.Child("kind"), resource.Kind, extensionsv1alpha1.AllExtensionKinds.UnsortedList()))
 		}
 
 		if len(resource.Type) == 0 {
diff --git a/pkg/apis/core/validation/controllerregistration_test.go b/pkg/apis/core/validation/controllerregistration_test.go
index 757f271dcd0..473aea735e2 100644
--- a/pkg/apis/core/validation/controllerregistration_test.go
+++ b/pkg/apis/core/validation/controllerregistration_test.go
@@ -117,8 +117,8 @@ var _ = Describe("validation", func() {
 		})
 
 		It("should allow all known extension kinds", func() {
-			controllerRegistration.Spec.Resources = make([]core.ControllerResource, 0, len(SupportedExtensionKinds))
-			for kind := range SupportedExtensionKinds {
+			controllerRegistration.Spec.Resources = make([]core.ControllerResource, 0, len(extensionsv1alpha1.AllExtensionKinds))
+			for kind := range extensionsv1alpha1.AllExtensionKinds {
 				controllerRegistration.Spec.Resources = append(controllerRegistration.Spec.Resources,
 					core.ControllerResource{
 						Kind: kind,
diff --git a/pkg/apis/extensions/v1alpha1/types.go b/pkg/apis/extensions/v1alpha1/types.go
index 3a94562195e..a6f252e8157 100644
--- a/pkg/apis/extensions/v1alpha1/types.go
+++ b/pkg/apis/extensions/v1alpha1/types.go
@@ -7,10 +7,26 @@ package v1alpha1
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
 )
 
+// AllExtensionKinds contains all supported extension kinds.
+var AllExtensionKinds = sets.New[string](
+	BackupBucketResource,
+	BackupEntryResource,
+	BastionResource,
+	ContainerRuntimeResource,
+	ControlPlaneResource,
+	DNSRecordResource,
+	ExtensionResource,
+	InfrastructureResource,
+	NetworkResource,
+	OperatingSystemConfigResource,
+	WorkerResource,
+)
+
 // Status is the status of an Object.
 type Status interface {
 	// GetProviderStatus retrieves the provider status.
diff --git a/pkg/gardenadm/botanist/botanist.go b/pkg/gardenadm/botanist/botanist.go
index 643a328d36b..c49e429655f 100644
--- a/pkg/gardenadm/botanist/botanist.go
+++ b/pkg/gardenadm/botanist/botanist.go
@@ -60,18 +60,24 @@ type Extension struct {
 var DirFS = os.DirFS
 
 // NewAutonomousBotanistFromManifests reads the manifests from dir and initializes a new AutonomousBotanist with them.
-func NewAutonomousBotanistFromManifests(ctx context.Context, log logr.Logger, clientSet kubernetes.Interface, dir string) (*AutonomousBotanist, error) {
+func NewAutonomousBotanistFromManifests(
+	ctx context.Context,
+	log logr.Logger,
+	clientSet kubernetes.Interface,
+	dir string,
+	runsControlPlane bool,
+) (*AutonomousBotanist, error) {
 	cloudProfile, project, shoot, controllerRegistrations, controllerDeployments, err := gardenadm.ReadManifests(log, DirFS(dir))
 	if err != nil {
 		return nil, fmt.Errorf("failed reading Kubernetes resources from config directory %s: %w", dir, err)
 	}
 
-	extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments)
+	extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments, runsControlPlane)
 	if err != nil {
 		return nil, fmt.Errorf("failed computing extensions: %w", err)
 	}
 
-	b, err := NewAutonomousBotanist(ctx, log, clientSet, project, cloudProfile, shoot, extensions)
+	b, err := NewAutonomousBotanist(ctx, log, clientSet, project, cloudProfile, shoot, extensions, runsControlPlane)
 	if err != nil {
 		return nil, fmt.Errorf("failed constructing botanist: %w", err)
 	}
@@ -88,6 +94,7 @@ func NewAutonomousBotanist(
 	cloudProfile *gardencorev1beta1.CloudProfile,
 	shoot *gardencorev1beta1.Shoot,
 	extensions []Extension,
+	runsControlPlane bool,
 ) (
 	*AutonomousBotanist,
 	error,
@@ -97,7 +104,7 @@ func NewAutonomousBotanist(
 		return nil, fmt.Errorf("failed creating garden object: %w", err)
 	}
 
-	shootObj, err := newShootObject(ctx, project.Name, cloudProfile, shoot)
+	shootObj, err := newShootObject(ctx, project.Name, cloudProfile, shoot, runsControlPlane)
 	if err != nil {
 		return nil, fmt.Errorf("failed creating shoot object: %w", err)
 	}
@@ -216,7 +223,13 @@ func newSeedObject(ctx context.Context, shootObj *shootpkg.Shoot) (*seedpkg.Seed
 	return obj, nil
 }
 
-func newShootObject(ctx context.Context, projectName string, cloudProfile *gardencorev1beta1.CloudProfile, shoot *gardencorev1beta1.Shoot) (*shootpkg.Shoot, error) {
+func newShootObject(
+	ctx context.Context,
+	projectName string,
+	cloudProfile *gardencorev1beta1.CloudProfile,
+	shoot *gardencorev1beta1.Shoot,
+	runsControlPlane bool,
+) (*shootpkg.Shoot, error) {
 	shoot.Status.TechnicalID = gardenerutils.ComputeTechnicalID(projectName, shoot)
 	shoot.Status.Gardener = gardencorev1beta1.Gardener{Name: "gardenadm", Version: version.Get().GitVersion}
 	// TODO(rfranzke): This UID is used to compute the name of the BackupEntry object. Consider persisting this random
@@ -240,7 +253,15 @@ func newShootObject(ctx context.Context, projectName string, cloudProfile *garde
 		return nil, fmt.Errorf("failed computing shoot networks: %w", err)
 	}
 
-	obj.ControlPlaneNamespace = metav1.NamespaceSystem
+	// In autonomous shoot clusters, kube-system is used as the control plane namespace.
+	// However, when bootstrapping an autonomous shoot cluster with `gardenadm bootstrap` using a temporary local cluster,
+	// we want to avoid conflicts with kube-system components of the bootstrap cluster by placing all shoot-related
+	// components in another namespace. In this case, we use the technical ID as the control plane namespace, as usual.
+	// TODO(timebertt): double-check if this causes problems when importing the state into the autonomous shoot cluster
+	if !runsControlPlane {
+		obj.ControlPlaneNamespace = shoot.Status.TechnicalID
+	}
+
 	return obj, nil
 }
 
diff --git a/pkg/gardenadm/botanist/botanist_test.go b/pkg/gardenadm/botanist/botanist_test.go
index 54d583c11fa..c5a991ba5f1 100644
--- a/pkg/gardenadm/botanist/botanist_test.go
+++ b/pkg/gardenadm/botanist/botanist_test.go
@@ -40,20 +40,48 @@ var _ = Describe("AutonomousBotanist", func() {
 		})
 
 		It("should fail if the directory does not exist", func() {
-			Expect(NewAutonomousBotanistFromManifests(ctx, log, nil, "does/not/exist")).Error().To(MatchError(fs.ErrNotExist))
+			Expect(NewAutonomousBotanistFromManifests(ctx, log, nil, "does/not/exist", false)).Error().To(MatchError(fs.ErrNotExist))
 		})
 
-		It("should create a new Autonomous Botanist", func() {
-			b, err := NewAutonomousBotanistFromManifests(ctx, log, nil, configDir)
-			Expect(err).NotTo(HaveOccurred())
-
-			Expect(b.Shoot.CloudProfile.Name).To(Equal("stackit"))
-			Expect(b.Shoot.GetInfo().Name).To(Equal("gardenadm"))
-			Expect(b.Garden.Project.Name).To(Equal("gardenadm"))
-			Expect(b.Extensions).To(ConsistOf(
-				HaveField("ControllerRegistration.Name", "provider-stackit"),
-				HaveField("ControllerRegistration.Name", "networking-cilium"),
-			))
+		When("running the control plane (acting on the autonomous shoot cluster)", func() {
+			It("should create a new Autonomous Botanist", func() {
+				b, err := NewAutonomousBotanistFromManifests(ctx, log, nil, configDir, true)
+				Expect(err).NotTo(HaveOccurred())
+
+				Expect(b.Shoot.CloudProfile.Name).To(Equal("stackit"))
+				Expect(b.Shoot.GetInfo().Name).To(Equal("gardenadm"))
+				Expect(b.Garden.Project.Name).To(Equal("gardenadm"))
+				Expect(b.Extensions).To(ConsistOf(
+					HaveField("ControllerRegistration.Name", "provider-stackit"),
+					HaveField("ControllerRegistration.Name", "networking-cilium"),
+				))
+			})
+
+			It("should use kube-system as the control plane namespace", func() {
+				b, err := NewAutonomousBotanistFromManifests(ctx, log, nil, configDir, true)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(b.Shoot.ControlPlaneNamespace).To(Equal("kube-system"))
+			})
+		})
+
+		When("not running the control plane (acting on the bootstrap cluster)", func() {
+			It("should create a new Autonomous Botanist", func() {
+				b, err := NewAutonomousBotanistFromManifests(ctx, log, nil, configDir, false)
+				Expect(err).NotTo(HaveOccurred())
+
+				Expect(b.Shoot.CloudProfile.Name).To(Equal("stackit"))
+				Expect(b.Shoot.GetInfo().Name).To(Equal("gardenadm"))
+				Expect(b.Garden.Project.Name).To(Equal("gardenadm"))
+				Expect(b.Extensions).To(ConsistOf(
+					HaveField("ControllerRegistration.Name", "provider-stackit"),
+				))
+			})
+
+			It("should use the technical ID as the control plane namespace", func() {
+				b, err := NewAutonomousBotanistFromManifests(ctx, log, nil, configDir, false)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(b.Shoot.ControlPlaneNamespace).To(Equal("shoot--gardenadm--gardenadm"))
+			})
 		})
 	})
 })
diff --git a/pkg/gardenadm/botanist/extensions.go b/pkg/gardenadm/botanist/extensions.go
index f90bfbf6681..de757a48209 100644
--- a/pkg/gardenadm/botanist/extensions.go
+++ b/pkg/gardenadm/botanist/extensions.go
@@ -21,6 +21,7 @@ import (
 
 	gardencorev1 "github.com/gardener/gardener/pkg/apis/core/v1"
 	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
+	extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
 	"github.com/gardener/gardener/pkg/gardenlet/controller/controllerinstallation/controllerinstallation"
 	"github.com/gardener/gardener/pkg/utils/flow"
 	gardenerutils "github.com/gardener/gardener/pkg/utils/gardener"
@@ -34,13 +35,14 @@ func ComputeExtensions(
 	shoot *gardencorev1beta1.Shoot,
 	controllerRegistrations []*gardencorev1beta1.ControllerRegistration,
 	controllerDeployments []*gardencorev1.ControllerDeployment,
+	runsControlPlane bool,
 ) (
 	[]Extension,
 	error,
 ) {
 	var extensions []Extension
 
-	wantedControllerRegistrationNames, err := computeWantedControllerRegistrationNames(shoot, controllerRegistrations)
+	wantedControllerRegistrationNames, err := computeWantedControllerRegistrationNames(shoot, controllerRegistrations, wantedExtensionKinds(runsControlPlane))
 	if err != nil {
 		return nil, fmt.Errorf("failed computing the names of the wanted ControllerRegistrations: %w", err)
 	}
@@ -87,7 +89,29 @@ func ComputeExtensions(
 	return extensions, nil
 }
 
-func computeWantedControllerRegistrationNames(shoot *gardencorev1beta1.Shoot, controllerRegistrations []*gardencorev1beta1.ControllerRegistration) (sets.Set[string], error) {
+// wantedExtensionKinds returns the set of extension kinds that are needed and supported for autonomous shoot clusters.
+// runsControlPlane indicates whether we are bootstrapping the control plane of the cluster (i.e., when executing
+// `gardenadm init`).
+func wantedExtensionKinds(runsControlPlane bool) sets.Set[string] {
+	if runsControlPlane {
+		// In `gardenadm init`, we deploy all extensions referenced by the shoot, except for Infrastructure and Worker
+		// (we only support the high-touch scenario here for now).
+		// TODO(timebertt): distinguish between high-touch and medium-touch scenario in `gardenadm init`
+		return extensionsv1alpha1.AllExtensionKinds.Clone().Delete(extensionsv1alpha1.InfrastructureResource, extensionsv1alpha1.WorkerResource)
+	}
+
+	// In `gardenadm bootstrap`, we create Infrastructure and Worker for the control plane of the autonomous shoot
+	// cluster, so we only need these extensions.
+	return sets.New[string](extensionsv1alpha1.InfrastructureResource, extensionsv1alpha1.WorkerResource)
+}
+
+// computeWantedControllerRegistrationNames returns the names of all ControllerRegistrations relevant for the autonomous
+// botanist based on the parsed manifests and the wanted extension kinds.
+func computeWantedControllerRegistrationNames(
+	shoot *gardencorev1beta1.Shoot,
+	controllerRegistrations []*gardencorev1beta1.ControllerRegistration,
+	wantedExtensionKinds sets.Set[string],
+) (sets.Set[string], error) {
 	var (
 		result                                   = sets.New[string]()
 		extensionIDToControllerRegistrationNames = make(map[string][]string)
@@ -105,6 +129,15 @@ func computeWantedControllerRegistrationNames(shoot *gardencorev1beta1.Shoot, co
 	}
 
 	for _, extensionID := range gardenerutils.ComputeRequiredExtensionsForShoot(shoot, nil, controllerRegistrationSliceToList(controllerRegistrations), nil, nil).UnsortedList() {
+		extensionKind, _, err := gardenerutils.ExtensionKindAndTypeForID(extensionID)
+		if err != nil {
+			return nil, err
+		}
+
+		if !wantedExtensionKinds.Has(extensionKind) {
+			continue
+		}
+
 		names, ok := extensionIDToControllerRegistrationNames[extensionID]
 		if !ok {
 			return nil, fmt.Errorf("need to install an extension controller for %q but no appropriate ControllerRegistration found", extensionID)
diff --git a/pkg/gardenadm/botanist/extensions_test.go b/pkg/gardenadm/botanist/extensions_test.go
index fa11b53458d..339f02d082c 100644
--- a/pkg/gardenadm/botanist/extensions_test.go
+++ b/pkg/gardenadm/botanist/extensions_test.go
@@ -35,7 +35,9 @@ var _ = Describe("Extensions", func() {
 			controllerRegistration1 *gardencorev1beta1.ControllerRegistration
 			controllerRegistration2 *gardencorev1beta1.ControllerRegistration
 			controllerRegistration3 *gardencorev1beta1.ControllerRegistration
+			controllerRegistration4 *gardencorev1beta1.ControllerRegistration
 			controllerDeployment1   *gardencorev1.ControllerDeployment
+			controllerDeployment2   *gardencorev1.ControllerDeployment
 			controllerDeployment3   *gardencorev1.ControllerDeployment
 
 			controllerRegistrations []*gardencorev1beta1.ControllerRegistration
@@ -55,11 +57,11 @@ var _ = Describe("Extensions", func() {
 				},
 			}
 			controllerRegistration1 = &gardencorev1beta1.ControllerRegistration{
-				ObjectMeta: metav1.ObjectMeta{Name: "ext1"},
+				ObjectMeta: metav1.ObjectMeta{Name: "ext1-controlplane"},
 				Spec: gardencorev1beta1.ControllerRegistrationSpec{
 					Deployment: &gardencorev1beta1.ControllerRegistrationDeployment{
 						DeploymentRefs: []gardencorev1beta1.DeploymentRef{
-							{Name: "ext1"},
+							{Name: "ext1-controlplane"},
 						},
 					},
 					Resources: []gardencorev1beta1.ControllerResource{
@@ -68,7 +70,18 @@ var _ = Describe("Extensions", func() {
 				},
 			}
 			controllerRegistration2 = &gardencorev1beta1.ControllerRegistration{
-				ObjectMeta: metav1.ObjectMeta{Name: "ext2"},
+				ObjectMeta: metav1.ObjectMeta{Name: "ext1-infra-worker"},
+				Spec: gardencorev1beta1.ControllerRegistrationSpec{
+					Deployment: &gardencorev1beta1.ControllerRegistrationDeployment{
+						DeploymentRefs: []gardencorev1beta1.DeploymentRef{
+							{Name: "ext1-infra-worker"},
+						},
+					},
+					Resources: []gardencorev1beta1.ControllerResource{
+						{Kind: "Infrastructure", Type: "ext1"},
+						{Kind: "Worker", Type: "ext1"},
+					},
+				},
 			}
 			controllerRegistration3 = &gardencorev1beta1.ControllerRegistration{
 				ObjectMeta: metav1.ObjectMeta{Name: "ext3"},
@@ -83,8 +96,16 @@ var _ = Describe("Extensions", func() {
 					},
 				},
 			}
+			controllerRegistration4 = &gardencorev1beta1.ControllerRegistration{
+				ObjectMeta: metav1.ObjectMeta{Name: "ext2"},
+			}
+
 			controllerDeployment1 = &gardencorev1.ControllerDeployment{
-				ObjectMeta:             metav1.ObjectMeta{Name: "ext1"},
+				ObjectMeta:             metav1.ObjectMeta{Name: "ext1-controlplane"},
+				InjectGardenKubeconfig: ptr.To(true),
+			}
+			controllerDeployment2 = &gardencorev1.ControllerDeployment{
+				ObjectMeta:             metav1.ObjectMeta{Name: "ext1-infra-worker"},
 				InjectGardenKubeconfig: ptr.To(true),
 			}
 			controllerDeployment3 = &gardencorev1.ControllerDeployment{
@@ -92,14 +113,14 @@ var _ = Describe("Extensions", func() {
 				InjectGardenKubeconfig: ptr.To(false),
 			}
 
-			controllerRegistrations = []*gardencorev1beta1.ControllerRegistration{controllerRegistration1, controllerRegistration2, controllerRegistration3}
-			controllerDeployments = []*gardencorev1.ControllerDeployment{controllerDeployment1, controllerDeployment3}
+			controllerRegistrations = []*gardencorev1beta1.ControllerRegistration{controllerRegistration1, controllerRegistration2, controllerRegistration3, controllerRegistration4}
+			controllerDeployments = []*gardencorev1.ControllerDeployment{controllerDeployment1, controllerDeployment2, controllerDeployment3}
 		})
 
 		It("should return an error because deployment is not set", func() {
 			controllerRegistration1.Spec.Deployment = nil
 
-			extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments)
+			extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments, true)
 			Expect(err).To(MatchError(ContainSubstring("has invalid deployment refs in its spec")))
 			Expect(extensions).To(BeNil())
 		})
@@ -107,46 +128,69 @@ var _ = Describe("Extensions", func() {
 		It("should return an error because more than one deployment ref is set", func() {
 			controllerRegistration1.Spec.Deployment.DeploymentRefs = append(controllerRegistration1.Spec.Deployment.DeploymentRefs, gardencorev1beta1.DeploymentRef{})
 
-			extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments)
+			extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments, true)
 			Expect(err).To(MatchError(ContainSubstring("has invalid deployment refs in its spec")))
 			Expect(extensions).To(BeNil())
 		})
 
 		It("should return an error because matching ControllerDeployment is not found", func() {
-			extensions, err := ComputeExtensions(shoot, controllerRegistrations, nil)
+			extensions, err := ComputeExtensions(shoot, controllerRegistrations, nil, true)
 			Expect(err).To(MatchError(ContainSubstring("was not found")))
 			Expect(extensions).To(BeNil())
 		})
 
-		It("should return the computed extensions", func() {
-			extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(extensions).To(Equal([]Extension{
-				{
-					ControllerRegistration: controllerRegistration1,
-					ControllerDeployment:   controllerDeploymentWithoutInjectGardenKubeconfig(controllerDeployment1),
-					ControllerInstallation: &gardencorev1beta1.ControllerInstallation{
-						ObjectMeta: metav1.ObjectMeta{Name: controllerRegistration1.Name},
-						Spec: gardencorev1beta1.ControllerInstallationSpec{
-							RegistrationRef: corev1.ObjectReference{Name: controllerRegistration1.Name},
-							DeploymentRef:   &corev1.ObjectReference{Name: controllerDeployment1.Name},
-							SeedRef:         corev1.ObjectReference{Name: shoot.Name},
+		When("running the control plane", func() {
+			It("should return all extensions referenced by shoot (except Infrastructure and Worker)", func() {
+				extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments, true)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(extensions).To(Equal([]Extension{
+					{
+						ControllerRegistration: controllerRegistration1,
+						ControllerDeployment:   controllerDeploymentWithoutInjectGardenKubeconfig(controllerDeployment1),
+						ControllerInstallation: &gardencorev1beta1.ControllerInstallation{
+							ObjectMeta: metav1.ObjectMeta{Name: controllerRegistration1.Name},
+							Spec: gardencorev1beta1.ControllerInstallationSpec{
+								RegistrationRef: corev1.ObjectReference{Name: controllerRegistration1.Name},
+								DeploymentRef:   &corev1.ObjectReference{Name: controllerDeployment1.Name},
+								SeedRef:         corev1.ObjectReference{Name: shoot.Name},
+							},
 						},
 					},
-				},
-				{
-					ControllerRegistration: controllerRegistration3,
-					ControllerDeployment:   controllerDeploymentWithoutInjectGardenKubeconfig(controllerDeployment3),
-					ControllerInstallation: &gardencorev1beta1.ControllerInstallation{
-						ObjectMeta: metav1.ObjectMeta{Name: controllerRegistration3.Name},
-						Spec: gardencorev1beta1.ControllerInstallationSpec{
-							RegistrationRef: corev1.ObjectReference{Name: controllerRegistration3.Name},
-							DeploymentRef:   &corev1.ObjectReference{Name: controllerDeployment3.Name},
-							SeedRef:         corev1.ObjectReference{Name: shoot.Name},
+					{
+						ControllerRegistration: controllerRegistration3,
+						ControllerDeployment:   controllerDeploymentWithoutInjectGardenKubeconfig(controllerDeployment3),
+						ControllerInstallation: &gardencorev1beta1.ControllerInstallation{
+							ObjectMeta: metav1.ObjectMeta{Name: controllerRegistration3.Name},
+							Spec: gardencorev1beta1.ControllerInstallationSpec{
+								RegistrationRef: corev1.ObjectReference{Name: controllerRegistration3.Name},
+								DeploymentRef:   &corev1.ObjectReference{Name: controllerDeployment3.Name},
+								SeedRef:         corev1.ObjectReference{Name: shoot.Name},
+							},
 						},
 					},
-				},
-			}))
+				}))
+			})
+		})
+
+		When("not running the control plane", func() {
+			It("should return the provider extension only (Infrastructure and Worker)", func() {
+				extensions, err := ComputeExtensions(shoot, controllerRegistrations, controllerDeployments, false)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(extensions).To(Equal([]Extension{
+					{
+						ControllerRegistration: controllerRegistration2,
+						ControllerDeployment:   controllerDeploymentWithoutInjectGardenKubeconfig(controllerDeployment2),
+						ControllerInstallation: &gardencorev1beta1.ControllerInstallation{
+							ObjectMeta: metav1.ObjectMeta{Name: controllerRegistration2.Name},
+							Spec: gardencorev1beta1.ControllerInstallationSpec{
+								RegistrationRef: corev1.ObjectReference{Name: controllerRegistration2.Name},
+								DeploymentRef:   &corev1.ObjectReference{Name: controllerDeployment2.Name},
+								SeedRef:         corev1.ObjectReference{Name: shoot.Name},
+							},
+						},
+					},
+				}))
+			})
 		})
 	})
 
diff --git a/pkg/gardenadm/botanist/system.go b/pkg/gardenadm/botanist/system.go
new file mode 100644
index 00000000000..17059481c6a
--- /dev/null
+++ b/pkg/gardenadm/botanist/system.go
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package botanist
+
+import (
+	"context"
+
+	schedulingv1 "k8s.io/api/scheduling/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
+	"github.com/gardener/gardener/pkg/controllerutils"
+)
+
+// DeployPriorityClassCritical deploys the gardener-system-critical PriorityClass needed for running
+// gardener-resource-manager. This is usually deployed to seed clusters via the gardenlet helm chart. When bootstrapping
+// an autonomous shoot cluster with `gardenadm bootstrap` there is no gardenlet. Hence, we need to deploy the
+// PriorityClass for gardener-resource-manager manually, as it is not taken over by the seedsystem component (it uses a
+// ManagedResource for deploying the seed PriorityClasses).
+// Using system-cluster-critical for gardener-resource-manager could also be good enough, but it's very simple to deploy
+// the gardener-system-critical PriorityClass, so we can also choose the cleaner way.
+func (b *AutonomousBotanist) DeployPriorityClassCritical(ctx context.Context) error {
+	priorityClass := &schedulingv1.PriorityClass{ObjectMeta: metav1.ObjectMeta{Name: v1beta1constants.PriorityClassNameSeedSystemCritical}}
+	_, err := controllerutils.CreateOrGetAndMergePatch(ctx, b.SeedClientSet.Client(), priorityClass, func() error {
+		priorityClass.Value = int32(999998950)
+		priorityClass.GlobalDefault = false
+		priorityClass.Description = "This class is used to ensure that the gardener-resource-manager has a high priority and is not preempted in favor of other pods."
+		return nil
+	})
+	return err
+}
diff --git a/pkg/gardenadm/cmd/bootstrap/bootstrap.go b/pkg/gardenadm/cmd/bootstrap/bootstrap.go
index fc757663c11..47197605c52 100644
--- a/pkg/gardenadm/cmd/bootstrap/bootstrap.go
+++ b/pkg/gardenadm/cmd/bootstrap/bootstrap.go
@@ -7,6 +7,7 @@ package bootstrap
 import (
 	"context"
 	"fmt"
+	"time"
 
 	"github.com/spf13/cobra"
 	appsv1 "k8s.io/api/apps/v1"
@@ -14,8 +15,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
+	seedsystem "github.com/gardener/gardener/pkg/component/seed/system"
+	gardenerextensions "github.com/gardener/gardener/pkg/extensions"
 	"github.com/gardener/gardener/pkg/gardenadm/botanist"
 	"github.com/gardener/gardener/pkg/gardenadm/cmd"
+	"github.com/gardener/gardener/pkg/utils/flow"
 )
 
 // NewCommand creates a new cobra.Command.
@@ -66,12 +70,93 @@ func run(ctx context.Context, opts *Options) error {
 		return err
 	}
 
-	b, err := botanist.NewAutonomousBotanistFromManifests(ctx, opts.Log, clientSet, opts.ConfigDir)
+	b, err := botanist.NewAutonomousBotanistFromManifests(ctx, opts.Log, clientSet, opts.ConfigDir, false)
 	if err != nil {
 		return err
 	}
 
-	_ = b
+	var (
+		g = flow.NewGraph("bootstrap")
+
+		deployNamespace = g.Add(flow.Task{
+			Name: "Deploying control plane namespace",
+			Fn:   b.DeployControlPlaneNamespace,
+		})
+		reconcileCustomResourceDefinitions = g.Add(flow.Task{
+			Name: "Reconciling CustomResourceDefinitions",
+			Fn:   b.ReconcileCustomResourceDefinitions,
+		})
+		ensureCustomResourceDefinitionsReady = g.Add(flow.Task{
+			Name:         "Ensuring CustomResourceDefinitions are ready",
+			Fn:           flow.TaskFn(b.EnsureCustomResourceDefinitionsReady).RetryUntilTimeout(time.Second, time.Minute),
+			Dependencies: flow.NewTaskIDs(reconcileCustomResourceDefinitions),
+		})
+		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),
+		})
+		deployPriorityClassCritical = g.Add(flow.Task{
+			Name:         "Deploying PriorityClass for gardener-resource-manager",
+			Fn:           b.DeployPriorityClassCritical,
+			Dependencies: flow.NewTaskIDs(deployNamespace, initializeSecretsManagement),
+		})
+		deployGardenerResourceManager = g.Add(flow.Task{
+			Name:         "Deploying gardener-resource-manager",
+			Fn:           b.Shoot.Components.ControlPlane.ResourceManager.Deploy,
+			Dependencies: flow.NewTaskIDs(deployNamespace, initializeSecretsManagement, deployPriorityClassCritical),
+		})
+		waitUntilGardenerResourceManagerReady = g.Add(flow.Task{
+			Name:         "Waiting until gardener-resource-manager reports readiness",
+			Fn:           b.Shoot.Components.ControlPlane.ResourceManager.Wait,
+			Dependencies: flow.NewTaskIDs(deployGardenerResourceManager),
+		})
+		_ = g.Add(flow.Task{
+			Name: "Deploying seed system resources",
+			Fn: func(ctx context.Context) error {
+				return seedsystem.New(b.SeedClientSet.Client(), b.Shoot.ControlPlaneNamespace, seedsystem.Values{}).Deploy(ctx)
+			},
+			Dependencies: flow.NewTaskIDs(waitUntilGardenerResourceManagerReady),
+		})
+		deployExtensionControllers = g.Add(flow.Task{
+			Name: "Deploying extension controllers",
+			Fn: func(ctx context.Context) error {
+				return b.ReconcileExtensionControllerInstallations(ctx, false)
+			},
+			Dependencies: flow.NewTaskIDs(waitUntilGardenerResourceManagerReady),
+		})
+		waitUntilExtensionControllersReady = g.Add(flow.Task{
+			Name:         "Waiting until extension controllers report readiness",
+			Fn:           b.WaitUntilExtensionControllerInstallationsHealthy,
+			Dependencies: flow.NewTaskIDs(deployExtensionControllers),
+		})
+		deployNetworkPolicies = g.Add(flow.Task{
+			Name:         "Deploying network policies",
+			Fn:           b.ApplyNetworkPolicies,
+			Dependencies: flow.NewTaskIDs(deployGardenerResourceManager, deployExtensionControllers),
+		})
+		syncPointBootstrapped = flow.NewTaskIDs(
+			deployNetworkPolicies,
+			waitUntilGardenerResourceManagerReady,
+			waitUntilExtensionControllersReady,
+		)
+
+		_ = syncPointBootstrapped
+	)
+
+	if err := g.Compile().Run(ctx, flow.Opts{
+		Log: opts.Log,
+	}); err != nil {
+		return flow.Errors(err)
+	}
+
 	opts.Log.Info("Command is work in progress")
 	return nil
 }
diff --git a/pkg/gardenadm/cmd/init/init.go b/pkg/gardenadm/cmd/init/init.go
index 755124db207..ae364c73946 100644
--- a/pkg/gardenadm/cmd/init/init.go
+++ b/pkg/gardenadm/cmd/init/init.go
@@ -289,7 +289,7 @@ see https://gardener.cloud/docs/gardener/shoot/shoot_access/.
 }
 
 func bootstrapControlPlane(ctx context.Context, opts *Options) (*botanist.AutonomousBotanist, error) {
-	b, err := botanist.NewAutonomousBotanistFromManifests(ctx, opts.Log, nil, opts.ConfigDir)
+	b, err := botanist.NewAutonomousBotanistFromManifests(ctx, opts.Log, nil, opts.ConfigDir, true)
 	if err != nil {
 		return nil, err
 	}
@@ -354,5 +354,5 @@ func bootstrapControlPlane(ctx context.Context, opts *Options) (*botanist.Autono
 		return nil, flow.Errors(err)
 	}
 
-	return botanist.NewAutonomousBotanistFromManifests(ctx, opts.Log, clientSet, opts.ConfigDir)
+	return botanist.NewAutonomousBotanistFromManifests(ctx, opts.Log, clientSet, opts.ConfigDir, true)
 }
diff --git a/pkg/gardenlet/controller/seed/seed/reconciler_reconcile.go b/pkg/gardenlet/controller/seed/seed/reconciler_reconcile.go
index 2dbfdc62c70..8a6aa834799 100644
--- a/pkg/gardenlet/controller/seed/seed/reconciler_reconcile.go
+++ b/pkg/gardenlet/controller/seed/seed/reconciler_reconcile.go
@@ -116,7 +116,7 @@ func (r *Reconciler) runReconcileSeedFlow(
 	gardenNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: r.GardenNamespace}}
 	log.Info("Labeling and annotating namespace", "namespaceName", gardenNamespace.Name)
 	if _, err := controllerutils.CreateOrGetAndMergePatch(ctx, r.SeedClientSet.Client(), gardenNamespace, func() error {
-		metav1.SetMetaDataLabel(&gardenNamespace.ObjectMeta, "role", v1beta1constants.GardenNamespace)
+		metav1.SetMetaDataLabel(&gardenNamespace.ObjectMeta, v1beta1constants.LabelRole, v1beta1constants.GardenNamespace)
 
 		// When the seed is the garden cluster then this information is managed by gardener-operator.
 		if !seedIsGarden {
diff --git a/pkg/gardenlet/operation/botanist/resource_manager.go b/pkg/gardenlet/operation/botanist/resource_manager.go
index b67fd166c40..573d27c9938 100644
--- a/pkg/gardenlet/operation/botanist/resource_manager.go
+++ b/pkg/gardenlet/operation/botanist/resource_manager.go
@@ -32,7 +32,8 @@ func (b *Botanist) DefaultResourceManager() (resourcemanager.Interface, error) {
 
 	var (
 		newFunc = shared.NewTargetGardenerResourceManager
-		values  = resourcemanager.Values{
+
+		values = resourcemanager.Values{
 			ClusterIdentity:                           b.Seed.GetInfo().Status.ClusterIdentity,
 			DefaultNotReadyToleration:                 defaultNotReadyTolerationSeconds,
 			DefaultUnreachableToleration:              defaultUnreachableTolerationSeconds,
@@ -56,11 +57,18 @@ func (b *Botanist) DefaultResourceManager() (resourcemanager.Interface, error) {
 		}
 	)
 
-	if b.Shoot.RunsControlPlane() {
-		newFunc = shared.NewCombinedGardenerResourceManager
+	if b.Shoot.IsAutonomous() {
 		values.NodeAgentAuthorizerEnabled = false // TODO(rfranzke): Revisit this once autonomous shoot clusters progress.
-		values.TargetNamespaces = nil
 		values.KubernetesServiceHost = nil
+
+		if b.Shoot.RunsControlPlane() {
+			newFunc = shared.NewCombinedGardenerResourceManager
+			values.TargetNamespaces = nil
+		} else {
+			newFunc = shared.NewRuntimeGardenerResourceManager
+			// TODO(timebertt): consider disabling the highavailabilityconfig webhook
+			values.PriorityClassName = v1beta1constants.PriorityClassNameSeedSystemCritical
+		}
 	}
 
 	return newFunc(b.SeedClientSet.Client(), b.Shoot.ControlPlaneNamespace, b.SecretsManager, values)
diff --git a/pkg/gardenlet/operation/botanist/secrets.go b/pkg/gardenlet/operation/botanist/secrets.go
index e9552656937..23b0848534d 100644
--- a/pkg/gardenlet/operation/botanist/secrets.go
+++ b/pkg/gardenlet/operation/botanist/secrets.go
@@ -72,7 +72,7 @@ func (b *Botanist) lastSecretRotationStartTimes() map[string]time.Time {
 
 	if shootStatus := b.Shoot.GetInfo().Status; shootStatus.Credentials != nil && shootStatus.Credentials.Rotation != nil {
 		if shootStatus.Credentials.Rotation.CertificateAuthorities != nil && shootStatus.Credentials.Rotation.CertificateAuthorities.LastInitiationTime != nil {
-			for _, config := range caCertConfigurations(b.Shoot.IsWorkerless, b.Shoot.RunsControlPlane()) {
+			for _, config := range caCertConfigurations(b.Shoot.IsWorkerless, b.Shoot.IsAutonomous()) {
 				rotation[config.GetName()] = shootStatus.Credentials.Rotation.CertificateAuthorities.LastInitiationTime.Time
 			}
 			// The static token secret contains token for the health check of the kube-apiserver.
@@ -131,7 +131,7 @@ func (b *Botanist) restoreSecretsFromShootStateForSecretsManagerAdoption(ctx con
 	return flow.Parallel(fns...)(ctx)
 }
 
-func caCertConfigurations(isWorkerless, runsControlPlane bool) []secretsutils.ConfigInterface {
+func caCertConfigurations(isWorkerless, isAutonomous bool) []secretsutils.ConfigInterface {
 	certificateSecretConfigs := []secretsutils.ConfigInterface{
 		// The CommonNames for CA certificates will be overridden with the secret name by the secrets manager when
 		// generated to ensure that each CA has a unique common name. For backwards-compatibility, we still keep the
@@ -150,7 +150,7 @@ func caCertConfigurations(isWorkerless, runsControlPlane bool) []secretsutils.Co
 			&secretsutils.CertificateSecretConfig{Name: v1beta1constants.SecretNameCAMetricsServer, CommonName: "metrics-server", CertType: secretsutils.CACert},
 		)
 
-		if !runsControlPlane {
+		if !isAutonomous {
 			certificateSecretConfigs = append(certificateSecretConfigs,
 				&secretsutils.CertificateSecretConfig{Name: v1beta1constants.SecretNameCAVPN, CommonName: "vpn", CertType: secretsutils.CACert},
 			)
@@ -192,7 +192,7 @@ func (b *Botanist) caCertGenerateOptionsFor(configName string) []secretsmanager.
 func (b *Botanist) generateCertificateAuthorities(ctx context.Context) error {
 	var caClientSecret *corev1.Secret
 
-	for _, config := range caCertConfigurations(b.Shoot.IsWorkerless, b.Shoot.RunsControlPlane()) {
+	for _, config := range caCertConfigurations(b.Shoot.IsWorkerless, b.Shoot.IsAutonomous()) {
 		caSecret, err := b.SecretsManager.Generate(ctx, config, b.caCertGenerateOptionsFor(config.GetName())...)
 		if err != nil {
 			return err
diff --git a/pkg/gardenlet/operation/shoot/shoot.go b/pkg/gardenlet/operation/shoot/shoot.go
index 7c27bdc7bdb..05f938b827a 100644
--- a/pkg/gardenlet/operation/shoot/shoot.go
+++ b/pkg/gardenlet/operation/shoot/shoot.go
@@ -662,7 +662,14 @@ func copyUniqueCIDRs(src []string, dst []net.IPNet, networkType string) ([]net.I
 	return dst, nil
 }
 
-// RunsControlPlane returns true in case the Kubernetes control plane runs inside the shoot cluster.
+// IsAutonomous returns true in case of an autonomous shoot cluster.
+func (s *Shoot) IsAutonomous() bool {
+	return v1beta1helper.IsShootAutonomous(s.GetInfo())
+}
+
+// RunsControlPlane returns true in case the Kubernetes control plane runs inside the cluster.
+// In contrast to IsAutonomous, this function returns false when bootstrapping autonomous shoot clusters using
+// `gardenadm bootstrap` (medium-touch scenario).
 func (s *Shoot) RunsControlPlane() bool {
 	return s.ControlPlaneNamespace == metav1.NamespaceSystem
 }
diff --git a/pkg/gardenlet/operation/shoot/shoot_test.go b/pkg/gardenlet/operation/shoot/shoot_test.go
index 3575a5460e7..e4bf9cc33bf 100644
--- a/pkg/gardenlet/operation/shoot/shoot_test.go
+++ b/pkg/gardenlet/operation/shoot/shoot_test.go
@@ -265,6 +265,35 @@ var _ = Describe("shoot", func() {
 			})
 		})
 
+		Describe("#IsAutonomous", func() {
+			It("should return true", func() {
+				shoot.SetInfo(&gardencorev1beta1.Shoot{
+					Spec: gardencorev1beta1.ShootSpec{
+						Provider: gardencorev1beta1.Provider{
+							Workers: []gardencorev1beta1.Worker{{
+								Name:         "control-plane",
+								ControlPlane: &gardencorev1beta1.WorkerControlPlane{},
+							}},
+						},
+					},
+				})
+				Expect(shoot.IsAutonomous()).To(BeTrue())
+			})
+
+			It("should return false", func() {
+				shoot.SetInfo(&gardencorev1beta1.Shoot{
+					Spec: gardencorev1beta1.ShootSpec{
+						Provider: gardencorev1beta1.Provider{
+							Workers: []gardencorev1beta1.Worker{{
+								Name: "worker",
+							}},
+						},
+					},
+				})
+				Expect(shoot.IsAutonomous()).To(BeFalse())
+			})
+		})
+
 		Describe("#RunsControlPlane", func() {
 			It("should return true", func() {
 				Expect((&Shoot{ControlPlaneNamespace: "kube-system"}).RunsControlPlane()).To(BeTrue())
diff --git a/pkg/utils/gardener/shoot.go b/pkg/utils/gardener/shoot.go
index dffa6213c0b..0dbc82a2047 100644
--- a/pkg/utils/gardener/shoot.go
+++ b/pkg/utils/gardener/shoot.go
@@ -670,12 +670,8 @@ func ComputeRequiredExtensionsForShoot(shoot *gardencorev1beta1.Shoot, seed *gar
 
 	if !v1beta1helper.IsWorkerless(shoot) {
 		requiredExtensions.Insert(ExtensionsID(extensionsv1alpha1.ControlPlaneResource, shoot.Spec.Provider.Type))
-
-		// TODO(timebertt): in the medium-touch scenario we require Infrastructure and Worker extensions too
-		if !v1beta1helper.IsShootAutonomous(shoot) {
-			requiredExtensions.Insert(ExtensionsID(extensionsv1alpha1.InfrastructureResource, shoot.Spec.Provider.Type))
-			requiredExtensions.Insert(ExtensionsID(extensionsv1alpha1.WorkerResource, shoot.Spec.Provider.Type))
-		}
+		requiredExtensions.Insert(ExtensionsID(extensionsv1alpha1.InfrastructureResource, shoot.Spec.Provider.Type))
+		requiredExtensions.Insert(ExtensionsID(extensionsv1alpha1.WorkerResource, shoot.Spec.Provider.Type))
 
 		if shoot.Spec.Networking != nil && shoot.Spec.Networking.Type != nil {
 			requiredExtensions.Insert(ExtensionsID(extensionsv1alpha1.NetworkResource, *shoot.Spec.Networking.Type))
diff --git a/pkg/utils/gardener/shoot_test.go b/pkg/utils/gardener/shoot_test.go
index c6dfbc89748..be6d2650b78 100644
--- a/pkg/utils/gardener/shoot_test.go
+++ b/pkg/utils/gardener/shoot_test.go
@@ -1286,21 +1286,6 @@ var _ = Describe("Shoot", func() {
 			)))
 		})
 
-		It("should compute the correct list of required extensions (autonomous shoot)", func() {
-			shoot.Spec.Provider.Workers[0].ControlPlane = &gardencorev1beta1.WorkerControlPlane{}
-
-			Expect(ComputeRequiredExtensionsForShoot(shoot, nil, controllerRegistrationList, internalDomain, externalDomain)).To(Equal(sets.New(
-				ExtensionsID(extensionsv1alpha1.ControlPlaneResource, shootProvider),
-				ExtensionsID(extensionsv1alpha1.NetworkResource, networkingType),
-				ExtensionsID(extensionsv1alpha1.ExtensionResource, extensionType1),
-				ExtensionsID(extensionsv1alpha1.OperatingSystemConfigResource, oscType),
-				ExtensionsID(extensionsv1alpha1.ContainerRuntimeResource, containerRuntimeType),
-				ExtensionsID(extensionsv1alpha1.DNSRecordResource, dnsProviderType1),
-				ExtensionsID(extensionsv1alpha1.DNSRecordResource, dnsProviderType2),
-				ExtensionsID(extensionsv1alpha1.ExtensionResource, extensionType2),
-			)))
-		})
-
 		It("should compute the correct list of required extensions (workerless Shoot and globally enabled extension)", func() {
 			shoot.Spec.Extensions = []gardencorev1beta1.Extension{}
 			shoot.Spec.Provider.Workers = nil
diff --git a/test/e2e/gardenadm/mediumtouch/exec.go b/test/e2e/gardenadm/mediumtouch/exec.go
index 5a29bbdb72c..7bd504a93f1 100644
--- a/test/e2e/gardenadm/mediumtouch/exec.go
+++ b/test/e2e/gardenadm/mediumtouch/exec.go
@@ -5,12 +5,16 @@
 package mediumtouch
 
 import (
+	"context"
 	"os/exec"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/onsi/gomega/gexec"
+	"k8s.io/client-go/tools/clientcmd"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/gardener/gardener/pkg/utils/imagevector"
 )
 
 var binaryPath string
@@ -28,7 +32,12 @@ func PrepareBinary() {
 
 // NewCommand creates a new exec.Cmd for gardenadm.
 func NewCommand(args ...string) *exec.Cmd { // #nosec G204 -- Used for e2e tests only.
-	return exec.Command(binaryPath, args...)
+	cmd := exec.Command(binaryPath, args...)
+	cmd.Env = append(cmd.Env,
+		clientcmd.RecommendedConfigPathEnvVar+"=../../../example/gardener-local/kind/local/kubeconfig",
+		imagevector.OverrideEnv+"=../../../example/gardenadm-local/.imagevector-overwrite.yaml",
+	)
+	return cmd
 }
 
 // RunCommand runs the given exec.Cmd and returns the gexec.Session.
@@ -46,10 +55,10 @@ func RunCommand(cmd *exec.Cmd) *gexec.Session {
 }
 
 // Wait waits for the given gexec.Session to finish and returns the session.
-func Wait(session *gexec.Session) *gexec.Session {
+func Wait(ctx context.Context, session *gexec.Session) *gexec.Session {
 	GinkgoHelper()
 
-	Eventually(session).Should(gexec.Exit(0))
+	Eventually(ctx, session).Should(gexec.Exit(0))
 	return session
 }
 
@@ -59,6 +68,6 @@ func Run(args ...string) *gexec.Session {
 }
 
 // RunAndWait runs gardenadm with the given arguments and waits for the session to finish.
-func RunAndWait(args ...string) *gexec.Session {
-	return Wait(Run(args...))
+func RunAndWait(ctx context.Context, args ...string) *gexec.Session {
+	return Wait(ctx, Run(args...))
 }
diff --git a/test/e2e/gardenadm/mediumtouch/gardenadm.go b/test/e2e/gardenadm/mediumtouch/gardenadm.go
index c284fb6e0cf..a103742fcb9 100644
--- a/test/e2e/gardenadm/mediumtouch/gardenadm.go
+++ b/test/e2e/gardenadm/mediumtouch/gardenadm.go
@@ -18,10 +18,10 @@ var _ = Describe("gardenadm medium-touch scenario tests", Label("gardenadm", "me
 	}, NodeTimeout(time.Minute))
 
 	Describe("Prepare infrastructure and machines", Ordered, func() {
-		It("should bootstrap the machine pods", func(SpecContext) {
-			Eventually(RunAndWait(
+		It("should bootstrap the machine pods", func(ctx SpecContext) {
+			Eventually(RunAndWait(ctx,
 				"bootstrap", "-d", "../../../example/gardenadm-local/medium-touch",
 			).Err).Should(gbytes.Say("work in progress"))
-		}, SpecTimeout(time.Minute))
+		}, SpecTimeout(2*time.Minute))
 	})
 })


Reviews:
user: login: ScheererJ
body: Nice to see the medium touch case of autonomous shoot clusters taking more and more shape.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T15:16:33Z
user: login: timebertt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:19:18Z
user: login: ScheererJ
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T07:13:04Z
user: login: rfranzke
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T07:19:31Z
user: login: timebertt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T07:52:36Z
user: login: rfranzke
body: /lgtm
/approve
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T08:34:36Z

Review comments:
diff_hunk: @@ -240,7 +253,15 @@ func newShootObject(ctx context.Context, projectName string, cloudProfile *garde
 		return nil, fmt.Errorf("failed computing shoot networks: %w", err)
 	}
 
-	obj.ControlPlaneNamespace = metav1.NamespaceSystem
+	// In autonomous shoot clusters, kube-system is used as the control plane namespace.
+	// However, when bootstrapping an autonomous shoot cluster with `gardenadm bootstrap` using a temporary local cluster,
+	// we want to avoid conflicts with kube-system components of the bootstrap cluster by placing all shoot-related
+	// components in another namespace. In this case, we use the technical ID as the control plane namespace, as usual.
+	// TODO(timebertt): double-check if this causes problems when importing the state into the autonomous shoot cluster
+	if !runsControlPlane {
+		obj.ControlPlaneNamespace = shoot.Status.TechnicalID
+	}
path: pkg/gardenadm/botanist/botanist.go
user: login: ScheererJ
body: I understand that you want to use the technical ID in the medium touch case, but I do not understand the removal of the `kube-system` assignment before the `if` block. It seems to me that we want `kube-system` as default and overwrite it with the technical ID in case the shoot currently does not run the control plane.
created_at: 2025-05-12T15:10:56Z
author_association: MEMBER
diff_hunk: @@ -240,7 +253,15 @@ func newShootObject(ctx context.Context, projectName string, cloudProfile *garde
 		return nil, fmt.Errorf("failed computing shoot networks: %w", err)
 	}
 
-	obj.ControlPlaneNamespace = metav1.NamespaceSystem
+	// In autonomous shoot clusters, kube-system is used as the control plane namespace.
+	// However, when bootstrapping an autonomous shoot cluster with `gardenadm bootstrap` using a temporary local cluster,
+	// we want to avoid conflicts with kube-system components of the bootstrap cluster by placing all shoot-related
+	// components in another namespace. In this case, we use the technical ID as the control plane namespace, as usual.
+	// TODO(timebertt): double-check if this causes problems when importing the state into the autonomous shoot cluster
+	if !runsControlPlane {
+		obj.ControlPlaneNamespace = shoot.Status.TechnicalID
+	}
path: pkg/gardenadm/botanist/botanist.go
user: login: timebertt
body: `ControlPlaneNamespace` is assigned `kube-system` for autonomous shoot clusters here:
https://github.com/gardener/gardener/blob/30a37320badd4a8190b26f1271411e36fcaaef0d/pkg/gardenlet/operation/shoot/shoot.go#L227

That's why I removed the `kube-system` assignment in this file.
However, we could also assign the technical ID in `shoot.go` and overwrite it with `kube-system` in this file, for autonomous shoot clusters if `runsControlPlane==true`. Would that be more intuitive?
created_at: 2025-05-12T18:19:17Z
author_association: MEMBER
diff_hunk: @@ -240,7 +253,15 @@ func newShootObject(ctx context.Context, projectName string, cloudProfile *garde
 		return nil, fmt.Errorf("failed computing shoot networks: %w", err)
 	}
 
-	obj.ControlPlaneNamespace = metav1.NamespaceSystem
+	// In autonomous shoot clusters, kube-system is used as the control plane namespace.
+	// However, when bootstrapping an autonomous shoot cluster with `gardenadm bootstrap` using a temporary local cluster,
+	// we want to avoid conflicts with kube-system components of the bootstrap cluster by placing all shoot-related
+	// components in another namespace. In this case, we use the technical ID as the control plane namespace, as usual.
+	// TODO(timebertt): double-check if this causes problems when importing the state into the autonomous shoot cluster
+	if !runsControlPlane {
+		obj.ControlPlaneNamespace = shoot.Status.TechnicalID
+	}
path: pkg/gardenadm/botanist/botanist.go
user: login: ScheererJ
body: Thanks for the clarification.
I think that it is hard to make it completely intuitive as attributes are changed at multiple locations. My primary confusion cam from the fact that you removed the assignment to `kube-system`, which appeared to become redundant at some time.
Let's leave it as is for now.
created_at: 2025-05-13T07:13:04Z
author_association: MEMBER
diff_hunk: @@ -56,11 +57,19 @@ func (b *Botanist) DefaultResourceManager() (resourcemanager.Interface, error) {
 		}
 	)
 
-	if b.Shoot.RunsControlPlane() {
-		newFunc = shared.NewCombinedGardenerResourceManager
-		values.NodeAgentAuthorizerEnabled = false // TODO(rfranzke): Revisit this once autonomous shoot clusters progress.
-		values.TargetNamespaces = nil
-		values.KubernetesServiceHost = nil
+	if b.Shoot.IsAutonomous() {
+		if b.Shoot.RunsControlPlane() {
+			newFunc = shared.NewCombinedGardenerResourceManager
+			values.NodeAgentAuthorizerEnabled = false // TODO(rfranzke): Revisit this once autonomous shoot clusters progress.
+			values.TargetNamespaces = nil
+			values.KubernetesServiceHost = nil
+		} else {
+			newFunc = shared.NewRuntimeGardenerResourceManager
+			// TODO(timebertt): consider disabling the highavailabilityconfig webhook
+			values.PriorityClassName = v1beta1constants.PriorityClassNameSeedSystemCritical
+			values.NodeAgentAuthorizerEnabled = false
+			values.KubernetesServiceHost = nil
+		}
path: pkg/gardenlet/operation/botanist/resource_manager.go
user: login: rfranzke
body: Nit: You could move 

```golang
values.NodeAgentAuthorizerEnabled = false // TODO(rfranzke): Revisit this once autonomous shoot clusters progress.
values.KubernetesServiceHost = nil
```

out to not repeat it 
created_at: 2025-05-13T07:18:55Z
author_association: MEMBER
diff_hunk: @@ -56,11 +57,19 @@ func (b *Botanist) DefaultResourceManager() (resourcemanager.Interface, error) {
 		}
 	)
 
-	if b.Shoot.RunsControlPlane() {
-		newFunc = shared.NewCombinedGardenerResourceManager
-		values.NodeAgentAuthorizerEnabled = false // TODO(rfranzke): Revisit this once autonomous shoot clusters progress.
-		values.TargetNamespaces = nil
-		values.KubernetesServiceHost = nil
+	if b.Shoot.IsAutonomous() {
+		if b.Shoot.RunsControlPlane() {
+			newFunc = shared.NewCombinedGardenerResourceManager
+			values.NodeAgentAuthorizerEnabled = false // TODO(rfranzke): Revisit this once autonomous shoot clusters progress.
+			values.TargetNamespaces = nil
+			values.KubernetesServiceHost = nil
+		} else {
+			newFunc = shared.NewRuntimeGardenerResourceManager
+			// TODO(timebertt): consider disabling the highavailabilityconfig webhook
+			values.PriorityClassName = v1beta1constants.PriorityClassNameSeedSystemCritical
+			values.NodeAgentAuthorizerEnabled = false
+			values.KubernetesServiceHost = nil
+		}
path: pkg/gardenlet/operation/botanist/resource_manager.go
user: login: timebertt
body: Done
created_at: 2025-05-13T07:52:36Z
author_association: MEMBER

PR comments:
user: login: ScheererJ
created_at: 2025-05-12T15:06:14Z
body: /assign
author_association: MEMBER
user: login: ScheererJ
created_at: 2025-05-13T07:13:10Z
body: /lgtm
author_association: MEMBER
user: login: gardener-prow[bot]
created_at: 2025-05-13T07:13:15Z
body: LGTM label has been added.  <details>Git tree hash: df25678d32e3df793e9d8c2ab74372ab1afd9d21</details>
author_association: CONTRIBUTOR
user: login: gardener-prow[bot]
created_at: 2025-05-13T08:34:41Z
body: LGTM label has been added.  <details>Git tree hash: 2bd90abf0c384c4ebaf9c9c71710bcb6b7129e2e</details>
author_association: CONTRIBUTOR
user: login: gardener-prow[bot]
created_at: 2025-05-13T08:34:44Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/gardener/gardener/pull/12052#pullrequestreview-2835795887" title="Approved">rfranzke</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)~~ [rfranzke]

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