diff --git a/changelogs/unreleased/34519-extend-api-group-secret-variable.yml b/changelogs/unreleased/34519-extend-api-group-secret-variable.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0b625c392f71f7888e811ce9ecdb72127591345
--- /dev/null
+++ b/changelogs/unreleased/34519-extend-api-group-secret-variable.yml
@@ -0,0 +1,4 @@
+---
+title: Extend API for Group Secret Variable
+merge_request: 12936
+author:
diff --git a/doc/api/README.md b/doc/api/README.md
index 95e7a457848b01ba22d0654bc6dc9e8e97ff2560..9c308254ab25d1551a21533c3e19ea5f9fa9a99c 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -11,7 +11,8 @@ following locations:
 - [Award Emoji](award_emoji.md)
 - [Branches](branches.md)
 - [Broadcast Messages](broadcast_messages.md)
-- [Build Variables](build_variables.md)
+- [Project-level Variables](project_level_variables.md)
+- [Group-level Variables](group_level_variables.md)
 - [Commits](commits.md)
 - [Deployments](deployments.md)
 - [Deploy Keys](deploy_keys.md)
diff --git a/doc/api/group_level_variables.md b/doc/api/group_level_variables.md
new file mode 100644
index 0000000000000000000000000000000000000000..e19be7b35c4d896b00310b1b9928b4de757c47eb
--- /dev/null
+++ b/doc/api/group_level_variables.md
@@ -0,0 +1,125 @@
+# Group-level Variables  API
+
+## List group variables
+
+Get list of a group's variables.
+
+```
+GET /groups/:id/variables
+```
+
+| Attribute | Type    | required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer/string | yes      | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/1/variables"
+```
+
+```json
+[
+    {
+        "key": "TEST_VARIABLE_1",
+        "value": "TEST_1"
+    },
+    {
+        "key": "TEST_VARIABLE_2",
+        "value": "TEST_2"
+    }
+]
+```
+
+## Show variable details
+
+Get the details of a group's specific variable.
+
+```
+GET /groups/:id/variables/:key
+```
+
+| Attribute | Type    | required | Description           |
+|-----------|---------|----------|-----------------------|
+| `id`      | integer/string | yes      | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user   |
+| `key`     | string  | yes      | The `key` of a variable |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/1/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+    "key": "TEST_VARIABLE_1",
+    "value": "TEST_1"
+}
+```
+
+## Create variable
+
+Create a new variable.
+
+```
+POST /groups/:id/variables
+```
+
+| Attribute   | Type    | required | Description           |
+|-------------|---------|----------|-----------------------|
+| `id`        | integer/string | yes      | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user   |
+| `key`       | string  | yes      | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value`     | string  | yes      | The `value` of a variable |
+| `protected` | boolean | no       | Whether the variable is protected |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
+```
+
+```json
+{
+    "key": "NEW_VARIABLE",
+    "value": "new value",
+    "protected": false
+}
+```
+
+## Update variable
+
+Update a group's variable.
+
+```
+PUT /groups/:id/variables/:key
+```
+
+| Attribute   | Type    | required | Description             |
+|-------------|---------|----------|-------------------------|
+| `id`        | integer/string | yes      | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user     |
+| `key`       | string  | yes      | The `key` of a variable   |
+| `value`     | string  | yes      | The `value` of a variable |
+| `protected` | boolean | no       | Whether the variable is protected |
+
+```
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/1/variables/NEW_VARIABLE" --form "value=updated value"
+```
+
+```json
+{
+    "key": "NEW_VARIABLE",
+    "value": "updated value",
+    "protected": true
+}
+```
+
+## Remove variable
+
+Remove a group's variable.
+
+```
+DELETE /groups/:id/variables/:key
+```
+
+| Attribute | Type    | required | Description             |
+|-----------|---------|----------|-------------------------|
+| `id`      | integer/string | yes      | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user     |
+| `key`     | string  | yes      | The `key` of a variable |
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/1/variables/VARIABLE_1"
+```
diff --git a/doc/api/build_variables.md b/doc/api/project_level_variables.md
similarity index 94%
rename from doc/api/build_variables.md
rename to doc/api/project_level_variables.md
index d4f00256ed33db30657cee2a665cb4fe7a519352..82ac0b090275dcf8cb8517c2af32a6476a4db8da 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/project_level_variables.md
@@ -1,8 +1,8 @@
-# Build Variables  API
+# Project-level Variables  API
 
 ## List project variables
 
-Get list of a project's build variables.
+Get list of a project's variables.
 
 ```
 GET /projects/:id/variables
