diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon
index 1f65fab763bdc1a2dbc175ffb2b94cf3997dc57b..6fccf167dab6f8b68a85db038aef187a2affde20 100644
--- a/Build/phpstan/phpstan-baseline.neon
+++ b/Build/phpstan/phpstan-baseline.neon
@@ -2880,11 +2880,6 @@ parameters:
 			count: 1
 			path: ../../typo3/sysext/indexed_search/Classes/Utility/DoubleMetaPhoneUtility.php
 
-		-
-			message: "#^Property TYPO3\\\\CMS\\\\Install\\\\Command\\\\UpgradeWizardListCommand\\:\\:\\$input is never read, only written\\.$#"
-			count: 1
-			path: ../../typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
-
 		-
 			message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Authentication\\\\AbstractAuthenticationService\\:\\:authUser\\(\\)\\.$#"
 			count: 1
diff --git a/typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99586-RegistrationOfUpgradeWizardsViaGLOBALS.rst b/typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99586-RegistrationOfUpgradeWizardsViaGLOBALS.rst
new file mode 100644
index 0000000000000000000000000000000000000000..aab395ecdcff0127e44970b7ce9b327a02d5679a
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.2/Deprecation-99586-RegistrationOfUpgradeWizardsViaGLOBALS.rst
@@ -0,0 +1,76 @@
+.. include:: /Includes.rst.txt
+
+.. _deprecation-99586-1673990657:
+
+==================================================================
+Deprecation: #99586 - Registration of upgrade wizards via $GLOBALS
+==================================================================
+
+See :issue:`99586`
+
+Description
+===========
+
+Registration of upgrade wizards via :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']`,
+usually placed in an extensions :file:`ext_localconf.php` has been deprecated
+in favour of the :ref:`new service tag <feature-99586-1673989775>`.
+
+Additionally, the :php:`UpgradeWizardInterface`, which all upgrade wizards must
+implement, does no longer require the :php:`getIdentifier()` method. TYPO3 does
+not use this method anymore since an upgrade wizards identifier is now
+defined using the new service tag.
+
+
+Impact
+======
+
+Upgrade wizards, registered via :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']`
+will no longer be recognized with TYPO3 v13.
+
+Definition of the :php:`getIdentifier()` method does no longer have any effect.
+
+
+Affected installations
+======================
+
+All installations registering custom upgrade wizards using
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']`.
+
+All installations implementing the :php:`getIdentifier()` method in their
+upgrade wizards.
+
+
+Migration
+=========
+
+Use the new service tag to register custom upgrade wizards and remove the
+registration via :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']`.
+
+Before
+~~~~~~
+
+..  code-block:: php
+
+    // ext_localconf.php
+
+    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['myUpgradeWizard'] = \Vendor\Extension\Updates\MyUpgradeWizard::class;
+
+After
+~~~~~
+
+..  code-block:: php
+
+    // Classes/Updates/MyUpgradeWizard.php
+
+    use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+    use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+    #[UpgradeWizard('myUpgradeWizard')]
+    class MyUpgradeWizard implements UpgradeWizardInterface
+    {
+
+    }
+
+Drop any :php:`getIdentifier()` method in custom upgrade wizards.
+
+.. index:: Backend, PHP-API, FullyScanned, ext:install
diff --git a/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99586-RegistrationOfUpgradeWizardsViaServiceTag.rst b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99586-RegistrationOfUpgradeWizardsViaServiceTag.rst
new file mode 100644
index 0000000000000000000000000000000000000000..78350d2ca7bddf604ecae62d04ae0c0c8796740e
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-99586-RegistrationOfUpgradeWizardsViaServiceTag.rst
@@ -0,0 +1,50 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-99586-1673989775:
+
+=================================================================
+Feature: #99586 - Registration of upgrade wizards via service tag
+=================================================================
+
+See :issue:`99586`
+
+Description
+===========
+
+Upgrade wizards are usually used to execute one time migrations when
+updating a TYPO3 installation. The registration was previously done
+in an extensions :php:`ext_localconf.php` file. This has now been
+improved by introducing the custom PHP attribute
+:php:`TYPO3\CMS\Install\Attribute\UpgradeWizard`. All upgrade wizards,
+defining the new attribute, are automatically tagged and registered
+in the service container. The registration via
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']`
+has been deprecated.
+
+The registration of an upgrade wizard is therefore now be done
+directly in the class by adding the new attribute with the upgrade
+wizards' unique identifier as constructor argument:
+
+..  code-block:: php
+
+    use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+    use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+    #[UpgradeWizard('myUpgradeWizard')]
+    class MyUpgradeWizard implements UpgradeWizardInterface
+    {
+
+    }
+
+.. note::
+
+    All upgrade wizards have to implement the :php:`UpgradeWizardInterface`.
+
+Impact
+======
+
+It's now possible to tag upgrade wizards with the PHP attribute
+:php:`TYPO3\CMS\Install\Attribute\UpgradeWizard` to have them
+auto-configured and auto-registered.
+
+.. index:: Backend, PHP-API, ext:install
diff --git a/typo3/sysext/install/Classes/Attribute/UpgradeWizard.php b/typo3/sysext/install/Classes/Attribute/UpgradeWizard.php
new file mode 100644
index 0000000000000000000000000000000000000000..8faaa5a57701019fc87fb3de912d7e11c4297ce5
--- /dev/null
+++ b/typo3/sysext/install/Classes/Attribute/UpgradeWizard.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Install\Attribute;
+
+use Attribute;
+
+/**
+ * Service tag to autoconfigure upgrade wizards
+ */
+#[Attribute(Attribute::TARGET_CLASS)]
+class UpgradeWizard
+{
+    public const TAG_NAME = 'install.upgradewizard';
+
+    public function __construct(
+        public string $identifier
+    ) {
+    }
+}
diff --git a/typo3/sysext/install/Classes/Command/SetupCommand.php b/typo3/sysext/install/Classes/Command/SetupCommand.php
index 1d4036f54f30aa811ba0bc6aff14889bd53794c5..3657f54d556227bcacf1ff26e7504a73b9f2af6e 100644
--- a/typo3/sysext/install/Classes/Command/SetupCommand.php
+++ b/typo3/sysext/install/Classes/Command/SetupCommand.php
@@ -32,6 +32,7 @@ use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
 use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\SetupDatabaseService;
 use TYPO3\CMS\Install\Service\SetupService;
 
@@ -52,6 +53,7 @@ class SetupCommand extends Command
     protected SetupDatabaseService $setupDatabaseService;
     protected SetupService $setupService;
     protected ConfigurationManager $configurationManager;
+    protected LateBootService $lateBootService;
     protected OutputInterface $output;
     protected InputInterface $input;
     protected QuestionHelper $questionHelper;
