diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a9391159541763fdcf327271e106367ea4502ba9..55e4b37c34c2a1dcee5b5a54a5e23f1c39115214 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -428,7 +428,7 @@ def project_feature_attributes
   def operations_feature_attributes
     if Feature.enabled?(:split_operations_visibility_permissions, project)
       %i[
-        environments_access_level feature_flags_access_level
+        environments_access_level feature_flags_access_level releases_access_level
       ]
     else
       %i[operations_access_level]
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index b257d9896a2e79cc3a0af884b9d072ffbbc0856c..dfc270adf8b644dc802d768e947db4e66d36df8c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -641,7 +641,8 @@ def project_permissions_settings(project)
       securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
       containerRegistryAccessLevel: feature.container_registry_access_level,
       environmentsAccessLevel: feature.environments_access_level,
-      featureFlagsAccessLevel: feature.feature_flags_access_level
+      featureFlagsAccessLevel: feature.feature_flags_access_level,
+      releasesAccessLevel: feature.releases_access_level
     }
   end
 
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 8f87e8b3f2249c577a5e7654acd8dd43213f06f3..7613691bc2e4c47b7914c9c524e41e1a76010977 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -102,6 +102,10 @@ def feature_flags_access_level=(value)
     write_feature_attribute_string(:feature_flags_access_level, value)
   end
 
+  def releases_access_level=(value)
+    write_feature_attribute_string(:releases_access_level, value)
+  end
+
   # TODO: Remove this method after we drop support for project create/edit APIs to set the
   # container_registry_enabled attribute. They can instead set the container_registry_access_level
   # attribute.
diff --git a/app/models/project.rb b/app/models/project.rb
index 16b3e1c09c516730beda5060d82b7caf64170d62..efb8e8c03e6f1c1ec36ad4251e5d9ca10fa1f4ef 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -453,6 +453,7 @@ def self.integration_association_name(name)
            :metrics_dashboard_access_level, :analytics_access_level,
            :operations_access_level, :security_and_compliance_access_level,
            :container_registry_access_level, :environments_access_level, :feature_flags_access_level,
+           :releases_access_level,
            to: :project_feature, allow_nil: true
 
   delegate :show_default_award_emojis, :show_default_award_emojis=,
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 82d98496b6b8bdf08446131a2ed2b977bf5c46a1..8623e477c06e56cc2bcfbf166b234298c46880ee 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -23,6 +23,7 @@ class ProjectFeature < ApplicationRecord
     package_registry
     environments
     feature_flags
+    releases
   ].freeze
 
   EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index c2bea7436eb17c9f6279d33bc92f9a9433e92af3..f4f7275a78a35e246e7e4d37778f684bccaee4e2 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -211,6 +211,7 @@ class ProjectPolicy < BasePolicy
     security_and_compliance
     environments
     feature_flags
+    releases
   ]
 
   features.each do |f|
@@ -396,6 +397,10 @@ class ProjectPolicy < BasePolicy
     prevent(:admin_feature_flags_client)
   end
 
+  rule { split_operations_visibility_permissions & releases_disabled }.policy do
+    prevent(*create_read_update_admin_destroy(:release))
+  end
+
   rule { can?(:metrics_dashboard) }.policy do
     enable :read_prometheus
     enable :read_deployment
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index b6c3da8e564c2b11c25bdfb07dbdf257152d295a..e6212132e963bfc22b1f8220c028f60d0e6cc43e 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -287,6 +287,7 @@ included_attributes:
     - :package_registry_access_level
     - :environments_access_level
     - :feature_flags_access_level
+    - :releases_access_level
   prometheus_metrics:
     - :created_at
     - :updated_at
@@ -693,6 +694,7 @@ included_attributes:
     - :package_registry_access_level
     - :environments_access_level
     - :feature_flags_access_level
+    - :releases_access_level
     - :allow_merge_on_skipped_pipeline
     - :auto_devops_deploy_strategy
     - :auto_devops_enabled
diff --git a/rubocop/cop/gitlab/feature_available_usage.rb b/rubocop/cop/gitlab/feature_available_usage.rb
index a153d3f7b2f5c93d1e212ff2f26390a080dc0539..f748b7d9111f149bfa2cbcf80cf8660329a6bd4d 100644
--- a/rubocop/cop/gitlab/feature_available_usage.rb
+++ b/rubocop/cop/gitlab/feature_available_usage.rb
@@ -25,6 +25,7 @@ class FeatureAvailableUsage < RuboCop::Cop::Cop
           container_registry
           environments
           feature_flags
+          releases
         ].freeze
         EE_FEATURES = %i[requirements].freeze
         ALL_FEATURES = (FEATURES + EE_FEATURES).freeze
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 388d7c2266b4e4fe6748550e2880758f4ff2e73d..94d75ab8d7d98e287187c0abe0dcdb20cdf2ecfc 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -919,6 +919,7 @@ def update_project_feature
           container_registry_access_level
           environments_access_level
           feature_flags_access_level
+          releases_access_level
         ]
       end
 
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 98dfecb2888e0ccad386e95abca279ea75485a51..95b72648cf549337918067e64f536c449666cdef 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -39,6 +39,7 @@
       security_and_compliance_access_level { ProjectFeature::PRIVATE }
       environments_access_level { ProjectFeature::ENABLED }
       feature_flags_access_level { ProjectFeature::ENABLED }
+      releases_access_level { ProjectFeature::ENABLED }
 
       # we can't assign the delegated `#ci_cd_settings` attributes directly, as the
       # `#ci_cd_settings` relation needs to be created first
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 214086a382321165282677a66e99c8809aac4ad5..04c066986b72a61e02284c299357709ef079d14c 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -968,7 +968,8 @@ def license_name
         securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
         containerRegistryAccessLevel: project.project_feature.container_registry_access_level,
         environmentsAccessLevel: project.project_feature.environments_access_level,
