diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
index 56e22a64529f5572195ee7830401810789b0553d..967224f2e1a972a22c7ff50dfe066e870c01e385 100644
--- a/app/services/ci/generate_kubeconfig_service.rb
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -43,7 +43,9 @@ def execute
     def agent_authorizations
       ::Clusters::Agents::Authorizations::CiAccess::FilterService.new(
         pipeline.cluster_agent_authorizations,
-        environment: environment
+        { environment: environment,
+          protected_ref: pipeline.protected_ref? },
+        pipeline.project
       ).execute
     end
 
diff --git a/app/services/clusters/agents/authorizations/ci_access/filter_service.rb b/app/services/clusters/agents/authorizations/ci_access/filter_service.rb
index cd08aaa12d47da25eda9bef00adeece416107388..3ac3170a2b7229f004a85b74ffa667798d07f8e9 100644
--- a/app/services/clusters/agents/authorizations/ci_access/filter_service.rb
+++ b/app/services/clusters/agents/authorizations/ci_access/filter_service.rb
@@ -5,20 +5,26 @@ module Agents
     module Authorizations
       module CiAccess
         class FilterService
-          def initialize(authorizations, filter_params)
+          def initialize(authorizations, filter_params, project)
             @authorizations = authorizations
             @filter_params = filter_params
+            @project = project
 
             @environments_matcher = {}
           end
 
           def execute
-            filter_by_environment(authorizations)
+            filtered_authorizations = filter_by_environment(authorizations)
+            if Feature.enabled?(:kubernetes_agent_protected_branches, project)
+              filtered_authorizations = filter_protected_ref(filtered_authorizations)
+            end
+
+            filtered_authorizations
           end
 
           private
 
-          attr_reader :authorizations, :filter_params
+          attr_reader :authorizations, :filter_params, :project
 
           def filter_by_environment(auths)
             return auths unless filter_by_environment?
@@ -47,6 +53,26 @@ def matches_environment?(environment_pattern)
           def environments_matcher(environment_pattern)
             @environments_matcher[environment_pattern] ||= ::Gitlab::Ci::EnvironmentMatcher.new(environment_pattern)
           end
+
+          def filter_protected_ref(authorizations)
+            # we deny all if the protected_ref is not set, since we can't check if the branch is protected:
+            return [] unless protected_ref_filter_present?
+
+            # when the branch is protected we don't need to check the authorization settings
+            return authorizations if filter_params[:protected_ref]
+
+            authorizations.reject do |authorization|
+              only_run_on_protected_ref?(authorization)
+            end
+          end
+
+          def protected_ref_filter_present?
+            filter_params.has_key?(:protected_ref)
+          end
+
+          def only_run_on_protected_ref?(authorization)
+            authorization.config['protected_branches_only']
+          end
         end
       end
     end
diff --git a/config/feature_flags/beta/kubernetes_agent_protected_branches.yml b/config/feature_flags/beta/kubernetes_agent_protected_branches.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e4b55239610e8dc5de76f0aef7a754b0d5b797ad
--- /dev/null
+++ b/config/feature_flags/beta/kubernetes_agent_protected_branches.yml
@@ -0,0 +1,9 @@
+---
+name: kubernetes_agent_protected_branches
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388323
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156626
+rollout_issue_url:
+milestone: '17.3'
+group: group::environments
+type: beta
+default_enabled: false
diff --git a/doc/user/clusters/agent/ci_cd_workflow.md b/doc/user/clusters/agent/ci_cd_workflow.md
index 49ba273d9a04ba7f9ba6679e186c3d556f592598..8503bbbac65a8b22080ec1c5e0549400ccc0718f 100644
--- a/doc/user/clusters/agent/ci_cd_workflow.md
+++ b/doc/user/clusters/agent/ci_cd_workflow.md
@@ -332,6 +332,55 @@ In this example:
   - `*` is a wildcard, so `review/*` matches all environments under `review`.
 - CI/CD jobs for projects under `group-1` with `production` environments can access the agent.
 
