diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 73a897ad7d566166f2a4dfd602ac8c25aff0a5b5..95e648bda3c694839ea0e79833e2cb1a308ba496 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -24,10 +24,6 @@ export default {
     InfrastructureSearch,
   },
   inject: {
-    emptyPageTitle: {
-      from: 'emptyPageTitle',
-      default: s__('PackageRegistry|There are no packages yet'),
-    },
     noResultsText: {
       from: 'noResultsText',
       default: s__(
@@ -40,6 +36,7 @@ export default {
       emptyListIllustration: (state) => state.config.emptyListIllustration,
       emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
       filter: (state) => state.filter,
+      isGroupPage: (state) => state.config.isGroupPage,
       selectedType: (state) => state.selectedType,
       packageHelpUrl: (state) => state.config.packageHelpUrl,
       packagesCount: (state) => state.pagination?.total,
@@ -49,7 +46,11 @@ export default {
         this.filter.filter((f) => f.type !== FILTERED_SEARCH_TERM || f.value?.data).length === 0
       );
     },
-
+    emptyPageTitle() {
+      return this.isGroupPage
+        ? s__('InfrastructureRegistry|You have no Terraform modules in your group')
+        : s__('InfrastructureRegistry|You have no Terraform modules in your project');
+    },
     emptyStateTitle() {
       return this.emptySearch
         ? this.emptyPageTitle
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
index 1467218dd4170aeb2e15be16af527f7058029f31..f897d588a7a256733f7ea84f5b7213613c2c71a5 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
@@ -18,7 +18,6 @@ export default () => {
       PackagesListApp,
     },
     provide: {
-      emptyPageTitle: s__('InfrastructureRegistry|You have no Terraform modules in your project'),
       noResultsText: s__(
         'InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab.',
       ),
diff --git a/app/assets/javascripts/pages/groups/infrastructure_registry/index/index.js b/app/assets/javascripts/pages/groups/infrastructure_registry/index/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..dfb750eca41ea039e98a896055c8ac90f8c61b88
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/infrastructure_registry/index/index.js
@@ -0,0 +1,3 @@
+import initList from '~/packages_and_registries/infrastructure_registry/list_app_bundle';
+
+initList();
diff --git a/app/controllers/groups/infrastructure_registry_controller.rb b/app/controllers/groups/infrastructure_registry_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..edd05900170416cfa8b6b3100fe0861b289a84e7
--- /dev/null
+++ b/app/controllers/groups/infrastructure_registry_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Groups
+  class InfrastructureRegistryController < Groups::ApplicationController
+    before_action :verify_packages_enabled!
+
+    feature_category :package_registry
+    urgency :low
+
+    private
+
+    def verify_packages_enabled!
+      unless group.packages_feature_enabled? &&
+          Feature.enabled?(:group_level_infrastructure_registry, group.root_ancestor, type: :gitlab_com_derisk)
+        render_404
+      end
+    end
+  end
+end
diff --git a/app/views/groups/infrastructure_registry/index.html.haml b/app/views/groups/infrastructure_registry/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..8801dc249dce28e5675eb37bb692bb6bffc19737
--- /dev/null
+++ b/app/views/groups/infrastructure_registry/index.html.haml
@@ -0,0 +1,9 @@
+- page_title _("Infrastructure Registry")
+
+.row
+  .col-12
+    #js-vue-packages-list{ data: { resource_id: @group.id,
+      page_type: 'groups',
+      empty_list_help_url: help_page_path('user/infrastructure/index'),
+      empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg'),
+      package_help_url: help_page_path('user/infrastructure/index') } }
diff --git a/config/feature_flags/gitlab_com_derisk/group_level_infrastructure_registry.yml b/config/feature_flags/gitlab_com_derisk/group_level_infrastructure_registry.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a0a7dfda520a7165d21a715fc4ba71955224091
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/group_level_infrastructure_registry.yml
@@ -0,0 +1,9 @@
+---
+name: group_level_infrastructure_registry
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352041
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140215
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17430
+milestone: '16.9'
+group: group::package registry
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/config/routes/group.rb b/config/routes/group.rb
index e1185b15d12c3b104879fd6a8075c9723927721b..115c6abeb5f5c1cc885ad6e57be7dac70fbd4ea2 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -82,6 +82,8 @@
 
     resources :packages, only: [:index, :show]
 
+    resources :infrastructure_registry, only: [:index]
+
     resources :milestones, constraints: { id: %r{[^/]+} } do
       member do
         get :issues
diff --git a/ee/spec/features/groups/navbar_spec.rb b/ee/spec/features/groups/navbar_spec.rb
index 2dcfe53c66af45d68a3b75226980b226e5cd9f70..d2c5b9035de20d2d6f3e0f878a1d72b6393aa8a5 100644
--- a/ee/spec/features/groups/navbar_spec.rb
+++ b/ee/spec/features/groups/navbar_spec.rb
@@ -16,10 +16,12 @@
     before do
       group.add_maintainer(user)
       stub_group_wikis(false)
+      stub_config(registry: { enabled: false })
       sign_in(user)
 
       create_package_nav(_('Operate'))
       insert_after_nav_item(_('Analyze'), new_nav_item: settings_for_maintainer_nav_item)
+      insert_infrastructure_registry_nav(_('Kubernetes'))
     end
 
     context 'when devops adoption analytics is available' do
@@ -98,7 +100,7 @@
 
     context 'when packages are available' do
       before do
-        stub_config(packages: { enabled: true }, registry: { enabled: false })
+        stub_config(packages: { enabled: true })
 
         visit group_path(group)
       end
@@ -173,7 +175,7 @@
       before do
         group.update!(harbor_integration: harbor_integration)
 
-        insert_harbor_registry_nav(_('Kubernetes'))
+        insert_harbor_registry_nav(_('Terraform modules'))
 
         visit group_path(group)
       end
@@ -185,10 +187,12 @@
   context 'for owners', :saas do
     before do
       group.add_owner(user)
+      stub_config(registry: { enabled: false })
       stub_group_wikis(false)
       stub_licensed_features(domain_verification: true)
       sign_in(user)
       create_package_nav(_('Operate'))
+      insert_infrastructure_registry_nav(_('Kubernetes'))
     end
 
     describe 'structure' do
diff --git a/ee/spec/features/projects/navbar_spec.rb b/ee/spec/features/projects/navbar_spec.rb
index 0a02f43713cc07f798cdf7e8453d9ae0e2ddc27e..7bfcab7e7f3ea6ef27fb4e2aa8e4117551457ebf 100644
--- a/ee/spec/features/projects/navbar_spec.rb
+++ b/ee/spec/features/projects/navbar_spec.rb
@@ -20,7 +20,7 @@
     stub_feature_flags(model_registry: false)
     stub_feature_flags(remove_monitor_metrics: false)
     insert_package_nav
-    insert_infrastructure_registry_nav
+    insert_infrastructure_registry_nav(s_('Terraform|Terraform states'))
     insert_infrastructure_google_cloud_nav
     insert_infrastructure_aws_nav
   end
diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb
index 19a6e210231249ac16c7619d3671ee2c5dc32ff8..0e762559fe2a527e88f15297f93a91841ff6e65e 100644
--- a/lib/sidebars/groups/menus/packages_registries_menu.rb
+++ b/lib/sidebars/groups/menus/packages_registries_menu.rb
@@ -8,6 +8,7 @@ class PackagesRegistriesMenu < ::Sidebars::Menu
         def configure_menu_items
           add_item(packages_registry_menu_item)
           add_item(container_registry_menu_item)
+          add_item(infrastructure_registry_menu_item)
           add_item(harbor_registry_menu_item)
           add_item(dependency_proxy_menu_item)
           true
@@ -56,6 +57,21 @@ def container_registry_menu_item
           )
         end
 
+        def infrastructure_registry_menu_item
+          unless context.group.packages_feature_enabled? &&
+              Feature.enabled?(:group_level_infrastructure_registry, context.group.root_ancestor, type: :gitlab_com_derisk)
+            return nil_menu_item(:infrastructure_registry)
+          end
+
+          ::Sidebars::MenuItem.new(
+            title: _('Terraform modules'),
+            link: group_infrastructure_registry_index_path(context.group),
+            super_sidebar_parent: Sidebars::Groups::SuperSidebarMenus::OperationsMenu,
+            active_routes: { controller: :infrastructure_registry },
+            item_id: :infrastructure_registry
+          )
+        end
+
         def harbor_registry_menu_item
           if context.group.harbor_integration.nil? ||
               !context.group.harbor_integration.activated?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 876c38f712654ec05863bbf7bc1438ae151de018..0f416c3e355d04e0857048efb77f58e09dd76fc7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25932,6 +25932,9 @@ msgstr ""
 msgid "InfrastructureRegistry|To authorize access to the Terraform registry:"
 msgstr ""
 
+msgid "InfrastructureRegistry|You have no Terraform modules in your group"
+msgstr ""
+
 msgid "InfrastructureRegistry|You have no Terraform modules in your project"
 msgstr ""
 
diff --git a/spec/features/groups/infrastructure_registry_spec.rb b/spec/features/groups/infrastructure_registry_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f9ed2bca68ff579557272758d8d2cd60704c9128
--- /dev/null
+++ b/spec/features/groups/infrastructure_registry_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Infrastructure Registry', feature_category: :package_registry do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:group) { create(:group) }
+  let_it_be(:project) { create(:project, :public, group: group) }
+
+  context 'when user is not signed in' do
+    before do
+      visit_group_infrastructure_registry
+    end
+
+    it 'returns 200' do
+      expect(status_code).to eq(200)
+    end
+
+    context 'when there are modules' do
+      let_it_be(:terraform_module) do
+        create(:terraform_module_package, project: project, created_at: 1.day.ago, version: '1.0.0')
+      end
+
+      it 'does not allow you to delete package', :js do
+        expect(page).to have_button('Remove package', disabled: true)
+      end
+    end
+  end
+
+  context 'when user is signed in' do
+    before do
+      sign_in(user)
+    end
+
+    context 'when user is not a group member' do
+      before do
+        visit_group_infrastructure_registry
+      end
+
+      it 'returns 200' do
+        expect(status_code).to eq(200)
+      end
+
+      context 'when there are modules' do
+        let_it_be(:terraform_module) do
+          create(:terraform_module_package, project: project, created_at: 1.day.ago, version: '1.0.0')
+        end
+
+        it 'does not allow you to delete package', :js do
+          expect(page).to have_button('Remove package', disabled: true)
+        end
+      end
+    end
+
+    context 'when user is a group member' do
+      before_all do
+        group.add_maintainer(user)
+      end
+
+      context 'when packages registry is not enabled' do
+        before do
+          stub_config(packages: { enabled: false })
+        end
+
+        it 'returns 404' do
+          visit_group_infrastructure_registry
+
+          expect(status_code).to eq(404)
+        end
+      end
+
+      context 'when packages registry is enabled', :js do
+        before do
+          visit_group_infrastructure_registry
+        end
+
+        context 'when there are modules' do
+          let_it_be(:terraform_module) do
+            create(:terraform_module_package, project: project, created_at: 1.day.ago, version: '1.0.0')
+          end
+
+          context 'and there is more than one' do
+            let_it_be(:terraform_module2) do
+              create(:terraform_module_package, project: project, created_at: 2.days.ago, version: '2.0.0')
+            end
+
+            let_it_be(:packages) { [terraform_module, terraform_module2] }
+
+            it_behaves_like 'packages list'
+          end
+
+          describe 'details link' do
+            it 'navigates to the correct url' do
+              page.within(packages_table_selector) do
+                click_link terraform_module.name
+              end
+
+              expect(page).to have_current_path(project_infrastructure_registry_path(terraform_module.project,
+                terraform_module))
+
+              expect(page).to have_css('.packages-app h2[data-testid="title"]', text: terraform_module.name)
+
+              expect(page).to have_content('Provision instructions')
+              expect(page).to have_content('Registry setup')
+            end
+          end
+
+          context 'when deleting a package' do
+            it 'allows you to delete a module', :aggregate_failures do
+              # this is still using the package copy in the UI too
+              click_button('Remove package')
+              click_button('Permanently delete')
+
+              expect(page).to have_content 'Package deleted successfully'
+              expect(page).not_to have_content(terraform_module.name)
+            end
+          end
+        end
+
+        it 'displays the empty message' do
+          expect(page).to have_content('You have no Terraform modules in your group')
+        end
+      end
+    end
+  end
+
+  def visit_group_infrastructure_registry
+    visit group_infrastructure_registry_index_path(group)
+  end
+end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 7d5cc704f9c8a413a6c581a9197e2b593968160e..e9cdacf82557d48b43dd6dfc63e2f0dccc737649 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -15,6 +15,7 @@
   before do
     create_package_nav(_('Operate'))
     insert_after_nav_item(_('Analyze'), new_nav_item: settings_for_maintainer_nav_item) if Gitlab.ee?
+    insert_infrastructure_registry_nav(_('Kubernetes'))
 
     stub_config(dependency_proxy: { enabled: false })
     stub_config(registry: { enabled: false })
@@ -85,7 +86,7 @@
     before do
       group.update!(harbor_integration: harbor_integration)
 
-      insert_harbor_registry_nav(_('Kubernetes'))
+      insert_harbor_registry_nav(_('Terraform modules'))
 
       visit group_path(group)
     end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 9babe3c3b71e48525bfa224994b40c5ddd940186..b63c8e72e850458e2ea7be3b446539a8a328f352 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -18,7 +18,7 @@
     stub_feature_flags(ml_experiment_tracking: false)
     stub_feature_flags(model_registry: false)
     insert_package_nav
-    insert_infrastructure_registry_nav
+    insert_infrastructure_registry_nav(s_('Terraform|Terraform states'))
     insert_infrastructure_google_cloud_nav
     insert_infrastructure_aws_nav
   end
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 009d9c959cb5d419fafcf2d9a9c4c0b4056c2c1c..6af5dd49f63f7130f627bb5dab47a4f1fb63780b 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -28,7 +28,7 @@ exports[`packages_list_app renders 1`] = `
         <h1
           class="gl-font-size-h-display gl-line-height-36 gl-mb-0 gl-mt-0 h4"
         >
-          There are no packages yet
+          You have no Terraform modules in your project
         </h1>
         <p
           class="gl-mb-0 gl-mt-4"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index eb905fbec400084f0084c5bf78dac80c9c341a57..c477c070ad0e90e59b670c2daad0b02ffe46813f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -34,7 +34,7 @@ describe('packages_list_app', () => {
   const findListComponent = () => wrapper.findComponent(PackageList);
   const findInfrastructureSearch = () => wrapper.findComponent(InfrastructureSearch);
 
-  const createStore = ({ filter = [], packageCount = 0 } = {}) => {
+  const createStore = ({ isGroupPage = false, filter = [], packageCount = 0 } = {}) => {
     store = new Vuex.Store({
       state: {
         isLoading: false,
@@ -43,6 +43,7 @@ describe('packages_list_app', () => {
           emptyListIllustration: 'helpSvg',
           emptyListHelpUrl,
           packageHelpUrl: 'foo',
+          isGroupPage,
         },
         filter,
         pagination: {
@@ -53,7 +54,7 @@ describe('packages_list_app', () => {
     store.dispatch = jest.fn();
   };
 
-  const mountComponent = (provide) => {
+  const mountComponent = () => {
     wrapper = shallowMount(PackageListApp, {
       store,
       stubs: {
@@ -63,7 +64,6 @@ describe('packages_list_app', () => {
         GlSprintf,
         GlLink,
       },
-      provide,
     });
   };
 
@@ -145,6 +145,8 @@ describe('packages_list_app', () => {
   });
 
   describe('empty state', () => {
+    const heading = () => findEmptyState().find('h1');
+
     it('generate the correct empty list link', () => {
       const link = findListComponent().findComponent(GlLink);
 
@@ -153,9 +155,18 @@ describe('packages_list_app', () => {
     });
 
     it('includes the right content on the default tab', () => {
-      const heading = findEmptyState().find('h1');
+      expect(heading().text()).toBe('You have no Terraform modules in your project');
+    });
 
-      expect(heading.text()).toBe('There are no packages yet');
+    describe('when group page', () => {
+      beforeEach(() => {
+        createStore({ isGroupPage: true });
+        mountComponent();
+      });
+
+      it('includes the right content', () => {
+        expect(heading().text()).toBe('You have no Terraform modules in your group');
+      });
     });
   });
 
diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
index 713e22e2e76805ec357c625bf72bb370158ad007..1e680f08e16242af7653ce25a0d6271fd7299d04 100644
--- a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
@@ -183,6 +183,42 @@
         it_behaves_like 'the menu entry is not available'
       end
     end
+
+    describe 'Infrastructure Registry' do
+      let(:item_id) { :infrastructure_registry }
+
+      context 'when user can read packages' do
+        before do
+          stub_config(packages: { enabled: packages_enabled })
+        end
+
+        context 'when config package setting is disabled' do
+          let(:packages_enabled) { false }
+
+          it_behaves_like 'the menu entry is not available'
+        end
+
+        context 'when config package setting is enabled' do
+          let(:packages_enabled) { true }
+
+          it_behaves_like 'the menu entry is available'
+
+          context 'when feature flag is disabled' do
+            before do
+              stub_feature_flags(group_level_infrastructure_registry: false)
+            end
+
+            it_behaves_like 'the menu entry is not available'
+          end
+        end
+      end
+
+      context 'when user cannot read packages' do
+        let(:user) { nil }
+
+        it_behaves_like 'the menu entry is available'
+      end
+    end
   end
 
   private
diff --git a/spec/requests/groups/infrastructure_registry_controller_spec.rb b/spec/requests/groups/infrastructure_registry_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f3935ebb7087e073cc4c67d3b83e2c441544db0f
--- /dev/null
+++ b/spec/requests/groups/infrastructure_registry_controller_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::InfrastructureRegistryController, feature_category: :package_registry do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:group) { create(:group, :private) }
+
+  describe 'GET #index' do
+    subject { get group_infrastructure_registry_index_path(group) }
+
+    context 'when user is not signed in' do
+      it { is_expected.to redirect_to(new_user_session_path) }
+    end
+
+    context 'when user is signed in' do
+      before do
+        sign_in(user)
+      end
+
+      context 'when user is not a group member' do
+        it_behaves_like 'returning response status', :not_found
+      end
+
+      context 'when user is group maintainer' do
+        before_all do
+          group.add_maintainer(user)
+        end
+
+        it_behaves_like 'returning response status', :ok
+
+        context 'when the packages registry is not available' do
+          before do
+            stub_config(packages: { enabled: false })
+          end
+
+          it_behaves_like 'returning response status', :not_found
+        end
+
+        context 'when feature flag is disabled' do
+          before do
+            stub_feature_flags(group_level_infrastructure_registry: false)
+          end
+
+          it_behaves_like 'returning response status', :not_found
+        end
+      end
+    end
+  end
+end
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index 2d3f7a1b8a9a0095c433f8c01f542ec2310c7e9f..e4b30df063fa43d63e8a387cb336f84b672d586b 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -92,9 +92,9 @@ def insert_dependency_proxy_nav
     )
   end
 
-  def insert_infrastructure_registry_nav
+  def insert_infrastructure_registry_nav(within)
     insert_after_sub_nav_item(
-      s_('Terraform|Terraform states'),
+      within,
       within: _('Operate'),
       new_sub_nav_item_name: _('Terraform modules')
     )