diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 62c30c941ebb3d3eb8ea1e125f6ea9bccf15dda3..a71097bb501e8f912a725e91a3a2ab3ebf92b193 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale';
 import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
 import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
 import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
 import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
 import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
@@ -29,28 +30,8 @@ export const i18n = {
   securityTraining: s__('SecurityConfiguration|Security training'),
 };
 
-// This will be removed and replaced with GraphQL query:
-// https://gitlab.com/gitlab-org/gitlab/-/issues/346480
-export const TRAINING_PROVIDERS = [
-  {
-    id: 101,
-    name: __('Kontra'),
-    description: __('Interactive developer security education.'),
-    url: 'https://application.security/',
-    isEnabled: false,
-  },
-  {
-    id: 102,
-    name: __('SecureCodeWarrior'),
-    description: __('Security training with guide and learning pathways.'),
-    url: 'https://www.securecodewarrior.com/',
-    isEnabled: true,
-  },
-];
-
 export default {
   i18n,
-  TRAINING_PROVIDERS,
   components: {
     AutoDevOpsAlert,
     AutoDevOpsEnabledAlert,
@@ -107,6 +88,7 @@ export default {
     return {
       autoDevopsEnabledAlertDismissedProjects: [],
       errorMessage: '',
+      securityTrainingProviders: [],
     };
   },
   computed: {
@@ -128,6 +110,11 @@ export default {
       );
     },
   },
+  apollo: {
+    securityTrainingProviders: {
+      query: securityTrainingProvidersQuery,
+    },
+  },
   methods: {
     dismissAutoDevopsEnabledAlert() {
       const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
@@ -264,7 +251,10 @@ export default {
       >
         <section-layout :heading="$options.i18n.securityTraining">
           <template #features>
-            <training-provider-list :providers="$options.TRAINING_PROVIDERS" />
+            <training-provider-list
+              :loading="$apollo.queries.securityTrainingProviders.loading"
+              :providers="securityTrainingProviders"
+            />
           </template>
         </section-layout>
       </gl-tab>
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index 160540f698950bea279c1eec9763d30b1b6c53bd..d6170e299de4574fde55bafe3657b8469101a53a 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -1,23 +1,39 @@
 <script>
-import { GlCard, GlToggle, GlLink } from '@gitlab/ui';
+import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
 
 export default {
   components: {
     GlCard,
     GlToggle,
     GlLink,
+    GlSkeletonLoader,
   },
   props: {
     providers: {
       type: Array,
       required: true,
     },
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
 };
 </script>
 
 <template>
-  <ul class="gl-list-style-none gl-m-0 gl-p-0">
+  <div
+    v-if="loading"
+    class="gl-mb-6 gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
+  >
+    <gl-skeleton-loader :width="350" :height="44">
+      <rect width="200" height="8" x="10" y="0" rx="4" />
+      <rect width="300" height="8" x="10" y="15" rx="4" />
+      <rect width="100" height="8" x="10" y="35" rx="4" />
+    </gl-skeleton-loader>
+  </div>
+  <ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
     <li v-for="{ id, isEnabled, name, description, url } in providers" :key="id" class="gl-mb-6">
       <gl-card>
         <div class="gl-display-flex">
diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..9a297d7f9d1c473f94fdf6e3791baa47d03bdc3a
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
@@ -0,0 +1,9 @@
+query Query {
+  securityTrainingProviders {
+    name
+    id
+    description
+    isEnabled
+    url
+  }
+}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c24130c99b76ef292d067e815f1bb5ceba237a5f..27b254410583b6a325c3b596dfc5564d626f3704 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19017,9 +19017,6 @@ msgstr ""
 msgid "Integrations|can't exceed %{recipients_limit}"
 msgstr ""
 
-msgid "Interactive developer security education."
-msgstr ""
-
 msgid "Interactive mode"
 msgstr ""
 
@@ -20349,9 +20346,6 @@ msgstr ""
 msgid "Ki"
 msgstr ""
 
-msgid "Kontra"
-msgstr ""
-
 msgid "Kroki"
 msgstr ""
 
@@ -30860,9 +30854,6 @@ msgstr ""
 msgid "Secure token that identifies an external storage request."
 msgstr ""
 
-msgid "SecureCodeWarrior"
-msgstr ""
-
 msgid "Security"
 msgstr ""
 
@@ -30887,9 +30878,6 @@ msgstr ""
 msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
 msgstr ""
 
-msgid "Security training with guide and learning pathways."
-msgstr ""
-
 msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
 msgstr ""
 
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 759844b941a45e5c98e95f10c51ef1d1f3b5064e..fdccd533fec1b5634a20da209c2c63fb338d5dfe 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,14 +1,13 @@
 import { GlTab } from '@gitlab/ui';
 import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
 import { useLocalStorageSpy } from 'helpers/local_storage_helper';
 import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
 import stubChildren from 'helpers/stub_children';
 import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import SecurityConfigurationApp, {
-  i18n,
-  TRAINING_PROVIDERS,
-} from '~/security_configuration/components/app.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
 import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
 import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
 import {
@@ -30,6 +29,8 @@ import {
   REPORT_TYPE_LICENSE_COMPLIANCE,
   REPORT_TYPE_SAST,
 } from '~/vue_shared/security_reports/constants';
+import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
+import { securityTrainingProvidersResponse, securityTrainingProviders } from '../mock_data';
 
 const upgradePath = '/upgrade';
 const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@@ -38,10 +39,12 @@ const gitlabCiHistoryPath = 'test/historyPath';
 const projectPath = 'namespace/project';
 
 useLocalStorageSpy();
+Vue.use(VueApollo);
 
 describe('App component', () => {
   let wrapper;
   let userCalloutDismissSpy;
+  let mockApollo;
 
   const createComponent = ({
     shouldShowCallout = true,
@@ -49,9 +52,16 @@ describe('App component', () => {
     ...propsData
   }) => {
     userCalloutDismissSpy = jest.fn();
+    mockApollo = createMockApollo([
+      [
+        securityTrainingProvidersQuery,
+        jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
+      ],
+    ]);
 
     wrapper = extendedWrapper(
       mount(SecurityConfigurationApp, {
+        apolloProvider: mockApollo,
         propsData,
         provide: {
           upgradePath,
@@ -134,6 +144,7 @@ describe('App component', () => {
 
   afterEach(() => {
     wrapper.destroy();
+    mockApollo = null;
   });
 
   describe('basic structure', () => {
@@ -187,7 +198,7 @@ describe('App component', () => {
     });
 
     it('renders training provider list with correct props', () => {
-      expect(findTrainingProviderList().props('providers')).toEqual(TRAINING_PROVIDERS);
+      expect(findTrainingProviderList().props('providers')).toEqual(securityTrainingProviders);
     });
   });
 
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 1169a977d442dc2cdd503773cedcdcdf9f38917c..2d5cc0e80c961b6dc7fbaba00a82e2e590ea0852 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,10 +1,10 @@
-import { GlLink, GlToggle, GlCard } from '@gitlab/ui';
+import { GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
 import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
-import { TRAINING_PROVIDERS } from '~/security_configuration/components/app.vue';
+import { securityTrainingProviders } from '../mock_data';
 
 const DEFAULT_PROPS = {
-  providers: TRAINING_PROVIDERS,
+  providers: securityTrainingProviders,
 };
 
 describe('TrainingProviderList component', () => {
@@ -22,6 +22,7 @@ describe('TrainingProviderList component', () => {
   const findCards = () => wrapper.findAllComponents(GlCard);
   const findLinks = () => wrapper.findAllComponents(GlLink);
   const findToggles = () => wrapper.findAllComponents(GlToggle);
+  const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
 
   afterEach(() => {
     wrapper.destroy();
@@ -57,4 +58,23 @@ describe('TrainingProviderList component', () => {
       });
     });
   });
+
+  describe('loading', () => {
+    beforeEach(() => {
+      createComponent({ loading: true });
+    });
+
+    it('shows the loader', () => {
+      expect(findLoader().exists()).toBe(true);
+    });
+
+    it('does not show the cards', () => {
+      expect(findCards().exists()).toBe(false);
+    });
+
+    it('does not show loader when not loading', () => {
+      createComponent({ loading: false });
+      expect(findLoader().exists()).toBe(false);
+    });
+  });
 });
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..b70259cef8d7bc9087cb7161c43ba7ecbc819211
--- /dev/null
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -0,0 +1,22 @@
+export const securityTrainingProviders = [
+  {
+    id: 101,
+    name: 'Kontra',
+    description: 'Interactive developer security education.',
+    url: 'https://application.security/',
+    isEnabled: false,
+  },
+  {
+    id: 102,
+    name: 'SecureCodeWarrior',
+    description: 'Security training with guide and learning pathways.',
+    url: 'https://www.securecodewarrior.com/',
+    isEnabled: true,
+  },
+];
+
+export const securityTrainingProvidersResponse = {
+  data: {
+    securityTrainingProviders,
+  },
+};