diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
index 7df277641bf32e412f9b8ce407ec0890f181a176..8875bbacee6c38822485cce8cfbc20cafbcb4a93 100644
--- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
@@ -32,6 +32,11 @@ class ProjectCiCdSettingsUpdate < BaseMutation
         description: 'Indicates CI/CD job tokens generated in other projects ' \
           'have restricted access to this project.'
 
+      argument :push_repository_for_job_token_allowed, GraphQL::Types::Boolean,
+        required: false,
+        description: 'Indicates the ability to push to the original project ' \
+          'repository using a job token'
+
       field :ci_cd_settings,
         Types::Ci::CiCdSettingType,
         null: false,
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index f6f2fe0ef9d0d4c1e054792848e3d8ea85ca8074..3ce1e65d28f95450f2b653ad246996da3bde92c1 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -37,6 +37,13 @@ class CiCdSettingType < BaseObject
         null: true,
         description: 'Project the CI/CD settings belong to.',
         authorize: :admin_project
+      field :push_repository_for_job_token_allowed,
+        GraphQL::Types::Boolean,
+        null: true,
+        description: 'Indicates the ability to push to the original project ' \
+          'repository using a job token',
+        method: :push_repository_for_job_token_allowed?,
+        authorize: :admin_project
     end
   end
 end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 2c829bce3ee1e8f798fd132e72d17d08f0cd0d6c..4073f2ec7d427523b50dca14a38ccf51c1bd89c6 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -247,7 +247,11 @@ class ProjectPolicy < BasePolicy
   end
 
   condition(:push_repository_for_job_token_allowed) do
-    @user&.from_ci_job_token? && project.ci_push_repository_for_job_token_allowed? && @user.ci_job_token_scope.self_referential?(project)
+    if ::Feature.enabled?(:allow_push_repository_for_job_token, @subject)
+      @user&.from_ci_job_token? && project.ci_push_repository_for_job_token_allowed? && @user.ci_job_token_scope.self_referential?(project)
+    else
+      false
+    end
   end
 
   condition(:packages_disabled, scope: :subject) { !@subject.packages_enabled }
diff --git a/config/feature_flags/development/allow_push_repository_for_job_token.yml b/config/feature_flags/development/allow_push_repository_for_job_token.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5c0e1013e7b2c79bf69c36af77db70141cdff16b
--- /dev/null
+++ b/config/feature_flags/development/allow_push_repository_for_job_token.yml
@@ -0,0 +1,8 @@
+---
+name: allow_push_repository_for_job_token
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154111
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468320
+milestone: "17.2"
+type: development
+group: group::pipeline security
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 8bdcb73ff84c7c6cba1bc2777d1d9913b472b34f..9ebe835af879babd652157a34a499aa4ca42944c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7419,6 +7419,7 @@ Input type: `ProjectCiCdSettingsUpdateInput`
 | <a id="mutationprojectcicdsettingsupdatemergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Indicates if merged results pipelines are enabled for the project. |
 | <a id="mutationprojectcicdsettingsupdatemergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Indicates if merge trains are enabled for the project. |
 | <a id="mutationprojectcicdsettingsupdatemergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean`](#boolean) | Indicates whether an option is allowed to merge without refreshing the merge train. Ignored unless the `merge_trains_skip_train` feature flag is also enabled. |
