diff --git a/app/finders/keys_finder.rb b/app/finders/keys_finder.rb
index d6ba7cb290dc6b2723ae8fad849dbc9af1370985..6fd914c88cdf6568a96fe2cf665bcb2e2f217956 100644
--- a/app/finders/keys_finder.rb
+++ b/app/finders/keys_finder.rb
@@ -15,15 +15,43 @@ def initialize(current_user, params)
 
   def execute
     raise GitLabAccessDeniedError unless current_user.admin?
-    raise InvalidFingerprint unless valid_fingerprint_param?
 
-    Key.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord
+    keys = by_key_type
+    keys = by_user(keys)
+    keys = sort(keys)
+
+    by_fingerprint(keys)
   end
 
   private
 
   attr_reader :current_user, :params
 
+  def by_key_type
+    if params[:key_type] == 'ssh'
+      Key.regular_keys
+    else
+      Key.all
+    end
+  end
+
+  def sort(keys)
+    keys.order_last_used_at_desc
+  end
+
+  def by_user(keys)
+    return keys unless params[:user]
+
+    keys.for_user(params[:user])
+  end
+
+  def by_fingerprint(keys)
+    return keys unless params[:fingerprint].present?
+    raise InvalidFingerprint unless valid_fingerprint_param?
+
+    keys.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord
+  end
+
   def valid_fingerprint_param?
     if fingerprint_type == "sha256"
       Base64.decode64(fingerprint).length == 32
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index bd95dcd323fd52e22d2a6569329edf0763024c8d..7b15a3b0c1034e88bc36b38675f4eff354fe25bf 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -13,18 +13,26 @@ def execute
     tokens = PersonalAccessToken.all
     tokens = by_user(tokens)
     tokens = by_impersonation(tokens)
-    by_state(tokens)
+    tokens = by_state(tokens)
+
+    sort(tokens)
   end
 
   private
 
-  # rubocop: disable CodeReuse/ActiveRecord
   def by_user(tokens)
     return tokens unless @params[:user]
 
-    tokens.where(user: @params[:user])
+    tokens.for_user(@params[:user])
+  end
+
+  def sort(tokens)
+    available_sort_orders = PersonalAccessToken.simple_sorts.keys
+
+    return tokens unless available_sort_orders.include?(params[:sort])
+
+    tokens.order_by(params[:sort])
   end
-  # rubocop: enable CodeReuse/ActiveRecord
 
   def by_impersonation(tokens)
     case @params[:impersonation]
diff --git a/app/models/key.rb b/app/models/key.rb
index f66aa4fb329f1d47c6cbe0cc2e9421aa3d5c01bd..e549c59b58fc7a91d2917fdd0f9d2ae4134b0f1b 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -39,6 +39,10 @@ class Key < ApplicationRecord
 
   alias_attribute :fingerprint_md5, :fingerprint
 
+  scope :preload_users, -> { preload(:user) }
+  scope :for_user, -> (user) { where(user: user) }
+  scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
+
   def self.regular_keys
     where(type: ['Key', nil])
   end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 9ccc90fb74d2302ff70d460f69a0cd45d9b80d90..af079f7ebc4a8ed40f86ee8aefb140eff09a271c 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,6 +3,7 @@
 class PersonalAccessToken < ApplicationRecord
   include Expirable
   include TokenAuthenticatable
+  include Sortable
 
   add_authentication_token_field :token, digest: true
 
@@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord
   scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
   scope :with_impersonation, -> { where(impersonation: true) }
   scope :without_impersonation, -> { where(impersonation: false) }
+  scope :for_user, -> (user) { where(user: user) }
+  scope :preload_users, -> { preload(:user) }
 
   validates :scopes, presence: true
   validate :validate_scopes
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 5bb92c8d1cf443e48d9fbc98e622c365852b0b82..71fef5df5bc09ce009b46a366fb5476eac962b5a 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -182,6 +182,8 @@
               %strong.fly-out-top-item-name
                 = _('Deploy Keys')
 