@@ -61,10 +63,12 @@ class SetupCommand extends Command
         SetupDatabaseService $setupDatabaseService,
         SetupService $setupService,
         ConfigurationManager $configurationManager,
+        LateBootService $lateBootService
     ) {
         $this->setupDatabaseService = $setupDatabaseService;
         $this->setupService = $setupService;
         $this->configurationManager = $configurationManager;
+        $this->lateBootService = $lateBootService;
         parent::__construct($name);
     }
 
@@ -250,7 +254,8 @@ EOT
             $this->setupService->createSiteConfiguration('main', (int)$pageUid, $siteUrl);
         }
 
-        $this->setupDatabaseService->markWizardsDone();
+        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
+        $this->setupDatabaseService->markWizardsDone($container);
         $this->writeSuccess('Congratulations - TYPO3 Setup is done.');
 
         return Command::SUCCESS;
diff --git a/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php b/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
index 46b1b3f40ab19320801d255292e2fed9af5f4c4f..a0e7f4181c74c873dbc1bc97a99e41fa57a613f1 100644
--- a/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
+++ b/typo3/sysext/install/Classes/Command/UpgradeWizardListCommand.php
@@ -24,7 +24,6 @@ use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
 use TYPO3\CMS\Core\Core\Bootstrap;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
 use TYPO3\CMS\Install\Updates\ChattyInterface;
@@ -44,11 +43,6 @@ class UpgradeWizardListCommand extends Command
      */
     private $output;
 
