diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb
index 3f0e849d7efd1e91f94c818754938e429ba802b8..52a9e057148aaf7cc75b3de38e67fa127b4e8214 100644
--- a/app/finders/concerns/packages/finder_helper.rb
+++ b/app/finders/concerns/packages/finder_helper.rb
@@ -38,6 +38,18 @@ def packages_visible_to_user(user, within_group:, with_package_registry_enabled:
       ::Packages::Package.for_projects(projects.select(:id)).installable
     end
 
+    def packages_visible_to_user_including_public_registries(user, within_group:)
+      return ::Packages::Package.none unless within_group
+
+      return ::Packages::Package.none unless Ability.allowed?(user, :read_package_within_public_registries,
+        within_group.packages_policy_subject)
+
+      projects = projects_visible_to_reporters(user, within_group: within_group,
+        within_public_package_registry: !Ability.allowed?(user, :read_group, within_group))
+
+      ::Packages::Package.for_projects(projects.select(:id)).installable
+    end
+
     def projects_visible_to_user(user, within_group:)
       return ::Project.none unless within_group
       return ::Project.none unless Ability.allowed?(user, :read_group, within_group)
@@ -45,13 +57,17 @@ def projects_visible_to_user(user, within_group:)
       projects_visible_to_reporters(user, within_group: within_group)
     end
 
-    def projects_visible_to_reporters(user, within_group:)
-      if user.is_a?(DeployToken)
-        user.accessible_projects
-      else
-        within_group.all_projects
-                    .public_or_visible_to_user(user, ::Gitlab::Access::REPORTER)
+    def projects_visible_to_reporters(user, within_group:, within_public_package_registry: false)
+      return user.accessible_projects if user.is_a?(DeployToken)
+
+      unless within_public_package_registry
+        return within_group.all_projects.public_or_visible_to_user(user, ::Gitlab::Access::REPORTER)
       end
+
+      ::Project
+        .public_or_visible_to_user(user, Gitlab::Access::REPORTER)
+        .or(::Project.with_public_package_registry)
+        .in_namespace(within_group.self_and_descendants)
     end
 
     def package_type
diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb
index 684b99f8647a928c13a285fbc82d0cfd5d3e3d0e..54c83286d20c2336bb16335583780a7a9c6b08a3 100644
--- a/app/finders/packages/nuget/package_finder.rb
+++ b/app/finders/packages/nuget/package_finder.rb
@@ -3,6 +3,8 @@
 module Packages
   module Nuget
     class PackageFinder < ::Packages::GroupOrProjectPackageFinder
+      extend ::Gitlab::Utils::Override
+
       MAX_PACKAGES_COUNT = 300
       FORCE_NORMALIZATION_CLIENT_VERSION = '>= 3'
 
@@ -36,6 +38,15 @@ def find_by_version(result)
           )
       end
 
+      override :group_packages
+      def group_packages
+        if ::Feature.disabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, @project_or_group)
+          return super
+        end
+
+        packages_visible_to_user_including_public_registries(@current_user, within_group: @project_or_group)
+      end
+
       def client_forces_normalized_version?
         return true if @params[:client_version].blank?
 
diff --git a/app/models/project.rb b/app/models/project.rb
index 236da1ca1bddc297673c099cea27b11db6c9de98..477d6d12522dfc53f44db2e30e3eda76fac36e80 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -778,6 +778,13 @@ def self.integration_association_name(name)
   scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
   scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
   scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) }
+  scope :with_public_package_registry, -> do
+    where_exists(
+      ::ProjectFeature
+        .where(::ProjectFeature.arel_table[:project_id].eq(arel_table[:id]))
+        .with_feature_access_level(:package_registry, ::ProjectFeature::PUBLIC)
+    )
+  end
   scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
   scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
   scope :with_issues_or_mrs_available_for_user, ->(user) do
diff --git a/app/policies/packages/policies/group_policy.rb b/app/policies/packages/policies/group_policy.rb
index d8c20c7a90aff66c84268885c2335cb8fb40f27b..3a03aa32cf9d2d0045ebd474b3ad99b788fa81bd 100644
--- a/app/policies/packages/policies/group_policy.rb
+++ b/app/policies/packages/policies/group_policy.rb
@@ -7,6 +7,13 @@ class GroupPolicy < BasePolicy
 
       overrides(:read_package)
 
+      # Because we need to defer the evaluation of this condition to be after :read_group is evaluated,
+      # we put its score higher than the score of :read_group (122)
+      condition(:has_projects_with_public_package_registry, scope: :subject, score: 150) do
+        ::Gitlab::CurrentSettings.package_registry_allow_anyone_to_pull_option &&
+          @subject.all_projects.with_public_package_registry.any?
+      end
+
       rule { group.public_group }.policy do
         enable :read_package
       end
@@ -22,6 +29,15 @@ class GroupPolicy < BasePolicy
       rule { group.write_package_registry_deploy_token }.policy do
         enable :read_package
       end
+
+      rule { can?(:read_group) | has_projects_with_public_package_registry }.policy do
+        # We add a new permission and don't reuse :read_group here for two reasons:
+        # 1. This's a bit of expensive rule to compute, so we need to narrow it down to a more targeted permission
+        #    that only allows access to the public package registry in private/internal groups
+        # 2. The :read_group permission is more broad and used in many places. This may grant access to other
+        #    package-related actions that we don't want to.
+        enable :read_package_within_public_registries
+      end
     end
   end
 end