+| <a id="mutationprojectcicdsettingsupdatepushrepositoryforjobtokenallowed"></a>`pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. |
 
 #### Fields
 
@@ -29392,6 +29393,7 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="projectcicdsettingmergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Whether merge trains are enabled. |
 | <a id="projectcicdsettingmergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean!`](#boolean) | Whether merge immediately is allowed for merge trains. |
 | <a id="projectcicdsettingproject"></a>`project` | [`Project`](#project) | Project the CI/CD settings belong to. |
+| <a id="projectcicdsettingpushrepositoryforjobtokenallowed"></a>`pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. |
 
 ### `ProjectDataTransfer`
 
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 983217820cc7770b7976ba81c36a120901314c89..639b96367e0ad7f22751d8ed7248450c3d53f00c 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -249,6 +249,8 @@ When the user is authenticated and `simple` is not set this returns something li
     "ci_job_token_scope_enabled": false,
     "ci_separated_caches": true,
     "ci_restrict_pipeline_cancellation_role": "developer",
+    "ci_pipeline_variables_minimum_override_role": "maintainer",
+    "ci_push_repository_for_job_token_allowed": false,
     "public_jobs": true,
     "build_timeout": 3600,
     "auto_cancel_pending_pipelines": "enabled",
@@ -425,6 +427,8 @@ GET /users/:user_id/projects
     "ci_allow_fork_pipelines_to_run_in_parent_project": true,
     "ci_separated_caches": true,
     "ci_restrict_pipeline_cancellation_role": "developer",
+    "ci_pipeline_variables_minimum_override_role": "maintainer",
+    "ci_push_repository_for_job_token_allowed": false,
     "public_jobs": true,
     "shared_with_groups": [],
     "only_allow_merge_if_pipeline_succeeds": false,
@@ -546,6 +550,8 @@ GET /users/:user_id/projects
     "ci_allow_fork_pipelines_to_run_in_parent_project": true,
     "ci_separated_caches": true,
     "ci_restrict_pipeline_cancellation_role": "developer",
+    "ci_pipeline_variables_minimum_override_role": "maintainer",
+    "ci_push_repository_for_job_token_allowed": false,
     "public_jobs": true,
     "shared_with_groups": [],
     "only_allow_merge_if_pipeline_succeeds": false,
@@ -1218,6 +1224,8 @@ GET /projects/:id
   "ci_allow_fork_pipelines_to_run_in_parent_project": true,
   "ci_separated_caches": true,
   "ci_restrict_pipeline_cancellation_role": "developer",
+  "ci_pipeline_variables_minimum_override_role": "maintainer",
+  "ci_push_repository_for_job_token_allowed": false,
   "public_jobs": true,
   "shared_with_groups": [
     {
@@ -1760,6 +1768,8 @@ General project attributes:
 | `ci_allow_fork_pipelines_to_run_in_parent_project` | boolean           | No       | Enable or disable [running pipelines in the parent project for merge requests from forks](../ci/pipelines/merge_request_pipelines.md#run-pipelines-in-the-parent-project). _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325189) in GitLab 15.3.)_ |
 | `ci_separated_caches`                              | boolean           | No       | Set whether or not caches should be [separated](../ci/caching/index.md#cache-key-names) by branch protection status. |
 | `ci_restrict_pipeline_cancellation_role`           | string            | No       | Set the [role required to cancel a pipeline or job](../ci/pipelines/settings.md#restrict-roles-that-can-cancel-pipelines-or-jobs). One of `developer`, `maintainer`, or `no_one`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429921) in GitLab 16.8. Premium and Ultimate only. |
+| `ci_pipeline_variables_minimum_override_role`           | string            | No       | When `restrict_user_defined_variables` is enabled, you can specify which role can override variables. One of `owner`, `maintainer`, `developer` or `no_one_allowed`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/440338) in GitLab 17.1. |
+| `ci_push_repository_for_job_token_allowed` | boolean           | No       | Enable or disable the ability to push to the project repository using job token. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. |
 | `container_expiration_policy_attributes`           | hash              | No       | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
 | `container_registry_enabled`                       | boolean           | No       | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. |
 | `default_branch`                                   | string            | No       | The [default branch](../user/project/repository/branches/default.md) name. |
@@ -2379,6 +2389,8 @@ Example response:
   "ci_allow_fork_pipelines_to_run_in_parent_project": true,
   "ci_separated_caches": true,
   "ci_restrict_pipeline_cancellation_role": "developer",
+  "ci_pipeline_variables_minimum_override_role": "maintainer",
+  "ci_push_repository_for_job_token_allowed": false,
   "public_jobs": true,
   "shared_with_groups": [],
   "only_allow_merge_if_pipeline_succeeds": false,
@@ -2511,6 +2523,8 @@ Example response:
   "ci_allow_fork_pipelines_to_run_in_parent_project": true,
   "ci_separated_caches": true,
   "ci_restrict_pipeline_cancellation_role": "developer",
+  "ci_pipeline_variables_minimum_override_role": "maintainer",
+  "ci_push_repository_for_job_token_allowed": false,
   "public_jobs": true,
   "shared_with_groups": [],
   "only_allow_merge_if_pipeline_succeeds": false,
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 1cfb6cb6a2d0d826092f437dd95978e18f78712c..2178f7b3a0cead30eac84bacad4d2dabbf249db8 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -276,3 +276,21 @@ While troubleshooting CI/CD job token authentication issues, be aware that:
   - To remove project access.
 - The CI job token becomes invalid if the job is no longer running, has been erased,
   or if the project is in the process of being deleted.
+
+### Push to a project repository using a job token
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. [with a flag](../../administration/feature_flags.md) named `allow_push_repository_for_job_token`. Disabled by default.
+
+WARNING:
+Pushing via job token is still in development and is not yet optimized for performance.
+If you enable this feature for testing, you must thoroughly test and implement validation measures
+to prevent infinite loops of "push" pipelines triggering more pipelines.
+
+By default, pushing to a project repository by authenticating with a job token is disabled.
+To enable this ability, you can:
+
+- Feature flag named `allow_push_repository_for_job_token` should be enabled.
+- Enable the [`pushRepositoryForJobTokenAllowed`](../../api/graphql/reference/index.md#mutationprojectcicdsettingsupdate) GraphQL endpoint.
+- Enable the [`ci_push_repository_for_job_token_allowed`](../../api/projects.md#edit-project) REST API endpoint.
+
+You are only permitted to push to the repository of the project where the job is running.
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index aaa12c8665f8e6e183393b256a85241be2c908bf..93ef6adbb9189c1c177ea5348483f2a7ab367edb 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -131,6 +131,7 @@ class Project < ProjectDetails
         expose :auto_devops_deploy_strategy, documentation: { type: 'string', example: 'continuous' } do |project, options|
           project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
         end
+        expose :ci_push_repository_for_job_token_allowed, documentation: { type: 'boolean' }
       end
 
       expose :ci_config_path, documentation: { type: 'string', example: '' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 4567335757123b7831ceb45668c8265edf1d1811..562af336d57b68924dc987eb440d8d3ad4871cab 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -117,6 +117,7 @@ module ProjectsHelpers
         optional :ci_separated_caches, type: Boolean, desc: 'Enable or disable separated caches based on branch protection.'
         optional :restrict_user_defined_variables, type: Boolean, desc: 'Restrict use of user-defined variables when triggering a pipeline'
         optional :ci_pipeline_variables_minimum_override_role, values: %w[no_one_allowed developer maintainer owner], type: String, desc: 'Limit ability to override CI/CD variables when triggering a pipeline to only users with at least the set minimum role'
+        optional :ci_push_repository_for_job_token_allowed, type: Boolean, desc: "Allow pushing to this project's repository by authenticating with a CI/CD job token generated in this project."
       end
 
       params :optional_update_params_ee do
@@ -210,6 +211,7 @@ def self.update_params_at_least_one_of
           :model_registry_access_level,
           :warn_about_potentially_unwanted_characters,
           :ci_pipeline_variables_minimum_override_role,
+          :ci_push_repository_for_job_token_allowed,
 
           # TODO: remove in API v5, replaced by *_access_level
           :issues_enabled,
diff --git a/spec/graphql/types/ci/ci_cd_setting_type_spec.rb b/spec/graphql/types/ci/ci_cd_setting_type_spec.rb
index 5fdfb405e239f876e11ae6f7c12c63b282dabd49..cac4a062a731f97c52308752de8d80a7be8adcb2 100644
--- a/spec/graphql/types/ci/ci_cd_setting_type_spec.rb
+++ b/spec/graphql/types/ci/ci_cd_setting_type_spec.rb
@@ -9,6 +9,7 @@
     expected_fields = %w[
       inbound_job_token_scope_enabled job_token_scope_enabled
       keep_latest_artifact merge_pipelines_enabled project
+      push_repository_for_job_token_allowed
     ]
 
     if Gitlab.ee?
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 93c26afadbecd615a01d2e71aacbf958d82be6ef..00ec396caf929d9c74454264ccfe926c6ea0c38a 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -27,6 +27,12 @@
     end
   end
 
+  describe '#push_repository_for_job_token_allowed' do
+    it 'is false by default' do
+      expect(described_class.new.push_repository_for_job_token_allowed).to be_falsey
+    end
+  end
+
   describe '#separated_caches' do
     it 'is true by default' do
       expect(described_class.new.separated_caches).to be_truthy
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 0126b61043306d3ad9cc9ec4227bc99be8745e40..01425e95d420ad020b9ee20f2920681f8401d965 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -3704,19 +3704,24 @@ def permissions_abilities(role)
 
     let(:policy) { :build_push_code }
 
-    where(:user_role, :project_visibility, :push_repository_for_job_token_allowed, :self_referential_project, :allowed) do
-      :maintainer | :public   | true  | true  | true
-      :owner      | :public   | true  | true  | true
-      :maintainer | :private  | true  | true  | true
-      :developer  | :public   | true  | true  | true
-      :reporter   | :public   | true  | true  | false
-      :guest      | :public   | true  | true  | false
-      :guest      | :private  | true  | true  | false
-      :guest      | :internal | true  | true  | false
-      :anonymous  | :public   | true  | true  | false
-      :maintainer | :public   | false | true  | false
-      :maintainer | :public   | true  | false | false
-      :maintainer | :public   | false | false | false
+    where(:user_role, :project_visibility, :push_repository_for_job_token_allowed, :self_referential_project, :allowed, :ff_disabled) do
+      :maintainer | :public   | true  | true  | true  | false
+      :owner      | :public   | true  | true  | true  | false
+      :maintainer | :private  | true  | true  | true  | false
+      :developer  | :public   | true  | true  | true  | false
+      :reporter   | :public   | true  | true  | false | false
+      :guest      | :public   | true  | true  | false | false
+      :guest      | :private  | true  | true  | false | false
+      :guest      | :internal | true  | true  | false | false
+      :anonymous  | :public   | true  | true  | false | false
+      :maintainer | :public   | false | true  | false | false
+      :maintainer | :public   | true  | false | false | false
+      :maintainer | :public   | false | false | false | false
+      :maintainer | :public   | true  | true  | false | true
+      :owner      | :public   | true  | true  | false | true
+      :maintainer | :private  | true  | true  | false | true
+      :developer  | :public   | true  | true  | false | true
+      :reporter   | :public   | true  | true  | false | true
     end
 
     with_them do
@@ -3730,6 +3735,8 @@ def permissions_abilities(role)
       let(:scope_project) { public_send(:private_project) }
 
       before do
+        stub_feature_flags(allow_push_repository_for_job_token: false) if ff_disabled
+
         project.add_guest(guest)
         project.add_reporter(reporter)
         project.add_developer(developer)
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index db9b6bfbf5c2fc4efffa1b666b23aa551a3602ee..2600c818391b755ebb8521cf3976b0eb8eaf7269 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -49,6 +49,8 @@
       expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
       expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
         project.ci_cd_settings.inbound_job_token_scope_enabled?)
+      expect(settings_data['pushRepositoryForJobTokenAllowed']).to eql(
+        project.ci_cd_settings.push_repository_for_job_token_allowed?)
 
       if Gitlab.ee?
         expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled?
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 6e101d07b9f20bca6e470e7424a3c0919d6cee68..8e07416a8f5f61376eb73cec97802b844141cc6d 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -18,7 +18,8 @@
       full_path: project.full_path,
       keep_latest_artifact: false,
       job_token_scope_enabled: false,
-      inbound_job_token_scope_enabled: false
+      inbound_job_token_scope_enabled: false,
+      push_repository_for_job_token_allowed: false
     }
   end
 
@@ -69,6 +70,23 @@
       expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
     end
 
+    context 'when push_repository_for_job_token_allowed requested to be true' do
+      let(:variables) do
+        {
+          full_path: project.full_path,
+          push_repository_for_job_token_allowed: true
+        }
+      end
+
+      it 'updates push_repository_for_job_token_allowed' do
+        post_graphql_mutation(mutation, current_user: user)
+        project.reload
+
+        expect(response).to have_gitlab_http_status(:success)
+        expect(project.ci_cd_settings.push_repository_for_job_token_allowed).to eq(true)
+      end
+    end
+
     context 'when job_token_scope_enabled: true' do
       let(:variables) do
         {
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index c5d798f004f11dad5ea554946c3be7adc07b0005..5f7597e841eefd5234600224a88f59b8945303bb 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -103,6 +103,7 @@ ci_cd_settings:
     - push_repository_for_job_token_allowed
   remapped_attributes:
     pipeline_variables_minimum_override_role: ci_pipeline_variables_minimum_override_role
+    push_repository_for_job_token_allowed: ci_push_repository_for_job_token_allowed
     default_git_depth: ci_default_git_depth
     forward_deployment_enabled: ci_forward_deployment_enabled
     forward_deployment_rollback_allowed: ci_forward_deployment_rollback_allowed
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2b888e4a177aa1a671257688c07814e2685cc0ca..b12e6afba767c8414613b77bc4254a9f9f73d8b4 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3323,7 +3323,8 @@ def failure_message(diff)
           'build_timeout',
           'auto_devops_enabled',
           'auto_devops_deploy_strategy',
-          'import_error'
+          'import_error',
+          'ci_push_repository_for_job_token_allowed'
         )
       end
     end
@@ -4074,6 +4075,20 @@ def failure_message(diff)
       let(:failed_status_code) { :not_found }
     end
 
+    describe 'updating ci_push_repository_for_job_token_allowed attribute' do
+      it 'is disabled by default' do
+        expect(project.ci_push_repository_for_job_token_allowed).to be_falsey
+      end
+
+      it 'enables push to repository using job token' do
+        put(api(path, user), params: { ci_push_repository_for_job_token_allowed: true })
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(project.reload.ci_push_repository_for_job_token_allowed).to be_truthy
+        expect(json_response['ci_push_repository_for_job_token_allowed']).to eq(true)
+      end
+    end
+
     describe 'updating packages_enabled attribute' do
       it 'is enabled by default' do
         expect(project.packages_enabled).to be true