diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index d8112f7ef8f295c4d841fb66ede27bb8d9a9d783..2a24084d5e264360d7411490a0acea72de377c12 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -8,7 +8,7 @@ class DeployToken < ApplicationRecord
 
   AVAILABLE_SCOPES = %i[read_repository read_registry write_registry
     read_package_registry write_package_registry
-    read_virtual_registry].freeze
+    read_virtual_registry write_virtual_registry].freeze
   GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
   DEPLOY_TOKEN_PREFIX = 'gldt-'
 
diff --git a/db/migrate/20241203052500_add_write_virtual_registry_scope_to_deploy_tokens.rb b/db/migrate/20241203052500_add_write_virtual_registry_scope_to_deploy_tokens.rb
new file mode 100644
index 0000000000000000000000000000000000000000..45f780113e6411bf192a5a1dc940ee635f419205
--- /dev/null
+++ b/db/migrate/20241203052500_add_write_virtual_registry_scope_to_deploy_tokens.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddWriteVirtualRegistryScopeToDeployTokens < Gitlab::Database::Migration[2.2]
+  milestone '17.9'
+
+  def change
+    add_column :deploy_tokens, :write_virtual_registry, :boolean, default: false, null: false
+  end
+end
diff --git a/db/schema_migrations/20241203052500 b/db/schema_migrations/20241203052500
new file mode 100644
index 0000000000000000000000000000000000000000..9a4d1ca27ec76807feae3136eca5ae4f5a47d7b8
--- /dev/null
+++ b/db/schema_migrations/20241203052500
@@ -0,0 +1 @@
+22a0025bf1ff0bf243698761c17bcfdb181599a4f31ac696c20511dfd1afbf9b
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 4642dc53cd7cbed44c38bc7b9833c4d179eb4e75..ee6b944cb8f828213cd6b2f506cbeb66ee9455a8 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12197,7 +12197,8 @@ CREATE TABLE deploy_tokens (
     creator_id bigint,
     read_virtual_registry boolean DEFAULT false NOT NULL,
     project_id bigint,
-    group_id bigint
+    group_id bigint,
+    write_virtual_registry boolean DEFAULT false NOT NULL
 );
 
 CREATE SEQUENCE deploy_tokens_id_seq
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index 78f55d188bc84dc81ce3df65aa7ded570a90c27c..5440f960ee69587f0682ea70c3fddf2bb417a290 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -19,6 +19,8 @@ def scope_params
         result_hash[:read_package_registry] = scopes.include?('read_package_registry')
         result_hash[:write_package_registry] = scopes.include?('write_package_registry')
         result_hash[:read_repository] = scopes.include?('read_repository')
+        result_hash[:read_virtual_registry] = scopes.include?('read_virtual_registry')
+        result_hash[:write_virtual_registry] = scopes.include?('write_virtual_registry')
         result_hash
       end
 
@@ -90,7 +92,7 @@ def scope_params
           type: Array[String],
           coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
           values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
