diff --git a/app/presenters/packages/nuget/v2/service_index_presenter.rb b/app/presenters/packages/nuget/v2/service_index_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a8fc9b673bfa78b3a7ca45db420b11cfddb5f920
--- /dev/null
+++ b/app/presenters/packages/nuget/v2/service_index_presenter.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Packages
+  module Nuget
+    module V2
+      class ServiceIndexPresenter
+        include API::Helpers::RelatedResourcesHelpers
+
+        ROOT_ATTRIBUTES = {
+          xmlns: 'http://www.w3.org/2007/app',
+          'xmlns:atom' => 'http://www.w3.org/2005/Atom'
+        }.freeze
+
+        def initialize(project_or_group)
+          @project_or_group = project_or_group
+        end
+
+        def xml
+          Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
+            xml.service(ROOT_ATTRIBUTES.merge('xml:base' => xml_base)) do
+              xml.workspace do
+                xml['atom'].title('Default', type: 'text')
+                xml.collection(href: 'Packages') do
+                  xml['atom'].title('Packages', type: 'text')
+                end
+              end
+            end
+          end
+        end
+
+        private
+
+        attr_reader :project_or_group
+
+        def xml_base
+          base_path = case project_or_group
+                      when Project
+                        api_v4_projects_packages_nuget_v2_path(id: project_or_group.id)
+                      when Group
+                        api_v4_groups___packages_nuget_v2_path(id: project_or_group.id)
+                      end
+
+          expose_url(base_path)
+        end
+      end
+    end
+  end
+end
diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md
index afe384b5a29c6d82b72f378cc29426713d63ea22..cbfd12c72339b4231d071a025b05056d4335f856 100644
--- a/doc/api/packages/nuget.md
+++ b/doc/api/packages/nuget.md
@@ -80,13 +80,22 @@ This writes the downloaded file to `MyNuGetPkg.1.3.0.17.nupkg` in the current di
 
 ## Upload a package file
 
-> Introduced in GitLab 12.8.
+> - Introduced in GitLab 12.8 for NuGet v3 feed.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2 for NuGet v2 feed.
 
 Upload a NuGet package file:
 
-```plaintext
-PUT projects/:id/packages/nuget
-```
+- For NuGet v3 feed:
+
+  ```plaintext
+  PUT projects/:id/packages/nuget
+  ```
+
+- For NuGet V2 feed:
+
+  ```plaintext
+  PUT projects/:id/packages/nuget/v2
+  ```
 
 | Attribute         | Type   | Required | Description |
 | ----------------- | ------ | -------- | ----------- |
@@ -95,12 +104,23 @@ PUT projects/:id/packages/nuget
 | `package_version` | string | yes      | The version of the package. |
 | `package_filename`| string | yes      | The name of the file. |
 
-```shell
-curl --request PUT \
-     --form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
-     --user <username>:<personal_access_token> \
-     "https://gitlab.example.com/api/v4/projects/1/packages/nuget/"
-```
+- For NuGet v3 feed:
+
+  ```shell
+  curl --request PUT \
+      --form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
+      --user <username>:<personal_access_token> \
+      "https://gitlab.example.com/api/v4/projects/1/packages/nuget/"
+  ```
+
+- For NuGet v2 feed:
+
+    ```shell
+  curl --request PUT \
+      --form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
+      --user <username>:<personal_access_token> \
+      "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"
+  ```
 
 ## Upload a symbol package file
 
@@ -158,6 +178,37 @@ The examples in this document all use the project-level prefix.
 
 ## Service Index
 