@@ -31,7 +31,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/
 
 ## Show variable details
 
-Get the details of a project's specific build variable.
+Get the details of a project's specific variable.
 
 ```
 GET /projects/:id/variables/:key
@@ -55,7 +55,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/
 
 ## Create variable
 
-Create a new build variable.
+Create a new variable.
 
 ```
 POST /projects/:id/variables
@@ -82,7 +82,7 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl
 
 ## Update variable
 
-Update a project's build variable.
+Update a project's variable.
 
 ```
 PUT /projects/:id/variables/:key
@@ -109,7 +109,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitla
 
 ## Remove variable
 
-Remove a project's build variable.
+Remove a project's variable.
 
 ```
 DELETE /projects/:id/variables/:key
diff --git a/lib/api/api.rb b/lib/api/api.rb
index efcf0976a819d097589ab2a1f48fc7293f0cda01..f6a310841e4fcf66ea2829f8ecce398c267496f4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -134,6 +134,7 @@ class API < Grape::API
     mount ::API::Triggers
     mount ::API::Users
     mount ::API::Variables
+    mount ::API::GroupVariables
     mount ::API::Version
 
     route :any, '*path' do
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0dd418887e0aa9471745309133f85daf90d88a5c
--- /dev/null
+++ b/lib/api/group_variables.rb
@@ -0,0 +1,95 @@
+module API
+  class GroupVariables < Grape::API
+    include PaginationParams
+
+    before { authenticate! }
+    before { authorize! :admin_build, user_group }
+
+    params do
+      requires :id, type: String, desc: 'The ID of a group'
+    end
+
+    resource :groups, requirements: { id: %r{[^/]+} } do
+      desc 'Get group-level variables' do
+        success Entities::Variable
+      end
+      params do
+        use :pagination
+      end
+      get ':id/variables' do
+        variables = user_group.variables
+        present paginate(variables), with: Entities::Variable
+      end
+
+      desc 'Get a specific variable from a group' do
+        success Entities::Variable
+      end
+      params do
+        requires :key, type: String, desc: 'The key of the variable'
+      end
+      get ':id/variables/:key' do
+        key = params[:key]
+        variable = user_group.variables.find_by(key: key)
+
+        return not_found!('GroupVariable') unless variable
+
+        present variable, with: Entities::Variable
+      end
+
+      desc 'Create a new variable in a group' do
+        success Entities::Variable
+      end
+      params do
+        requires :key, type: String, desc: 'The key of the variable'
+        requires :value, type: String, desc: 'The value of the variable'
+        optional :protected, type: String, desc: 'Whether the variable is protected'
+      end
+      post ':id/variables' do
+        variable_params = declared_params(include_missing: false)
+
+        variable = user_group.variables.create(variable_params)
+
+        if variable.valid?
+          present variable, with: Entities::Variable
+        else
+          render_validation_error!(variable)
+        end
+      end
+
+      desc 'Update an existing variable from a group' do
+        success Entities::Variable
+      end
+      params do
+        optional :key, type: String, desc: 'The key of the variable'
+        optional :value, type: String, desc: 'The value of the variable'
+        optional :protected, type: String, desc: 'Whether the variable is protected'
+      end
+      put ':id/variables/:key' do
+        variable = user_group.variables.find_by(key: params[:key])
+
+        return not_found!('GroupVariable') unless variable
+
+        variable_params = declared_params(include_missing: false).except(:key)
+
+        if variable.update(variable_params)
+          present variable, with: Entities::Variable
+        else
+          render_validation_error!(variable)
+        end
+      end
+
+      desc 'Delete an existing variable from a group' do
+        success Entities::Variable
+      end
+      params do
+        requires :key, type: String, desc: 'The key of the variable'
+      end
+      delete ':id/variables/:key' do
+        variable = user_group.variables.find_by(key: params[:key])
+        not_found!('GroupVariable') unless variable
+
+        variable.destroy
+      end
+    end
+  end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 0f4791841d26eeb04dbadfdab014a55095c307c4..56cd1f3df5a5fe28acbfc8962175d5a9737a5b72 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -29,6 +29,10 @@ def user_project
       @project ||= find_project!(params[:id])
     end
 