-          desc: 'Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`.'
+          desc: 'Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, `write_package_registry`, `read_virtual_registry`, or `write_virtual_registry`.'
         optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`).'
         optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
       end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 4fa771b447c7d5f56610ca56577da53cf74b693f..b7dec9bce85fcbd9c57c7b323685550a225058c1 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -46,6 +46,13 @@ module Auth
     WRITE_REGISTRY_SCOPE = :write_registry
     REGISTRY_SCOPES = [READ_REGISTRY_SCOPE, WRITE_REGISTRY_SCOPE].freeze
 
+    # Scopes used for Virtual Registry access
+    READ_VIRTUAL_REGISTRY_SCOPE = :read_virtual_registry
+    WRITE_VIRTUAL_REGISTRY_SCOPE = :write_virtual_registry
+    VIRTUAL_REGISTRY_SCOPES = [
+      READ_VIRTUAL_REGISTRY_SCOPE, WRITE_VIRTUAL_REGISTRY_SCOPE
+    ].freeze
+
     # Scopes used for GitLab Observability access which is outside of the GitLab app itself.
     # Hence the lack of ability mapping in `abilities_for_scopes`.
     READ_OBSERVABILITY_SCOPE = :read_observability
@@ -289,6 +296,8 @@ def abilities_for_scopes(scopes)
           read_api: read_only_authentication_abilities,
           read_registry: %i[read_container_image],
           write_registry: %i[create_container_image],
+          read_virtual_registry: %i[read_dependency_proxy],
+          write_virtual_registry: %i[write_dependency_proxy],
           read_repository: %i[download_code],
           write_repository: %i[download_code push_code],
           create_runner: %i[create_instance_runner create_runner],
@@ -435,6 +444,12 @@ def registry_scopes
         REGISTRY_SCOPES
       end
 
+      def virtual_registry_scopes
+        return [] unless Gitlab.config.dependency_proxy.enabled
+
+        VIRTUAL_REGISTRY_SCOPES
+      end
+
       def resource_bot_scopes
         non_admin_available_scopes - [READ_USER_SCOPE]
       end
@@ -479,7 +494,7 @@ def unavailable_observability_scopes_for_resource(resource)
       end
 
       def non_admin_available_scopes
-        API_SCOPES + REPOSITORY_SCOPES + registry_scopes + OBSERVABILITY_SCOPES + AI_FEATURES_SCOPES
+        API_SCOPES + REPOSITORY_SCOPES + registry_scopes + virtual_registry_scopes + OBSERVABILITY_SCOPES + AI_FEATURES_SCOPES
       end
 
       def find_build_by_token(token)
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index d0a8a57102f490a193d6881128f77a8dc43ce95c..c53de4d7aebcbae385781c9ea7a910bf9126996c 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -38,6 +38,7 @@
       read_package_registry { true }
       write_package_registry { true }
       read_virtual_registry { true }
+      write_virtual_registry { true }
     end
 
     trait :dependency_proxy_scopes do
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
index 664740c2a3cf1b4ba056f5ff8ae905af8bd3ccde..a52bae4666e8ccbb14458784920bb76907a37e7d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
@@ -25,7 +25,15 @@
     "scopes": {
       "type": "array",
       "items": {
-        "type": "string"
+        "enum": [
+          "read_repository",
+          "read_registry",
+          "write_registry",
+          "read_package_registry",
+          "write_package_registry",
+          "read_virtual_registry",
+          "write_virtual_registry"
+        ]
       }
     },
     "token": {
@@ -38,4 +46,4 @@
       "type": "boolean"
     }
   }
-}
\ No newline at end of file
+}
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b1df7cf55d2d07b4a97fdf54e78e883b12571a02..06b33d5e9cb50ebe28ec9ccda1babae360b76bb8 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -37,6 +37,10 @@
     it 'DEFAULT_SCOPES contains all default scopes' do
       expect(subject::DEFAULT_SCOPES).to match_array [:api]
     end
+
+    it 'VIRTUAL_REGISTRY_SCOPES contains all scopes for Virtual Registry access' do
+      expect(subject::VIRTUAL_REGISTRY_SCOPES).to match_array %i[read_virtual_registry write_virtual_registry]
+    end
   end
 
   describe 'available_scopes' do
@@ -227,6 +231,28 @@
         end
       end
     end
+
+    context 'virtual_registry_scopes' do
+      context 'when dependency proxy and virtual registry are both disabled' do
+        before do
+          stub_config(dependency_proxy: { enabled: false })
+        end
+
+        it 'is empty' do
+          expect(subject.virtual_registry_scopes).to eq []
+        end
+      end
+
+      context 'when dependency proxy is enabled' do
+        before do
+          stub_config(dependency_proxy: { enabled: true })
+        end
+
+        it 'contains all virtual registry related scopes' do
+          expect(subject.virtual_registry_scopes).to eq %i[read_virtual_registry write_virtual_registry]
+        end
+      end
+    end
   end
 
   describe 'find_for_git_client' do
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 2f215cd5bd131fca372254d751a89311181bd34c..6820c325c9bd53d4879dd4f0549fc880101e8afd 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -386,16 +386,38 @@
           send(entity).send("add_#{authorized_role}", user)
         end
 
-        it 'creates the deploy token' do
-          expect { subject }.to change { DeployToken.count }.by(1)
-
-          expect(response).to have_gitlab_http_status(:created)
-          expect(response).to match_response_schema('public_api/v4/deploy_token')
-          expect(json_response['name']).to eq('Foo')
-          expect(json_response['scopes']).to eq(['read_repository'])
-          expect(json_response['username']).to eq('Bar')
-          expect(json_response['expires_at'].to_time.to_i).to eq(expires_time.to_i)
-          expect(json_response['token']).to match(/gldt-[A-Za-z0-9_-]{20}/)
+        ::DeployToken::AVAILABLE_SCOPES.map(&:to_s).each do |scope|
+          context "with valid scope #{scope}" do
+            before do
+              params[:scopes] = [scope.to_sym]
+            end
+
+            it 'creates the deploy token' do
+              expect { subject }.to change { DeployToken.count }.by(1)
+
+              expect(response).to have_gitlab_http_status(:created)
+              expect(response).to match_response_schema('public_api/v4/deploy_token')
+              expect(json_response['name']).to eq('Foo')
+              expect(json_response['scopes']).to eq([scope])
+              expect(json_response['username']).to eq('Bar')
+              expect(json_response['expires_at'].to_time.to_i).to eq(expires_time.to_i)
+              expect(json_response['token']).to match(/gldt-[A-Za-z0-9_-]{20}/)
+            end
+          end
+
+          context 'with all scopes' do
+            before do
+              params[:scopes] = ::DeployToken::AVAILABLE_SCOPES
+            end
+
+            it 'creates the deploy token with all scopes' do
+              expect { subject }.to change { DeployToken.count }.by(1)
+
+              expect(response).to have_gitlab_http_status(:created)
+              expect(response).to match_response_schema('public_api/v4/deploy_token')
+              expect(json_response['scopes']).to eq(::DeployToken::AVAILABLE_SCOPES.map(&:to_s))
+            end
+          end
         end
 
         context 'with no optional params given' do