+### V2 source feed/protocol
+
+Returns an XML document that represents the service index of the v2 NuGet source feed.
+Authentication is not required:
+
+```plaintext
+GET <route-prefix>/v2
+```
+
+Example Request:
+
+```shell
+curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"
+```
+
+Example response:
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2">
+  <workspace>
+    <atom:title type="text">Default</atom:title>
+    <collection href="Packages">
+      <atom:title type="text">Packages</atom:title>
+    </collection>
+  </workspace>
+</service>
+```
+
+### V3 source feed/protocol
+
 > - Introduced in GitLab 12.6.
 > - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214674) to be public in GitLab 16.1.
 
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index ea9bfd7defb93491a47c249900a0d60029d22088..fb457bb98b51c715b7dac335bdd78f4fe049da68 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -70,6 +70,7 @@ You can now add a new source to NuGet with:
 - [Visual Studio](#add-a-source-with-visual-studio)
 - [.NET CLI](#add-a-source-with-the-net-cli)
 - [Configuration file](#add-a-source-with-a-configuration-file)
+- [Chocolatey CLI](#add-a-source-with-chocolatey-cli)
 
 ### Add a source with the NuGet CLI
 
@@ -281,6 +282,22 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Re
    export GITLAB_PACKAGE_REGISTRY_PASSWORD=<gitlab_personal_access_token or deploy_token>
    ```
 
+### Add a source with Chocolatey CLI
+
+You can add a source feed with the Chocolatey CLI. If you use Chocolatey CLI v1.x, you can add only a NuGet v2 source feed.
+
+#### Configure a project-level endpoint
+
+You need a project-level endpoint to publish NuGet packages to the Package Registry.
+
+To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Registry as a source for Chocolatey:
+
+- Add the Package Registry as a source with `choco`:
+
+  ```shell
+  choco source add -n=gitlab -s "'https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2'" -u=<gitlab_username or deploy_token_username> -p=<gitlab_personal_access_token or deploy_token>
+  ```
+
 ## Publish a NuGet package
 
 Prerequisite:
@@ -385,6 +402,31 @@ updated:
 
 1. Commit the changes and push it to your GitLab repository to trigger a new CI/CD build.
 
+### Publish a NuGet package with Chocolatey CLI
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2.
+
+Prerequisite:
+
+- The project-level Package Registry is a source for Chocolatey.
+
+To publish a package with the Chocolatey CLI:
+
+```shell
+choco push <package_file> --source <source_url> --api-key <gitlab_personal_access_token, deploy_token or job token>
+```
+
+In this command:
+
+- `<package_file>` is your package filename and ends with `.nupkg`.
+- `<source_url>` is the URL of the NuGet v2 feed Package Registry.
+
+For example:
+
+```shell
+choco push MyPackage.1.0.0.nupkg --source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2" --api-key <gitlab_personal_access_token, deploy_token or job token>
+```
+
 ### Publishing a package with the same name or version
 
 When you publish a package with the same name or version as an existing package,
diff --git a/lib/api/concerns/packages/nuget/public_endpoints.rb b/lib/api/concerns/packages/nuget/public_endpoints.rb
index 37b503212d92aae6f21bf839f64b99f219101036..d5be136c7a28d8795a1af38bd154206467f3d9f2 100644
--- a/lib/api/concerns/packages/nuget/public_endpoints.rb
+++ b/lib/api/concerns/packages/nuget/public_endpoints.rb
@@ -16,7 +16,7 @@ module PublicEndpoints
 
           included do
             # https://docs.microsoft.com/en-us/nuget/api/service-index
-            desc 'The NuGet Service Index' do
+            desc 'The NuGet V3 Feed Service Index' do
               detail 'This feature was introduced in GitLab 12.6'
               success code: 200, model: ::API::Entities::Nuget::ServiceIndex
               failure [
@@ -34,6 +34,33 @@ module PublicEndpoints
               present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group_without_auth),
                 with: ::API::Entities::Nuget::ServiceIndex
             end
+
+            desc 'The NuGet V2 Feed Service Index' do
+              detail 'This feature was introduced in GitLab 16.2'
+              success code: 200
+              failure [
+                { code: 404, message: 'Not Found' }
+              ]
+              tags %w[nuget_packages]
+            end
+            namespace '/v2' do
+              get format: :xml, urgency: :low do
+                env['api.format'] = :xml
+                content_type 'application/xml; charset=utf-8'
+                # needed to allow browser default inline styles in xml response
+                header 'Content-Security-Policy', "nonce-#{SecureRandom.base64(16)}"
+
+                track_package_event(
+                  'cli_metadata',
+                  :nuget,
+                  **snowplow_gitlab_standard_context_without_auth.merge(category: 'API::NugetPackages', feed: 'v2')
+                )
+
+                present ::Packages::Nuget::V2::ServiceIndexPresenter
+                          .new(project_or_group_without_auth)
+                          .xml
+              end
+            end
           end
         end
       end
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index 2716d6f0b64a877c13d36b1ca94f668fd7802b48..1631f7b2a9be72425d45681d53f307f936a49ff0 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -98,6 +98,22 @@ def upload_nuget_package_file(symbol_package: false)
         created!
       end
 