diff --git a/config/feature_flags/gitlab_com_derisk/allow_anyone_to_pull_public_nuget_packages_on_group_level.yml b/config/feature_flags/gitlab_com_derisk/allow_anyone_to_pull_public_nuget_packages_on_group_level.yml
new file mode 100644
index 0000000000000000000000000000000000000000..61840591af92fc3bc1f2e146f75d16d39d6b7a2d
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/allow_anyone_to_pull_public_nuget_packages_on_group_level.yml
@@ -0,0 +1,9 @@
+---
+name: allow_anyone_to_pull_public_nuget_packages_on_group_level
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/383537
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155119
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/464556
+milestone: '17.1'
+group: group::package registry
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/db/post_migrate/20240607140843_add_index_on_project_feature_project_id_when_public_package_registry.rb b/db/post_migrate/20240607140843_add_index_on_project_feature_project_id_when_public_package_registry.rb
new file mode 100644
index 0000000000000000000000000000000000000000..214f15f1d10c3fc9de6b24fb98161155ba923260
--- /dev/null
+++ b/db/post_migrate/20240607140843_add_index_on_project_feature_project_id_when_public_package_registry.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnProjectFeatureProjectIdWhenPublicPackageRegistry < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '17.1'
+
+  INDEX_NAME = 'index_project_features_on_project_id_on_public_package_registry'
+  PROJECT_FEATURES_PUBLIC = 30
+
+  def up
+    add_concurrent_index :project_features, :project_id,
+      where: "package_registry_access_level = #{PROJECT_FEATURES_PUBLIC}", name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :project_features, INDEX_NAME
+  end
+end
diff --git a/db/post_migrate/20240607140927_add_index_packages_project_id_lower_name_when_nuget_installable_with_version.rb b/db/post_migrate/20240607140927_add_index_packages_project_id_lower_name_when_nuget_installable_with_version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea5a6740c423e11463d0e2942496582de0631d3b
--- /dev/null
+++ b/db/post_migrate/20240607140927_add_index_packages_project_id_lower_name_when_nuget_installable_with_version.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddIndexPackagesProjectIdLowerNameWhenNugetInstallableWithVersion < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '17.1'
+
+  INDEX_NAME = 'idx_pkgs_project_id_lower_name_when_nuget_installable_version'
+  NUGET_TYPE = 'package_type = 4'
+  WITH_VERSION = 'version IS NOT NULL'
+  INSTALLABLE_STATUS = 'status IN (0, 1)'
+
+  def up
+    add_concurrent_index :packages_packages, 'project_id, LOWER(name)', # rubocop:disable Migration/PreventIndexCreation -- I'm replicating an existing index with a more selective where clause
+      where: "#{NUGET_TYPE} AND #{WITH_VERSION} AND #{INSTALLABLE_STATUS}",
+      name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :packages_packages, name: INDEX_NAME
+  end
+end
diff --git a/db/post_migrate/20240607141037_remove_index_packages_packages_on_project_id_and_lower_name_to_packages.rb b/db/post_migrate/20240607141037_remove_index_packages_packages_on_project_id_and_lower_name_to_packages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..55931e15833154b7ceaa69278e6b3bbe5f44c12d
--- /dev/null
+++ b/db/post_migrate/20240607141037_remove_index_packages_packages_on_project_id_and_lower_name_to_packages.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class RemoveIndexPackagesPackagesOnProjectIdAndLowerNameToPackages < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+  milestone '17.1'
+
+  INDEX_NAME = 'index_packages_packages_on_project_id_and_lower_name'
+  NUGET_PACKAGE_TYPE = 4
+
+  def up
+    remove_concurrent_index_by_name :packages_packages, INDEX_NAME
+  end
+
+  def down
+    add_concurrent_index(
+      :packages_packages,
+      'project_id, LOWER(name)',
+      name: INDEX_NAME,
+      where: "package_type = #{NUGET_PACKAGE_TYPE}"
+    )
+  end
+end
diff --git a/db/schema_migrations/20240607140843 b/db/schema_migrations/20240607140843
new file mode 100644
index 0000000000000000000000000000000000000000..b0b435afeaa56788f3e3f6e4d7f407f76d7f4892
--- /dev/null
+++ b/db/schema_migrations/20240607140843
@@ -0,0 +1 @@
+1f72fabfcc43e0fdf24e9d28caa8f5643c0a659bbfe62f084ed7926f4ff8c197
\ No newline at end of file
diff --git a/db/schema_migrations/20240607140927 b/db/schema_migrations/20240607140927
new file mode 100644
index 0000000000000000000000000000000000000000..e825ce57dc2015616ab1765c9a334e1f4ba9d8a8
--- /dev/null
+++ b/db/schema_migrations/20240607140927
@@ -0,0 +1 @@
+7f380a2f886edb615f5a6216211132d7f8f59d65d8ab5179541ef3aba9c852e4
\ No newline at end of file
diff --git a/db/schema_migrations/20240607141037 b/db/schema_migrations/20240607141037
new file mode 100644
index 0000000000000000000000000000000000000000..752cde900e3ce74018cccd84c759991c32917eb1
--- /dev/null
+++ b/db/schema_migrations/20240607141037
@@ -0,0 +1 @@
+382ac12d66f4777b5743d86428b0acb1a3db7fa7384b42640d4da6c9fcbcf304
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index eb601d410cb72fc71510716a56bfee7757ec7f4b..37ac387f8d2984bca11e217cb6324390fbb7a657 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -25219,6 +25219,8 @@ CREATE INDEX idx_pkgs_nuget_symbols_on_lowercase_signature_and_file_name ON pack
 
 CREATE INDEX idx_pkgs_on_project_id_name_version_on_installable_terraform ON packages_packages USING btree (project_id, name, version, id) WHERE ((package_type = 12) AND (status = ANY (ARRAY[0, 1])));
 