+    def user_group
+      @group ||= find_group!(params[:id])
+    end
+
     def available_labels
       @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
     end
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..402ea057cc57c04640a335dae7e6e22012445864
--- /dev/null
+++ b/spec/requests/api/group_variables_spec.rb
@@ -0,0 +1,221 @@
+require 'spec_helper'
+
+describe API::GroupVariables do
+  let(:group) { create(:group) }
+  let(:user) { create(:user) }
+
+  describe 'GET /groups/:id/variables' do
+    let!(:variable) { create(:ci_group_variable, group: group) }
+
+    context 'authorized user with proper permissions' do
+      before do
+        group.add_master(user)
+      end
+
+      it 'returns group variables' do
+        get api("/groups/#{group.id}/variables", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_a(Array)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'does not return group variables' do
+        get api("/groups/#{group.id}/variables", user)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not return group variables' do
+        get api("/groups/#{group.id}/variables")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'GET /groups/:id/variables/:key' do
+    let!(:variable) { create(:ci_group_variable, group: group) }
+
+    context 'authorized user with proper permissions' do
+      before do
+        group.add_master(user)
+      end
+
+      it 'returns group variable details' do
+        get api("/groups/#{group.id}/variables/#{variable.key}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['value']).to eq(variable.value)
+        expect(json_response['protected']).to eq(variable.protected?)
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing variable' do
+        get api("/groups/#{group.id}/variables/non_existing_variable", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'does not return group variable details' do
+        get api("/groups/#{group.id}/variables/#{variable.key}", user)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not return group variable details' do
+        get api("/groups/#{group.id}/variables/#{variable.key}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'POST /groups/:id/variables' do
+    context 'authorized user with proper permissions' do
+      let!(:variable) { create(:ci_group_variable, group: group) }
+
+      before do
+        group.add_master(user)
+      end
+
+      it 'creates variable' do
+        expect do
+          post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
+        end.to change{group.variables.count}.by(1)
+
+        expect(response).to have_http_status(201)
+        expect(json_response['key']).to eq('TEST_VARIABLE_2')
+        expect(json_response['value']).to eq('VALUE_2')
+        expect(json_response['protected']).to be_truthy
+      end
+
+      it 'creates variable with optional attributes' do
+        expect do
+          post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+        end.to change{group.variables.count}.by(1)
+
+        expect(response).to have_http_status(201)
+        expect(json_response['key']).to eq('TEST_VARIABLE_2')
+        expect(json_response['value']).to eq('VALUE_2')
+        expect(json_response['protected']).to be_falsey
+      end
+
+      it 'does not allow to duplicate variable key' do
+        expect do
+          post api("/groups/#{group.id}/variables", user), key: variable.key, value: 'VALUE_2'
+        end.to change{group.variables.count}.by(0)
+
+        expect(response).to have_http_status(400)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'does not create variable' do
+        post api("/groups/#{group.id}/variables", user)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not create variable' do
+        post api("/groups/#{group.id}/variables")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'PUT /groups/:id/variables/:key' do
+    let!(:variable) { create(:ci_group_variable, group: group) }
+
+    context 'authorized user with proper permissions' do
+      before do
+        group.add_master(user)
+      end
+
+      it 'updates variable data' do
+        initial_variable = group.variables.first
+        value_before = initial_variable.value
+
+        put api("/groups/#{group.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true
+
+        updated_variable = group.variables.first
+
+        expect(response).to have_http_status(200)
+        expect(value_before).to eq(variable.value)
+        expect(updated_variable.value).to eq('VALUE_1_UP')
+        expect(updated_variable).to be_protected
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing variable' do
+        put api("/groups/#{group.id}/variables/non_existing_variable", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'does not update variable' do
+        put api("/groups/#{group.id}/variables/#{variable.key}", user)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not update variable' do
+        put api("/groups/#{group.id}/variables/#{variable.key}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+
+  describe 'DELETE /groups/:id/variables/:key' do
+    let!(:variable) { create(:ci_group_variable, group: group) }
+
+    context 'authorized user with proper permissions' do
+      before do
+        group.add_master(user)
+      end
+
+      it 'deletes variable' do
+        expect do
+          delete api("/groups/#{group.id}/variables/#{variable.key}", user)
+
+          expect(response).to have_http_status(204)
+        end.to change{group.variables.count}.by(-1)
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing variable' do
+        delete api("/groups/#{group.id}/variables/non_existing_variable", user)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'does not delete variable' do
+        delete api("/groups/#{group.id}/variables/#{variable.key}", user)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'does not delete variable' do
+        delete api("/groups/#{group.id}/variables/#{variable.key}")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+  end
+end