+      def publish_package(symbol_package: false)
+        upload_nuget_package_file(symbol_package: symbol_package) do |package|
+          track_package_event(
+            symbol_package ? 'push_symbol_package' : 'push_package',
+            :nuget,
+            **{ category: 'API::NugetPackages',
+                project: package.project,
+                namespace: package.project.namespace }.tap { |args| args[:feed] = 'v2' if request.path.include?('nuget/v2') }
+          )
+        end
+      rescue ObjectStorage::RemoteStoreError => e
+        Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
+
+        forbidden!
+      end
+
       def required_permission
         :read_package
       end
@@ -179,7 +195,7 @@ def required_permission
           end
         end
 
-        # To support an additional authentication option for download endpoints,
+        # To support an additional authentication option for publish endpoints,
         # we redefine the `authenticate_with` method by combining the previous
         # authentication option with the new one.
         authenticate_with do |accept|
@@ -191,7 +207,7 @@ def required_permission
 
         namespace '/nuget' do
           # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
-          desc 'The NuGet Package Publish endpoint' do
+          desc 'The NuGet V3 Feed Package Publish endpoint' do
             detail 'This feature was introduced in GitLab 12.6'
             success code: 201
             failure [
@@ -207,19 +223,7 @@ def required_permission
             use :file_params
           end
           put urgency: :low do
-            upload_nuget_package_file do |package|
-              track_package_event(
-                'push_package',
-                :nuget,
-                category: 'API::NugetPackages',
-                project: package.project,
-                namespace: package.project.namespace
-              )
-            end
-          rescue ObjectStorage::RemoteStoreError => e
-            Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
-
-            forbidden!
+            publish_package
           end
 
           desc 'The NuGet Package Authorize endpoint' do
@@ -252,19 +256,7 @@ def required_permission
             use :file_params
           end
           put 'symbolpackage', urgency: :low do
-            upload_nuget_package_file(symbol_package: true) do |package|
-              track_package_event(
-                'push_symbol_package',
-                :nuget,
-                category: 'API::NugetPackages',
-                project: package.project,
-                namespace: package.project.namespace
-              )
-            end
-          rescue ObjectStorage::RemoteStoreError => e
-            Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
-
-            forbidden!
+            publish_package(symbol_package: true)
           end
 
           desc 'The NuGet Symbol Package Authorize endpoint' do
@@ -280,6 +272,42 @@ def required_permission
           put 'symbolpackage/authorize', urgency: :low do
             authorize_nuget_upload
           end
+
+          namespace '/v2' do
+            desc 'The NuGet V2 Feed Package Publish endpoint' do
+              detail 'This feature was introduced in GitLab 16.2'
+              success code: 201
+              failure [
+                { code: 400, message: 'Bad Request' },
+                { code: 401, message: 'Unauthorized' },
+                { code: 403, message: 'Forbidden' },
+                { code: 404, message: 'Not Found' }
+              ]
+              tags %w[nuget_packages]
+            end
+
+            params do
+              use :file_params
+            end
+            put do
+              publish_package
+            end
+
+            desc 'The NuGet V2 Feed Package Authorize endpoint' do
+              detail 'This feature was introduced in GitLab 16.2'
+              success code: 200
+              failure [
+                { code: 401, message: 'Unauthorized' },
+                { code: 403, message: 'Forbidden' },
+                { code: 404, message: 'Not Found' }
+              ]
+              tags %w[nuget_packages]
+            end
+
+            put 'authorize', urgency: :low do
+              authorize_nuget_upload
+            end
+          end
         end
       end
     end
diff --git a/spec/presenters/packages/nuget/v2/service_index_presenter_spec.rb b/spec/presenters/packages/nuget/v2/service_index_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09dd3ff7fe4911080674fc8d770c2cc30f0a3a32
--- /dev/null
+++ b/spec/presenters/packages/nuget/v2/service_index_presenter_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::V2::ServiceIndexPresenter, feature_category: :package_registry do
+  let_it_be(:project) { build_stubbed(:project) }
+  let_it_be(:group) { build_stubbed(:group) }
+
+  describe '#xml' do
+    let(:project_or_group) { project }
+    let(:presenter) { described_class.new(project_or_group) }
+    let(:xml_doc) { Nokogiri::XML::Document.parse(presenter.xml.to_xml) }
+    let(:service_node) { xml_doc.at_xpath('//xmlns:service') }
+
+    it { expect(xml_doc.root.name).to eq('service') }
+
+    it 'includes the workspace and collection nodes' do
+      workspace = xml_doc.at_xpath('//xmlns:service/xmlns:workspace')
+      collection = xml_doc.at_xpath('//xmlns:service/xmlns:workspace/xmlns:collection')
+
+      expect(workspace).to be_present
+      expect(workspace.children).to include(collection)
+      expect(collection).to be_present
+    end
+
+    it 'sets the appropriate XML namespaces on the root node' do
+      expect(service_node.namespaces['xmlns']).to eq('http://www.w3.org/2007/app')
+      expect(service_node.namespaces['xmlns:atom']).to eq('http://www.w3.org/2005/Atom')
+    end
+
+    context 'when the presenter is initialized with a project' do
+      it 'sets the XML base path correctly for a project scope' do
+        expect(service_node['xml:base']).to include(expected_xml_base(project))
+      end
+    end
+
+    context 'when the presenter is initialized with a group' do
+      let(:project_or_group) { group }
+
+      it 'sets the XML base path correctly for a group scope' do
+        expect(service_node['xml:base']).to include(expected_xml_base(group))
+      end
+    end
+  end
+
+  def expected_xml_base(project_or_group)
+    case project_or_group
+    when Project
+      api_v4_projects_packages_nuget_v2_path(id: project_or_group.id)
+    when Group
+      api_v4_groups___packages_nuget_v2_path(id: project_or_group.id)
+    end
+  end
+end
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index 07199119cb583610caf6af4aba5e03c393665425..92eb869b871023415a67211fda738dc40690b447 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -31,6 +31,12 @@ def snowplow_context(user_role: :developer)
       end
     end
 
+    describe 'GET /api/v4/groups/:id/-/packages/nuget/v2' do
+      it_behaves_like 'handling nuget service requests', v2: true do
+        let(:url) { "/groups/#{target.id}/-/packages/nuget/v2" }
+      end
+    end
+
     describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do
       it_behaves_like 'handling nuget metadata requests with package name',
                       example_names_with_status:
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index 887dfd4beeb83e6f3df69a3df9e356c819d72906..40068f101f788896ce25f105d6d88316b19ed94b 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -42,6 +42,14 @@ def snowplow_context(user_role: :developer)
     it_behaves_like 'accept get request on private project with access to package registry for everyone'
   end
 
+  describe 'GET /api/v4/projects/:id/packages/nuget/v2' do
+    let(:url) { "/projects/#{target.id}/packages/nuget/v2" }
+
+    it_behaves_like 'handling nuget service requests', v2: true
+
+    it_behaves_like 'accept get request on private project with access to package registry for everyone'
+  end
+
   describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do
     let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
 
@@ -183,75 +191,39 @@ def snowplow_context(user_role: :developer)
   end
 
   describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
-    include_context 'workhorse headers'
-
-    let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
-    let(:headers) { {} }
-
-    subject { put api(url), headers: headers }
-
-    it_behaves_like 'nuget authorize upload endpoint'
+    it_behaves_like 'nuget authorize upload endpoint' do
+      let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
+    end
   end
 
   describe 'PUT /api/v4/projects/:id/packages/nuget' do
-    include_context 'workhorse headers'
-
-    let_it_be(:file_name) { 'package.nupkg' }
-
-    let(:url) { "/projects/#{target.id}/packages/nuget" }
-    let(:headers) { {} }
-    let(:params) { { package: temp_file(file_name) } }
-    let(:file_key) { :package }
-    let(:send_rewritten_field) { true }
-
-    subject do
-      workhorse_finalize(
-        api(url),
-        method: :put,
-        file_key: file_key,
-        params: params,
-        headers: headers,
-        send_rewritten_field: send_rewritten_field
-      )
+    it_behaves_like 'nuget upload endpoint' do
+      let(:url) { "/projects/#{target.id}/packages/nuget" }
     end
-
-    it_behaves_like 'nuget upload endpoint'
   end
 
   describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage/authorize' do
-    include_context 'workhorse headers'
-
-    let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
-    let(:headers) { {} }
-
-    subject { put api(url), headers: headers }
-
-    it_behaves_like 'nuget authorize upload endpoint'
+    it_behaves_like 'nuget authorize upload endpoint' do
+      let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
+    end
   end
 
   describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage' do
-    include_context 'workhorse headers'
-
-    let_it_be(:file_name) { 'package.snupkg' }
-
-    let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
-    let(:headers) { {} }
-    let(:params) { { package: temp_file(file_name) } }
-    let(:file_key) { :package }
-    let(:send_rewritten_field) { true }
-
-    subject do
-      workhorse_finalize(
-        api(url),
-        method: :put,
-        file_key: file_key,
-        params: params,
-        headers: headers,
-        send_rewritten_field: send_rewritten_field
-      )
+    it_behaves_like 'nuget upload endpoint', symbol_package: true do
+      let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
     end
+  end
 
-    it_behaves_like 'nuget upload endpoint', symbol_package: true
+  describe 'PUT /api/v4/projects/:id/packages/nuget/v2/authorize' do
+    it_behaves_like 'nuget authorize upload endpoint' do
+      let(:url) { "/projects/#{target.id}/packages/nuget/v2/authorize" }
+    end
+  end
+
+  describe 'PUT /api/v4/projects/:id/packages/nuget/v2' do
+    it_behaves_like 'nuget upload endpoint' do
+      let(:url) { "/projects/#{target.id}/packages/nuget/v2" }
+    end
   end
 
   def update_visibility_to(visibility)
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index 432e67ee21e75a21a41fd51f653a3bca63d5fead..150e9a4e00436ede81466ece2632f6ba97e735c2 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -1,8 +1,10 @@
 # frozen_string_literal: true
 
-RSpec.shared_examples 'handling nuget service requests' do
+RSpec.shared_examples 'handling nuget service requests' do |v2: false|
   subject { get api(url) }
 
+  it { is_expected.to have_request_urgency(v2 ? :low : :default) }
+
   context 'with valid target' do
     using RSpec::Parameterized::TableSyntax
 
@@ -20,15 +22,17 @@
     end
 
     with_them do
-      let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
-
-      subject { get api(url) }
+      let(:snowplow_gitlab_standard_context) do
+        snowplow_context(user_role: :anonymous).tap do |ctx|
+          ctx[:feed] = 'v2' if v2
+        end
+      end
 
       before do
         update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
       end
 
-      it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+      it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], v2
     end
   end
 
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index d6a0055700dbf6152ead09711759ae57b5f1ae1e..0f1ec14a0b6ec4eab10e16d70b8940b95423c262 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -18,7 +18,7 @@
   end
 end
 
-RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
+RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true, v2 = false|
   context "for user type #{user_type}" do
     before do
       target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
@@ -28,15 +28,22 @@
 
     it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata'
 
-    it 'returns a valid json response' do
+    it 'returns a valid json or xml response' do
       subject
 
-      expect(response.media_type).to eq('application/json')
-      expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
-      expect(json_response).to be_a(Hash)
+      if v2
+        expect(response.media_type).to eq('application/xml')
+        expect(body).to have_xpath('//service')
+          .and have_xpath('//service/workspace')
+          .and have_xpath('//service/workspace/collection[@href]')
+      else
+        expect(response.media_type).to eq('application/json')
+        expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
+        expect(json_response).to be_a(Hash)
+      end
     end
 
-    context 'with invalid format' do
+    context 'with invalid format', unless: v2 do
       let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" }
 
       it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
@@ -439,6 +446,13 @@
 
 RSpec.shared_examples 'nuget authorize upload endpoint' do
   using RSpec::Parameterized::TableSyntax
+  include_context 'workhorse headers'
+
+  let(:headers) { {} }
+
+  subject { put api(url), headers: headers }
+
+  it { is_expected.to have_request_urgency(:low) }
 
   context 'with valid project' do
     where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
@@ -517,6 +531,26 @@
 
 RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
   using RSpec::Parameterized::TableSyntax