+CREATE INDEX idx_pkgs_project_id_lower_name_when_nuget_installable_version ON packages_packages USING btree (project_id, lower((name)::text)) WHERE ((package_type = 4) AND (version IS NOT NULL) AND (status = ANY (ARRAY[0, 1])));
+
 CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_cloud_last_sync_at, project_id) WHERE (jira_dvcs_cloud_last_sync_at IS NOT NULL);
 
 CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_server_last_sync_at, project_id) WHERE (jira_dvcs_server_last_sync_at IS NOT NULL);
@@ -27725,8 +27727,6 @@ CREATE INDEX index_packages_packages_on_name_trigram ON packages_packages USING
 
 CREATE INDEX index_packages_packages_on_project_id_and_created_at ON packages_packages USING btree (project_id, created_at);
 
-CREATE INDEX index_packages_packages_on_project_id_and_lower_name ON packages_packages USING btree (project_id, lower((name)::text)) WHERE (package_type = 4);
-
 CREATE INDEX index_packages_packages_on_project_id_and_lower_version ON packages_packages USING btree (project_id, lower((version)::text)) WHERE (package_type = 4);
 
 CREATE INDEX index_packages_packages_on_project_id_and_package_type ON packages_packages USING btree (project_id, package_type);
@@ -27903,6 +27903,8 @@ CREATE UNIQUE INDEX index_project_features_on_project_id_include_container_regis
 
 COMMENT ON INDEX index_project_features_on_project_id_include_container_registry IS 'Included column (container_registry_access_level) improves performance of the ContainerRepository.for_group_and_its_subgroups scope query';
 
+CREATE INDEX index_project_features_on_project_id_on_public_package_registry ON project_features USING btree (project_id) WHERE (package_registry_access_level = 30);
+
 CREATE INDEX index_project_features_on_project_id_ral_20 ON project_features USING btree (project_id) WHERE (repository_access_level = 20);
 
 CREATE INDEX index_project_group_links_on_group_id_and_project_id ON project_group_links USING btree (group_id, project_id);
diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb
index 4f301d7038a1a2ae71ae52ee762324a16f29e918..1237621e8b24707ce499434ee7a66b6eb68eea0f 100644
--- a/lib/api/helpers/packages/basic_auth_helpers.rb
+++ b/lib/api/helpers/packages/basic_auth_helpers.rb
@@ -40,16 +40,24 @@ def authorized_project_find!(action: :read_project)
           project
         end
 
-        def find_authorized_group!
-          group = find_group(params[:id])
+        def find_authorized_group!(action: :read_group)
+          strong_memoize_with(:find_authorized_group, action) do
+            group = find_group(params[:id])
+
+            subject = case action
+                      when :read_package_within_public_registries
+                        group&.packages_policy_subject
+                      when :read_group
+                        group
+                      end
+
+            unless group && can?(current_user, action, subject)
+              break unauthorized_or! { not_found! }
+            end
 
-          unless group && can?(current_user, :read_group, group)
-            return unauthorized_or! { not_found! }
+            group
           end
-
-          group
         end
-        strong_memoize_attr :find_authorized_group!
 
         def authorize!(action, subject = :global, reason = nil)
           return if can?(current_user, action, subject)
diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb
index 037cad29b990fb15d05e4e97b6e93f1e4d15ffc9..632aaa1c528e30918447ce1b364b2ac2df128032 100644
--- a/lib/api/helpers/packages_helpers.rb
+++ b/lib/api/helpers/packages_helpers.rb
@@ -7,7 +7,6 @@ module PackagesHelpers
       include ::Gitlab::Utils::StrongMemoize
 
       MAX_PACKAGE_FILE_SIZE = 50.megabytes.freeze
-      ALLOWED_REQUIRED_PERMISSIONS = %i[read_package read_group].freeze
 
       def require_packages_enabled!
         not_found! unless ::Gitlab.config.packages.enabled
@@ -35,12 +34,16 @@ def authorize_destroy_package!(subject = user_project)
 
       def authorize_packages_access!(subject = user_project, required_permission = :read_package)
         require_packages_enabled!
-        return forbidden! unless required_permission.in?(ALLOWED_REQUIRED_PERMISSIONS)
 
-        if required_permission == :read_package
+        case required_permission
+        when :read_package
           authorize_read_package!(subject)
-        else
+        when :read_package_within_public_registries
+          authorize!(required_permission, subject.packages_policy_subject)
+        when :read_group
           authorize!(required_permission, subject)
+        else
+          forbidden!
         end
       end
 
diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb
index 394f8911e9ed02ef5916b1d87379b624016fc049..ac652ebebec24179bcddfd6be0eea0a7b4de8483 100644
--- a/lib/api/nuget_group_packages.rb
+++ b/lib/api/nuget_group_packages.rb
@@ -30,7 +30,7 @@ class NugetGroupPackages < ::API::Base
       include ::Gitlab::Utils::StrongMemoize
 
       def project_or_group
-        find_authorized_group!
+        find_authorized_group!(action: required_permission)
       end
 
       def project_or_group_without_auth
@@ -55,7 +55,16 @@ def snowplow_gitlab_standard_context_without_auth
       end
 
       def required_permission
-        :read_group
+        if allow_anyone_to_pull_public_packages?
+          :read_package_within_public_registries
+        else
+          :read_group
+        end
+      end
+
+      def allow_anyone_to_pull_public_packages?
+        options[:path].first.in?(%w[index *package_version]) &&
+          ::Feature.enabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, project_or_group_without_auth)
       end
     end
 
@@ -77,7 +86,7 @@ def required_permission
         namespace '/nuget' do
           after_validation do
             # This API can't be accessed anonymously
-            require_authenticated!
+            require_authenticated! unless allow_anyone_to_pull_public_packages?
           end
 
           include ::API::Concerns::Packages::Nuget::PrivateEndpoints
diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb
index 4145e1e2a54187c151317da7fba3a0000abb97da..de7ead202337b747204a4983df360c790bef334e 100644
--- a/spec/finders/concerns/packages/finder_helper_spec.rb
+++ b/spec/finders/concerns/packages/finder_helper_spec.rb
@@ -3,6 +3,22 @@
 require 'spec_helper'
 
 RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do
+  let_it_be(:finder_class) do
+    Class.new do
+      include ::Packages::FinderHelper
+
+      def method_missing(method_name, *args, **kwargs)
+        send(method_name, *args, **kwargs)
+      end
+
+      def respond_to_missing?
+        true
+      end
+    end
+  end
+
+  let_it_be(:finder) { finder_class.new }
+
   describe '#packages_for_project' do
     let_it_be_with_reload(:project1) { create(:project) }
     let_it_be(:package1) { create(:package, project: project1) }
@@ -10,19 +26,7 @@
     let_it_be(:project2) { create(:project) }
     let_it_be(:package3) { create(:package, project: project2) }
 
-    let(:finder_class) do
-      Class.new do
-        include ::Packages::FinderHelper
-
-        def execute(project1)
-          packages_for_project(project1)
-        end
-      end
-    end
-
-    let(:finder) { finder_class.new }
-
-    subject { finder.execute(project1) }
+    subject { finder.packages_for_project(project1) }
 
     it { is_expected.to eq [package1] }
   end
@@ -38,23 +42,7 @@ def execute(project1)
     let_it_be(:package2) { create(:package, project: project2) }
     let_it_be(:package3) { create(:package, :error, project: project2) }
 
-    let(:finder_class) do
-      Class.new do
-        include ::Packages::FinderHelper
-
-        def initialize(user)
-          @current_user = user
-        end
-
-        def execute(group)
-          packages_for(@current_user, within_group: group)
-        end
-      end
-    end
-
-    let(:finder) { finder_class.new(user) }
-
-    subject { finder.execute(group) }
+    subject { finder.packages_for(user, within_group: group) }
 
     shared_examples 'returning both packages' do
       it { is_expected.to contain_exactly(package1, package2) }
@@ -91,13 +79,13 @@ def execute(group)
       end
 
       context 'without a group' do
-        subject { finder.execute(nil) }
+        let(:group) { nil }
 
         it_behaves_like 'returning no packages'
       end
 
       context 'with a subgroup' do
-        subject { finder.execute(subgroup) }
+        let(:group) { subgroup }
 
         it_behaves_like 'returning package2'
       end
@@ -123,20 +111,20 @@ def execute(group)
       end
 
       context 'without a group' do
-        subject { finder.execute(nil) }
+        let(:group) { nil }
 
         it_behaves_like 'returning no packages'
       end
 
       context 'with a subgroup' do
-        subject { finder.execute(subgroup) }
+        let(:group) { subgroup }
 
         it_behaves_like 'returning both packages'
       end
     end
   end
 
-  describe '#packages_visible_to_user' do
+  context 'for packages visible to user' do
     using RSpec::Parameterized::TableSyntax
 
     let_it_be_with_reload(:group) { create(:group) }
@@ -147,24 +135,6 @@ def execute(group)
     let_it_be(:package2) { create(:package, project: project2) }
     let_it_be(:package3) { create(:package, :error, project: project2) }
 
-    let(:finder_class) do
-      Class.new do
-        include ::Packages::FinderHelper
-
-        def initialize(user)
-          @current_user = user
-        end
-
-        def execute(group)
-          packages_visible_to_user(@current_user, within_group: group)
-        end
-      end
-    end
-
-    let(:finder) { finder_class.new(user) }
-
-    subject { finder.execute(group) }
-
     shared_examples 'returning both packages' do
       it { is_expected.to contain_exactly(package1, package2) }
     end
@@ -173,98 +143,125 @@ def execute(group)
       it { is_expected.to eq [package1] }
     end
 