-        featureFlagsAccessLevel: project.project_feature.feature_flags_access_level
+        featureFlagsAccessLevel: project.project_feature.feature_flags_access_level,
+        releasesAccessLevel: project.project_feature.releases_access_level
       )
     end
 
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 72e13943bf5f403269652c126f8200586065ac6a..5cbe05ccf5359f099536dae12d48e7d0b8ae6698 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -585,6 +585,7 @@ ProjectFeature:
 - package_registry_access_level
 - environments_access_level
 - feature_flags_access_level
+- releases_access_level
 - created_at
 - updated_at
 ProtectedBranch::MergeAccessLevel:
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index 48c391451edf7c84df3fecc599f1ca45767979ca..b49b9ce8a2a0a7ffd1eedb869263364246d4e08c 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -6,7 +6,9 @@
   let(:project) { create(:project) }
   let(:features_enabled) { %w(issues wiki builds merge_requests snippets security_and_compliance) }
   let(:features) do
-    features_enabled + %w(repository pages operations container_registry package_registry environments feature_flags)
+    features_enabled + %w(
+      repository pages operations container_registry package_registry environments feature_flags releases
+    )
   end
 
   # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e2911f2201ec620e02a4df835e45425acf8cf54e..d69a358d4f9d09593bf5e8b8629dce5af49c340c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -835,6 +835,7 @@
     it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
     it { is_expected.to delegate_method(:environments_access_level).to(:project_feature) }
     it { is_expected.to delegate_method(:feature_flags_access_level).to(:project_feature) }
+    it { is_expected.to delegate_method(:releases_access_level).to(:project_feature) }
 
     describe 'read project settings' do
       %i(
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index c3817f671585f847fd64786e7b0dd3f90288a6ef..e8fdf9a8e256c2d28db875aca1c1b16fea261b39 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -2171,6 +2171,74 @@ def permissions_abilities(role)
     end
   end
 
+  describe 'Releases feature' do
+    using RSpec::Parameterized::TableSyntax
+
+    let(:guest_permissions) { [:read_release] }
+
+    let(:developer_permissions) do
+      guest_permissions + [:create_release, :update_release, :destroy_release]
+    end
+
+    let(:maintainer_permissions) do
+      developer_permissions
+    end
+
+    where(:project_visibility, :access_level, :role, :allowed) do
+      :public   | ProjectFeature::ENABLED   | :maintainer | true
+      :public   | ProjectFeature::ENABLED   | :developer  | true
+      :public   | ProjectFeature::ENABLED   | :guest      | true
+      :public   | ProjectFeature::ENABLED   | :anonymous  | true
+      :public   | ProjectFeature::PRIVATE   | :maintainer | true
+      :public   | ProjectFeature::PRIVATE   | :developer  | true
+      :public   | ProjectFeature::PRIVATE   | :guest      | true
+      :public   | ProjectFeature::PRIVATE   | :anonymous  | false
+      :public   | ProjectFeature::DISABLED  | :maintainer | false
+      :public   | ProjectFeature::DISABLED  | :developer  | false
+      :public   | ProjectFeature::DISABLED  | :guest      | false
+      :public   | ProjectFeature::DISABLED  | :anonymous  | false
+      :internal | ProjectFeature::ENABLED   | :maintainer | true
+      :internal | ProjectFeature::ENABLED   | :developer  | true
+      :internal | ProjectFeature::ENABLED   | :guest      | true
+      :internal | ProjectFeature::ENABLED   | :anonymous  | false
+      :internal | ProjectFeature::PRIVATE   | :maintainer | true
+      :internal | ProjectFeature::PRIVATE   | :developer  | true
+      :internal | ProjectFeature::PRIVATE   | :guest      | true
+      :internal | ProjectFeature::PRIVATE   | :anonymous  | false
+      :internal | ProjectFeature::DISABLED  | :maintainer | false
+      :internal | ProjectFeature::DISABLED  | :developer  | false
+      :internal | ProjectFeature::DISABLED  | :guest      | false
+      :internal | ProjectFeature::DISABLED  | :anonymous  | false
+      :private  | ProjectFeature::ENABLED   | :maintainer | true
+      :private  | ProjectFeature::ENABLED   | :developer  | true
+      :private  | ProjectFeature::ENABLED   | :guest      | true
+      :private  | ProjectFeature::ENABLED   | :anonymous  | false
+      :private  | ProjectFeature::PRIVATE   | :maintainer | true
+      :private  | ProjectFeature::PRIVATE   | :developer  | true
+      :private  | ProjectFeature::PRIVATE   | :guest      | true
+      :private  | ProjectFeature::PRIVATE   | :anonymous  | false
+      :private  | ProjectFeature::DISABLED  | :maintainer | false
+      :private  | ProjectFeature::DISABLED  | :developer  | false
+      :private  | ProjectFeature::DISABLED  | :guest      | false
+      :private  | ProjectFeature::DISABLED  | :anonymous  | false
+    end
+
+    with_them do
+      let(:current_user) { user_subject(role) }
+      let(:project) { project_subject(project_visibility) }
+
+      it 'allows/disallows the abilities based on the Releases access level' do
+        project.project_feature.update!(releases_access_level: access_level)
+
+        if allowed
+          expect_allowed(*permissions_abilities(role))
+        else
+          expect_disallowed(*permissions_abilities(role))
+        end
+      end
+    end
+  end
+
   describe 'access_security_and_compliance' do
     context 'when the "Security & Compliance" is enabled' do
       before do