diff --git a/app/graphql/types/ci/variable_type.rb b/app/graphql/types/ci/variable_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d2acfb9c9fc63162d8116bb5cbc5c25c9eda2e8
--- /dev/null
+++ b/app/graphql/types/ci/variable_type.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop: disable Graphql/AuthorizeTypes
+    class VariableType < BaseObject
+      graphql_name 'CiVariable'
+
+      field :id, GraphQL::Types::ID, null: false,
+        description: 'ID of the variable.'
+
+      field :key, GraphQL::Types::String, null: true,
+        description: 'Name of the variable.'
+
+      field :value, GraphQL::Types::String, null: true,
+        description: 'Value of the variable.'
+
+      field :variable_type, ::Types::Ci::VariableTypeEnum, null: true,
+        description: 'Type of the variable.'
+
+      field :protected, GraphQL::Types::Boolean, null: true,
+        description: 'Indicates whether the variable is protected.'
+
+      field :masked, GraphQL::Types::Boolean, null: true,
+        description: 'Indicates whether the variable is masked.'
+
+      field :raw, GraphQL::Types::Boolean, null: true,
+        description: 'Indicates whether the variable is raw.'
+    end
+  end
+end
diff --git a/app/graphql/types/ci/variable_type_enum.rb b/app/graphql/types/ci/variable_type_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44430754a2efd02fc7872265f198663a584ea3fc
--- /dev/null
+++ b/app/graphql/types/ci/variable_type_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    class VariableTypeEnum < BaseEnum
+      graphql_name 'CiVariableType'
+
+      ::Ci::Variable.variable_types.keys.each do |variable_type|
+        value variable_type.upcase, value: variable_type, description: "#{variable_type.humanize} type."
+      end
+    end
+  end
+end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 49971d52a30944e66feb4eb674db64359b7bba8d..52e9f8080666749e3aa540af174aa9f263d3f9c6 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -194,6 +194,13 @@ class GroupType < NamespaceType
           complexity: 5,
           resolver: Resolvers::GroupsResolver
 
+    field :ci_variables,
+          Types::Ci::VariableType.connection_type,
+          null: true,
+          description: "List of the group's CI/CD variables.",
+          authorize: :admin_group,
+          method: :variables
+
     field :runners, Types::Ci::RunnerType.connection_type,
           null: true,
           resolver: Resolvers::Ci::GroupRunnersResolver,
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 603d5ead5409814e806d24c316552ea1cac26d10..c2e47e063616e46d598a500463c1b53e074d8786 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -220,6 +220,13 @@ class ProjectType < BaseObject
           description: 'Build pipeline counts of the project.',
           resolver: Resolvers::Ci::ProjectPipelineCountsResolver
 
+    field :ci_variables,
+          Types::Ci::VariableType.connection_type,
+          null: true,
+          description: "List of the project's CI/CD variables.",
+          authorize: :admin_build,
+          method: :variables
+
     field :ci_cd_settings,
           Types::Ci::CiCdSettingType,
           null: true,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 46d121f6552f5aa39fb56528e030ebd1bce5fe46..f23d37b29aa1e3a407fc27282760402289174539 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -123,6 +123,11 @@ class QueryType < ::Types::BaseObject
           resolver: Resolvers::Ci::RunnersResolver,
           description: "Find runners visible to the current user."
 
+    field :ci_variables,
+          Types::Ci::VariableType.connection_type,
+          null: true,
+          description: "List of the instance's CI/CD variables."
+
     field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_MAX_COMPLEXITY / 2 + 1
 
     field :timelogs, Types::TimelogType.connection_type,
@@ -174,6 +179,12 @@ def ci_application_settings
       application_settings
     end
 