+    shared_examples 'returning package2' do
+      it { is_expected.to eq [package2] }
+    end
+
     shared_examples 'returning no packages' do
       it { is_expected.to be_empty }
     end
 
-    context 'with a user' do
-      let_it_be(:user) { create(:user) }
+    describe '#packages_visible_to_user' do
+      subject { finder.packages_visible_to_user(user, within_group: group) }
+
+      context 'with a user' do
+        let_it_be(:user) { create(:user) }
+
+        where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do
+          'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :maintainer | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :developer  | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :guest      | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :anonymous  | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :maintainer | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :developer  | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :guest      | 'returning package1'
+          'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :anonymous  | 'returning package1'
+          'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages'
+          'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :developer  | 'returning both packages'
+          'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :guest      | 'returning package1'
+          'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :anonymous  | 'returning package1'
+          'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages'
+          'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer  | 'returning both packages'
+          'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest      | 'returning no packages'
+          'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous  | 'returning no packages'
+        end
 
-      where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do
-        'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :maintainer | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :developer  | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :guest      | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | :anonymous  | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :maintainer | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :developer  | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :guest      | 'returning package1'
-        'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | :anonymous  | 'returning package1'
-        'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages'
-        'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :developer  | 'returning both packages'
-        'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :guest      | 'returning package1'
-        'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | :anonymous  | 'returning package1'
-        'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages'
-        'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer  | 'returning both packages'
-        'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest      | 'returning no packages'
-        'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous  | 'returning no packages'
-      end
+        with_them do
+          before do
+            unless user_role == :anonymous
+              group.send("add_#{user_role}", user)
+              subgroup.send("add_#{user_role}", user)
+              project1.send("add_#{user_role}", user)
+              project2.send("add_#{user_role}", user)
+            end
 
-      with_them do
-        before do
-          unless user_role == :anonymous
-            group.send("add_#{user_role}", user)
-            subgroup.send("add_#{user_role}", user)
-            project1.send("add_#{user_role}", user)
-            project2.send("add_#{user_role}", user)
+            project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
+            subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
+            project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+            group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
           end
 
-          project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
-          subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
-          project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
-          group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+          it_behaves_like params[:shared_example_name]
         end
 
-        it_behaves_like params[:shared_example_name]
-      end
+        context 'when the second project has the package registry disabled' do
+          before do
+            project1.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+            project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+              package_registry_access_level: 'disabled', packages_enabled: false)
+          end
 
-      context 'when the second project has the package registry disabled' do
-        before do
-          project1.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
-          project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC,
-            package_registry_access_level: 'disabled', packages_enabled: false)
-        end
+          it_behaves_like 'returning both packages'
 
-        it_behaves_like 'returning both packages'
+          context 'with with_package_registry_enabled set to true' do
+            subject do
+              finder.packages_visible_to_user(user, within_group: group, with_package_registry_enabled: true)
+            end
 
-        context 'with with_package_registry_enabled set to true' do
-          let(:finder_class) do
-            Class.new do
-              include ::Packages::FinderHelper
+            it_behaves_like 'returning package1'
+          end
+        end
+      end
 
-              def initialize(user)
-                @current_user = user
-              end
+      context 'with a group deploy token' do
+        let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) }
+        let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
 
-              def execute(group)
-                packages_visible_to_user(@current_user, within_group: group, with_package_registry_enabled: true)
-              end
-            end
+        where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do
+          'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | 'returning both packages'
+          'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | 'returning both packages'
+          'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
+          'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
+        end
+
+        with_them do
+          before do
+            project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
+            subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
+            project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+            group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
           end
 
-          it_behaves_like 'returning package1'
+          it_behaves_like params[:shared_example_name]
         end
       end
     end
 
-    context 'with a group deploy token' do
-      let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) }
-      let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
+    describe '#packages_visible_to_user_including_public_registries' do
+      subject { finder.packages_visible_to_user_including_public_registries(user, within_group: group) }
 
-      where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do
-        'PUBLIC'  | 'PUBLIC'  | 'PUBLIC'  | 'returning both packages'
-        'PUBLIC'  | 'PUBLIC'  | 'PRIVATE' | 'returning both packages'
-        'PUBLIC'  | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
-        'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
-      end
+      let(:user) { nil }
 
-      with_them do
-        before do
-          project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
-          subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
-          project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
-          group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+      before do
+        [subgroup, group, project1, project2].each do |entity|
+          entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false))
         end
+        project1.project_feature.update!(package_registry_access_level: project1_package_registry_access_level)
+        project2.project_feature.update!(package_registry_access_level: project2_package_registry_access_level)
+      end
 
+      where(:project1_package_registry_access_level, :project2_package_registry_access_level, :shared_example_name) do
+        ::ProjectFeature::PUBLIC   | ::ProjectFeature::PUBLIC   | 'returning both packages'
+        ::ProjectFeature::PUBLIC   | ::ProjectFeature::PRIVATE  | 'returning package1'
+        ::ProjectFeature::PUBLIC   | ::ProjectFeature::DISABLED | 'returning package1'
+        ::ProjectFeature::PUBLIC   | ::ProjectFeature::ENABLED  | 'returning package1'
+        ::ProjectFeature::PRIVATE  | ::ProjectFeature::PUBLIC   | 'returning package2'
+        ::ProjectFeature::DISABLED | ::ProjectFeature::PUBLIC   | 'returning package2'
+        ::ProjectFeature::ENABLED  | ::ProjectFeature::PUBLIC   | 'returning package2'
+        ::ProjectFeature::PRIVATE  | ::ProjectFeature::PRIVATE  | 'returning no packages'
+      end
+
+      with_them do
         it_behaves_like params[:shared_example_name]
       end
     end