+## Restrict access to the agent to protected branches
+
+DETAILS:
+**Tier:** Free, Premium, Ultimate
+**Offering:** Self-managed, GitLab Dedicated
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/388323) in GitLab 17.3.
+
+To restrict access to the agent to only jobs run on [protected branches](../../project/protected_branches.md):
+
+- Add `protected_branches_only: true` to `ci_access.projects` or `ci_access.groups`.
+  For example:
+
+  ```yaml
+  ci_access:
+    projects:
+      - id: path/to/project-1
+        protected_branches_only: true
+    groups:
+      - id: path/to/group-1
+        protected_branches_only: true
+        environments:
+          - production
+  ```
+
+By default, `protected_branches_only` is set to `false`, and the agent can be accessed from unprotected and protected branches.
+
+For additional security, you can combine this feature with [environment restrictions](#restrict-project-and-group-access-to-specific-environments).
+
+If a project has multiple configurations, only the most specific configuration is used.
+For example, the following configuration grants access to unprotected branches in `example/my-project`, even though the `example` group is configured to grant access to only protected branches:
+
+```yaml
+# .gitlab/agents/my-agent/config.yaml
+ci_access:
+  project:
+    - id: example/my-project # Project of the group below
+      protected_branches_only: false # This configuration supercedes the group configuration
+      environments:
+        - dev
+  groups:
+    - id: example
+      protected_branches_only: true
+      environments:
+        - dev
+```
+
+For more details, see [Access to Kubernetes from CI](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/b601fa21cac24f0cdedc8b8eb59ebcba0b70f459/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api).
+
 ## Related topics
 
 - [Self-paced classroom workshop](https://gitlab-for-eks.awsworkshop.io) (Uses AWS EKS, but you can use for other Kubernetes clusters)
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
index 383d96328a5a312665701098f5d1ea1124edd72e..726c4b1d99f782dd352a26a7bd52d31e67b9808c 100644
--- a/lib/api/ci/jobs.rb
+++ b/lib/api/ci/jobs.rb
@@ -269,7 +269,9 @@ class Jobs < ::API::Base
 
           agent_authorizations = ::Clusters::Agents::Authorizations::CiAccess::FilterService.new(
             ::Clusters::Agents::Authorizations::CiAccess::Finder.new(project).execute,
-            environment: persisted_environment&.name
+            { environment: persisted_environment&.name,
+              protected_ref: pipeline.protected_ref? },
+            pipeline.project
           ).execute
 
           # See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api
diff --git a/spec/factories/clusters/agents/authorizations/ci_access/group_authorizations.rb b/spec/factories/clusters/agents/authorizations/ci_access/group_authorizations.rb
index 659114eef8e45702c44e62a5bdcea0fd02dc13cc..d8a15902ddb5374f312fc96e332389b364cdea95 100644
--- a/spec/factories/clusters/agents/authorizations/ci_access/group_authorizations.rb
+++ b/spec/factories/clusters/agents/authorizations/ci_access/group_authorizations.rb
@@ -4,14 +4,15 @@
   factory :agent_ci_access_group_authorization, class: 'Clusters::Agents::Authorizations::CiAccess::GroupAuthorization' do
     association :agent, factory: :cluster_agent
     group
-
     transient do
       environments { nil }
+      protected_branches_only { false }
     end
 
     config do
       { default_namespace: 'production' }.tap do |c|
         c[:environments] = environments if environments
+        c[:protected_branches_only] = protected_branches_only
       end
     end
   end
diff --git a/spec/factories/clusters/agents/authorizations/ci_access/project_authorizations.rb b/spec/factories/clusters/agents/authorizations/ci_access/project_authorizations.rb
index 10d4f8fb946d0747fc19c0fadccab3a3fce6f3c8..f23c7d8bf279277f126bd0c11c8202c521e64320 100644
--- a/spec/factories/clusters/agents/authorizations/ci_access/project_authorizations.rb
+++ b/spec/factories/clusters/agents/authorizations/ci_access/project_authorizations.rb
@@ -7,11 +7,13 @@
 
     transient do
       environments { nil }
+      protected_branches_only { false }
     end
 
     config do
       { default_namespace: 'production' }.tap do |c|
         c[:environments] = environments if environments
+        c[:protected_branches_only] = protected_branches_only
       end
     end
   end
diff --git a/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
index 6cfe07ec628c2fcc6046a089c364e8a2a2942d82..fbb568792c851a734fe9e0a8aa3e88ca7b4a4fa8 100644
--- a/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
+++ b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
@@ -50,7 +50,8 @@
 
       expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
       expect(authorized_agent['agent']['name']).to eq(agent.name)
-      expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+      expect(authorized_agent['config']).to eq({ "default_namespace" => "production",
+                                                 "protected_branches_only" => false })
       expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
     end
 
@@ -87,7 +88,8 @@
 
       expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
       expect(authorized_agent['agent']['name']).to eq(agent.name)
-      expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+      expect(authorized_agent['config']).to eq({ "default_namespace" => "production",
+                                                "protected_branches_only" => false })
       expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
     end
 
diff --git a/spec/services/ci/generate_kubeconfig_service_spec.rb b/spec/services/ci/generate_kubeconfig_service_spec.rb
index a03c6ef0c9d7eac3d8b9472dbd5244f66a1884b4..92bc139d21d8b49baa26c5d5393e7dce6f923259 100644
--- a/spec/services/ci/generate_kubeconfig_service_spec.rb
+++ b/spec/services/ci/generate_kubeconfig_service_spec.rb
@@ -61,7 +61,9 @@
     it "filters the pipeline's agents by `nil` environment" do
       expect(::Clusters::Agents::Authorizations::CiAccess::FilterService).to receive(:new).with(
         pipeline.cluster_agent_authorizations,
-        environment: nil
+        { environment: nil,
+          protected_ref: false },
+        project
       )
 
       execute
@@ -91,7 +93,9 @@
       it "filters the pipeline's agents by the specified environment" do
         expect(::Clusters::Agents::Authorizations::CiAccess::FilterService).to receive(:new).with(
           pipeline.cluster_agent_authorizations,
-          environment: 'production'
+          { environment: 'production',
+            protected_ref: false },
+          project
         )
 
         execute
diff --git a/spec/services/clusters/agents/authorizations/ci_access/filter_service_spec.rb b/spec/services/clusters/agents/authorizations/ci_access/filter_service_spec.rb
index 45443cfd88780b5148b66f767d5df0b6aaea6a9d..70ec0192b3acdf5c1d97525771a0b88c52c7aa09 100644
--- a/spec/services/clusters/agents/authorizations/ci_access/filter_service_spec.rb
+++ b/spec/services/clusters/agents/authorizations/ci_access/filter_service_spec.rb
@@ -15,9 +15,9 @@
       ]
     end
 
-    let(:filter_params) { {} }
+    let(:filter_params) { { protected_ref: true } }
 
-    subject(:execute_filter) { described_class.new(agent_authorizations, filter_params).execute }
+    subject(:execute_filter) { described_class.new(agent_authorizations, filter_params, project).execute }
 
     context 'when there are no filters' do
       let(:agent_authorizations) { agent_authorizations_without_env }
@@ -34,13 +34,15 @@
             :agent_ci_access_project_authorization,
             project: project,
             agent: build(:cluster_agent, project: project),
-            environments: ['staging', 'review/*', 'production']
+            environments: ['staging', 'review/*', 'production'],
+            protected_branches_only: false
           ),
           build(
             :agent_ci_access_group_authorization,
             group: group,
             agent: build(:cluster_agent, project: project),
-            environments: ['staging', 'review/*', 'production']
+            environments: ['staging', 'review/*', 'production'],
+            protected_branches_only: false
           )
         ]
       end
