diff --git a/app/policies/gitlab/git/tag_policy.rb b/app/policies/gitlab/git/tag_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee406f4d1a017ef5c1c35f0866abcc2182550a8c
--- /dev/null
+++ b/app/policies/gitlab/git/tag_policy.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Git
+    class TagPolicy < BasePolicy
+      delegate { project }
+
+      condition(:protected_tag, scope: :subject) do
+        ProtectedTag.protected?(project, @subject.name)
+      end
+
+      rule { can?(:admin_tag) & (~protected_tag | can?(:maintainer_access)) }.enable :delete_tag
+
+      def project
+        @subject.repository.container
+      end
+    end
+  end
+end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 4d1f4043b01ab3b48c6e7fb3edee90979fcc78d4..ae3f989194378cc660ff782724a0b380e9174e5d 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -2,21 +2,17 @@
 
 module Tags
   class DestroyService < BaseService
-    def execute(tag_name)
+    def execute(tag_name, skip_find: false)
       repository = project.repository
-      tag = repository.find_tag(tag_name)
 
-      unless tag
-        return error('No such tag', 404)
-      end
+      # If we've found the tag upstream we don't need to refind it so we can
+      # pass skip_find: true
+      return error('No such tag', 404) unless skip_find || tag_exists?(tag_name)
 
       if repository.rm_tag(current_user, tag_name)
-        ##
-        # When a tag in a repository is destroyed,
-        # release assets will be destroyed too.
-        Releases::DestroyService
-          .new(project, current_user, tag: tag_name)
-          .execute
+        # When a tag in a repository is destroyed, release assets will be
+        # destroyed too.
+        destroy_releases(tag_name)
 
         unlock_artifacts(tag_name)
 
@@ -38,6 +34,14 @@ def success(message)
 
     private
 
