diff --git a/db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb b/db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb
new file mode 100644
index 0000000000000000000000000000000000000000..89222664d014066b8b504c3ffcb350b0fd38d111
--- /dev/null
+++ b/db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddAdminTerraformStateToMemberRoles < Gitlab::Database::Migration[2.2]
+  milestone '16.8'
+  enable_lock_retries!
+
+  def change
+    add_column :member_roles, :admin_terraform_state, :boolean, default: false, null: false
+  end
+end
diff --git a/db/schema_migrations/20240101031938 b/db/schema_migrations/20240101031938
new file mode 100644
index 0000000000000000000000000000000000000000..5b9395a568f8eec12442eaf3e69c6bab878dc2e2
--- /dev/null
+++ b/db/schema_migrations/20240101031938
@@ -0,0 +1 @@
+d0cb92dc098f069e02d457f7c497dc24f544f6a27a8426dcd3446ad16bd9cc44
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c74d4b69bafe64d0fd8553e801587647bdc37046..0a048082a9351b3cced303b06dff08fe36ab4c0b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18948,6 +18948,7 @@ CREATE TABLE member_roles (
     archive_project boolean DEFAULT false NOT NULL,
     manage_group_access_tokens boolean DEFAULT false NOT NULL,
     remove_project boolean DEFAULT false NOT NULL,
+    admin_terraform_state boolean DEFAULT false NOT NULL,
     CONSTRAINT check_4364846f58 CHECK ((char_length(description) <= 255)),
     CONSTRAINT check_9907916995 CHECK ((char_length(name) <= 255))
 );
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index ee2f526f7f3ab05eeba3dca89e0fc0d5dedafb16..fed954b7199a76de35349f86e57cfe84ab9478c4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -30859,6 +30859,7 @@ Member role permission.
 | ----- | ----------- |
 | <a id="memberrolepermissionadmin_group_member"></a>`ADMIN_GROUP_MEMBER` | Allows to admin group members. |
 | <a id="memberrolepermissionadmin_merge_request"></a>`ADMIN_MERGE_REQUEST` | Allows to approve merge requests. |
+| <a id="memberrolepermissionadmin_terraform_state"></a>`ADMIN_TERRAFORM_STATE` | Allows to admin terraform state. |
 | <a id="memberrolepermissionadmin_vulnerability"></a>`ADMIN_VULNERABILITY` | Allows admin access to the vulnerability reports. |
 | <a id="memberrolepermissionarchive_project"></a>`ARCHIVE_PROJECT` | Allows to archive projects. |
 | <a id="memberrolepermissionmanage_group_access_tokens"></a>`MANAGE_GROUP_ACCESS_TOKENS` | Allows manage access to the group access tokens. |
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index 2fd10d99fda89dda472645f32500b3913bbca8e7..2bfbc29081fa1fae07b0693b19ac748bc5f22a5b 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -19,6 +19,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
 > - [Archive project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134998) in GitLab 16.7.
 > - [Delete project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139696) in GitLab 16.8.
 > - [Manage group access tokens introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140115) in GitLab 16.8.