@@ -279,23 +276,7 @@ def execute(group)
     let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
     let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) }
 
-    let(:finder_class) do
-      Class.new do
-        include ::Packages::FinderHelper
-
-        def initialize(user)
-          @current_user = user
-        end
-
-        def execute(group)
-          projects_visible_to_user(@current_user, within_group: group)
-        end
-      end
-    end
-
-    let(:finder) { finder_class.new(user) }
-
-    subject { finder.execute(group) }
+    subject { finder.projects_visible_to_user(user, within_group: group) }
 
     shared_examples 'returning both projects' do
       it { is_expected.to contain_exactly(project1, project2) }
diff --git a/spec/finders/packages/nuget/package_finder_spec.rb b/spec/finders/packages/nuget/package_finder_spec.rb
index 8230d132d75d64fcb582b8386b76985860075c72..d2730a9c0483307b26c65e3f3476c2666916ec49 100644
--- a/spec/finders/packages/nuget/package_finder_spec.rb
+++ b/spec/finders/packages/nuget/package_finder_spec.rb
@@ -158,5 +158,26 @@
 
       it { is_expected.to be_empty }
     end
+
+    context 'with public package registry in private group' do
+      let(:target) { group }
+
+      before_all do
+        [subgroup, group, project].each do |entity|
+          entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false))
+        end
+        project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC)
+      end
+
+      it { is_expected.to match_array([package1, package2]) }
+
+      context 'when allow_anyone_to_pull_public_nuget_packages_on_group_level FF is disabled' do
+        before do
+          stub_feature_flags(allow_anyone_to_pull_public_nuget_packages_on_group_level: false)
+        end
+
+        it { is_expected.to be_empty }
+      end
+    end
   end
 end
diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb
index f44581dcd853dc71ce97a435e0ae7247ca5ed442..e20bb01e0605b5b8168e429bece86b756edb89b0 100644
--- a/spec/lib/api/helpers/packages_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages_helpers_spec.rb
@@ -42,6 +42,17 @@
         expect(subject).to eq nil
       end
     end
+
+    context 'with read_public_package_registry permission' do
+      subject { helper.authorize_packages_access!(group, :read_package_within_public_registries) }
+
+      it 'authorizes packages access' do
+        expect(helper).to receive(:require_packages_enabled!)
+        expect(helper).to receive(:authorize!).with(:read_package_within_public_registries, instance_of(::Packages::Policies::Group))
+
+        expect(subject).to eq nil
+      end
+    end
   end
 
   describe 'authorize_read_package!' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 08417fd3a4df285fa2c193524f77e878ce12c15e..8af36ef57d088d9ebdb4931f5666906b49edc4d0 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -7057,6 +7057,15 @@ def has_external_wiki
     end
   end
 
+  describe '.with_public_package_registry' do
+    let_it_be(:project) { create(:project, package_registry_access_level: ::ProjectFeature::PUBLIC) }
+    let_it_be(:other_project) { create(:project, package_registry_access_level: ::ProjectFeature::ENABLED) }
+
+    subject { described_class.with_public_package_registry }
+
+    it { is_expected.to contain_exactly(project) }
+  end
+
   describe '.not_a_fork' do
     let_it_be(:project) { create(:project, :public) }
 
diff --git a/spec/policies/packages/policies/group_policy_spec.rb b/spec/policies/packages/policies/group_policy_spec.rb
index d0d9a9a22f5cab3c4032f17d1c4266523ad387cc..165984dea1dcd12ea567770214e332d667c360ad 100644
--- a/spec/policies/packages/policies/group_policy_spec.rb
+++ b/spec/policies/packages/policies/group_policy_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Packages::Policies::GroupPolicy do
+RSpec.describe Packages::Policies::GroupPolicy, feature_category: :package_registry do
   include_context 'GroupPolicy context'
 
   subject { described_class.new(current_user, group.packages_policy_subject) }
@@ -76,4 +76,67 @@
       it { is_expected.to be_allowed(:read_package) }
     end
   end
+
+  describe 'read public package registry' do
+    using RSpec::Parameterized::TableSyntax
+
+    let_it_be_with_reload(:project) { create(:project, group: group) }
+    let(:current_user) { can_read_group ? reporter : external_user }
+
+    subject { described_class.new(current_user, group.packages_policy_subject) }
+
+    before do
+      group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+      project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false))
+      project.project_feature.update!(package_registry_access_level: package_registry_access_level)
+      stub_application_setting(package_registry_allow_anyone_to_pull_option: application_setting)
+    end
+
+    where(:group_visibility, :project_visibility, :package_registry_access_level, :can_read_group,
+      :application_setting, :result) do
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | true  | true  | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | true  | false | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | true  | false
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | false | false
+
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | true  | true  | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | true  | false | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | false | true  | false
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | false | false | false
+
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED  | true  | true  | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED  | true  | false | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED  | false | true  | false
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED  | false | false | false
+
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | true  | true  | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | true  | false | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | false | true  | true
+      'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | false | false | false
+
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | true  | true  | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | true  | false | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | true  | false
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | false | false
+
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | true  | true  | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | true  | false | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | false | true  | false
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE  | false | false | false
+
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED  | true  | true  | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED  | true  | false | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED  | false | true  | false
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED  | false | false | false
+
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | true  | true  | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | true  | false | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | false | true  | true
+      'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC   | false | false | false
+    end
+
+    with_them do
+      it { is_expected.to public_send(result ? :be_allowed : :be_disallowed, :read_package_within_public_registries) }
+    end
+  end
 end
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index 8b8c28a13c5dcea2920e2268c9386e32d2319f15..50f5b780754aa688311066d05af64dca0a84a235 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -38,29 +38,31 @@ def snowplow_context(user_role: :developer)
     end
 
     describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do