+  include_context 'workhorse headers'
+
+  let(:headers) { {} }
+  let(:file_name) { symbol_package ? 'package.snupkg' : 'package.nupkg' }
+  let(:params) { { package: temp_file(file_name) } }
+  let(:file_key) { :package }
+  let(:send_rewritten_field) { true }
+
+  subject do
+    workhorse_finalize(
+      api(url),
+      method: :put,
+      file_key: file_key,
+      params: params,
+      headers: headers,
+      send_rewritten_field: send_rewritten_field
+    )
+  end
+
+  it { is_expected.to have_request_urgency(:low) }
 
   context 'with valid project' do
     where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
@@ -573,7 +607,12 @@
       end
 
       let(:headers) { user_headers.merge(workhorse_headers) }
-      let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } }
+
+      let(:snowplow_gitlab_standard_context) do
+        { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' }.tap do |ctx|
+          ctx[:feed] = 'v2' if url.include?('nuget/v2')
+        end
+      end
 
       before do
         update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
@@ -604,4 +643,16 @@
 
     it_behaves_like 'returning response status', :bad_request
   end
+
+  context 'when ObjectStorage::RemoteStoreError is raised' do
+    let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
+
+    before do
+      allow_next_instance_of(::Packages::CreatePackageFileService) do |instance|
+        allow(instance).to receive(:execute).and_raise(ObjectStorage::RemoteStoreError)
+      end
+    end
+
+    it_behaves_like 'returning response status', :forbidden
+  end
 end
diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go
index dd1725a723e515310e4a1127cc8b9bee0999e1a4..e634f0ca66cccfdbedd062c04418e81c9861c7cd 100644
--- a/workhorse/internal/upstream/routes.go
+++ b/workhorse/internal/upstream/routes.go
@@ -285,6 +285,9 @@ func configureRoutes(u *upstream) {
 		// NuGet Artifact Repository
 		u.route("PUT", apiProjectPattern+`/packages/nuget/`, mimeMultipartUploader),
 
+		// NuGet v2 Artifact Repository
+		u.route("PUT", apiProjectPattern+`/packages/nuget/v2`, mimeMultipartUploader),
+
 		// PyPI Artifact Repository
 		u.route("POST", apiProjectPattern+`/packages/pypi`, mimeMultipartUploader),
 
diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go
index 8effe29197930a712b70de2627a325dc93d69af6..62a78dd9464b48b8bf177d9afe1a62340e1918ad 100644
--- a/workhorse/upload_test.go
+++ b/workhorse/upload_test.go
@@ -159,6 +159,9 @@ func TestAcceleratedUpload(t *testing.T) {
 		{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},
 		{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true},
 		{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true},
+		{"PUT", "/api/v4/projects/9001/packages/nuget/v2/files", true},
+		{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v2/files", true},
+		{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v2/files", true},
 		{"POST", `/api/v4/groups/import`, true},
 		{"POST", `/api/v4/groups/import/`, true},
 		{"POST", `/api/v4/projects/import`, true},
@@ -289,6 +292,8 @@ func TestUnacceleratedUploads(t *testing.T) {
 		{"POST", `/api/v4/projects/group/project/wikis/attachments`},
 		{"PUT", "/api/v4/projects/group/subgroup/project/packages/nuget/v1/files"},
 		{"PUT", "/api/v4/projects/group/project/packages/nuget/v1/files"},
+		{"POST", "/api/v4/projects/group/subgroup/project/packages/nuget/v2/files"},
+		{"POST", "/api/v4/projects/group/project/packages/nuget/v2/files"},
 		{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
 		{"POST", `/api/v4/projects/group/project/packages/pypi`},
 		{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},