+> - [Admin terraform state introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140759) in GitLab 16.8.
 
 FLAG:
 On self-managed GitLab, by default these features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_group_member` and `manage_project_access_tokens`.
@@ -46,6 +47,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
 | `[].group_id`                      | integer | The ID of the group that the member role belongs to. |
 | `[].base_access_level`             | integer | Base access level for member role. Valid values are 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), or 50 (Owner).|
 | `[].admin_merge_request`           | boolean | Permission to admin project merge requests and enables the ability to `download_code`. |
+| `[].admin_terraform_state`         | boolean | Permission to admin project terraform state. |
 | `[].admin_vulnerability`           | boolean | Permission to admin project vulnerabilities. |
 | `[].read_code`                     | boolean | Permission to read project code. |
 | `[].read_dependency`               | boolean | Permission to read project dependencies. |
@@ -73,6 +75,7 @@ Example response:
     "group_id": 84,
     "base_access_level": 10,
     "admin_merge_request": false,
+    "admin_terraform_state": false,
     "admin_vulnerability": false,
     "read_code": true,
     "read_dependency": false,
@@ -88,8 +91,9 @@ Example response:
     "description: "Custom guest that read and admin security entities",
     "group_id": 84,
     "base_access_level": 10,
-    "admin_merge_request": false,
     "admin_vulnerability": true,
+    "admin_merge_request": false,
+    "admin_terraform_state": false,
     "read_code": false,
     "read_dependency": true,
     "read_vulnerability": true,
@@ -120,6 +124,7 @@ To add a member role to a group, the group must be at root-level (have no parent
 | `description`  | string         | no       | The description of the member role. |
 | `base_access_level` | integer   | yes      | Base access level for configured role. Valid values are 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), or 50 (Owner).|
 | `admin_merge_request` | boolean | no       | Permission to admin project merge requests. |
+| `admin_terraform_state` | boolean | no       | Permission to admin project terraform state. |
 | `admin_vulnerability` | boolean | no       | Permission to admin project vulnerabilities. |
 | `read_code`           | boolean | no       | Permission to read project code. |
 | `read_dependency`     | boolean | no       | Permission to read project dependencies. |
@@ -135,6 +140,7 @@ If successful, returns [`201`](rest/index.md#status-codes) and the following att
 | `group_id`               | integer | The ID of the group that the member role belongs to. |
 | `base_access_level`      | integer | Base access level for member role. |
 | `admin_merge_request`    | boolean | Permission to admin project merge requests. |
+| `admin_terraform_state`    | boolean | Permission to admin project terraform state. |
 | `admin_vulnerability`    | boolean | Permission to admin project vulnerabilities. |
 | `read_code`              | boolean | Permission to read project code. |
 | `read_dependency`        | boolean | Permission to read project dependencies. |
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index 86689a19adb96d70a57b3698fe9850566842966a..36ddf81df6561265074d5d4ee71b4e4f8ca40c20 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -257,6 +257,15 @@ module ProjectPolicy
         ).has_ability?
       end
 
+      desc "Custom role on project that enables admin terraform state"
+      condition(:role_enables_admin_terraform_state) do
+        ::Auth::MemberRoleAbilityLoader.new(
+          user: @user,
+          resource: @subject,
+          ability: :admin_terraform_state
+        ).has_ability?
+      end
+
       desc "Custom role on project that enables admin vulnerability"
       condition(:role_enables_admin_vulnerability) do
         ::Auth::MemberRoleAbilityLoader.new(
@@ -769,6 +778,11 @@ module ProjectPolicy
         enable :download_code # required to negate https://gitlab.com/gitlab-org/gitlab/-/blob/3061d30d9b3d6d4c4dd5abe68bc1e4a8a93c7966/app/policies/project_policy.rb#L603-607
       end
 
+      rule { custom_roles_allowed & role_enables_admin_terraform_state }.policy do
+        enable :read_terraform_state
+        enable :admin_terraform_state
+      end
+
       rule { custom_roles_allowed & role_enables_admin_vulnerability }.policy do
         enable :admin_vulnerability
       end
diff --git a/ee/config/custom_abilities/admin_terraform_state.yml b/ee/config/custom_abilities/admin_terraform_state.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1408c5c6a5230adae1bceeeb204d4bbeb3dce753
--- /dev/null
+++ b/ee/config/custom_abilities/admin_terraform_state.yml
@@ -0,0 +1,10 @@
+---
+name: admin_terraform_state
+description: Allows to admin terraform state
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/421789
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140759
+feature_category: infrastructure_as_code
+milestone: '16.8'
+group_ability: false
+project_ability: true
+requirement: ''
diff --git a/ee/spec/factories/member_roles.rb b/ee/spec/factories/member_roles.rb
index 42abbfe88c44c88a5f01b9f6a10407dcdcacdb10..cfe42c09a070f0e3b8ca6a3bdd269e1da68bbee2 100644
--- a/ee/spec/factories/member_roles.rb
+++ b/ee/spec/factories/member_roles.rb
@@ -18,6 +18,10 @@
       read_vulnerability { true }
     end
 
+    trait :admin_terraform_state do
+      admin_terraform_state { true }
+    end
+
     # this trait can be used only for self-managed
     trait(:instance) { namespace { nil } }
   end
diff --git a/ee/spec/lib/ee/api/entities/member_role_spec.rb b/ee/spec/lib/ee/api/entities/member_role_spec.rb
index 8bdc4f85897ad3c5b7dbf09953c831f3e40cedbb..eb773e1f3218a0def4b5fc20f7fa29fb4c036276 100644
--- a/ee/spec/lib/ee/api/entities/member_role_spec.rb
+++ b/ee/spec/lib/ee/api/entities/member_role_spec.rb
@@ -19,6 +19,7 @@
       expect(subject[:base_access_level]).to eq member_role.base_access_level
       expect(subject[:read_code]).to eq member_role.read_code
       expect(subject[:read_vulnerability]).to eq member_role.read_vulnerability
+      expect(subject[:admin_terraform_state]).to eq member_role.admin_terraform_state
       expect(subject[:admin_vulnerability]).to eq member_role.admin_vulnerability
       expect(subject[:manage_group_access_tokens]).to eq member_role.manage_group_access_tokens
       expect(subject[:manage_project_access_tokens]).to eq member_role.manage_project_access_tokens
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index 6fd4f88ca23d9b6c77930a044a782cff6806a419..4464aa2101836aae789826baff1adcb945e153ec 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -1222,6 +1222,7 @@
              OR "members"."access_level" = 10
              AND \(admin_group_member = true
              OR admin_merge_request = true
+             OR admin_terraform_state = true
              OR admin_vulnerability = true
              OR archive_project = true
              OR manage_group_access_tokens = true
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index 9caa4cd833297c0c0811466686f116529aefb65f..1acbdd393da7c67fc2caef71300576db3516cb1e 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -2700,6 +2700,13 @@ def create_member_role(member, abilities = member_role_abilities)
       end
     end
 
+    context 'for a member role with admin_terraform_state true' do
+      let(:member_role_abilities) { { admin_terraform_state: true } }
+      let(:allowed_abilities) { [:read_terraform_state, :admin_terraform_state] }
+
+      it_behaves_like 'custom roles abilities'
+    end
+
     context 'for a member role with admin_vulnerability true' do
       let(:member_role_abilities) { { read_vulnerability: true, admin_vulnerability: true } }
       let(:allowed_abilities) do
diff --git a/ee/spec/requests/api/member_roles_spec.rb b/ee/spec/requests/api/member_roles_spec.rb
index cacfa8f9b5e1b0157cd9c2107cbe249fe3de3096..f4affed98450526be3f6c0903ed7f7254056a688 100644
--- a/ee/spec/requests/api/member_roles_spec.rb
+++ b/ee/spec/requests/api/member_roles_spec.rb
@@ -104,6 +104,7 @@
                   "read_vulnerability" => true,
                   "admin_group_member" => false,
                   "admin_merge_request" => false,
+                  "admin_terraform_state" => false,
                   "admin_vulnerability" => false,
                   "manage_group_access_tokens" => false,
                   "manage_project_access_tokens" => false,
@@ -121,6 +122,7 @@
                   "read_vulnerability" => false,
                   "admin_group_member" => false,
                   "admin_merge_request" => true,
+                  "admin_terraform_state" => false,
                   "admin_vulnerability" => false,
                   "manage_group_access_tokens" => false,
                   "manage_project_access_tokens" => false,
diff --git a/ee/spec/requests/custom_roles/admin_terraform_state/request_spec.rb b/ee/spec/requests/custom_roles/admin_terraform_state/request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b57adceb484c2277f1821f0cbb308c1baddb4a4
--- /dev/null
+++ b/ee/spec/requests/custom_roles/admin_terraform_state/request_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User with admin_terraform_state custom role', feature_category: :permissions do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project, :in_group) }
+
+  let_it_be(:role) { create(:member_role, :guest, namespace: project.group, admin_terraform_state: true) }
+  let_it_be(:member) { create(:project_member, :guest, member_role: role, user: user, project: project) }
+
+  before do
+    stub_licensed_features(custom_roles: true)
+
+    sign_in(user)
+  end
+
+  describe Projects::TerraformController do
+    describe '#index' do
+      it 'user has access via a custom role' do
+        get project_terraform_index_path(project)
+
+        expect(response).to have_gitlab_http_status(:ok)
+      end
+    end
+  end
+
+  describe Mutations::Terraform::State do
+    include GraphqlHelpers
+
+    before do
+      post_graphql_mutation(mutation, current_user: user)
+    end
+
+    context 'when locking a terraform state' do
+      let(:state) { create(:terraform_state, project: project) }
+      let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) }
+
+      it_behaves_like 'a working graphql query'
+    end
+
+    context 'when unlocking a terraform state' do
+      let(:state) { create(:terraform_state, :locked, project: project) }
+      let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) }
+
+      it_behaves_like 'a working graphql query'
+    end
+
+    context 'when deleting a terraform state' do
+      let(:state) { create(:terraform_state, project: project) }
+      let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) }
+
+      it_behaves_like 'a working graphql query'
+    end
+  end
+end