diff --git a/config/patches/gitlab-rails/security-patch-13.12.patch b/config/patches/gitlab-rails/security-patch-13.12.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTEzLjEyLnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-13.12.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index 96977f3c6c3..c3ebd618afd 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -44,7 +44,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   default_value_for :system, false
+ 
+@@ -561,6 +561,41 @@ def skip_notification?
+     review.present? || !author.can_trigger_notifications?
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
diff --git a/config/patches/gitlab-rails/security-patch-14.0.patch b/config/patches/gitlab-rails/security-patch-14.0.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTE0LjAucGF0Y2g=
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-14.0.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index a76f4170a5b..70df20c0486 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -44,7 +44,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   default_value_for :system, false
+ 
+@@ -569,6 +569,41 @@ def skip_notification?
+     review.present? || !author.can_trigger_notifications?
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
diff --git a/config/patches/gitlab-rails/security-patch-14.1.patch b/config/patches/gitlab-rails/security-patch-14.1.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTE0LjEucGF0Y2g=
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-14.1.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index 2ad6df85e5f..ed64a01756a 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -44,7 +44,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   default_value_for :system, false
+ 
+@@ -576,6 +576,41 @@ def skip_notification?
+     review.present? || !author.can_trigger_notifications?
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
diff --git a/config/patches/gitlab-rails/security-patch-14.2.patch b/config/patches/gitlab-rails/security-patch-14.2.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTE0LjIucGF0Y2g=
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-14.2.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index 34ffd7c91af..18199b34c0e 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -46,7 +46,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   default_value_for :system, false
+ 
+@@ -601,6 +601,41 @@ def user_mention_identifier
+     })
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
diff --git a/config/patches/gitlab-rails/security-patch-14.3.patch b/config/patches/gitlab-rails/security-patch-14.3.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTE0LjMucGF0Y2g=
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-14.3.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index a8f5c305d9b..439323a6ee9 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -46,7 +46,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   # Attribute used to determine whether keep_around_commits will be skipped for diff notes.
+   attr_accessor :skip_keep_around_commits
+@@ -604,6 +604,41 @@ def user_mention_identifier
+     })
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   def system_note_viewable_by?(user)
diff --git a/config/patches/gitlab-rails/security-patch-14.4.patch b/config/patches/gitlab-rails/security-patch-14.4.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTE0LjQucGF0Y2g=
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-14.4.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index 37473518892..c37297bdc87 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -46,7 +46,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   # Attribute used to determine whether keep_around_commits will be skipped for diff notes.
+   attr_accessor :skip_keep_around_commits
+@@ -602,6 +602,41 @@ def user_mention_identifier
+     })
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   def system_note_viewable_by?(user)
diff --git a/config/patches/gitlab-rails/security-patch-14.5.patch b/config/patches/gitlab-rails/security-patch-14.5.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3NlY3VyaXR5LXBhdGNoLTE0LjUucGF0Y2g=
--- /dev/null
+++ b/config/patches/gitlab-rails/security-patch-14.5.patch
@@ -0,0 +1,55 @@
+diff --git a/app/models/note.rb b/app/models/note.rb
+index cb285028203..e8342017bfe 100644
+--- a/app/models/note.rb
++++ b/app/models/note.rb
+@@ -46,7 +46,7 @@ class Note < ApplicationRecord
+   attr_accessor :user_visible_reference_count
+ 
+   # Attribute used to store the attributes that have been changed by quick actions.
+-  attr_accessor :commands_changes
++  attr_writer :commands_changes
+ 
+   # Attribute used to determine whether keep_around_commits will be skipped for diff notes.
+   attr_accessor :skip_keep_around_commits
+@@ -603,6 +603,41 @@ def user_mention_identifier
+     })
+   end
+ 
++  def commands_changes
++    @commands_changes&.slice(
++      :due_date,
++      :label_ids,
++      :remove_label_ids,
++      :add_label_ids,
++      :canonical_issue_id,
++      :clone_with_notes,
++      :confidential,
++      :create_merge_request,
++      :add_contacts,
++      :remove_contacts,
++      :assignee_ids,
++      :milestone_id,
++      :time_estimate,
++      :spend_time,
++      :discussion_locked,
++      :merge,
++      :rebase,
++      :wip_event,
++      :target_branch,
++      :reviewer_ids,
++      :health_status,
++      :promote_to_epic,
++      :weight,
++      :emoji_award,
++      :todo_event,
++      :subscription_event,
++      :state_event,
++      :title,
++      :tag_message,
++      :tag_name
++    )
++  end
++
+   private
+ 
+   def system_note_viewable_by?(user)
diff --git a/config/patches/gitlab-rails/token-prefix-patch-13.12.patch b/config/patches/gitlab-rails/token-prefix-patch-13.12.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xMy4xMi5wYXRjaA==
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-13.12.patch
@@ -0,0 +1,201 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index 8114d75f682..1ceb326358a 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -17,9 +17,15 @@ class Group < Namespace
+   include GroupAPICompatibility
+   include EachBatch
+   include HasTimelogsReport
++  extend ::Gitlab::Utils::Override
+ 
+   ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+   has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+   alias_method :members, :group_members
+@@ -98,7 +104,9 @@ class Group < Namespace
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -640,6 +648,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index 0e2c63e625c..ad65902b2aa 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -70,6 +70,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -91,7 +96,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1795,6 +1802,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
diff --git a/config/patches/gitlab-rails/token-prefix-patch-14.0.patch b/config/patches/gitlab-rails/token-prefix-patch-14.0.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xNC4wLnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-14.0.patch
@@ -0,0 +1,198 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index e645572d841..c2130a36401 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -17,6 +17,12 @@ class Group < Namespace
+   include GroupAPICompatibility
+   include EachBatch
+   include BulkMemberAccessLoad
++  extend ::Gitlab::Utils::Override
++
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
+ 
+   has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+   has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+@@ -98,7 +104,9 @@ class Group < Namespace
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -630,6 +638,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index 41fc9a2bcdf..f0a03e1f10c 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -68,6 +68,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -89,7 +94,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1792,6 +1799,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
diff --git a/config/patches/gitlab-rails/token-prefix-patch-14.1.patch b/config/patches/gitlab-rails/token-prefix-patch-14.1.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xNC4xLnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-14.1.patch
@@ -0,0 +1,198 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index 1e7308499a0..5d4185d2355 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -17,6 +17,12 @@ class Group < Namespace
+   include GroupAPICompatibility
+   include EachBatch
+   include BulkMemberAccessLoad
++  extend ::Gitlab::Utils::Override
++
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
+ 
+   has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+   has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+@@ -98,7 +104,9 @@ class Group < Namespace
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -629,6 +637,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index c5522737b87..c71eb3fe0da 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -68,6 +68,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -89,7 +94,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1781,6 +1788,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
diff --git a/config/patches/gitlab-rails/token-prefix-patch-14.2.patch b/config/patches/gitlab-rails/token-prefix-patch-14.2.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xNC4yLnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-14.2.patch
@@ -0,0 +1,198 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index f6b45a755e4..e1d55514cff 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -17,6 +17,12 @@ class Group < Namespace
+   include GroupAPICompatibility
+   include EachBatch
+   include BulkMemberAccessLoad
++  extend ::Gitlab::Utils::Override
++
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
+ 
+   has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+   has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+@@ -98,7 +104,9 @@ class Group < Namespace
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -630,6 +638,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index 67fc53c5ff3..0addf0d8724 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -73,6 +73,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -93,7 +98,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1796,6 +1803,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
diff --git a/config/patches/gitlab-rails/token-prefix-patch-14.3.patch b/config/patches/gitlab-rails/token-prefix-patch-14.3.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xNC4zLnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-14.3.patch
@@ -0,0 +1,199 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index a667a908707..57a33b11221 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -18,6 +18,13 @@ class Group < Namespace
+   include EachBatch
+   include BulkMemberAccessLoad
+ 
++  extend ::Gitlab::Utils::Override
++
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   def self.sti_name
+     'Group'
+   end
+@@ -105,7 +112,9 @@ def self.sti_name
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -646,6 +655,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index b0dafeccc92..0b82cef584d 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -74,6 +74,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -94,7 +99,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1854,6 +1861,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
diff --git a/config/patches/gitlab-rails/token-prefix-patch-14.4.patch b/config/patches/gitlab-rails/token-prefix-patch-14.4.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xNC40LnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-14.4.patch
@@ -0,0 +1,199 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index c5e119451e3..97bc07ff274 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -18,6 +18,13 @@ class Group < Namespace
+   include EachBatch
+   include BulkMemberAccessLoad
+ 
++  extend ::Gitlab::Utils::Override
++
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   def self.sti_name
+     'Group'
+   end
+@@ -105,7 +112,9 @@ def self.sti_name
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -652,6 +661,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index 6a5cf00aba1..7a927c559f3 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -74,6 +74,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -94,7 +99,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1846,6 +1853,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
diff --git a/config/patches/gitlab-rails/token-prefix-patch-14.5.patch b/config/patches/gitlab-rails/token-prefix-patch-14.5.patch
new file mode 100644
index 0000000000000000000000000000000000000000..02a4fe8c79ede43e225477ca1404ed0dfab23746_Y29uZmlnL3BhdGNoZXMvZ2l0bGFiLXJhaWxzL3Rva2VuLXByZWZpeC1wYXRjaC0xNC41LnBhdGNo
--- /dev/null
+++ b/config/patches/gitlab-rails/token-prefix-patch-14.5.patch
@@ -0,0 +1,199 @@
+diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+index 50a2613bb10..e957d09fbc6 100644
+--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
++++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+@@ -5,16 +5,18 @@ class Encrypted < Base
+     def find_token_authenticatable(token, unscoped = false)
+       return if token.blank?
+ 
+-      if required?
+-        find_by_encrypted_token(token, unscoped)
+-      elsif optional?
+-        find_by_encrypted_token(token, unscoped) ||
+-          find_by_plaintext_token(token, unscoped)
+-      elsif migrating?
+-        find_by_plaintext_token(token, unscoped)
+-      else
+-        raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
+-      end
++      instance = if required?
++                   find_by_encrypted_token(token, unscoped)
++                 elsif optional?
++                   find_by_encrypted_token(token, unscoped) ||
++                     find_by_plaintext_token(token, unscoped)
++                 elsif migrating?
++                   find_by_plaintext_token(token, unscoped)
++                 else
++                   raise ArgumentError, _("Unknown encryption strategy: %{encrypted_strategy}!") % { encrypted_strategy: encrypted_strategy }
++                 end
++
++      instance if instance && matches_prefix?(instance, token)
+     end
+ 
+     def ensure_token(instance)
+@@ -41,9 +43,7 @@ def ensure_token(instance)
+     def get_token(instance)
+       return insecure_strategy.get_token(instance) if migrating?
+ 
+-      encrypted_token = instance.read_attribute(encrypted_field)
+-      token = EncryptionHelper.decrypt_token(encrypted_token)
+-      token || (insecure_strategy.get_token(instance) if optional?)
++      get_encrypted_token(instance)
+     end
+ 
+     def set_token(instance, token)
+@@ -69,6 +69,12 @@ def optional?
+ 
+     protected
+ 
++    def get_encrypted_token(instance)
++      encrypted_token = instance.read_attribute(encrypted_field)
++      token = EncryptionHelper.decrypt_token(encrypted_token)
++      token || (insecure_strategy.get_token(instance) if optional?)
++    end
++
+     def encrypted_strategy
+       value = options[:encrypted]
+       value = value.call if value.is_a?(Proc)
+@@ -95,14 +101,22 @@ def insecure_strategy
+         .new(klass, token_field, options)
+     end
+ 
++    def matches_prefix?(instance, token)
++      prefix = options[:prefix]
++      prefix = prefix.call(instance) if prefix.is_a?(Proc)
++      prefix = '' unless prefix.is_a?(String)
++
++      token.start_with?(prefix)
++    end
++
+     def token_set?(instance)
+-      raw_token = instance.read_attribute(encrypted_field)
++      token = get_encrypted_token(instance)
+ 
+       unless required?
+-        raw_token ||= insecure_strategy.get_token(instance)
++        token ||= insecure_strategy.get_token(instance)
+       end
+ 
+-      raw_token.present?
++      token.present? && matches_prefix?(instance, token)
+     end
+ 
+     def encrypted_field
+diff --git a/app/models/group.rb b/app/models/group.rb
+index 2dd20300ad2..49d6c832ca7 100644
+--- a/app/models/group.rb
++++ b/app/models/group.rb
+@@ -18,6 +18,13 @@ class Group < Namespace
+   include EachBatch
+   include BulkMemberAccessLoad
+ 
++  extend ::Gitlab::Utils::Override
++
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   def self.sti_name
+     'Group'
+   end
+@@ -108,7 +115,9 @@ def self.sti_name
+                       message: Gitlab::Regex.group_name_regex_message },
+             if: :name_changed?
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   after_create :post_create_hook
+   after_destroy :post_destroy_hook
+@@ -652,6 +661,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def project_creation_level
+     super || ::Gitlab::CurrentSettings.default_project_creation
+   end
+diff --git a/app/models/project.rb b/app/models/project.rb
+index 45999da7839..dd0da082ad2 100644
+--- a/app/models/project.rb
++++ b/app/models/project.rb
+@@ -73,6 +73,11 @@ class Project < ApplicationRecord
+ 
+   GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
+ 
++  # Prefix for runners_token which can be used to invalidate existing tokens.
++  # The value chosen here is GR (for Gitlab Runner) combined with the rotation
++  # date (20220225) decimal to hex encoded.
++  RUNNERS_TOKEN_PREFIX = 'GR1348941'
++
+   cache_markdown_field :description, pipeline: :description
+ 
+   default_value_for :packages_enabled, true
+@@ -93,7 +98,9 @@ class Project < ApplicationRecord
+   default_value_for :autoclose_referenced_issues, true
+   default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
+ 
+-  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
++  add_authentication_token_field :runners_token,
++                                 encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
++                                 prefix: ->(instance) { instance.runners_token_prefix }
+ 
+   before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
+ 
+@@ -1863,6 +1870,15 @@ def runners_token
+     ensure_runners_token!
+   end
+ 
++  def runners_token_prefix
++    Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
++  end
++
++  override :format_runners_token
++  def format_runners_token(token)
++    "#{runners_token_prefix}#{token}"
++  end
++
+   def pages_deployed?
+     pages_metadatum&.deployed?
+   end
+diff --git a/config/feature_flags/development/groups_runners_token_prefix.yml b/config/feature_flags/development/groups_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..87b87266673
+--- /dev/null
++++ b/config/feature_flags/development/groups_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: groups_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true
+diff --git a/config/feature_flags/development/projects_runners_token_prefix.yml b/config/feature_flags/development/projects_runners_token_prefix.yml
+new file mode 100644
+index 00000000000..5dd21d115f6
+--- /dev/null
++++ b/config/feature_flags/development/projects_runners_token_prefix.yml
+@@ -0,0 +1,8 @@
++---
++name: projects_runners_token_prefix
++introduced_by_url:
++rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
++milestone: '14.9'
++type: development
++group: group::database
++default_enabled: true