@@ -62,15 +64,35 @@
         ]
       end
 
+      let(:agent_authorizations_with_env_and_protected_branches) do
+        [
+          build(
+            :agent_ci_access_project_authorization,
+            project: project,
+            agent: build(:cluster_agent, project: project),
+            environments: ['staging', 'review/*', 'production'],
+            protected_branches_only: true
+          ),
+          build(
+            :agent_ci_access_group_authorization,
+            group: group,
+            agent: build(:cluster_agent, project: project),
+            environments: ['staging', 'review/*', 'production'],
+            protected_branches_only: true
+          )
+        ]
+      end
+
       let(:agent_authorizations) do
         (
           agent_authorizations_without_env +
           agent_authorizations_with_env +
-          agent_authorizations_with_different_env
+          agent_authorizations_with_different_env +
+          agent_authorizations_with_env_and_protected_branches
         )
       end
 
-      let(:filter_params) { { environment: 'production' } }
+      let(:filter_params) { { environment: 'production', protected_ref: false } }
 
       it 'returns the authorizations with the given environment AND authorizations without any environment' do
         expected_authorizations = agent_authorizations_with_env + agent_authorizations_without_env
@@ -79,7 +101,7 @@
       end
 
       context 'when environment filter has a wildcard' do
-        let(:filter_params) { { environment: 'review/123' } }
+        let(:filter_params) { { environment: 'review/123', protected_ref: false } }
 
         it 'returns the authorizations with matching environments AND authorizations without any environment' do
           expected_authorizations = agent_authorizations_with_env + agent_authorizations_without_env
@@ -89,12 +111,85 @@
       end
 
       context 'when environment filter is nil' do
