diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index f35bbd02caeb9ccfaadfd5c0f857101121bd2296..e7fc2c9406f63ddac9976fcfe96d65fab741ca5d 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -92,6 +92,7 @@ The following API resources are available in the project context:
 | [Project-level variables](project_level_variables.md)                   | `/projects/:id/variables`                                                                                                                                                                             |
 | [Projects](projects.md) including setting Webhooks                      | `/projects`, `/projects/:id/hooks` (also available for users)                                                                                                                                         |
 | [Protected branches](protected_branches.md)                             | `/projects/:id/protected_branches`                                                                                                                                                                    |
+| [Protected container registry](project_container_registry_protection_rules.md)                             | `/projects/:id/registry/protection/rules`                                                                                                                                                                 |
 | [Protected environments](protected_environments.md)                     | `/projects/:id/protected_environments`                                                                                                                                                                |
 | [Protected packages](project_packages_protection_rules.md)         | `/projects/:id/protection/rules`                                                                                                                                                                    |
 | [Protected tags](protected_tags.md)                                     | `/projects/:id/protected_tags`                                                                                                                                                                        |
diff --git a/doc/api/project_container_registry_protection_rules.md b/doc/api/project_container_registry_protection_rules.md
index 99a0e1fad659ffd2f0b9b98f5b73bbcabdbabd22..cdbd8be23ee3835a30f5077a25e94edd1b38aa45 100644
--- a/doc/api/project_container_registry_protection_rules.md
+++ b/doc/api/project_container_registry_protection_rules.md
@@ -71,3 +71,47 @@ Example response:
   },
 ]
 ```
+
+## Create a container registry protection rule
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/457518) in GitLab 17.2.
+
+Create a container registry protection rule for a project.
+
+```plaintext
+POST /api/v4/projects/:id/registry/protection/rules
+```
+
+Supported attributes:
+
+| Attribute                         | Type           | Required | Description |
+|-----------------------------------|----------------|----------|-------------|
+| `id`                              | integer/string | Yes      | ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
+| `repository_path_pattern`         | string         | Yes      | Container repository path pattern protected by the protection rule. For example `flight/flight-*`. Wildcard character `*` allowed. |
+| `minimum_access_level_for_push`   | string         | No       | Minimum GitLab access level to allow to push container images to the container registry. For example `maintainer`, `owner` or `admin`. Must be provided when `minimum_access_level_for_delete` is not set. |
+| `minimum_access_level_for_delete` | string         | No       | Minimum GitLab access level to allow to delete container images in the container registry. For example `maintainer`, `owner`, `admin`. Must be provided when  `minimum_access_level_for_push` is not set. |
+
+If successful, returns [`201`](rest/index.md#status-codes) and the created container registry protection rule.
+
+Can return the following status codes:
+
+- `201 Created`: The container registry protection rule was created successfully.
+- `400 Bad Request`: The container registry protection rule is invalid.
+- `401 Unauthorized`: The access token is invalid.
+- `403 Forbidden`: The user does not have permission to create a container registry protection rule.
+- `404 Not Found`: The project was not found.
+- `422 Unprocessable Entity`: The container registry protection rule could not be created, for example, because the `repository_path_pattern` is already taken.
+
+Example request:
+
+```shell
+curl --request POST \
+  --header "PRIVATE-TOKEN: <your_access_token>" \
+  --header "Content-Type: application/json" \
+  --url "https://gitlab.example.com/api/v4/projects/7/registry/protection/rules" \
+  --data '{
+        "repository_path_pattern": "flightjs/flight-needs-to-be-a-unique-path",
+        "minimum_access_level_for_push": "maintainer",
+        "minimum_access_level_for_delete": "maintainer"
+    }'
+```
diff --git a/lib/api/project_container_registry_protection_rules.rb b/lib/api/project_container_registry_protection_rules.rb
index 233f88c3b4f7118ce311e9050acb94726e505c72..55520481e120e3e16042c846d3a10605900e065b 100644
--- a/lib/api/project_container_registry_protection_rules.rb
+++ b/lib/api/project_container_registry_protection_rules.rb
@@ -13,26 +13,62 @@ class ProjectContainerRegistryProtectionRules < ::API::Base
       authorize! :admin_container_image, user_project
     end
 
-    resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
-      desc 'Get list of container registry protection rules for a project' do
-        success Entities::Projects::ContainerRegistry::Protection::Rule
-        failure [
-          { code: 401, message: 'Unauthorized' },
-          { code: 403, message: 'Forbidden' },
-          { code: 404, message: 'Not Found' }
-        ]
-        tags %w[projects]
-        is_array true
-        hidden true
-      end
-    end
     params do
       requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
     end
     resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
-      get ':id/registry/protection/rules' do
-        present user_project.container_registry_protection_rules,
-          with: Entities::Projects::ContainerRegistry::Protection::Rule
+      resource ':id/registry/protection/rules' do
+        desc 'Get list of container registry protection rules for a project' do
+          success Entities::Projects::ContainerRegistry::Protection::Rule
+          failure [
+            { code: 401, message: 'Unauthorized' },
+            { code: 403, message: 'Forbidden' },
+            { code: 404, message: 'Not Found' }
+          ]
+          tags %w[projects]
+          is_array true
+          hidden true
+        end
+        get do
+          present user_project.container_registry_protection_rules,
+            with: Entities::Projects::ContainerRegistry::Protection::Rule
+        end
+
+        desc 'Create a container protection rule for a project' do
+          success Entities::Projects::ContainerRegistry::Protection::Rule
+          failure [
+            { code: 400, message: 'Bad Request' },
+            { code: 401, message: 'Unauthorized' },
+            { code: 403, message: 'Forbidden' },
+            { code: 404, message: 'Not Found' },
+            { code: 422, message: 'Unprocessable Entity' }
+          ]
+          tags %w[projects]
+          hidden true
+        end
+        params do
+          requires :repository_path_pattern, type: String,
+            desc: 'Container repository path pattern protected by the protection rule.
+            For example `flight/flight-*`. Wildcard character `*` allowed.'
+          optional :minimum_access_level_for_push, type: String,
+            values: ContainerRegistry::Protection::Rule.minimum_access_level_for_pushes.keys,
+            desc: 'Minimum GitLab access level to allow to push container images to the container registry.
+            For example maintainer, owner or admin.'
+          optional :minimum_access_level_for_delete, type: String,
+            values: ContainerRegistry::Protection::Rule.minimum_access_level_for_deletes.keys,
+            desc: 'Minimum GitLab access level to allow to delete container images in the container registry.
+            For example maintainer, owner or admin.'
+          at_least_one_of :minimum_access_level_for_push, :minimum_access_level_for_delete
+        end
+        post do
+          response = ::ContainerRegistry::Protection::CreateRuleService.new(user_project,
+            current_user, declared_params).execute
+
+          render_api_error!({ error: response.message }, :unprocessable_entity) if response.error?
+
+          present response[:container_registry_protection_rule],
+            with: Entities::Projects::ContainerRegistry::Protection::Rule
+        end
       end
     end
   end
diff --git a/spec/requests/api/project_container_registry_protection_rules_spec.rb b/spec/requests/api/project_container_registry_protection_rules_spec.rb
index dd00a28519b2dfaec03d4f4d2fc55cd7aa7ab3db..4c29daf73ec5948aa4b3407a6226e3a613309b05 100644
--- a/spec/requests/api/project_container_registry_protection_rules_spec.rb
+++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe API::ProjectContainerRegistryProtectionRules, feature_category: :container_registry do
+RSpec.describe API::ProjectContainerRegistryProtectionRules, :aggregate_failures, feature_category: :container_registry do
   include ExclusiveLeaseHelpers
 
   let_it_be(:project) { create(:project, :private) }
@@ -15,7 +15,13 @@
   let_it_be(:invalid_token) { 'invalid-token123' }
   let_it_be(:headers_with_invalid_token) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => invalid_token } }
 
-  shared_examples 'rejecting project container protection rules request' do
+  let(:params) do
+    { repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-unique",
+      minimum_access_level_for_push: container_registry_protection_rule.minimum_access_level_for_push,
+      minimum_access_level_for_delete: container_registry_protection_rule.minimum_access_level_for_delete }
+  end
+
+  shared_examples 'rejecting project container protection rules request when not enough permissions' do
     using RSpec::Parameterized::TableSyntax
 
     where(:user_role, :status) do
@@ -34,14 +40,34 @@
     end
   end
 
+  shared_examples 'rejecting container registry protection rules request when enough permissions' do
+    context 'when feature flag is disabled' do
+      before do
+        stub_feature_flags(container_registry_protected_containers: false)
+      end
+
+      it_behaves_like 'returning response status', :not_found
+    end
+
+    context 'when the project id is invalid' do
+      let(:url) { "/projects/invalid/registry/protection/rules" }
+
+      it_behaves_like 'returning response status', :not_found
+    end
+
+    context 'when the project id does not exist' do
+      let(:url) { "/projects/#{non_existing_record_id}/registry/protection/rules" }
+
+      it_behaves_like 'returning response status', :not_found
+    end
+  end
+
   describe 'GET /projects/:id/registry/protection/rules' do
     let(:url) { "/projects/#{project.id}/registry/protection/rules" }
 
     subject(:get_container_registry_rules) { get(api(url, api_user)) }
 
-    context 'when not enough permissions' do
-      it_behaves_like 'rejecting project container protection rules request'
-    end
+    it_behaves_like 'rejecting project container protection rules request when not enough permissions'
 
     context 'for maintainer' do
       let(:api_user) { maintainer }
@@ -74,29 +100,103 @@
         )
       end
 
-      context 'when the project id is invalid' do
-        let(:url) { '/projects/invalid/registry/protection/rules' }
+      it_behaves_like 'rejecting container registry protection rules request when enough permissions'
+    end
+
+    context 'with invalid token' do
+      subject(:get_container_registry_rules) { get(api(url), headers: headers_with_invalid_token) }
+
+      it_behaves_like 'returning response status', :unauthorized
+    end
+  end
+
+  describe 'POST /projects/:id/registry/protection/rules' do
+    let(:url) { "/projects/#{project.id}/registry/protection/rules" }
+
+    subject(:post_container_registry_rule) { post(api(url, api_user), params: params) }
+
+    it_behaves_like 'rejecting project container protection rules request when not enough permissions'
+
+    context 'for maintainer' do
+      let(:api_user) { maintainer }
+
+      it 'creates a container registry protection rule' do
+        expect { post_container_registry_rule }.to change { ContainerRegistry::Protection::Rule.count }.by(1)
+        expect(response).to have_gitlab_http_status(:created)
+      end
+
+      context 'with empty minimum_access_level_for_push' do
+        before do
+          params[:minimum_access_level_for_push] = nil
+        end
+
+        it 'creates a container registry protection rule' do
+          expect { post_container_registry_rule }.to change { ContainerRegistry::Protection::Rule.count }.by(1)
+          expect(response).to have_gitlab_http_status(:created)
+        end
+      end
+
+      context 'with invalid minimum_access_level_for_delete' do
+        before do
+          params[:minimum_access_level_for_delete] = "not in enum"
+        end
+
+        it 'does not create a container registry protection rule' do
+          expect { post_container_registry_rule }.to not_change(ContainerRegistry::Protection::Rule, :count)
+          expect(response).to have_gitlab_http_status(:bad_request)
+        end
+      end
+
+      context 'with empty minimum_access_level_for_delete' do
+        before do
+          params[:minimum_access_level_for_delete] = nil
+        end
 
-        it_behaves_like 'returning response status', :not_found
+        it 'creates a container registry protection rule' do
+          expect { post_container_registry_rule }.to change { ContainerRegistry::Protection::Rule.count }.by(1)
+          expect(response).to have_gitlab_http_status(:created)
+        end
       end
 
-      context 'when the project id does not exist' do
-        let(:url) { "/projects/#{non_existing_record_id}/registry/protection/rules" }
+      context 'with invalid minimum_access_level_for_push' do
+        before do
+          params[:minimum_access_level_for_push] = "not in enum"
+        end
 
-        it_behaves_like 'returning response status', :not_found
+        it 'does not create a container registry protection rule' do
+          expect { post_container_registry_rule }.to not_change(ContainerRegistry::Protection::Rule, :count)
+          expect(response).to have_gitlab_http_status(:bad_request)
+        end
       end
 
-      context 'when container_registry_protected_containers is disabled' do
+      context 'with already existing repository_path_pattern' do
         before do
-          stub_feature_flags(container_registry_protected_containers: false)
+          params[:repository_path_pattern] = container_registry_protection_rule.repository_path_pattern
         end
 
-        it_behaves_like 'returning response status', :not_found
+        it 'does not create a container registry protection rule' do
+          expect { post_container_registry_rule }.to not_change(ContainerRegistry::Protection::Rule, :count)
+          expect(response).to have_gitlab_http_status(:unprocessable_entity)
+        end
       end
+
+      context 'with neither minimum_access_level_for_push nor minimum_access_level_for_delete' do
+        before do
+          params[:minimum_access_level_for_push] = nil
+          params[:minimum_access_level_for_delete] = nil
+        end
+
+        it 'does not create a container registry protection rule' do
+          expect { post_container_registry_rule }.to not_change(ContainerRegistry::Protection::Rule, :count)
+          expect(response).to have_gitlab_http_status(:unprocessable_entity)
+        end
+      end
+
+      it_behaves_like 'rejecting container registry protection rules request when enough permissions'
     end
 
     context 'with invalid token' do
-      subject(:get_container_registry_rules) { get(api(url), headers: headers_with_invalid_token) }
+      subject(:post_container_registry_rules) { post(api(url), headers: headers_with_invalid_token, params: params) }
 
       it_behaves_like 'returning response status', :unauthorized
     end