+      let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/index.json" }
+
       it_behaves_like 'handling nuget metadata requests with package name',
         example_names_with_status:
         {
-          anonymous_requests_example_name: 'rejects nuget packages access',
-          anonymous_requests_status: :unauthorized,
           guest_requests_example_name: 'rejects nuget packages access',
-          guest_requests_status: :not_found
-        } do
-        let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/index.json" }
-      end
+          guest_requests_status: :not_found,
+          invalid_target_not_found_status: :not_found
+        }
+
+      it_behaves_like 'allows anyone to pull public nuget packages on group level'
     end
 
     describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/*package_version' do
+      let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
+
       it_behaves_like 'handling nuget metadata requests with package name and package version',
         example_names_with_status:
         {
-          anonymous_requests_example_name: 'rejects nuget packages access',
-          anonymous_requests_status: :unauthorized,
           guest_requests_example_name: 'rejects nuget packages access',
-          guest_requests_status: :not_found
-        } do
-        let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
-      end
+          guest_requests_status: :not_found,
+          invalid_target_not_found_status: :not_found
+        }
+
+      it_behaves_like 'allows anyone to pull public nuget packages on group level'
     end
 
     describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do
@@ -109,11 +111,11 @@ def update_visibility_to(visibility)
 
       subject { get api(url), headers: {} }
 
-      shared_examples 'handling mixed visibilities' do
+      shared_examples 'handling mixed visibilities' do |public_status: :success, non_public_status: :not_found|
         where(:group_visibility, :subgroup_visibility, :expected_status) do
-          'PUBLIC'   | 'PUBLIC'   | :unauthorized
-          'PUBLIC'   | 'INTERNAL' | :unauthorized
-          'PUBLIC'   | 'PRIVATE'  | :unauthorized
+          'PUBLIC'   | 'PUBLIC'   | public_status
+          'PUBLIC'   | 'INTERNAL' | non_public_status
+          'PUBLIC'   | 'PRIVATE'  | non_public_status
           'INTERNAL' | 'INTERNAL' | :unauthorized
           'INTERNAL' | 'PRIVATE'  | :unauthorized
           'PRIVATE'  | 'PRIVATE'  | :unauthorized
@@ -143,7 +145,7 @@ def update_visibility_to(visibility)
       end
 
       describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do
-        it_behaves_like 'handling mixed visibilities' do
+        it_behaves_like 'handling mixed visibilities', public_status: :unauthorized, non_public_status: :unauthorized do
           let(:url) { "/groups/#{target.id}/-/packages/nuget/query?#{query_parameters.to_query}" }
         end
       end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index b3e0b6e27ee791ac15b8eace5bf23f0b2a31e235..0e41bee4f4f5f6b210c4406e0f6cf6c0dfc68689 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -14,6 +14,7 @@
   let_it_be(:owner) { create(:user, owner_of: group) }
   let_it_be(:admin) { create(:admin) }
   let_it_be(:non_group_member) { create(:user) }
+  let_it_be(:external_user) { create(:user, :external) }
 
   let_it_be(:organization_owner) { create(:organization_user, :owner, organization: organization).user }
 
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index 150e9a4e00436ede81466ece2632f6ba97e735c2..d5b90456dd0c536d941e66a32e3eaa8239b00170 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -44,10 +44,9 @@
 RSpec.shared_examples 'handling nuget metadata requests with package name' do |example_names_with_status: {}|
   include_context 'with expected presenters dependency groups'
 
-  anonymous_requests_example_name = example_names_with_status.fetch(:anonymous_requests_example_name, 'process nuget metadata request at package name level')
-  anonymous_requests_status = example_names_with_status.fetch(:anonymous_requests_status, :success)
   guest_requests_example_name = example_names_with_status.fetch(:guest_requests_example_name, 'rejects nuget packages access')
   guest_requests_status = example_names_with_status.fetch(:guest_requests_status, :forbidden)
+  invalid_target_not_found_status = example_names_with_status.fetch(:invalid_target_not_found_status, :unauthorized)
 
   let_it_be(:package_name) { 'Dummy.Package' }
   let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) }
@@ -71,7 +70,7 @@
       'PUBLIC'  | :guest      | false | true  | 'process nuget metadata request at package name level' | :success
       'PUBLIC'  | :developer  | false | false | 'rejects nuget packages access'                        | :unauthorized
       'PUBLIC'  | :guest      | false | false | 'rejects nuget packages access'                        | :unauthorized
-      'PUBLIC'  | :anonymous  | false | true  | anonymous_requests_example_name                        | anonymous_requests_status
+      'PUBLIC'  | :anonymous  | false | true  | 'process nuget metadata request at package name level' | :success
       'PRIVATE' | :developer  | true  | true  | 'process nuget metadata request at package name level' | :success
       'PRIVATE' | :guest      | true  | true  | guest_requests_example_name                            | guest_requests_status
       'PRIVATE' | :developer  | true  | false | 'rejects nuget packages access'                        | :unauthorized
@@ -103,19 +102,18 @@
       end
     end
 
-    it_behaves_like 'rejects nuget access with unknown target id'
+    it_behaves_like 'rejects nuget access with unknown target id', not_found_response: invalid_target_not_found_status
 
-    it_behaves_like 'rejects nuget access with invalid target id'
+    it_behaves_like 'rejects nuget access with invalid target id', not_found_response: invalid_target_not_found_status
   end
 end
 
 RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do |example_names_with_status: {}|
   include_context 'with expected presenters dependency groups'
 
-  anonymous_requests_example_name = example_names_with_status.fetch(:anonymous_requests_example_name, 'process nuget metadata request at package name and package version level')
-  anonymous_requests_status = example_names_with_status.fetch(:anonymous_requests_status, :success)
   guest_requests_example_name = example_names_with_status.fetch(:guest_requests_example_name, 'rejects nuget packages access')
   guest_requests_status = example_names_with_status.fetch(:guest_requests_status, :forbidden)
+  invalid_target_not_found_status = example_names_with_status.fetch(:invalid_target_not_found_status, :unauthorized)
 
   let_it_be(:package_name) { 'Dummy.Package' }
   let_it_be(:package) { create(:nuget_package, :with_metadatum, name: package_name, project: project) }
@@ -139,7 +137,7 @@
       'PUBLIC'  | :guest      | false | true  | 'process nuget metadata request at package name and package version level' | :success
       'PUBLIC'  | :developer  | false | false | 'rejects nuget packages access'                                            | :unauthorized
       'PUBLIC'  | :guest      | false | false | 'rejects nuget packages access'                                            | :unauthorized
-      'PUBLIC'  | :anonymous  | false | true  | anonymous_requests_example_name                                            | anonymous_requests_status
+      'PUBLIC'  | :anonymous  | false | true  | 'process nuget metadata request at package name and package version level' | :success
       'PRIVATE' | :developer  | true  | true  | 'process nuget metadata request at package name and package version level' | :success
       'PRIVATE' | :guest      | true  | true  | guest_requests_example_name                                                | guest_requests_status
       'PRIVATE' | :developer  | true  | false | 'rejects nuget packages access'                                            | :unauthorized
@@ -172,9 +170,9 @@
     end
   end
 
-  it_behaves_like 'rejects nuget access with unknown target id'
+  it_behaves_like 'rejects nuget access with unknown target id', not_found_response: invalid_target_not_found_status
 
-  it_behaves_like 'rejects nuget access with invalid target id'
+  it_behaves_like 'rejects nuget access with invalid target id', not_found_response: invalid_target_not_found_status
 end
 
 RSpec.shared_examples 'handling nuget search requests' do |example_names_with_status: {}|
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index 372b1b7c21d536c4eb42266cc27e79e9e8a9487d..7019392a91a5a758d974799224e9df1787fabaad 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -472,7 +472,7 @@
 
 RSpec.shared_examples 'rejects nuget access with unknown target id' do |not_found_response: :unauthorized|
   context 'with an unknown target' do
-    let(:target) { double(id: 1234567890) }
+    let(:target) { double(id: non_existing_record_id) }
 
     context 'as anonymous' do
       it_behaves_like 'rejects nuget packages access', :anonymous, not_found_response
@@ -486,6 +486,33 @@
   end
 end
 
+RSpec.shared_examples 'allows anyone to pull public nuget packages on group level' do
+  let_it_be(:package_name) { 'dummy.package' }
+  let_it_be(:package) { create(:nuget_package, project: project, name: package_name) }
+  let_it_be(:external_user) { create(:user, external: true) }
+  let_it_be(:personal_access_token) { create(:personal_access_token, user: external_user) }
+
+  subject { get api(url), headers: basic_auth_header(external_user.username, personal_access_token.token) }
+
+  before do
+    [subgroup, group, project].each do |entity|
+      entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false))
+    end
+    project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC)
+    stub_application_setting(package_registry_allow_anyone_to_pull_option: true)
+  end
+
+  it_behaves_like 'returning response status', :ok
+
+  context 'when allow_anyone_to_pull_public_nuget_packages_on_group_level FF is disabled' do
+    before do
+      stub_feature_flags(allow_anyone_to_pull_public_nuget_packages_on_group_level: false)
+    end
+
+    it_behaves_like 'returning response status', :not_found
+  end
+end
+
 RSpec.shared_examples 'nuget authorize upload endpoint' do
   using RSpec::Parameterized::TableSyntax
   include_context 'workhorse headers'
@@ -776,7 +803,7 @@
     end
 
     context 'when target does not exist' do
-      let(:target) { double(id: 1234567890) }
+      let(:target) { double(id: non_existing_record_id) }
 
       it_behaves_like 'returning response status', :not_found
     end