-        let(:filter_params) { { environment: nil } }
+        let(:filter_params) { { environment: nil, protected_ref: false } }
 
         it 'returns the authorizations without any environment' do
           expect(execute_filter).to match_array agent_authorizations_without_env
         end
       end
+
+      context 'when executed on protected branch' do
+        let(:filter_params) { { environment: 'production', protected_ref: true } }
+
+        it 'returns the authorizations with the given environment AND authorizations without any environment AND the authorizations with protected branches' do
+          expected_authorizations = agent_authorizations_with_env + agent_authorizations_without_env + agent_authorizations_with_env_and_protected_branches
+
+          expect(execute_filter).to match_array expected_authorizations
+        end
+      end
+    end
+
+    context 'when filtering protected branches' do
+      let(:agent_authorizations_with_protected_agent) do
+        build(
+          :agent_ci_access_project_authorization,
+          project: project,
+          agent: build(:cluster_agent, project: project),
+          protected_branches_only: protected
+        )
+      end
+
+      let(:agent_authorizations) { [agent_authorizations_with_protected_agent] }
+
+      context 'with protected agent' do
+        let(:protected) { true }
+
+        context 'on protected branch' do
+          let(:filter_params) { { protected_ref: true } }
+
+          it 'does return the authorizations as is' do
+            expect(execute_filter).to match_array agent_authorizations_with_protected_agent
+          end
+        end
+
+        context 'on unprotected branch' do
+          let(:filter_params) { { protected_ref: false } }
+
+          it 'does not return any authorizations' do
+            expect(execute_filter).to eq []
+          end
+
+          context 'when kubernetes_agent_protected_branches is disabled' do
+            before do
+              stub_feature_flags(kubernetes_agent_protected_branches: false)
+            end
+
+            it 'does not filter for protected_ref' do
+              expect(execute_filter).to match_array agent_authorizations_with_protected_agent
+            end
+          end
+        end
+      end
+
+      context 'with unprotected agent' do
+        let(:protected) { false }
+
+        context 'on protected branch' do
+          let(:filter_params) { { protected_ref: true } }
+
+          it 'does return the authorizations as is' do
+            expect(execute_filter).to match_array agent_authorizations_with_protected_agent
+          end
+        end
+
+        context 'on unprotected branch' do
+          let(:filter_params) { { protected_ref: false } }
+
+          it 'does return the authorizations as is' do
+            expect(execute_filter).to match_array agent_authorizations_with_protected_agent
+          end
+        end
+      end
     end
   end
 end
diff --git a/spec/services/clusters/agents/authorizations/ci_access/refresh_service_spec.rb b/spec/services/clusters/agents/authorizations/ci_access/refresh_service_spec.rb
index c12592cc071cf654b61d941d67fbffc2962d3e46..ba93b4f4ce62bcd3e9008a5eb752249f72b93316 100644
--- a/spec/services/clusters/agents/authorizations/ci_access/refresh_service_spec.rb
+++ b/spec/services/clusters/agents/authorizations/ci_access/refresh_service_spec.rb
@@ -23,12 +23,13 @@
           groups: [
             { id: added_group.full_path, default_namespace: 'default' },
             # Uppercase path verifies case-insensitive matching.
-            { id: modified_group.full_path.upcase, default_namespace: 'new-namespace' }
+            { id: modified_group.full_path.upcase, default_namespace: 'new-namespace', protected_branches_only: 'true' }
           ],
           projects: [
             { id: added_project.full_path, default_namespace: 'default' },
             # Uppercase path verifies case-insensitive matching.
-            { id: modified_project.full_path.upcase, default_namespace: 'new-namespace' }
+            { id: modified_project.full_path.upcase, default_namespace: 'new-namespace',
+              protected_branches_only: 'true' }
           ]
         }
       }.deep_stringify_keys
@@ -84,7 +85,8 @@
         expect(added_authorization.config).to eq({ 'default_namespace' => 'default' })
 
         modified_authorization = agent.ci_access_group_authorizations.find_by(group: modified_group)
-        expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace' })
+        expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace',
+                                                      'protected_branches_only' => 'true' })
       end
 
       context 'config contains too many groups' do
@@ -112,7 +114,8 @@
         expect(added_authorization.config).to eq({ 'default_namespace' => 'default' })
 
         modified_authorization = agent.ci_access_project_authorizations.find_by(project: modified_project)
-        expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace' })
+        expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace',
+                                                      'protected_branches_only' => 'true' })
       end
 
       context 'project does not belong to a group, and is in the same namespace as the agent' do