+      = render_if_exists 'layouts/nav/sidebar/credentials_link'
+
       = nav_link(controller: :services) do
         = link_to admin_application_settings_services_path do
           .nav-icon-container
diff --git a/db/migrate/20191205060723_add_index_to_keys.rb b/db/migrate/20191205060723_add_index_to_keys.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e8c725f62e3326f897440fcf2c2a25fe50b6a7b
--- /dev/null
+++ b/db/migrate/20191205060723_add_index_to_keys.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToKeys < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :keys, :last_used_at, order: { last_used_at: 'DESC NULLS LAST' }
+  end
+
+  def down
+    remove_concurrent_index :keys, :last_used_at
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 870eb22e1f1ef1d661ee6cf652fb1b9948b4afd6..7b0ecce825cb430e9cc05193599cd86d170741d5 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2217,6 +2217,7 @@
     t.index ["fingerprint"], name: "index_keys_on_fingerprint", unique: true
     t.index ["fingerprint_sha256"], name: "index_keys_on_fingerprint_sha256"
     t.index ["id", "type"], name: "index_on_deploy_keys_id_and_type_and_public", unique: true, where: "(public = true)"
+    t.index ["last_used_at"], name: "index_keys_on_last_used_at", order: "DESC NULLS LAST"
     t.index ["user_id"], name: "index_keys_on_user_id"
   end
 
diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md
index 246addb6dc9a1f1e13896593a3b882cbd8c476e3..44e1cc8059adda25da44b8a9e00cdfdc90732c60 100644
--- a/doc/administration/compliance.md
+++ b/doc/administration/compliance.md
@@ -16,3 +16,4 @@ GitLab’s [security features](../security/README.md) may also help you meet rel
 |**[LDAP group sync filters](auth/ldap-ee.md#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+||
 |**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+||
 |**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
+|**[Credentials inventory](../user/admin_area/credentials_inventory.md)**<br>With a credentials inventory, GitLab administrators can keep track of the credentials used by all of the users in their GitLab instance. |Ultimate||
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 1652b2872585d0d86d147f5323566a0c0b949e8f..2a9980cddb33ec0f2c168c8aa9e5076df4b78d88 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -124,6 +124,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
   basic Postfix mail server with IMAP authentication on Ubuntu for incoming
   emails.
 - [Abuse reports](../user/admin_area/abuse_reports.md): View and resolve abuse reports from your users.
+- [Credentials Inventory](../user/admin_area/credentials_inventory.md): With Credentials inventory, GitLab administrators can keep track of the credentials used by their users in their GitLab self-managed instance. **(ULTIMATE ONLY)**
 
 ## Project settings
 
diff --git a/doc/user/admin_area/credentials_inventory.md b/doc/user/admin_area/credentials_inventory.md
new file mode 100644
index 0000000000000000000000000000000000000000..30ebbb5b6dbd4e8bb489a77375e749d471943fdd
--- /dev/null
+++ b/doc/user/admin_area/credentials_inventory.md
@@ -0,0 +1,19 @@
+# Credentials inventory **(ULTIMATE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/20912) in GitLab 12.6.
+
+## Overview
+
+GitLab administrators are responsible for the overall security of their instance. To assist, GitLab provides a Credentials inventory to keep track of all the credentials that can be used to access their self-managed instance.
+
+Using Credentials inventory, GitLab administrators can see all the personal access tokens and SSH keys that exist in their instance and:
+
+- Who they belong to.
+- Their access scope.
+- Their usage pattern.
+
+To access the Credentials inventory, navigate to **Admin Area > Credentials**.
+
+The following is an example of the Credentials inventory page:
+
+![Credentials inventory page](img/credentials_inventory_v12_6.png)
diff --git a/doc/user/admin_area/img/credentials_inventory_v12_6.png b/doc/user/admin_area/img/credentials_inventory_v12_6.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff46db61cdb6ffa159c44ac3a80aef1413b13d7c
Binary files /dev/null and b/doc/user/admin_area/img/credentials_inventory_v12_6.png differ
diff --git a/ee/app/controllers/admin/credentials_controller.rb b/ee/app/controllers/admin/credentials_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..895766acdc4d7cc0a38720d70a4d51eedf1eea1b
--- /dev/null
+++ b/ee/app/controllers/admin/credentials_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Admin::CredentialsController < Admin::ApplicationController
+  include Admin::CredentialsHelper
+
+  before_action :check_license_credentials_inventory_available!
+
+  def index
+    @credentials = filter_credentials.page(params[:page]).preload_users.without_count
+  end
+
+  private
+
+  def filter_credentials
+    if show_personal_access_tokens?
+      ::PersonalAccessTokensFinder.new({ user: nil, impersonation: false, state: 'active', sort: 'id_desc' }).execute
+    elsif show_ssh_keys?
+      ::KeysFinder.new(current_user, { user: nil, key_type: 'ssh' }).execute
+    end
+  end
+
+  def check_license_credentials_inventory_available!
+    render_404 unless credentials_inventory_feature_available?
+  end
+end
diff --git a/ee/app/helpers/admin/credentials_helper.rb b/ee/app/helpers/admin/credentials_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3219388d7ea0dc3bd0eb6f10d3465d01d64d9859
--- /dev/null
+++ b/ee/app/helpers/admin/credentials_helper.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Admin
+  module CredentialsHelper
+    VALID_FILTERS = %w(ssh_keys personal_access_tokens).freeze
+
+    def show_personal_access_tokens?
+      return true if params[:filter] == 'personal_access_tokens'
+
+      VALID_FILTERS.exclude? params[:filter]
+    end
+
+    def show_ssh_keys?
+      params[:filter] == 'ssh_keys'
+    end
+
+    def credentials_inventory_feature_available?
+      License.feature_available?(:credentials_inventory)
+    end
+  end
+end
diff --git a/ee/app/models/license.rb b/ee/app/models/license.rb
index 4bb47ef0344283414496c6f9ab2220d17024897a..7bb944ccd82140c98803f0db651b2b77d35cbfae 100644
--- a/ee/app/models/license.rb
+++ b/ee/app/models/license.rb
@@ -106,6 +106,7 @@ class License < ApplicationRecord
   EEU_FEATURES = EEP_FEATURES + %i[
     cluster_health
     container_scanning
+    credentials_inventory
     dast
     dependency_scanning
     epics
diff --git a/ee/app/views/admin/credentials/_personal_access_tokens.html.haml b/ee/app/views/admin/credentials/_personal_access_tokens.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..613952f0cf5ff461154c3d09bbf229c107de3fb6
--- /dev/null
+++ b/ee/app/views/admin/credentials/_personal_access_tokens.html.haml
@@ -0,0 +1,8 @@
+.table-holder
+  .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
+    .table-section.section-25{ role: 'rowheader' }= _('Owner')
+    .table-section.section-30{ role: 'rowheader' }= _('Scope')
+    .table-section.section-10{ role: 'rowheader' }= _('Created On')
+    .table-section.section-10{ role: 'rowheader' }= _('Expiration')
+
+  = render partial: 'admin/credentials/personal_access_tokens/personal_access_token', collection: credentials
diff --git a/ee/app/views/admin/credentials/_ssh_keys.html.haml b/ee/app/views/admin/credentials/_ssh_keys.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d7a7d608e0c8ccde16ff2b0fab3c536ccd9aacfa
--- /dev/null
+++ b/ee/app/views/admin/credentials/_ssh_keys.html.haml
@@ -0,0 +1,7 @@
+.table-holder
+  .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
+    .table-section.section-40{ role: 'rowheader' }= _('Owner')
+    .table-section.section-15{ role: 'rowheader' }= _('Created On')
+    .table-section.section-15{ role: 'rowheader' }= _('Last Accessed On')
+
+  = render partial: 'admin/credentials/ssh_keys/ssh_key', collection: credentials
diff --git a/ee/app/views/admin/credentials/index.html.haml b/ee/app/views/admin/credentials/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6104fc98687bfb28319dc87ee81ef5c264c481a8
--- /dev/null
+++ b/ee/app/views/admin/credentials/index.html.haml
@@ -0,0 +1,25 @@
+- page_title "Credentials"
+
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+  .fade-left
+    = icon('angle-left')
+  .fade-right
+    = icon('angle-right')
+  %ul.nav-links.nav.nav-tabs.scrolling-tabs
+    = nav_link(html_options: { class: active_when(show_personal_access_tokens?) }) do
+      = link_to admin_credentials_path(filter: 'personal_access_tokens') do
+        = s_('AdminCredentials|Personal Access Tokens')
+    = nav_link(html_options: { class: active_when(show_ssh_keys?) }) do
+      = link_to admin_credentials_path(filter: 'ssh_keys') do
+        = s_('AdminCredentials|SSH Keys')
+
+- if @credentials.empty?
+  .nothing-here-block.border-top-0
+    = s_('AdminUsers|No credentials found')
+- else
+  - if show_personal_access_tokens?
+    = render partial: 'admin/credentials/personal_access_tokens', locals: { credentials: @credentials }
+  - elsif show_ssh_keys?
+    = render partial: 'admin/credentials/ssh_keys', locals: { credentials: @credentials }
+
+= paginate_without_count @credentials
diff --git a/ee/app/views/admin/credentials/personal_access_tokens/_personal_access_token.html.haml b/ee/app/views/admin/credentials/personal_access_tokens/_personal_access_token.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b1e1a82e36c69466c9ca40b7d82d8ed7abb85dbb
--- /dev/null
+++ b/ee/app/views/admin/credentials/personal_access_tokens/_personal_access_token.html.haml
@@ -0,0 +1,25 @@
+.gl-responsive-table-row{ role: 'row', data: { qa_selector: 'credentials_personal_access_token_row_content' } }
+  .table-section.section-25
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Owner')
+    .table-mobile-content
+      = render 'admin/users/user_detail', user: personal_access_token.user
+  .table-section.section-30
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Scope')
+    .table-mobile-content
+      - scopes = personal_access_token.scopes
+      = scopes.present? ? scopes.join(", ") : _('No Scopes')
+  .table-section.section-10
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Created On')
+    .table-mobile-content
+      = personal_access_token.created_at.to_date
+  .table-section.section-10
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Expiration')
+    .table-mobile-content
+      - if personal_access_token.expires?
+        = personal_access_token.expires_at
+      - else
+        = _('Never')
diff --git a/ee/app/views/admin/credentials/ssh_keys/_ssh_key.html.haml b/ee/app/views/admin/credentials/ssh_keys/_ssh_key.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..fe0be36500896bfdd7e35743d3754d5965e836ab
--- /dev/null
+++ b/ee/app/views/admin/credentials/ssh_keys/_ssh_key.html.haml
@@ -0,0 +1,16 @@
+.gl-responsive-table-row{ role: 'row', data: { qa_selector: 'credentials_ssh_key_row_content' } }
+  .table-section.section-40
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Owner')
+    .table-mobile-content
+      = render 'admin/users/user_detail', user: ssh_key.user
+  .table-section.section-15
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Created On')
+    .table-mobile-content
+      = ssh_key.created_at.to_date
+  .table-section.section-15
+    .table-mobile-header{ role: 'rowheader' }
+      = _('Last Accessed On')
+    .table-mobile-content
+      = (last_used_at = ssh_key.last_used_at).present? ? last_used_at.to_date : _('Never')
diff --git a/ee/app/views/layouts/nav/sidebar/_credentials_link.html.haml b/ee/app/views/layouts/nav/sidebar/_credentials_link.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..25acdc66299d159cc26d77476c0e83f03a7d1c46
--- /dev/null
+++ b/ee/app/views/layouts/nav/sidebar/_credentials_link.html.haml
@@ -0,0 +1,12 @@
+- if credentials_inventory_feature_available?
+  = nav_link(controller: :credentials) do
+    = link_to admin_credentials_path do
+      .nav-icon-container
+        = sprite_icon('lock')
+      %span.nav-item-name
+        = _('Credentials')
+    %ul.sidebar-sub-level-items.is-fly-out-only
+      = nav_link(controller: :credentials, html_options: { class: "fly-out-top-item" } ) do
+        = link_to admin_credentials_path do
+          %strong.fly-out-top-item-name
+            = _('Credentials')
diff --git a/ee/changelogs/unreleased/36742-pat-ssh-inventory-mvc.yml b/ee/changelogs/unreleased/36742-pat-ssh-inventory-mvc.yml
new file mode 100644
index 0000000000000000000000000000000000000000..77eb5b345f52656482779a3bd01e1dfed65aa4f7
--- /dev/null
+++ b/ee/changelogs/unreleased/36742-pat-ssh-inventory-mvc.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce Credentials Inventory
+merge_request: 20912
+author:
+type: added
diff --git a/ee/config/routes/admin.rb b/ee/config/routes/admin.rb
index fba5a82b00203684cfaca51cb7cf71686b113e5b..bd8174c56076334ef9399b365c7792d417a0b41c 100644
--- a/ee/config/routes/admin.rb
+++ b/ee/config/routes/admin.rb
@@ -20,6 +20,7 @@
   resource :push_rule, only: [:show, :update]
   resource :email, only: [:show, :create]
   resources :audit_logs, controller: 'audit_logs', only: [:index]
+  resources :credentials, only: [:index]
 
   resource :license, only: [:show, :new, :create, :destroy] do
     get :download, on: :member
diff --git a/ee/spec/controllers/admin/credentials_controller_spec.rb b/ee/spec/controllers/admin/credentials_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2ab732cfc6d2480caac22ee063117531ba900f44
--- /dev/null
+++ b/ee/spec/controllers/admin/credentials_controller_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::CredentialsController do
+  describe 'GET #index' do
+    context 'admin user' do
+      before do
+        sign_in(create(:admin))
+      end
+
+      context 'when `credentials_inventory` feature is enabled' do
+        before do
+          stub_licensed_features(credentials_inventory: true)
+        end
+
+        it 'responds with 200' do
+          get :index
+
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        describe 'filtering by type of credential' do
+          let_it_be(:personal_access_tokens) { create_list(:personal_access_token, 2) }
+
+          shared_examples_for 'filtering by `personal_access_tokens`' do
+            it do
+              get :index, params: params
+
+              expect(assigns(:credentials)).to match_array(personal_access_tokens)
+            end
+          end
+
+          context 'no credential type specified' do
+            let(:params) { {} }
+
+            it_behaves_like 'filtering by `personal_access_tokens`'
+          end
+
+          context 'non-existent credential type specified' do
+            let(:params) { { filter: 'non_existent_credential_type' } }
+
+            it_behaves_like 'filtering by `personal_access_tokens`'
+          end
+
+          context 'credential type specified as `personal_access_tokens`' do
+            let(:params) { { filter: 'personal_access_tokens' } }
+
+            it_behaves_like 'filtering by `personal_access_tokens`'
+          end
+
+          context 'credential type specified as `ssh_keys`' do
+            it 'filters by ssh keys' do
+              ssh_keys =  create_list(:personal_key, 2)
+
+              get :index, params: { filter: 'ssh_keys' }
+
+              expect(assigns(:credentials)).to match_array(ssh_keys)
+            end
+          end
+        end
+      end
+
+      context 'when `credentials_inventory` feature is disabled' do
+        before do
+          stub_licensed_features(credentials_inventory: false)
+        end
+
+        it 'returns 404' do
+          get :index
+
+          expect(response).to have_gitlab_http_status(404)
+        end
+      end
+    end
+
+    context 'non-admin user' do
+      before do
+        sign_in(create(:user))
+      end
+
+      it 'returns 404' do
+        get :index
+
+        expect(response).to have_gitlab_http_status(404)
+      end
+    end
+  end
+end
diff --git a/ee/spec/helpers/admin/credentials_helper_spec.rb b/ee/spec/helpers/admin/credentials_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a6487850706c64fb1e010722a10ddcf55548a1af
--- /dev/null
+++ b/ee/spec/helpers/admin/credentials_helper_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::CredentialsHelper do
+  let(:filter) { nil }
+
+  before do
+    controller.params[:filter] = filter
+  end
+
+  describe '#credentials_inventory_feature_available?' do
+    subject { credentials_inventory_feature_available? }
+
+    context 'when credentials inventory feature is enabled' do
+      before do
+        stub_licensed_features(credentials_inventory: true)
+      end
+
+      it { is_expected.to be_truthy }
+    end
+
+    context 'when credentials inventory feature is disabled' do
+      before do
+        stub_licensed_features(credentials_inventory: false)
+      end
+
+      it { is_expected.to be_falsey }
+    end
+  end
+
+  describe '#show_ssh_keys?' do
+    subject { show_ssh_keys? }
+
+    context 'when filtering by ssh_keys' do
+      let(:filter) { 'ssh_keys' }
+
+      it { is_expected.to be_truthy }
+    end
+
+    context 'when filtering by a different, existent credential type' do
+      let(:filter) { 'personal_access_tokens' }
+
+      it { is_expected.to be_falsey }
+    end
+
+    context 'when filtering by a different, non-existent credential type' do
+      let(:filter) { 'non-existent-filter' }
+
+      it { is_expected.to be_falsey }
+    end
+  end
+
+  describe '#show_personal_access_tokens?' do
+    subject { show_personal_access_tokens? }
+
+    context 'when filtering by personal_access_tokens' do
+      let(:filter) { 'personal_access_tokens' }
+
+      it { is_expected.to be_truthy }
+    end
+
+    context 'when filtering by a different, existent credential type' do
+      let(:filter) { 'ssh_keys' }
+
+      it { is_expected.to be_falsey }
+    end
+
+    context 'when filtering by a different, non-existent credential type' do
+      let(:filter) { 'non-existent-filter' }
+
+      it { is_expected.to be_truthy }
+    end
+  end
+end
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index 8f2fd8cbae26ed5e43878e6795aa80848937981b..af73fd4f0dc96da0320b0ff17569bcdb1bf4f107 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -26,7 +26,9 @@ class Keys < Grape::API
       get do
         authenticated_with_full_private_access!
 
-        key = KeysFinder.new(current_user, params).execute
+        finder_params = params.merge(key_type: 'ssh')
+
+        key = KeysFinder.new(current_user, finder_params).execute
 
         not_found!('Key') unless key
         present key, with: Entities::SSHKeyWithUser, current_user: current_user
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 86f838f0eb987d0f4c82234edff378fd6a6c736c..006e5df4d7e70f18546caa48eca28d8ea700a060 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1184,6 +1184,12 @@ msgstr ""
 msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
 msgstr ""
 
+msgid "AdminCredentials|Personal Access Tokens"
+msgstr ""
+
+msgid "AdminCredentials|SSH Keys"
+msgstr ""
+
 msgid "AdminDashboard|Error loading the statistics. Please try again"
 msgstr ""
 
@@ -1328,6 +1334,9 @@ msgstr ""
 msgid "AdminUsers|New user"
 msgstr ""
 
+msgid "AdminUsers|No credentials found"
+msgstr ""
+
 msgid "AdminUsers|No users found"
 msgstr ""
 
@@ -5117,6 +5126,9 @@ msgstr ""
 msgid "Created At"
 msgstr ""
 
+msgid "Created On"
+msgstr ""
+
 msgid "Created a branch and a merge request to resolve this issue."
 msgstr ""
 
@@ -5165,6 +5177,9 @@ msgstr ""
 msgid "Creation date"
 msgstr ""
 
+msgid "Credentials"
+msgstr ""
+
 msgid "Critical vulnerabilities present"
 msgstr ""
 
@@ -7208,6 +7223,9 @@ msgstr ""
 msgid "Expand up"
 msgstr ""
 
+msgid "Expiration"
+msgstr ""
+
 msgid "Expiration date"
 msgstr ""
 
@@ -10237,6 +10255,9 @@ msgstr[1] ""
 msgid "Last %{days} days"
 msgstr ""
 
+msgid "Last Accessed On"
+msgstr ""
+
 msgid "Last Pipeline"
 msgstr ""
 
@@ -11721,6 +11742,9 @@ msgstr ""
 msgid "No Milestone"
 msgstr ""
 
+msgid "No Scopes"
+msgstr ""
+
 msgid "No Tag"
 msgstr ""
 
diff --git a/spec/finders/keys_finder_spec.rb b/spec/finders/keys_finder_spec.rb
index 147e6ee3d844e489e07e8c5d2bd5aca7d25ed678..f80abdcdb386be8fdfc9ba105ea004e4fba7a538 100644
--- a/spec/finders/keys_finder_spec.rb
+++ b/spec/finders/keys_finder_spec.rb
@@ -3,74 +3,145 @@
 require 'spec_helper'
 
 describe KeysFinder do
-  subject(:keys_finder) { described_class.new(user, params) }
+  subject { described_class.new(user, params).execute }
 
   let(:user) { create(:user) }
-  let(:fingerprint_type) { 'md5' }
-  let(:fingerprint) { 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1' }
-
-  let(:params) do
-    {
-      type: fingerprint_type,
-      fingerprint: fingerprint
-    }
-  end
+  let(:params) { {} }
 
-  let!(:key) do
-    create(:key, user: user,
+  let!(:key_1) do
+    create(:personal_key,
+      last_used_at: 7.days.ago,
+      user: user,
       key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
       fingerprint: 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1',
-      fingerprint_sha256: 'nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
-    )
+      fingerprint_sha256: 'nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg')
   end
 
+  let!(:key_2) { create(:personal_key, last_used_at: nil, user: user) }
+  let!(:key_3) { create(:personal_key, last_used_at: 2.days.ago) }
+
   context 'with a regular user' do
     it 'raises GitLabAccessDeniedError' do
-      expect do
-        keys_finder.execute
-      end.to raise_error(KeysFinder::GitLabAccessDeniedError)
+      expect { subject }.to raise_error(KeysFinder::GitLabAccessDeniedError)
     end
   end
 
   context 'with an admin user' do
     let(:user) {create(:admin)}
 
-    context 'with invalid MD5 fingerprint' do
-      let(:fingerprint) { '11:11:11:11' }
+    context 'key_type' do
+      let!(:deploy_key) { create(:deploy_key) }
 
-      it 'raises InvalidFingerprint' do
-        expect { keys_finder.execute }
-          .to raise_error(KeysFinder::InvalidFingerprint)
-      end
-    end
+      context 'when `key_type` is `ssh`' do
+        before do
+          params[:key_type] = 'ssh'
+        end
 
-    context 'with invalid SHA fingerprint' do
-      let(:fingerprint_type) { 'sha256' }
-      let(:fingerprint) { 'nUhzNyftwAAKs7HufskYTte2g' }
+        it 'returns only SSH keys' do
+          expect(subject).to contain_exactly(key_1, key_2, key_3)
+        end
+      end
 
-      it 'raises InvalidFingerprint' do
-        expect { keys_finder.execute }
-          .to raise_error(KeysFinder::InvalidFingerprint)
+      context 'when `key_type` is not specified' do
+        it 'returns all types of keys' do
+          expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
+        end
       end
     end
 
-    context 'with valid MD5 params' do
-      it 'returns key if the fingerprint is found' do
-        result = keys_finder.execute
+    context 'fingerprint' do
+      context 'with invalid fingerprint' do
+        context 'with invalid MD5 fingerprint' do
+          before do
+            params[:fingerprint] = '11:11:11:11'
+          end
+
+          it 'raises InvalidFingerprint' do
+            expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
+          end
+        end
+
+        context 'with invalid SHA fingerprint' do
+          before do
+            params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
+          end
+
+          it 'raises InvalidFingerprint' do
+            expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
+          end
+        end
+      end
 
-        expect(result).to eq(key)
-        expect(key.user).to eq(user)
+      context 'with valid fingerprints' do
+        context 'with valid MD5 params' do
+          context 'with an existent fingerprint' do
+            before do
+              params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
+            end
+
+            it 'returns the key' do
+              expect(subject).to eq(key_1)
+              expect(subject.user).to eq(user)
+            end
+          end
+
+          context 'with a non-existent fingerprint' do
+            before do
+              params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
+            end
+
+            it 'returns nil' do
+              expect(subject).to be_nil
+            end
+          end
+        end
+
+        context 'with valid SHA256 params' do
+          context 'with an existent fingerprint' do
+            before do
+              params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
+            end
+
+            it 'returns key' do
+              expect(subject).to eq(key_1)
+              expect(subject.user).to eq(user)
+            end
+          end
+
+          context 'with a non-existent fingerprint' do
+            before do
+              params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
+            end
+
+            it 'returns nil' do
+              expect(subject).to be_nil
+            end
+          end
+        end
       end
     end
 
-    context 'with valid SHA256 params' do
-      let(:fingerprint) { 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1' }
+    context 'user' do
+      context 'without user' do
+        it 'contains ssh_keys of all users in the system' do
+          expect(subject).to contain_exactly(key_1, key_2, key_3)
+        end
+      end
+
+      context 'with user' do
+        before do
+          params[:user] = user
+        end
 
-      it 'returns key if the fingerprint is found' do
-        result = keys_finder.execute
+        it 'contains ssh_keys of only the specified users' do
+          expect(subject).to contain_exactly(key_1, key_2)
+        end
+      end
+    end
 
-        expect(result).to eq(key)
-        expect(key.user).to eq(user)
+    context 'sort order' do
+      it 'sorts in last_used_at_desc order' do
+        expect(subject).to eq([key_3, key_1, key_2])
       end
     end
   end
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index a44daf585ba7dcfa42245425d873e3fa165a8781..ce8ef80bb998b35aea4c1083d77ec5e8811bcf79 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -26,6 +26,16 @@ def finder(options = {})
           revoked_impersonation_token, expired_impersonation_token)
       end
 
+      describe 'with sort order' do
+        before do
+          params[:sort] = 'id_asc'
+        end
+
+        it 'sorts records as per the specified sort order' do
+          expect(subject).to match_array(PersonalAccessToken.all.order(id: :asc))
+        end
+      end
+
       describe 'without impersonation' do
         before do
           params[:impersonation] = false
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 559dc95768a1526b83fb9c8e66160de0d4c96545..2dd9583087f7ed99e4b2d2a6a123379a2b0535d0 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -50,6 +50,32 @@
     end
   end
 
+  describe 'scopes' do
+    describe '.for_user' do
+      let(:user_1) { create(:user) }
+      let(:key_of_user_1) { create(:personal_key, user: user_1) }
+
+      before do
+        create_list(:personal_key, 2, user: create(:user))
+      end
+
+      it 'returns keys of the specified user only' do
+        expect(described_class.for_user(user_1)).to contain_exactly(key_of_user_1)
+      end
+    end
+
+    describe '.order_last_used_at_desc' do
+      it 'sorts by last_used_at descending, with null values at last' do
+        key_1 = create(:personal_key, last_used_at: 7.days.ago)
+        key_2 = create(:personal_key, last_used_at: nil)
+        key_3 = create(:personal_key, last_used_at: 2.days.ago)
+
+        expect(described_class.order_last_used_at_desc)
+          .to eq([key_3, key_1, key_2])
+      end
+    end
+  end
+
   context "validation of uniqueness (based on fingerprint uniqueness)" do
     let(:user) { create(:user) }
 
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index aaf9ecb80895e3de6357e6aa90f5f9eb889ec3d3..b16d1f58be515e925ab9f0046ede66dd7c1a848b 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -21,6 +21,18 @@
     end
   end
 
+  describe 'scopes' do
+    describe '.for_user' do
+      it 'returns personal access tokens of specified user only' do
+        user_1 = create(:user)
+        token_of_user_1 = create(:personal_access_token, user: user_1)
+        create_list(:personal_access_token, 2)
+
+        expect(described_class.for_user(user_1)).to contain_exactly(token_of_user_1)
+      end
+    end
+  end
+
   describe ".active?" do
     let(:active_personal_access_token) { build(:personal_access_token) }
     let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) }