+    def ci_variables
+      return unless current_user.can_admin_all_resources?
+
+      ::Ci::InstanceVariable.all
+    end
+
     def application_settings
       Gitlab::CurrentSettings.current_application_settings
     end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index af91c842838ecfbf5f0d0829b381f093d7403c77..1fdb2b2f18b249a10637412e46519e80c460de43 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -87,6 +87,16 @@ four standard [pagination arguments](#connection-pagination-arguments):
 | ---- | ---- | ----------- |
 | <a id="queryciminutesusagenamespaceid"></a>`namespaceId` | [`NamespaceID`](#namespaceid) | Global ID of the Namespace for the monthly CI/CD minutes usage. |
 
+### `Query.ciVariables`
+
+List of the instance's CI/CD variables.
+
+Returns [`CiVariableConnection`](#civariableconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
 ### `Query.containerRepository`
 
 Find a container repository.
@@ -6235,6 +6245,29 @@ The edge type for [`CiStage`](#cistage).
 | <a id="cistageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="cistageedgenode"></a>`node` | [`CiStage`](#cistage) | The item at the end of the edge. |
 
+#### `CiVariableConnection`
+
+The connection type for [`CiVariable`](#civariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="civariableconnectionedges"></a>`edges` | [`[CiVariableEdge]`](#civariableedge) | A list of edges. |
+| <a id="civariableconnectionnodes"></a>`nodes` | [`[CiVariable]`](#civariable) | A list of nodes. |
+| <a id="civariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiVariableEdge`
+
+The edge type for [`CiVariable`](#civariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="civariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="civariableedgenode"></a>`node` | [`CiVariable`](#civariable) | The item at the end of the edge. |
+
 #### `ClusterAgentActivityEventConnection`
 
 The connection type for [`ClusterAgentActivityEvent`](#clusteragentactivityevent).
@@ -9979,6 +10012,20 @@ GitLab CI/CD configuration template.
 | <a id="citemplatecontent"></a>`content` | [`String!`](#string) | Contents of the CI template. |
 | <a id="citemplatename"></a>`name` | [`String!`](#string) | Name of the CI template. |
 
+### `CiVariable`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="civariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
+| <a id="civariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
+| <a id="civariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
+| <a id="civariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
+| <a id="civariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
+| <a id="civariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
+| <a id="civariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+
 ### `ClusterAgent`
 
 #### Fields
@@ -11734,6 +11781,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
 | <a id="groupallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean!`](#boolean) | Indicates whether to regularly prune stale group runners. Defaults to false. |
 | <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
 | <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
+| <a id="groupcivariables"></a>`ciVariables` | [`CiVariableConnection`](#civariableconnection) | List of the group's CI/CD variables. (see [Connections](#connections)) |
 | <a id="groupclusteragents"></a>`clusterAgents` | [`ClusterAgentConnection`](#clusteragentconnection) | Cluster agents associated with projects in the group and its subgroups. (see [Connections](#connections)) |
 | <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
 | <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. |
@@ -14990,6 +15038,7 @@ Represents vulnerability finding of a security report on the pipeline.
 | <a id="projectcicdsettings"></a>`ciCdSettings` | [`ProjectCiCdSetting`](#projectcicdsetting) | CI/CD settings for the project. |
 | <a id="projectciconfigpathordefault"></a>`ciConfigPathOrDefault` | [`String!`](#string) | Path of the CI configuration file. |
 | <a id="projectcijobtokenscope"></a>`ciJobTokenScope` | [`CiJobTokenScopeType`](#cijobtokenscopetype) | The CI Job Tokens scope of access. |
+| <a id="projectcivariables"></a>`ciVariables` | [`CiVariableConnection`](#civariableconnection) | List of the project's CI/CD variables. (see [Connections](#connections)) |
 | <a id="projectclusteragents"></a>`clusterAgents` | [`ClusterAgentConnection`](#clusteragentconnection) | Cluster agents associated with the project. (see [Connections](#connections)) |
 | <a id="projectcodecoveragesummary"></a>`codeCoverageSummary` | [`CodeCoverageSummary`](#codecoveragesummary) | Code coverage summary associated with the project. |
 | <a id="projectcomplianceframeworks"></a>`complianceFrameworks` | [`ComplianceFrameworkConnection`](#complianceframeworkconnection) | Compliance frameworks associated with the project. (see [Connections](#connections)) |
@@ -18645,6 +18694,13 @@ Values for sorting runners.
 | <a id="cirunnerupgradestatustyperecommended"></a>`RECOMMENDED` | Upgrade is available and recommended for the runner. |
 | <a id="cirunnerupgradestatustypeunknown"></a>`UNKNOWN` | Upgrade status is unknown. |
 
+### `CiVariableType`
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="civariabletypeenv_var"></a>`ENV_VAR` | Env var type. |
+| <a id="civariabletypefile"></a>`FILE` | File type. |
+
 ### `CodeQualityDegradationSeverity`
 
 | Value | Description |
diff --git a/spec/graphql/types/ci/variable_type_enum_spec.rb b/spec/graphql/types/ci/variable_type_enum_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5604caebffff0f22a58deb2d8476bd0a841d468a
--- /dev/null
+++ b/spec/graphql/types/ci/variable_type_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiVariableType'] do
+  it 'matches the keys of Ci::Variable.variable_types' do
+    expect(described_class.values.keys).to contain_exactly('ENV_VAR', 'FILE')
+  end
+end
diff --git a/spec/graphql/types/ci/variables_type_spec.rb b/spec/graphql/types/ci/variables_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a97a0f72f39f581628247bff845943511a0ffe8
--- /dev/null
+++ b/spec/graphql/types/ci/variables_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiVariable'] do
+  it 'contains attributes related to CI variables' do
+    expect(described_class).to have_graphql_fields(
+      :id, :key, :value, :variable_type, :protected, :masked, :raw
+    )
+  end
+end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 82703948cea6d32f1f48d0f5f78d17faa0b62a01..69c7eaf111fd5ad2e9ba5af761341776680802bf 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -23,7 +23,7 @@
       dependency_proxy_blob_count dependency_proxy_total_size
       dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
       shared_runners_setting timelogs organizations contacts work_item_types
-      recent_issue_boards
+      recent_issue_boards ci_variables
     ]
 
     expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 2e994bf78201edcde779cd1a49f851d72f5db786..ed93d31da0f78d7b77e5f3bc3abe2dd8aeb3e7e9 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -36,7 +36,8 @@
       pipeline_analytics squash_read_only sast_ci_configuration
       cluster_agent cluster_agents agent_configurations
       ci_template timelogs merge_commit_template squash_commit_template work_item_types
-      recent_issue_boards ci_config_path_or_default packages_cleanup_policy
+      recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
+      recent_issue_boards ci_config_path_or_default ci_variables
     ]
 
     expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 8b8c44c10f6ac77e12460f5bbfe0963f8ab9329c..514d24a209e5f44144265a1ecf8d26492a82461f 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -30,6 +30,7 @@
       board_list
       topics
       gitpod_enabled
+      ci_variables
     ]
 
     expect(described_class).to have_graphql_fields(*expected_fields).at_least
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0a571f1fef27f3491246e84d01ea778b0013061
--- /dev/null
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.group(fullPath).ciVariables' do
+  include GraphqlHelpers
+
+  let_it_be(:group) { create(:group) }
+  let_it_be(:user) { create(:user) }
+
+  let(:query) do
+    %(
+      query {
+        group(fullPath: "#{group.full_path}") {
+          ciVariables {
+            nodes {
+              id
+              key
+              value
+              variableType
+              protected
+              masked
+              raw
+            }
+          }
+        }
+      }
+    )
+  end
+
+  context 'when the user can administer the group' do
+    before do
+      group.add_owner(user)
+    end
+
+    it "returns the group's CI variables" do
+      variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test',
+                        masked: false, protected: true, raw: true)
+
+      post_graphql(query, current_user: user)
+
+      expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({
+        'id' => variable.to_global_id.to_s,
+        'key' => 'TEST_VAR',
+        'value' => 'test',
+        'variableType' => 'ENV_VAR',
+        'masked' => false,
+        'protected' => true,
+        'raw' => true
+      })
+    end
+  end
+
+  context 'when the user cannot administer the group' do
+    it 'returns nothing' do
+      create(:ci_group_variable, group: group, value: 'verysecret', masked: true)
+
+      group.add_developer(user)
+
+      post_graphql(query, current_user: user)
+
+      expect(graphql_data.dig('group', 'ciVariables')).to be_nil
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1faa4289029674ebd862109ab5cb61e1e98ef2b0
--- /dev/null
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciVariables' do
+  include GraphqlHelpers
+
+  let(:query) do
+    %(
+      query {
+        ciVariables {
+          nodes {
+            id
+            key
+            value
+            variableType
+            protected
+            masked
+            raw
+          }
+        }
+      }
+    )
+  end
+
+  context 'when the user is an admin' do
+    let_it_be(:user) { create(:admin) }
+
+    it "returns the instance's CI variables" do
+      variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test',
+                        masked: false, protected: true, raw: true)
+
+      post_graphql(query, current_user: user)
+
+      expect(graphql_data.dig('ciVariables', 'nodes')).to contain_exactly({
+        'id' => variable.to_global_id.to_s,
+        'key' => 'TEST_VAR',
+        'value' => 'test',
+        'variableType' => 'ENV_VAR',
+        'masked' => false,
+        'protected' => true,
+        'raw' => true
+      })
+    end
+  end
+
+  context 'when the user is not an admin' do
+    let_it_be(:user) { create(:user) }
+
+    it 'returns nothing' do
+      create(:ci_instance_variable, value: 'verysecret', masked: true)
+
+      post_graphql(query, current_user: user)
+
+      expect(graphql_data.dig('ciVariables')).to be_nil
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a4c1ef9c6504c0eacec2cf6d36c2b0a2674e2cb4
--- /dev/null
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).ciVariables' do
+  include GraphqlHelpers
+
+  let_it_be(:project) { create(:project) }
+  let_it_be(:user) { create(:user) }
+
+  let(:query) do
+    %(
+      query {
+        project(fullPath: "#{project.full_path}") {
+          ciVariables {
+            nodes {
+              id
+              key
+              value
+              variableType
+              protected
+              masked
+              raw
+            }
+          }
+        }
+      }
+    )
+  end
+
+  context 'when the user can administer builds' do
+    before do
+      project.add_maintainer(user)
+    end
+
+    it "returns the project's CI variables" do
+      variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test',
+                        masked: false, protected: true, raw: true)
+
+      post_graphql(query, current_user: user)
+
+      expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({
+        'id' => variable.to_global_id.to_s,
+        'key' => 'TEST_VAR',
+        'value' => 'test',
+        'variableType' => 'ENV_VAR',
+        'masked' => false,
+        'protected' => true,
+        'raw' => true
+      })
+    end
+  end
+
+  context 'when the user cannot administer builds' do
+    it 'returns nothing' do
+      create(:ci_variable, project: project, value: 'verysecret', masked: true)
+
+      project.add_developer(user)
+
+      post_graphql(query, current_user: user)
+
+      expect(graphql_data.dig('project', 'ciVariables')).to be_nil
+    end
+  end
+end