-    /**
-     * @var InputInterface
-     */
-    private $input;
-
     public function __construct(string $name, private readonly LateBootService $lateBootService)
     {
         parent::__construct($name);
@@ -86,16 +80,15 @@ class UpgradeWizardListCommand extends Command
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
         $this->output = new SymfonyStyle($input, $output);
-        $this->input = $input;
         $this->bootstrap();
 
         $wizards = [];
         $all = $input->getOption('all');
-        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $wizardToExecute) {
-            $upgradeWizard = $this->getWizard($wizardToExecute, $identifier, (bool)$all);
+        foreach ($this->upgradeWizardsService->getUpgradeWizardIdentifiers() as $identifier) {
+            $upgradeWizard = $this->getWizard($identifier, (bool)$all);
             if ($upgradeWizard !== null) {
                 $wizardInfo = [
-                    'identifier' => $upgradeWizard->getIdentifier(),
+                    'identifier' => $identifier,
                     'title' => $upgradeWizard->getTitle(),
                     'description' => wordwrap($upgradeWizard->getDescription()),
                 ];
@@ -107,38 +100,34 @@ class UpgradeWizardListCommand extends Command
         }
         if (empty($wizards)) {
             $this->output->success('No wizards available.');
+        } elseif ($all === true) {
+            $this->output->table(['Identifier', 'Title', 'Description', 'Status'], $wizards);
         } else {
-            if ($all === true) {
-                $this->output->table(['Identifier', 'Title', 'Description', 'Status'], $wizards);
-            } else {
-                $this->output->table(['Identifier', 'Title', 'Description'], $wizards);
-            }
+            $this->output->table(['Identifier', 'Title', 'Description'], $wizards);
         }
         return Command::SUCCESS;
     }
 
     /**
-     * Get Wizard instance by class name and identifier
+     * Get Wizard instance by identifier
      * Returns null if wizard is already done
-     *
-     * @param bool $all
      */
-    protected function getWizard(string $className, string $identifier, $all = false): ?UpgradeWizardInterface
+    protected function getWizard(string $identifier, bool $all = false): ?UpgradeWizardInterface
     {
         // already done
         if (!$all && $this->upgradeWizardsService->isWizardDone($identifier)) {
             return null;
         }
 
-        $wizardInstance = GeneralUtility::makeInstance($className);
-        if ($wizardInstance instanceof ChattyInterface) {
-            $wizardInstance->setOutput($this->output);
+        $wizard = $this->upgradeWizardsService->getUpgradeWizard($identifier);
+        if ($wizard === null) {
+            return null;
         }
 
-        if (!($wizardInstance instanceof UpgradeWizardInterface)) {
-            return null;
+        if ($wizard instanceof ChattyInterface) {
+            $wizard->setOutput($this->output);
         }
 
-        return !$all ? $wizardInstance->updateNecessary() ? $wizardInstance : null : $wizardInstance;
+        return !$all ? $wizard->updateNecessary() ? $wizard : null : $wizard;
     }
 }
diff --git a/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php b/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
index bad921f19202c3321fc41f55b67365e9f057bb4f..48cf2d81f4db64bef431a26e68b2fc9b597a42b1 100644
--- a/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
+++ b/typo3/sysext/install/Classes/Command/UpgradeWizardRunCommand.php
@@ -103,20 +103,15 @@ class UpgradeWizardRunCommand extends Command
         $this->input = $input;
         $this->bootstrap();
 
-        $result = Command::SUCCESS;
         if ($input->getArgument('wizardName')) {
             $wizardToExecute = $input->getArgument('wizardName');
             $wizardToExecute = is_string($wizardToExecute) ? $wizardToExecute : '';
-            if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardToExecute])) {
-                $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardToExecute];
-                $upgradeWizard = $this->getWizard($className, $wizardToExecute);
-                if ($upgradeWizard !== null) {
-                    $prerequisitesFulfilled = $this->handlePrerequisites([$upgradeWizard]);
-                    if ($prerequisitesFulfilled === true) {
-                        $result = $this->runSingleWizard($upgradeWizard);
-                    } else {
-                        $result = Command::FAILURE;
-                    }
+            if (($upgradeWizard = $this->getWizard($wizardToExecute)) !== null) {
+                $prerequisitesFulfilled = $this->handlePrerequisites([$upgradeWizard]);
+                if ($prerequisitesFulfilled === true) {
+                    $result = $this->runSingleWizard($upgradeWizard);
+                } else {
+                    $result = Command::FAILURE;
                 }
             } else {
                 $this->output->error('No such wizard: ' . $wizardToExecute);
@@ -132,36 +127,30 @@ class UpgradeWizardRunCommand extends Command
      * Get Wizard instance by class name and identifier
      * Returns null if wizard is already done
      */
-    protected function getWizard(string $className, string $identifier): ?UpgradeWizardInterface
+    protected function getWizard(string $identifier): ?UpgradeWizardInterface
     {
         // already done
         if ($this->upgradeWizardsService->isWizardDone($identifier)) {
             return null;
         }
 
-        $wizardInstance = GeneralUtility::makeInstance($className);
-        if ($wizardInstance instanceof ChattyInterface) {
-            $wizardInstance->setOutput($this->output);
+        $wizard = $this->upgradeWizardsService->getUpgradeWizard($identifier);
+        if ($wizard === null) {
+            return null;
         }
 
-        if (!($wizardInstance instanceof UpgradeWizardInterface)) {
-            $this->output->writeln(
-                'Wizard ' .
-                $identifier .
-                ' needs to be manually run from the install tool, as it does not implement ' .
-                UpgradeWizardInterface::class
-            );
-            return null;
+        if ($wizard instanceof ChattyInterface) {
+            $wizard->setOutput($this->output);
         }
 
-        if ($wizardInstance->updateNecessary()) {
-            return $wizardInstance;
+        if ($wizard->updateNecessary()) {
+            return $wizard;
         }
-        if ($wizardInstance instanceof RepeatableInterface) {
+        if ($wizard instanceof RepeatableInterface) {
             $this->output->note('Wizard ' . $identifier . ' does not need to make changes.');
         } else {
             $this->output->note('Wizard ' . $identifier . ' does not need to make changes. Marking wizard as done.');
-            $this->upgradeWizardsService->markWizardAsDone($identifier);
+            $this->upgradeWizardsService->markWizardAsDone($wizard);
         }
         return null;
     }
@@ -234,7 +223,7 @@ class UpgradeWizardRunCommand extends Command
                 if ($instance instanceof RepeatableInterface) {
                     $this->output->note('No changes applied.');
                 } else {
-                    $this->upgradeWizardsService->markWizardAsDone($instance->getIdentifier());
+                    $this->upgradeWizardsService->markWizardAsDone($instance);
                     $this->output->note('No changes applied, marking wizard as done.');
                 }
                 return Command::SUCCESS;
@@ -243,7 +232,7 @@ class UpgradeWizardRunCommand extends Command
         if ($instance->executeUpdate()) {
             $this->output->success('Successfully ran wizard ' . $instance->getTitle());
             if (!$instance instanceof RepeatableInterface) {
-                $this->upgradeWizardsService->markWizardAsDone($instance->getIdentifier());
+                $this->upgradeWizardsService->markWizardAsDone($instance);
             }
             return Command::SUCCESS;
         }
@@ -260,8 +249,8 @@ class UpgradeWizardRunCommand extends Command
     {
         $returnCode = Command::SUCCESS;
         $wizardInstances = [];
-        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $class) {
-            $wizardInstances[] = $this->getWizard($class, $identifier);
+        foreach ($this->upgradeWizardsService->getUpgradeWizardIdentifiers() as $identifier) {
+            $wizardInstances[] = $this->getWizard($identifier);
         }
         $wizardInstances = array_filter($wizardInstances);
         if (count($wizardInstances) > 0) {
diff --git a/typo3/sysext/install/Classes/Controller/InstallerController.php b/typo3/sysext/install/Classes/Controller/InstallerController.php
index a9c2246817d764eab04e78c86def3e3bdea24e1a..5578af9193459509a803376bb06b6aa374ba2753 100644
--- a/typo3/sysext/install/Classes/Controller/InstallerController.php
+++ b/typo3/sysext/install/Classes/Controller/InstallerController.php
@@ -636,7 +636,7 @@ final class InstallerController
         }
 
         // Mark upgrade wizards as done
-        $this->setupDatabaseService->markWizardsDone();
+        $this->setupDatabaseService->markWizardsDone($container);
 
         $this->configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues);
 
diff --git a/typo3/sysext/install/Classes/Service/SetupDatabaseService.php b/typo3/sysext/install/Classes/Service/SetupDatabaseService.php
index 48bbcbaa3ec0547939c0c65bee43e07086d60d31..7a79cc25467cd509e93483e624887b91f6bf8092 100644
--- a/typo3/sysext/install/Classes/Service/SetupDatabaseService.php
+++ b/typo3/sysext/install/Classes/Service/SetupDatabaseService.php
@@ -20,6 +20,7 @@ namespace TYPO3\CMS\Install\Service;
 use Doctrine\DBAL\DriverManager;
 use Doctrine\DBAL\Exception as DBALException;
 use Doctrine\DBAL\Exception\ConnectionException;
+use Psr\Container\ContainerInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Crypto\Random;
@@ -41,7 +42,6 @@ use TYPO3\CMS\Install\Configuration\Exception;
 use TYPO3\CMS\Install\Database\PermissionsCheck;
 use TYPO3\CMS\Install\SystemEnvironment\DatabaseCheck;
 use TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard;
-use TYPO3\CMS\Install\Updates\RepeatableInterface;
 
 /**
  * Service class helping to manage database related settings and operations required to set up TYPO3
@@ -699,15 +699,10 @@ class SetupDatabaseService
         ]);
     }
 
-    public function markWizardsDone(): void
+    public function markWizardsDone(ContainerInterface $container): void
     {
-        // Mark upgrade wizards as done
-        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
-            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $updateClassName) {
-                if (!in_array(RepeatableInterface::class, class_implements($updateClassName) ?: [], true)) {
-                    $this->registry->set('installUpdate', $updateClassName, 1);
-                }
-            }
+        foreach ($container->get(UpgradeWizardsService::class)->getNonRepeatableUpgradeWizards() as $className) {
+            $this->registry->set('installUpdate', $className, 1);
         }
         $this->registry->set('installUpdateRows', 'rowUpdatersDone', GeneralUtility::makeInstance(DatabaseRowsUpdateWizard::class)->getAvailableRowUpdater());
     }
diff --git a/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php b/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
index eba3b2ed48ded75f1a5c6ce08053a0df2ccfe9fb..857539eb15efad021461ffff120422bfc39523d2 100644
--- a/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
+++ b/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
@@ -29,6 +29,7 @@ use TYPO3\CMS\Install\Updates\ConfirmableInterface;
 use TYPO3\CMS\Install\Updates\RepeatableInterface;
 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
 use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+use TYPO3\CMS\Install\Updates\UpgradeWizardRegistry;
 
 /**
  * Service class helping managing upgrade wizards
@@ -41,7 +42,7 @@ class UpgradeWizardsService
      */
     private $output;
 
-    public function __construct()
+    public function __construct(private readonly UpgradeWizardRegistry $upgradeWizardRegistry)
     {
         $fileName = 'php://temp';
         if (($stream = fopen($fileName, 'wb')) === false) {
@@ -57,13 +58,13 @@ class UpgradeWizardsService
     {
         $wizardsDoneInRegistry = [];
         $registry = GeneralUtility::makeInstance(Registry::class);
-        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
-            if ($registry->get('installUpdate', $className, false)) {
-                $wizardInstance = GeneralUtility::makeInstance($className);
+        foreach ($this->upgradeWizardRegistry->getUpgradeWizards() as $identifier => $serviceName) {
+            if ($registry->get('installUpdate', $serviceName, false)) {
                 $wizardsDoneInRegistry[] = [
-                    'class' => $className,
+                    'class' => $serviceName,
                     'identifier' => $identifier,
-                    'title' => $wizardInstance->getTitle(),
+                    // @todo fetching the service to get the title should be improved
+                    'title' => $this->upgradeWizardRegistry->getUpgradeWizard($identifier)->getTitle(),
                 ];
             }
         }
@@ -148,8 +149,7 @@ class UpgradeWizardsService
     public function getUpgradeWizardsList(): array
     {
         $wizards = [];
-        foreach (array_keys($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']) as $identifier) {
-            $identifier = (string)$identifier;
+        foreach (array_keys($this->upgradeWizardRegistry->getUpgradeWizards()) as $identifier) {
             if ($this->isWizardDone($identifier)) {
                 continue;
             }
@@ -161,31 +161,18 @@ class UpgradeWizardsService
 
     public function getWizardInformationByIdentifier(string $identifier): array
     {
-        if (class_exists($identifier)) {
-            $class = $identifier;
-        } else {
-            $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
-        }
-        /** @var UpgradeWizardInterface $wizardInstance */
-        $wizardInstance = GeneralUtility::makeInstance($class);
-        $explanation = '';
-
-        // $explanation is changed by reference in Update objects!
-        $shouldRenderWizard = false;
-        if ($wizardInstance instanceof UpgradeWizardInterface) {
-            if ($wizardInstance instanceof ChattyInterface) {
-                $wizardInstance->setOutput($this->output);
-            }
-            $shouldRenderWizard = $wizardInstance->updateNecessary();
-            $explanation = $wizardInstance->getDescription();
+        $wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
+
+        if ($wizard instanceof ChattyInterface) {
+            $wizard->setOutput($this->output);
         }
 
         return [
-            'class' => $class,
+            'class' => $wizard::class,
             'identifier' => $identifier,
-            'title' => $wizardInstance->getTitle(),
-            'shouldRenderWizard' => $shouldRenderWizard,
-            'explanation' => $explanation,
+            'title' => $wizard->getTitle(),
+            'shouldRenderWizard' => $wizard->updateNecessary(),
+            'explanation' => $wizard->getDescription(),
         ];
     }
 
@@ -198,31 +185,30 @@ class UpgradeWizardsService
     {
         $this->assertIdentifierIsValid($identifier);
 
-        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
-        $updateObject = GeneralUtility::makeInstance($class);
+        $wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
         $wizardHtml = '';
-        if ($updateObject instanceof UpgradeWizardInterface && $updateObject instanceof ConfirmableInterface) {
+        if ($wizard instanceof ConfirmableInterface) {
             $markup = [];
             $radioAttributes = [
                 'type' => 'radio',
                 'class' => 'btn-check',
-                'name' => 'install[values][' . $updateObject->getIdentifier() . '][install]',
+                'name' => 'install[values][' . $identifier . '][install]',
                 'value' => '0',
             ];
             $markup[] = '<div class="panel panel-danger">';
             $markup[] = '   <div class="panel-heading">';
-            $markup[] = htmlspecialchars($updateObject->getConfirmation()->getTitle());
+            $markup[] = htmlspecialchars($wizard->getConfirmation()->getTitle());
             $markup[] = '    </div>';
             $markup[] = '    <div class="panel-body">';
-            $markup[] = '        <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>';
+            $markup[] = '        <p>' . nl2br(htmlspecialchars($wizard->getConfirmation()->getMessage())) . '</p>';
             $markup[] = '        <div class="btn-group">';
-            if (!$updateObject->getConfirmation()->isRequired()) {
+            if (!$wizard->getConfirmation()->isRequired()) {
                 $markup[] = '        <input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' checked id="upgrade-wizard-deny">';
-                $markup[] = '        <label class="btn btn-default" for="upgrade-wizard-deny">' . $updateObject->getConfirmation()->getDeny() . '</label>';
+                $markup[] = '        <label class="btn btn-default" for="upgrade-wizard-deny">' . $wizard->getConfirmation()->getDeny() . '</label>';
             }
             $radioAttributes['value'] = '1';
             $markup[] = '            <input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' id="upgrade-wizard-confirm">';
-            $markup[] = '            <label class="btn btn-default" for="upgrade-wizard-confirm">' . $updateObject->getConfirmation()->getConfirm() . '</label>';
+            $markup[] = '            <label class="btn btn-default" for="upgrade-wizard-confirm">' . $wizard->getConfirmation()->getConfirm() . '</label>';
             $markup[] = '        </div>';
             $markup[] = '    </div>';
             $markup[] = '</div>';
@@ -231,8 +217,8 @@ class UpgradeWizardsService
 
         $result = [
             'identifier' => $identifier,
-            'title' => $updateObject->getTitle(),
-            'description' => $updateObject->getDescription(),
+            'title' => $wizard->getTitle(),
+            'description' => $wizard->getDescription(),
             'wizardHtml' => $wizardHtml,
         ];
 
@@ -249,47 +235,44 @@ class UpgradeWizardsService
         $performResult = false;
         $this->assertIdentifierIsValid($identifier);
 
-        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
-        $updateObject = GeneralUtility::makeInstance($class);
+        $wizard = $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
 
-        if ($updateObject instanceof ChattyInterface) {
-            $updateObject->setOutput($this->output);
+        if ($wizard instanceof ChattyInterface) {
+            $wizard->setOutput($this->output);
         }
         $messages = new FlashMessageQueue('install');
 
-        if ($updateObject instanceof UpgradeWizardInterface) {
-            $requestParams = GeneralUtility::_GP('install');
-            if ($updateObject instanceof ConfirmableInterface) {
-                // value is set in request but is empty
-                $isSetButEmpty = isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
-                    && empty($requestParams['values'][$updateObject->getIdentifier()]['install']);
-
-                $checkValue = (int)$requestParams['values'][$updateObject->getIdentifier()]['install'];
-
-                if ($checkValue === 1) {
-                    // confirmation = yes, we do the update
-                    $performResult = $updateObject->executeUpdate();
-                } elseif ($updateObject->getConfirmation()->isRequired()) {
-                    // confirmation = no, but is required, we do *not* the update and fail
-                    $performResult = false;
-                } elseif ($isSetButEmpty) {
-                    // confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
-                    $this->output->writeln('No changes applied, marking wizard as done.');
-                    // confirmation was set to "no"
-                    $performResult = true;
-                }
-            } else {
-                // confirmation yes or non-confirmable
-                $performResult = $updateObject->executeUpdate();
+        $requestParams = GeneralUtility::_GP('install');
+        if ($wizard instanceof ConfirmableInterface) {
+            // value is set in request but is empty
+            $isSetButEmpty = isset($requestParams['values'][$identifier]['install'])
+                && empty($requestParams['values'][$identifier]['install']);
+
+            $checkValue = (int)$requestParams['values'][$identifier]['install'];
+
+            if ($checkValue === 1) {
+                // confirmation = yes, we do the update
+                $performResult = $wizard->executeUpdate();
+            } elseif ($wizard->getConfirmation()->isRequired()) {
+                // confirmation = no, but is required, we do *not* the update and fail
+                $performResult = false;
+            } elseif ($isSetButEmpty) {
+                // confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
+                $this->output->writeln('No changes applied, marking wizard as done.');
+                // confirmation was set to "no"
+                $performResult = true;
             }
+        } else {
+            // confirmation yes or non-confirmable
+            $performResult = $wizard->executeUpdate();
         }
 
         $stream = $this->output->getStream();
         rewind($stream);
         if ($performResult) {
-            if ($updateObject instanceof UpgradeWizardInterface && !($updateObject instanceof RepeatableInterface)) {
+            if (!$wizard instanceof RepeatableInterface) {
                 // mark wizard as done if it's not repeatable and was successful
-                $this->markWizardAsDone($updateObject->getIdentifier());
+                $this->markWizardAsDone($wizard);
             }
             $messages->enqueue(
                 new FlashMessage(
@@ -315,12 +298,9 @@ class UpgradeWizardsService
      *
      * @throws \RuntimeException
      */
-    public function markWizardAsDone(string $identifier): void
+    public function markWizardAsDone(UpgradeWizardInterface $upgradeWizard): void
     {
-        $this->assertIdentifierIsValid($identifier);
-
-        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
-        GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $class, 1);
+        GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $upgradeWizard::class, 1);
     }
 
     /**
@@ -333,8 +313,39 @@ class UpgradeWizardsService
     {
         $this->assertIdentifierIsValid($identifier);
 
-        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
-        return (bool)GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false);
+        return (bool)GeneralUtility::makeInstance(Registry::class)->get(
+            'installUpdate',
+            $this->upgradeWizardRegistry->getUpgradeWizard($identifier)::class,
+            false
+        );
+    }
+
+    /**
+     * Wrapper to catch \UnexpectedValueException for backwards compatibility reasons
+     */
+    public function getUpgradeWizard(string $identifier): ?UpgradeWizardInterface
+    {
+        try {
+            return $this->upgradeWizardRegistry->getUpgradeWizard($identifier);
+        } catch (\UnexpectedValueException) {
+            return null;
+        }
+    }
+
+    public function getUpgradeWizardIdentifiers(): array
+    {
+        return array_keys($this->upgradeWizardRegistry->getUpgradeWizards());
+    }
+
+    public function getNonRepeatableUpgradeWizards(): array
+    {
+        $nonRepeatableUpgradeWizards = [];
+        foreach ($this->upgradeWizardRegistry->getUpgradeWizards() as $identifier => $updateClassName) {
+            if (!in_array(RepeatableInterface::class, class_implements($updateClassName) ?: [], true)) {
+                $nonRepeatableUpgradeWizards[$identifier] = $updateClassName;
+            }
+        }
+        return $nonRepeatableUpgradeWizards;
     }
 
     /**
@@ -347,12 +358,11 @@ class UpgradeWizardsService
         if ($identifier === '') {
             throw new \RuntimeException('Empty upgrade wizard identifier given', 1650579934);
         }
-        if (
-            !isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier])
-            && !is_subclass_of($identifier, RowUpdaterInterface::class)
+        if (!is_subclass_of($identifier, RowUpdaterInterface::class)
+            && !$this->upgradeWizardRegistry->hasUpgradeWizard($identifier)
         ) {
             throw new \RuntimeException(
-                'The upgrade wizard identifier "' . $identifier . '" must either be found in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'ext/install\'][\'update\'] or it must implement TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface',
+                'The upgrade wizard identifier "' . $identifier . '" must either be registered as upgrade wizard or it must implement TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface',
                 1650546252
             );
         }
diff --git a/typo3/sysext/install/Classes/ServiceProvider.php b/typo3/sysext/install/Classes/ServiceProvider.php
index 9b091f523246f9d9f4b9e46b750931b3f32d0301..f7e43dc634f3a88bdc41ffb39aadaa5f446103ee 100644
--- a/typo3/sysext/install/Classes/ServiceProvider.php
+++ b/typo3/sysext/install/Classes/ServiceProvider.php
@@ -44,6 +44,7 @@ use TYPO3\CMS\Core\TypoScript\AST\CommentAwareAstBuilder;
 use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
 use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
 use TYPO3\CMS\Install\Database\PermissionsCheck;
+use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\SetupDatabaseService;
 use TYPO3\CMS\Install\Service\SetupService;
 use TYPO3\CMS\Install\Service\WebServerConfigurationFileService;
@@ -346,7 +347,8 @@ class ServiceProvider extends AbstractServiceProvider
             'setup',
             $container->get(Service\SetupDatabaseService::class),
             $container->get(Service\SetupService::class),
-            $container->get(ConfigurationManager::class)
+            $container->get(ConfigurationManager::class),
+            $container->get(LateBootService::class)
         );
     }
 
diff --git a/typo3/sysext/install/Classes/Updates/BackendGroupsExplicitAllowDenyMigration.php b/typo3/sysext/install/Classes/Updates/BackendGroupsExplicitAllowDenyMigration.php
index cc8ce159363ceaf05770ca1f52c12a3d6d9201a4..011ed75e556c48953abc396185940984bd1afa07 100644
--- a/typo3/sysext/install/Classes/Updates/BackendGroupsExplicitAllowDenyMigration.php
+++ b/typo3/sysext/install/Classes/Updates/BackendGroupsExplicitAllowDenyMigration.php
@@ -22,21 +22,18 @@ use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('backendGroupsExplicitAllowDenyMigration')]
 final class BackendGroupsExplicitAllowDenyMigration implements UpgradeWizardInterface, ChattyInterface
 {
     private const TABLE_NAME = 'be_groups';
 
     private OutputInterface $output;
 
-    public function getIdentifier(): string
-    {
-        return 'backendGroupsExplicitAllowDenyMigration';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate backend groups "explicit_allowdeny" field to simplified format.';
diff --git a/typo3/sysext/install/Classes/Updates/BackendModulePermissionMigration.php b/typo3/sysext/install/Classes/Updates/BackendModulePermissionMigration.php
index e46789317cd24add86bbc2287bc2b36b9284b44e..0be7a70aef9972d53deb3a95c33d1aa2649ab9d8 100644
--- a/typo3/sysext/install/Classes/Updates/BackendModulePermissionMigration.php
+++ b/typo3/sysext/install/Classes/Updates/BackendModulePermissionMigration.php
@@ -21,10 +21,12 @@ use TYPO3\CMS\Backend\Module\ModuleRegistry;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('backendModulePermission')]
 class BackendModulePermissionMigration implements UpgradeWizardInterface
 {
     protected array $aliases = [];
@@ -34,11 +36,6 @@ class BackendModulePermissionMigration implements UpgradeWizardInterface
         $this->aliases = GeneralUtility::makeInstance(ModuleRegistry::class)->getModuleAliases();
     }
 
-    public function getIdentifier(): string
-    {
-        return 'backendModulePermission';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate backend user and groups to new module names.';
diff --git a/typo3/sysext/install/Classes/Updates/BackendUserLanguageMigration.php b/typo3/sysext/install/Classes/Updates/BackendUserLanguageMigration.php
index 04970ab619bde862bd17287602eb17e22c865951..deb838c950e9381a3b3a52f2ba61d96581427b0a 100644
--- a/typo3/sysext/install/Classes/Updates/BackendUserLanguageMigration.php
+++ b/typo3/sysext/install/Classes/Updates/BackendUserLanguageMigration.php
@@ -20,19 +20,16 @@ namespace TYPO3\CMS\Install\Updates;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('backendUserLanguage')]
 class BackendUserLanguageMigration implements UpgradeWizardInterface
 {
     private const TABLE_NAME = 'be_users';
 
-    public function getIdentifier(): string
-    {
-        return 'backendUserLanguage';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate backend users\' selected UI languages to new format.';
diff --git a/typo3/sysext/install/Classes/Updates/CollectionsExtractionUpdate.php b/typo3/sysext/install/Classes/Updates/CollectionsExtractionUpdate.php
index 90a9a453cb04caadf15b35b333cb207b37fce514..a6a5913818a7d4e9cf2d610541eb6fb5f21c9167 100644
--- a/typo3/sysext/install/Classes/Updates/CollectionsExtractionUpdate.php
+++ b/typo3/sysext/install/Classes/Updates/CollectionsExtractionUpdate.php
@@ -20,11 +20,13 @@ namespace TYPO3\CMS\Install\Updates;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * Installs and downloads EXT:legacy_collections if requested
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('legacyCollectionsExtension')]
 class CollectionsExtractionUpdate extends AbstractDownloadExtensionUpdate
 {
     /**
@@ -62,15 +64,6 @@ class CollectionsExtractionUpdate extends AbstractDownloadExtensionUpdate
         return $this->confirmation;
     }
 
-    /**
-     * Return the identifier for this wizard
-     * This should be the same string as used in the ext_localconf class registration
-     */
-    public function getIdentifier(): string
-    {
-        return 'legacyCollectionsExtension';
-    }
-
     /**
      * Return the speaking name of this wizard
      */
diff --git a/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php b/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
index 83217b8e8593756218da19608dfc151662cef5c8..06379ae49d8773af63bdd62f4c8060cc98267cc4 100644
--- a/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
+++ b/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Registry;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 use TYPO3\CMS\Install\Updates\RowUpdater\L18nDiffsourceToJsonMigration;
 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
 use TYPO3\CMS\Install\Updates\RowUpdater\SysRedirectRootPageMoveMigration;
@@ -45,6 +46,7 @@ use TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceNewPlaceholderRemovalMigration
  * the job can restart at the position it stopped.
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('databaseRowsUpdateWizard')]
 class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInterface
 {
     /**
@@ -66,14 +68,6 @@ class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInte
         return $this->rowUpdater;
     }
 
-    /**
-     * @return string Unique identifier of this updater
-     */
-    public function getIdentifier(): string
-    {
-        return 'databaseRowsUpdateWizard';
-    }
-
     /**
      * @return string Title of this updater
      */
diff --git a/typo3/sysext/install/Classes/Updates/FeLoginModeExtractionUpdate.php b/typo3/sysext/install/Classes/Updates/FeLoginModeExtractionUpdate.php
index 5743a718a64ac68896cf22a76cf149eb6ef71865..d1c88ec279d073cf1290d704592586a247e2bf40 100644
--- a/typo3/sysext/install/Classes/Updates/FeLoginModeExtractionUpdate.php
+++ b/typo3/sysext/install/Classes/Updates/FeLoginModeExtractionUpdate.php
@@ -20,11 +20,13 @@ namespace TYPO3\CMS\Install\Updates;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * Installs and downloads EXT:fe_login_mode
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('feLoginModeExtension')]
 class FeLoginModeExtractionUpdate extends AbstractDownloadExtensionUpdate
 {
     private const TABLE_NAME = 'pages';
@@ -57,15 +59,6 @@ class FeLoginModeExtractionUpdate extends AbstractDownloadExtensionUpdate
         return $this->confirmation;
     }
 
-    /**
-     * Return the identifier for this wizard
-     * This should be the same string as used in the ext_localconf class registration
-     */
-    public function getIdentifier(): string
-    {
-        return 'feLoginModeExtension';
-    }
-
     /**
      * Return the speaking name of this wizard
      */
diff --git a/typo3/sysext/install/Classes/Updates/MigrateSiteSettingsConfigUpdate.php b/typo3/sysext/install/Classes/Updates/MigrateSiteSettingsConfigUpdate.php
index a91aeb431816c93228df7e0d51f0ca80d41179b3..a0f9c58aa7248b411584b047851c08ad6b0c49ed 100644
--- a/typo3/sysext/install/Classes/Updates/MigrateSiteSettingsConfigUpdate.php
+++ b/typo3/sysext/install/Classes/Updates/MigrateSiteSettingsConfigUpdate.php
@@ -20,12 +20,14 @@ namespace TYPO3\CMS\Install\Updates;
 use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal
  *
  * The upgrade wizard cuts the settings part of the config.yaml and moves it into settings.yaml.
  */
+#[UpgradeWizard('migrateSiteSettings')]
 class MigrateSiteSettingsConfigUpdate implements UpgradeWizardInterface
 {
     protected const SETTINGS_FILENAME = 'settings.yaml';
@@ -39,11 +41,6 @@ class MigrateSiteSettingsConfigUpdate implements UpgradeWizardInterface
         $this->sitePathsToMigrate = $this->getSitePathsToMigrate();
     }
 
-    public function getIdentifier(): string
-    {
-        return 'migrateSiteSettings';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate site settings to separate file';
diff --git a/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php b/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php
index cd8e9ec520d5ef58f9a1b4dedcff820c2ce5af2c..7ab45a633aba1477406b9426bdf85df6fb53e45b 100644
--- a/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php
+++ b/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php
@@ -22,10 +22,12 @@ use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('shortcutRecordsMigration')]
 class ShortcutRecordsMigration implements UpgradeWizardInterface
 {
     private const TABLE_NAME = 'sys_be_shortcuts';
@@ -33,11 +35,6 @@ class ShortcutRecordsMigration implements UpgradeWizardInterface
     protected ?ModuleProvider $moduleProvider = null;
     protected ?Router $router = null;
 
-    public function getIdentifier(): string
-    {
-        return 'shortcutRecordsMigration';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate shortcut records to new format.';
diff --git a/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php b/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php
index 3a3660fd69d25a6ac30287c696719b44f0abf920..af179759651f2483840297476a43edaabd5ecab9 100644
--- a/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php
+++ b/typo3/sysext/install/Classes/Updates/SvgFilesSanitization.php
@@ -24,7 +24,12 @@ use TYPO3\CMS\Core\Resource\ResourceStorage;
 use TYPO3\CMS\Core\Resource\Security\SvgSanitizer;
 use TYPO3\CMS\Core\Resource\StorageRepository;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
+/**
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+#[UpgradeWizard('svgFilesSanitization')]
 class SvgFilesSanitization implements UpgradeWizardInterface, ConfirmableInterface
 {
     /**
@@ -50,15 +55,6 @@ class SvgFilesSanitization implements UpgradeWizardInterface, ConfirmableInterfa
         );
     }
 
-    /**
-     * Return the identifier for this wizard
-     * This should be the same string as used in the ext_localconf class registration
-     */
-    public function getIdentifier(): string
-    {
-        return 'svgFilesSanitization';
-    }
-
     /**
      * Return the speaking name of this wizard
      */
diff --git a/typo3/sysext/install/Classes/Updates/SysFileMountIdentifierMigration.php b/typo3/sysext/install/Classes/Updates/SysFileMountIdentifierMigration.php
index 02bb195c1acceaafbfd1dda804a3928dc38999b2..31f5713cfa23c0eb1c97d339023fc858593c8596 100644
--- a/typo3/sysext/install/Classes/Updates/SysFileMountIdentifierMigration.php
+++ b/typo3/sysext/install/Classes/Updates/SysFileMountIdentifierMigration.php
@@ -21,16 +21,16 @@ use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
+/**
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+#[UpgradeWizard('sysFileMountIdentifierMigration')]
 class SysFileMountIdentifierMigration implements UpgradeWizardInterface
 {
     protected const TABLE_NAME = 'sys_filemounts';
 
-    public function getIdentifier(): string
-    {
-        return 'sysFileMountIdentifierMigration';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate base and path to the new identifier property of the "sys_filemounts" table.';
diff --git a/typo3/sysext/install/Classes/Updates/SysLogChannel.php b/typo3/sysext/install/Classes/Updates/SysLogChannel.php
index 94491d4d25f9abf6027e261d98ffb325b2cba411..fb9f901dfbd57feaeabcd63a60ada54e09125b35 100644
--- a/typo3/sysext/install/Classes/Updates/SysLogChannel.php
+++ b/typo3/sysext/install/Classes/Updates/SysLogChannel.php
@@ -25,8 +25,13 @@ use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\SysLog\Type;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 use TYPO3\CMS\Install\Service\ClearCacheService;
 
+/**
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+#[UpgradeWizard('sysLogChannel')]
 class SysLogChannel implements UpgradeWizardInterface
 {
     protected Connection $sysLogTable;
@@ -36,11 +41,6 @@ class SysLogChannel implements UpgradeWizardInterface
         $this->sysLogTable = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
     }
 
-    public function getIdentifier(): string
-    {
-        return 'sysLogChannel';
-    }
-
     public function getTitle(): string
     {
         return 'Populates a new channel column of the sys_log table.';
diff --git a/typo3/sysext/install/Classes/Updates/SysLogSerializationUpdate.php b/typo3/sysext/install/Classes/Updates/SysLogSerializationUpdate.php
index b02aac0f0ad182dcf968aa9e0456030942908d66..f54714cddc197ae8ec2619a891a33455ebcfe31a 100644
--- a/typo3/sysext/install/Classes/Updates/SysLogSerializationUpdate.php
+++ b/typo3/sysext/install/Classes/Updates/SysLogSerializationUpdate.php
@@ -21,20 +21,17 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Log\LogDataTrait;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('sysLogSerialization')]
 class SysLogSerializationUpdate implements UpgradeWizardInterface
 {
     use LogDataTrait;
     private const TABLE_NAME = 'sys_log';
 
-    public function getIdentifier(): string
-    {
-        return 'sysLogSerialization';
-    }
-
     public function getTitle(): string
     {
         return 'Migrate sys_log entries to a JSON formatted value.';
diff --git a/typo3/sysext/install/Classes/Updates/SysTemplateNoWorkspaceMigration.php b/typo3/sysext/install/Classes/Updates/SysTemplateNoWorkspaceMigration.php
index 8a095b81c9795ffcc54d475bc7679ffbb42f74d4..3b89430a69071926e5b892fc772eab6e2d3da6f3 100644
--- a/typo3/sysext/install/Classes/Updates/SysTemplateNoWorkspaceMigration.php
+++ b/typo3/sysext/install/Classes/Updates/SysTemplateNoWorkspaceMigration.php
@@ -22,19 +22,16 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
 
 /**
  * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
  */
+#[UpgradeWizard('sysTemplateNoWorkspaceMigration')]
 final class SysTemplateNoWorkspaceMigration implements UpgradeWizardInterface
 {
     private const TABLE_NAME = 'sys_template';
 
-    public function getIdentifier(): string
-    {
-        return 'sysTemplateNoWorkspaceMigration';
-    }
-
     public function getTitle(): string
     {
         return 'Set workspace records in table "sys_template" to deleted.';
diff --git a/typo3/sysext/install/Classes/Updates/UpgradeWizardInterface.php b/typo3/sysext/install/Classes/Updates/UpgradeWizardInterface.php
index 5e6a254ea11ee3490384fe730e2172fb5967999e..84410153fc6323dbfb7e42bb8a8393908ace1bb0 100644
--- a/typo3/sysext/install/Classes/Updates/UpgradeWizardInterface.php
+++ b/typo3/sysext/install/Classes/Updates/UpgradeWizardInterface.php
@@ -22,12 +22,6 @@ namespace TYPO3\CMS\Install\Updates;
  */
 interface UpgradeWizardInterface
 {
-    /**
-     * Return the identifier for this wizard
-     * This should be the same string as used in the ext_localconf class registration
-     */
-    public function getIdentifier(): string;
-
     /**
      * Return the speaking name of this wizard
      */
diff --git a/typo3/sysext/install/Classes/Updates/UpgradeWizardRegistry.php b/typo3/sysext/install/Classes/Updates/UpgradeWizardRegistry.php
new file mode 100644
index 0000000000000000000000000000000000000000..a09f41a27b3d57a96efe6edcd2afa203afd91d22
--- /dev/null
+++ b/typo3/sysext/install/Classes/Updates/UpgradeWizardRegistry.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Install\Updates;
+
+use Symfony\Component\DependencyInjection\ServiceLocator;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Registry for upgrade wizards. The registry receives all services, tagged with "install.upgradewizard".
+ * The tagging of upgrade wizards is automatically done based on the PHP Attribute UpgradeWizard.
+ *
+ * @internal
+ */
+class UpgradeWizardRegistry
+{
+    public function __construct(
+        private readonly ServiceLocator $upgradeWizards
+    ) {
+    }
+
+    /**
+     * Whether a registered upgrade wizard exists for the given identifier
+     */
+    public function hasUpgradeWizard(string $identifier): bool
+    {
+        return $this->upgradeWizards->has($identifier) || $this->getLegacyUpgradeWizardClassName($identifier) !== null;
+    }
+
+    /**
+     * Get registered upgrade wizard by identifier
+     */
+    public function getUpgradeWizard(string $identifier): UpgradeWizardInterface
+    {
+        if (!$this->hasUpgradeWizard($identifier)) {
+            throw new \UnexpectedValueException('Upgrade wizard with identifier ' . $identifier . ' is not registered.', 1673964964);
+        }
+
+        return $this->getLegacyUpgradeWizard($identifier) ?? $this->upgradeWizards->get($identifier);
+    }
+
+    /**
+     * Get all registered upgrade wizards
+     *
+     * @return array
+     */
+    public function getUpgradeWizards(): array
+    {
+        return array_replace(
+            $this->upgradeWizards->getProvidedServices(),
+            $this->getLegacyUpgradeWizards()
+        );
+    }
+
+    /**
+     * @deprecated Remove with TYPO3 v13
+     */
+    private function getLegacyUpgradeWizardClassName(string $identifier): ?string
+    {
+        if (class_exists($identifier)) {
+            return $identifier;
+        }
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier])
+            && class_exists($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier])
+        ) {
+            return $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
+        }
+        return null;
+    }
+
+    /**
+     * @deprecated Remove with TYPO3 v13
+     */
+    private function getLegacyUpgradeWizard(string $identifier): ?UpgradeWizardInterface
+    {
+        $className = $this->getLegacyUpgradeWizardClassName($identifier);
+        if ($className === null) {
+            return null;
+        }
+
+        $instance = GeneralUtility::makeInstance($className);
+        return $instance instanceof UpgradeWizardInterface ? $instance : null;
+    }
+
+    /**
+     * @deprecated Remove with TYPO3 v13
+     */
+    private function getLegacyUpgradeWizards(): array
+    {
+        return $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] ?? [];
+    }
+}
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php
index ee61615b05f690c3406fdadf2a2ee0f7816e09cd..1edb3099de1924c4897516c2a93b40a9f7fae7ef 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php
@@ -951,4 +951,9 @@ return [
             'Deprecation-99075-Fe_usersAndFe_groupsTSconfig.rst',
         ],
     ],
+    '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'ext/install\'][\'update\']' => [
+        'restFiles' => [
+            'Deprecation-99586-RegistrationOfUpgradeWizardsViaGLOBALS.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/install/Configuration/Services.php b/typo3/sysext/install/Configuration/Services.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ac424d53a844e1b9fa743334dbd510fd7ba4f40
--- /dev/null
+++ b/typo3/sysext/install/Configuration/Services.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Install;
+
+use Symfony\Component\DependencyInjection\ChildDefinition;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+
+return static function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
+    $containerBuilder->registerAttributeForAutoconfiguration(
+        UpgradeWizard::class,
+        static function (ChildDefinition $definition, UpgradeWizard $attribute): void {
+            $definition->addTag(UpgradeWizard::TAG_NAME, ['identifier' => $attribute->identifier]);
+        }
+    );
+};
diff --git a/typo3/sysext/install/Configuration/Services.yaml b/typo3/sysext/install/Configuration/Services.yaml
index f7c5103b8b9949e2e707cb0d19607e72ecea8d6b..83376c4ebee50b4f2148aceba5511efc653d088e 100644
--- a/typo3/sysext/install/Configuration/Services.yaml
+++ b/typo3/sysext/install/Configuration/Services.yaml
@@ -12,6 +12,9 @@ services:
     autoconfigure: true
     public: false
 
+  TYPO3\CMS\Install\Updates\:
+    resource: '../Classes/Updates/*'
+
   TYPO3\CMS\Install\Controller\BackendModuleController:
     tags: ['backend.controller']
 
@@ -35,3 +38,7 @@ services:
 
   TYPO3\CMS\Install\Service\UpgradeWizardsService:
     public: true
+
+  TYPO3\CMS\Install\Updates\UpgradeWizardRegistry:
+    arguments:
+      $upgradeWizards: !tagged_locator { tag: 'install.upgradewizard', index_by: 'identifier' }
diff --git a/typo3/sysext/install/ext_localconf.php b/typo3/sysext/install/ext_localconf.php
deleted file mode 100644
index 780249d30cf0e274fab6ac5ce6dc5ba0c86063fd..0000000000000000000000000000000000000000
--- a/typo3/sysext/install/ext_localconf.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-use TYPO3\CMS\Install\Updates\BackendGroupsExplicitAllowDenyMigration;
-use TYPO3\CMS\Install\Updates\BackendModulePermissionMigration;
-use TYPO3\CMS\Install\Updates\BackendUserLanguageMigration;
-use TYPO3\CMS\Install\Updates\CollectionsExtractionUpdate;
-use TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard;
-use TYPO3\CMS\Install\Updates\FeLoginModeExtractionUpdate;
-use TYPO3\CMS\Install\Updates\MigrateSiteSettingsConfigUpdate;
-use TYPO3\CMS\Install\Updates\ShortcutRecordsMigration;
-use TYPO3\CMS\Install\Updates\SvgFilesSanitization;
-use TYPO3\CMS\Install\Updates\SysFileMountIdentifierMigration;
-use TYPO3\CMS\Install\Updates\SysLogChannel;
-use TYPO3\CMS\Install\Updates\SysLogSerializationUpdate;
-use TYPO3\CMS\Install\Updates\SysTemplateNoWorkspaceMigration;
-
-defined('TYPO3') or die();
-
-// Row updater wizard scans all table rows for update jobs.
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['databaseRowsUpdateWizard'] = DatabaseRowsUpdateWizard::class;
-
-// v10->v11 wizards below this line
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['svgFilesSanitization'] = SvgFilesSanitization::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['shortcutRecordsMigration'] = ShortcutRecordsMigration::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['legacyCollectionsExtension'] = CollectionsExtractionUpdate::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['backendUserLanguage'] = BackendUserLanguageMigration::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['sysLogChannel'] = SysLogChannel::class;
-
-// v11->v12 wizards below this line
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['feLoginModeExtension'] = FeLoginModeExtractionUpdate::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['sysLogSerialization'] = SysLogSerializationUpdate::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['backendGroupsExplicitAllowDenyMigration'] = BackendGroupsExplicitAllowDenyMigration::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['sysFileMountIdentifierMigration'] = SysFileMountIdentifierMigration::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['backendModulePermission'] = BackendModulePermissionMigration::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['sysTemplateNoWorkspaceMigration'] = SysTemplateNoWorkspaceMigration::class;
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['migrateSiteSettings'] = MigrateSiteSettingsConfigUpdate::class;