+    def tag_exists?(tag_name)
+      repository.find_tag(tag_name)
+    end
+
+    def destroy_releases(tag_name)
+      Releases::DestroyService.new(project, current_user, tag: tag_name).execute
+    end
+
     def unlock_artifacts(tag_name)
       Ci::RefDeleteUnlockArtifactsWorker.perform_async(project.id, current_user.id, "#{::Gitlab::Git::TAG_REF_PREFIX}#{tag_name}")
     end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 3720032f984755d8e18cd1e6abdcd64c636f4a8c..2df9c62dcb7e62694738b0c5f96eb20c35a60b89 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -119,6 +119,7 @@ def find_releases(tags)
       desc 'Delete a repository tag' do
         success code: 204
         failure [
+          { code: 400, message: 'Bad request' },
           { code: 403, message: 'Unauthenticated' },
           { code: 404, message: 'Not found' },
           { code: 412, message: 'Precondition failed' }
@@ -129,16 +130,14 @@ def find_releases(tags)
         requires :tag_name, type: String, desc: 'The name of the tag'
       end
       delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :source_code_management do
-        authorize_admin_tag
-
         tag = user_project.repository.find_tag(params[:tag_name])
         not_found!('Tag') unless tag
+        authorize!(:delete_tag, tag)
 
         commit = user_project.repository.commit(tag.dereferenced_target)
 
         destroy_conditionally!(commit, last_updated: commit.authored_date) do
-          result = ::Tags::DestroyService.new(user_project, current_user)
-                    .execute(params[:tag_name])
+          result = ::Tags::DestroyService.new(user_project, current_user).execute(params[:tag_name], skip_find: true)
 
           if result[:status] != :success
             render_api_error!(result[:message], result[:return_code])
diff --git a/spec/policies/gitlab/git/tag_policy_spec.rb b/spec/policies/gitlab/git/tag_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c66488dc18dbbba715a4f3b355681062facd01d4
--- /dev/null
+++ b/spec/policies/gitlab/git/tag_policy_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Git::TagPolicy, feature_category: :source_code_management do
+  let_it_be(:project) { create(:project, :public, :repository) }
+  let_it_be(:guest) { create(:user, guest_of: project) }
+  let_it_be(:developer) { create(:user, developer_of: project) }
+  let_it_be(:maintainer) { create(:user, maintainer_of: project) }
+  let_it_be(:owner) { create(:user, owner_of: project) }
+  let_it_be(:tag) { project.repository.tags.first }
+
+  subject { described_class.new(user, tag) }
+
+  context 'when user is a project guest' do
+    let(:user) { guest }
+
+    it { is_expected.to be_disallowed(:delete_tag) }
+  end
+
+  context 'when user is a project developer' do
+    let(:user) { developer }
+
+    it { is_expected.to be_allowed(:delete_tag) }
+
+    context 'when the tag is protected' do
+      let_it_be(:protected_tag) { create(:protected_tag, project: project, name: tag.name) }
+
+      it { is_expected.to be_disallowed(:delete_tag) }
+    end
+  end
+
+  context 'when user is a project maintainer' do
+    let(:user) { maintainer }
+
+    it { is_expected.to be_allowed(:delete_tag) }
+
+    context 'when the tag is protected' do
+      let_it_be(:protected_tag) { create(:protected_tag, project: project, name: tag.name) }
+
+      it { is_expected.to be_allowed(:delete_tag) }
+    end
+  end
+
+  context 'when user is a project owner' do
+    let(:user) { owner }
+
+    it { is_expected.to be_allowed(:delete_tag) }
+
+    context 'when the tag is protected' do
+      let_it_be(:protected_tag) { create(:protected_tag, project: project, name: tag.name) }
+
+      it { is_expected.to be_allowed(:delete_tag) }
+    end
+  end
+end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 6794e05161fd271d29255730fd77737c1d0de0db..8556fd046bd1dc68fcdbc104f45c0a10c46b6ea2 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -537,8 +537,16 @@
       end
     end
 
-    context 'when authenticated', 'as a maintainer' do
-      let(:current_user) { user }
+    context 'when authenticated as a guest' do
+      let(:current_user) { create(:user, guest_of: project) }
+
+      it_behaves_like '403 response' do
+        let(:request) { delete api(route, current_user) }
+      end
+    end
+
+    context 'when authenticated as a developer' do
+      let(:current_user) { create(:user, developer_of: project) }
 
       it_behaves_like 'repository delete tag'
 
@@ -547,6 +555,42 @@
 
         it_behaves_like 'repository delete tag'
       end
+
+      context 'when the tag is protected' do
+        before do
+          create(:protected_tag, project: project, name: tag_name)
+        end
+
+        it_behaves_like '403 response' do
+          let(:request) { delete api(route, current_user) }
+        end
+      end
+    end
+
+    context 'when authenticated as a maintainer' do
+      let(:current_user) { create(:user, maintainer_of: project) }
+
+      it_behaves_like 'repository delete tag'
+
+      context 'when the tag is protected' do
+        before do
+          create(:protected_tag, project: project, name: tag_name)
+        end
+
+        it_behaves_like 'repository delete tag'
+      end
+    end
+
+    context 'when authenticated as an owner' do
+      let(:current_user) { create(:user, owner_of: project) }
+
+      context 'when the tag is protected' do
+        before do
+          create(:protected_tag, project: project, name: tag_name)
+        end
+
+        it_behaves_like 'repository delete tag'
+      end
     end
   end
 
diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb
index 343a87785ad8badb6fc1ffbbb6f900ca560c1ca7..6d4b2f42bfc2cb9bbc35e6d5b3d83656d291965d 100644
--- a/spec/services/tags/destroy_service_spec.rb
+++ b/spec/services/tags/destroy_service_spec.rb
@@ -7,25 +7,46 @@
   let(:repository) { project.repository }
   let(:user) { create(:user) }
   let(:service) { described_class.new(project, user) }
+  let(:skip_find) { false }
 
-  describe '#execute' do
-    subject { service.execute(tag_name) }
+  describe '#execute(tag_name, skip_find: false)' do
+    subject(:execute) { service.execute(tag_name, skip_find: skip_find) }
 
     before do
       allow(Ci::RefDeleteUnlockArtifactsWorker).to receive(:perform_async)
     end
 
-    it 'removes the tag' do
-      expect(repository).to receive(:before_remove_tag)
-      expect(service).to receive(:success)
+    context 'with tag named v1.1.0' do
+      let(:tag_name) { 'v1.1.0' }
 
-      service.execute('v1.1.0')
-    end
+      it 'removes the tag' do
+        allow(repository).to receive(:before_remove_tag)
+        allow(service).to receive(:success)
+
+        execute
+
+        expect(repository).to have_received(:before_remove_tag)
+        expect(service).to have_received(:success)
+      end
+
+      context 'when skip_find is true' do
+        let(:skip_find) { true }
 
-    it 'calls the RefDeleteUnlockArtifactsWorker' do
-      expect(Ci::RefDeleteUnlockArtifactsWorker).to receive(:perform_async).with(project.id, user.id, 'refs/tags/v1.1.0')
+        before do
+          allow(repository).to receive(:find_tag)
+          execute
+        end
 
-      service.execute('v1.1.0')
+        it 'does not verify the tag exists in the repository' do
+          expect(repository).not_to have_received(:find_tag)
+        end
+      end
+
+      it 'calls the RefDeleteUnlockArtifactsWorker' do
+        expect(Ci::RefDeleteUnlockArtifactsWorker).to receive(:perform_async).with(project.id, user.id, "refs/tags/#{tag_name}")
+
+        execute
+      end
     end
 
     context 'when there is an associated release on the tag' do