From 179df0626724dc0695eb6a3b4c074e1cb8b0abe5 Mon Sep 17 00:00:00 2001
From: LP B <xAt0mZ@users.noreply.github.com>
Date: Wed, 14 Jul 2021 11:15:21 +0200
Subject: [PATCH] feat(app): rework private registries and support private
 registries in kubernetes EE-30 (#5131)

* feat(app): rework private registries and support private registries in kubernetes

[EE-30]

feat(api): backport private registries backend changes (#5072)

* feat(api/bolt): backport bolt changes

* feat(api/exec): backport exec changes

* feat(api/http): backport http/handler/dockerhub changes

* feat(api/http): backport http/handler/endpoints changes

* feat(api/http): backport http/handler/registries changes

* feat(api/http): backport http/handler/stacks changes

* feat(api/http): backport http/handler changes

* feat(api/http): backport http/proxy/factory/azure changes

* feat(api/http): backport http/proxy/factory/docker changes

* feat(api/http): backport http/proxy/factory/utils changes

* feat(api/http): backport http/proxy/factory/kubernetes changes

* feat(api/http): backport http/proxy/factory changes

* feat(api/http): backport http/security changes

* feat(api/http): backport http changes

* feat(api/internal): backport internal changes

* feat(api): backport api changes

* feat(api/kubernetes): backport kubernetes changes

* fix(api/http): changes on backend following backport

feat(app): backport private registries frontend changes (#5056)

* feat(app/docker): backport docker/components changes

* feat(app/docker): backport docker/helpers changes

* feat(app/docker): backport docker/views/container changes

* feat(app/docker): backport docker/views/images changes

* feat(app/docker): backport docker/views/registries changes

* feat(app/docker): backport docker/views/services changes

* feat(app/docker): backport docker changes

* feat(app/kubernetes): backport kubernetes/components changes

* feat(app/kubernetes): backport kubernetes/converters changes

* feat(app/kubernetes): backport kubernetes/models changes

* feat(app/kubernetes): backport kubernetes/registries changes

* feat(app/kubernetes): backport kubernetes/services changes

* feat(app/kubernetes): backport kubernetes/views/applications changes

* feat(app/kubernetes): backport kubernetes/views/configurations changes

* feat(app/kubernetes): backport kubernetes/views/configure changes

* feat(app/kubernetes): backport kubernetes/views/resource-pools changes

* feat(app/kubernetes): backport kubernetes/views changes

* feat(app/portainer): backport portainer/components/accessManagement changes

* feat(app/portainer): backport portainer/components/datatables changes

* feat(app/portainer): backport portainer/components/forms changes

* feat(app/portainer): backport portainer/components/registry-details changes

* feat(app/portainer): backport portainer/models changes

* feat(app/portainer): backport portainer/rest changes

* feat(app/portainer): backport portainer/services changes

* feat(app/portainer): backport portainer/views changes

* feat(app/portainer): backport portainer changes

* feat(app): backport app changes

* config(project): gitignore + jsconfig changes

gitignore all files under api/cmd/portainer but main.go and enable Code Editor autocomplete on import ... from '@/...'

fix(app): fix pull rate limit checker

fix(app/registries): sidebar menus and registry accesses users filtering

fix(api): add missing kube client factory

fix(kube): fetch dockerhub pull limits (#5133)

fix(app): pre review fixes (#5142)

* fix(app/registries): remove checkbox for endpointRegistries view

* fix(endpoints): allow access to default namespace

* fix(docker): fetch pull limits

* fix(kube/ns): show selected registries for non admin

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>

chore(webpack): ignore missing sourcemaps

fix(registries): fetch registry config from url

feat(kube/registries): ignore not found when deleting secret

feat(db): move migration to db 31

fix(registries): fix bugs in PR EE-869 (#5169)

* fix(registries): hide role

* fix(endpoints): set empty access policy to edge endpoint

* fix(registry): remove double arguments

* fix(admin): ignore warning

* feat(kube/configurations): tag registry secrets (#5157)

* feat(kube/configurations): tag registry secrets

* feat(kube/secrets): show registry secrets for admins

* fix(registries): move dockerhub to beginning

* refactor(registries): use endpoint scoped registries

feat(registries): filter by namespace if supplied

feat(access-managment): filter users for registry (#5191)

* refactor(access-manage): move users selector to component

* feat(access-managment): filter users for registry

refactor(registries): sync code with CE (#5200)

* refactor(registry): add inspect handler under endpoints

* refactor(endpoint): sync endpoint_registries_list

* refactor(endpoints): sync registry_access

* fix(db): rename migration functions

* fix(registries): show accesses for admin

* fix(kube): set token on transport

* refactor(kube): move secret help to bottom

* fix(kuberentes): remove shouldLog parameter

* style(auth): add description of security.IsAdmin

* feat(security): allow admin access to registry

* feat(edge): connect to edge endpoint when creating client

* style(portainer): change deprecation version

* refactor(sidebar): hide manage

* refactor(containers): revert changes

* style(container): remove whitespace

* fix(endpoint): add handler to registy on endpointService

* refactor(image): use endpointService.registries

* fix(kueb/namespaces): rename resource pool to namespace

* fix(kube/namespace): move selected registries

* fix(api/registries): hide accesses on registry creation

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>

refactor(api): remove code duplication after rebase

fix(app/registries): replace last registry api usage by endpoint registry api

fix(api/endpoints): update registry access policies on endpoint deletion (#5226)

[EE-1027]

fix(db): update db version

* fix(dockerhub): fetch rate limits

* fix(registry/tests): supply restricred context

* fix(registries): show proget registry only when selected

* fix(registry): create dockerhub registry

* feat(db): move migrations to db 32

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
---
 api/bolt/datastore.go                         |   1 +
 api/bolt/init.go                              |  16 -
 api/bolt/log/log.go                           |  41 ++
 api/bolt/log/log.test.go                      |   1 +
 api/bolt/migrator/migrate_dbversion32.go      | 124 ++++
 api/bolt/migrator/migrator.go                 |  15 +
 api/bolt/services.go                          |   5 -
 api/cmd/portainer/main.go                     |   6 +-
 api/exec/swarm_stack.go                       |   9 +-
 api/go.mod                                    |   1 +
 .../handler/dockerhub/dockerhub_inspect.go    |  28 -
 .../handler/dockerhub/dockerhub_update.go     |  68 --
 api/http/handler/dockerhub/handler.go         |  33 -
 api/http/handler/endpoints/endpoint_create.go |   4 +-
 api/http/handler/endpoints/endpoint_delete.go |  16 +
 .../endpoints/endpoint_dockerhub_status.go    |  31 +-
 .../endpoints/endpoint_registries_inspect.go  |  50 ++
 .../endpoints/endpoint_registries_list.go     | 130 ++++
 .../endpoints/endpoint_registry_access.go     | 149 +++++
 api/http/handler/endpoints/handler.go         |  10 +-
 api/http/handler/handler.go                   |   6 -
 api/http/handler/registries/handler.go        |  32 +-
 .../handler/registries/registry_configure.go  |  14 +-
 .../handler/registries/registry_create.go     |  53 +-
 .../registries/registry_create_test.go        |   9 +
 .../handler/registries/registry_delete.go     |  10 +
 .../handler/registries/registry_inspect.go    |   9 +-
 api/http/handler/registries/registry_list.go  |  22 +-
 .../handler/registries/registry_update.go     | 113 ++--
 .../registries/registry_update_test.go        |   9 +
 .../handler/stacks/create_compose_stack.go    |  15 +-
 api/http/handler/stacks/create_swarm_stack.go |  15 +-
 .../proxy/factory/azure/containergroup.go     |  18 +-
 .../proxy/factory/azure/containergroups.go    |   8 +-
 .../proxy/factory/docker/access_control.go    |  10 +-
 api/http/proxy/factory/docker/configs.go      |  12 +-
 api/http/proxy/factory/docker/containers.go   |  14 +-
 api/http/proxy/factory/docker/networks.go     |  10 +-
 api/http/proxy/factory/docker/registry.go     |  24 +-
 api/http/proxy/factory/docker/secrets.go      |  12 +-
 api/http/proxy/factory/docker/services.go     |  12 +-
 api/http/proxy/factory/docker/swarm.go        |   6 +-
 api/http/proxy/factory/docker/tasks.go        |  12 +-
 api/http/proxy/factory/docker/transport.go    |  55 +-
 api/http/proxy/factory/docker/volumes.go      |  14 +-
 api/http/proxy/factory/kubernetes.go          |   6 +-
 .../factory/kubernetes/agent_transport.go     |  60 ++
 .../factory/kubernetes/edge_transport.go      |  57 ++
 .../factory/kubernetes/local_transport.go     |  45 ++
 .../proxy/factory/kubernetes/namespaces.go    |  45 ++
 api/http/proxy/factory/kubernetes/secrets.go  | 170 +++++
 .../proxy/factory/kubernetes/transport.go     | 236 ++++---
 api/http/proxy/factory/responseutils/json.go  |  11 -
 api/http/proxy/factory/utils/json.go          |  91 +++
 api/http/proxy/factory/utils/request.go       |  45 ++
 .../{responseutils => utils}/response.go      |  68 +-
 api/http/security/authorization.go            |  35 +-
 api/http/security/bouncer.go                  |  27 +-
 api/http/security/context.go                  |   6 +-
 api/http/security/filter.go                   |  19 +-
 api/http/server.go                            |   7 +-
 api/internal/authorization/authorizations.go  |   7 +-
 api/internal/endpoint/endpoint.go             |   4 +-
 api/internal/testhelpers/datastore.go         |   2 -
 api/kubernetes/cli/access.go                  |  44 +-
 api/kubernetes/cli/client.go                  |  29 +-
 api/kubernetes/cli/registries.go              |  96 +++
 api/kubernetes/privateregistries/labels.go    |   5 +
 api/portainer.go                              |  37 +-
 app/agent/rest/dockerhub.js                   |   4 +-
 app/constants.js                              |   1 -
 app/docker/__module.js                        |  22 +
 .../dockerSidebarContent.html                 |  24 +-
 ...r-image-registry-rate-limits.controller.js |  38 +-
 .../por-image-registry-rate-limits.html       |   2 +-
 .../por-image-registry-rate-limits.js         |   2 +
 .../por-image-registry.controller.js          |  67 +-
 .../imageRegistry/por-image-registry.html     |   8 +-
 .../imageRegistry/por-image-registry.js       |   2 +-
 app/docker/helpers/imageHelper.js             | 140 +++--
 .../create/createContainerController.js       |   2 +-
 .../containers/create/createcontainer.html    |   3 +-
 .../views/containers/edit/container.html      |   2 +-
 .../containers/edit/containerController.js    |   5 +-
 app/docker/views/images/edit/image.html       |   2 +-
 .../views/images/edit/imageController.js      |  67 +-
 app/docker/views/images/images.html           |   1 -
 .../registries/access/registryAccess.html     |  16 +
 .../views/registries/access/registryAccess.js |   7 +
 .../access/registryAccessController.js        |  67 ++
 .../views/services/edit/serviceController.js  |   6 +-
 .../editEdgeGroupViewController.js            |   3 +-
 app/kubernetes/__module.js                    |  26 +-
 .../configurationsDatatableController.js      |   2 +-
 .../kubernetesSidebarContent.html             |  14 +-
 app/kubernetes/converters/application.js      |  11 +-
 app/kubernetes/converters/configuration.js    |   1 +
 app/kubernetes/converters/daemonSet.js        |  12 +-
 app/kubernetes/converters/deployment.js       |   8 +-
 app/kubernetes/converters/resourcePool.js     |  11 +-
 app/kubernetes/converters/secret.js           |   3 +
 app/kubernetes/converters/statefulSet.js      |  10 +-
 .../models/application/formValues.js          |  55 +-
 app/kubernetes/models/daemon-set/models.js    |   2 +-
 app/kubernetes/models/deployment/models.js    |   2 +-
 .../models/resource-pool/formValues.js        |  14 +-
 app/kubernetes/models/stateful-set/models.js  |   2 +-
 app/kubernetes/registries/index.js            |   5 +
 .../kube-registry-access-view/index.js        |   9 +
 .../kube-registry-access-view.controller.js   |  70 +++
 .../kube-registry-access-view.html            |  72 +++
 .../services/resourcePoolService.js           |  24 +-
 app/kubernetes/services/statefulSetService.js |   1 +
 .../create/createApplication.html             |  99 ++-
 .../applications/create/createApplication.js  |   1 -
 .../create/createApplicationController.js     | 142 ++---
 .../configurations/edit/configuration.html    |   5 +-
 .../edit/configurationController.js           |  21 +-
 .../views/configure/configureController.js    |  11 +-
 .../create/createResourcePool.html            | 122 +++-
 .../create/createResourcePool.js              |   9 +-
 .../create/createResourcePoolController.js    | 196 +++---
 .../resource-pools/edit/resourcePool.html     |  55 ++
 .../views/resource-pools/edit/resourcePool.js |   2 +-
 .../edit/resourcePoolController.js            | 241 ++++----
 app/portainer/__module.js                     |  17 +-
 .../components/accessManagement/index.js      |   9 +
 .../index.js                                  |   7 +
 .../por-access-management-users-selector.html |  23 +
 .../accessManagement/por-access-management.js |   5 +-
 .../accessManagement/porAccessManagement.html |  26 +-
 .../porAccessManagementController.js          |   6 +
 .../registriesDatatable.html                  |  31 +-
 .../registriesDatatable.js                    |   5 +-
 .../registriesDatatableController.js          |  85 +++
 .../datatables/strings-datatable/index.js     |  20 +
 .../strings-datatable/strings-datatable.html  |  65 ++
 .../registry-form-dockerhub.html              |  64 ++
 .../registry-form-dockerhub.js                |   9 +
 .../forms/template-form/template-form.js      |  13 -
 .../forms/template-form/templateForm.html     | 580 ------------------
 .../template-form/templateFormController.js   |  55 --
 app/portainer/components/index.js             |   3 +-
 .../components/registry-details/index.js      |  10 +
 .../registry-details/registry-details.html    |  25 +
 app/portainer/helpers/endpointHelper.js       |  57 +-
 app/portainer/models/dockerhub.js             |  13 +-
 app/portainer/models/registry.js              |   7 +-
 app/portainer/models/registryTypes.js         |   2 +
 app/portainer/rest/dockerhub.js               |  15 -
 app/portainer/rest/endpoint.js                |  21 +-
 app/portainer/rest/registry.js                |   3 +-
 app/portainer/services/api/accessService.js   |  15 +-
 .../services/api/dockerhubService.js          |  68 +-
 app/portainer/services/api/endpointService.js |  15 +
 app/portainer/services/api/registryService.js | 105 ++--
 app/portainer/services/api/templateService.js | 152 +++--
 .../views/endpoint-registries/registries.html |  21 +
 .../views/endpoint-registries/registries.js   |   7 +
 .../registriesController.js                   |  51 ++
 .../views/endpoints/endpointsController.js    |   3 +-
 app/portainer/views/home/homeController.js    |   3 +-
 .../views/init/admin/initAdminController.js   |   7 +-
 .../registries/access/registryAccess.html     |  35 --
 .../access/registryAccessController.js        |  34 -
 ...reateregistry.html => createRegistry.html} |  86 ++-
 .../views/registries/create/createRegistry.js |  10 +
 .../create/createRegistryController.js        | 209 +++----
 .../registries/edit/registryController.js     |   6 +-
 .../views/registries/registries.html          |  74 ---
 .../views/registries/registriesController.js  |  35 +-
 app/portainer/views/sidebar/sidebar.html      |   4 +-
 .../views/sidebar/sidebarController.js        |  25 +-
 jsconfig.json                                 |   3 +-
 webpack/webpack.common.js                     |   4 +-
 175 files changed, 3757 insertions(+), 2544 deletions(-)
 create mode 100644 api/bolt/log/log.go
 create mode 100644 api/bolt/log/log.test.go
 create mode 100644 api/bolt/migrator/migrate_dbversion32.go
 delete mode 100644 api/http/handler/dockerhub/dockerhub_inspect.go
 delete mode 100644 api/http/handler/dockerhub/dockerhub_update.go
 delete mode 100644 api/http/handler/dockerhub/handler.go
 create mode 100644 api/http/handler/endpoints/endpoint_registries_inspect.go
 create mode 100644 api/http/handler/endpoints/endpoint_registries_list.go
 create mode 100644 api/http/handler/endpoints/endpoint_registry_access.go
 create mode 100644 api/http/proxy/factory/kubernetes/agent_transport.go
 create mode 100644 api/http/proxy/factory/kubernetes/edge_transport.go
 create mode 100644 api/http/proxy/factory/kubernetes/local_transport.go
 create mode 100644 api/http/proxy/factory/kubernetes/namespaces.go
 create mode 100644 api/http/proxy/factory/kubernetes/secrets.go
 delete mode 100644 api/http/proxy/factory/responseutils/json.go
 create mode 100644 api/http/proxy/factory/utils/json.go
 create mode 100644 api/http/proxy/factory/utils/request.go
 rename api/http/proxy/factory/{responseutils => utils}/response.go (62%)
 create mode 100644 api/kubernetes/cli/registries.go
 create mode 100644 api/kubernetes/privateregistries/labels.go
 create mode 100644 app/docker/views/registries/access/registryAccess.html
 create mode 100644 app/docker/views/registries/access/registryAccess.js
 create mode 100644 app/docker/views/registries/access/registryAccessController.js
 create mode 100644 app/kubernetes/registries/index.js
 create mode 100644 app/kubernetes/registries/kube-registry-access-view/index.js
 create mode 100644 app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js
 create mode 100644 app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html
 create mode 100644 app/portainer/components/accessManagement/index.js
 create mode 100644 app/portainer/components/accessManagement/por-access-management-users-selector/index.js
 create mode 100644 app/portainer/components/accessManagement/por-access-management-users-selector/por-access-management-users-selector.html
 create mode 100644 app/portainer/components/datatables/registries-datatable/registriesDatatableController.js
 create mode 100644 app/portainer/components/datatables/strings-datatable/index.js
 create mode 100644 app/portainer/components/datatables/strings-datatable/strings-datatable.html
 create mode 100644 app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html
 create mode 100644 app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js
 delete mode 100644 app/portainer/components/forms/template-form/template-form.js
 delete mode 100644 app/portainer/components/forms/template-form/templateForm.html
 delete mode 100644 app/portainer/components/forms/template-form/templateFormController.js
 create mode 100644 app/portainer/components/registry-details/index.js
 create mode 100644 app/portainer/components/registry-details/registry-details.html
 delete mode 100644 app/portainer/rest/dockerhub.js
 create mode 100644 app/portainer/views/endpoint-registries/registries.html
 create mode 100644 app/portainer/views/endpoint-registries/registries.js
 create mode 100644 app/portainer/views/endpoint-registries/registriesController.js
 delete mode 100644 app/portainer/views/registries/access/registryAccess.html
 delete mode 100644 app/portainer/views/registries/access/registryAccessController.js
 rename app/portainer/views/registries/create/{createregistry.html => createRegistry.html} (52%)
 create mode 100644 app/portainer/views/registries/create/createRegistry.js

diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go
index b0904102c..09fcf89c7 100644
--- a/api/bolt/datastore.go
+++ b/api/bolt/datastore.go
@@ -169,6 +169,7 @@ func (store *Store) MigrateData(force bool) error {
 			UserService:             store.UserService,
 			VersionService:          store.VersionService,
 			FileService:             store.fileService,
+			DockerhubService:        store.DockerHubService,
 			AuthorizationService:    authorization.NewService(store),
 		}
 		migrator := migrator.NewMigrator(migratorParams)
diff --git a/api/bolt/init.go b/api/bolt/init.go
index 7ce23f138..a91a6b2e6 100644
--- a/api/bolt/init.go
+++ b/api/bolt/init.go
@@ -55,22 +55,6 @@ func (store *Store) Init() error {
 		return err
 	}
 
-	_, err = store.DockerHubService.DockerHub()
-	if err == errors.ErrObjectNotFound {
-		defaultDockerHub := &portainer.DockerHub{
-			Authentication: false,
-			Username:       "",
-			Password:       "",
-		}
-
-		err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
-		if err != nil {
-			return err
-		}
-	} else if err != nil {
-		return err
-	}
-
 	groups, err := store.EndpointGroupService.EndpointGroups()
 	if err != nil {
 		return err
diff --git a/api/bolt/log/log.go b/api/bolt/log/log.go
new file mode 100644
index 000000000..5ae90946a
--- /dev/null
+++ b/api/bolt/log/log.go
@@ -0,0 +1,41 @@
+package log
+
+import (
+	"fmt"
+	"log"
+)
+
+const (
+	INFO  = "INFO"
+	ERROR = "ERROR"
+	DEBUG = "DEBUG"
+	FATAL = "FATAL"
+)
+
+type ScopedLog struct {
+	scope string
+}
+
+func NewScopedLog(scope string) *ScopedLog {
+	return &ScopedLog{scope: scope}
+}
+
+func (slog *ScopedLog) print(kind string, message string) {
+	log.Printf("[%s] [%s] %s", kind, slog.scope, message)
+}
+
+func (slog *ScopedLog) Debug(message string) {
+	slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
+}
+
+func (slog *ScopedLog) Info(message string) {
+	slog.print(INFO, fmt.Sprintf("[message: %s]", message))
+}
+
+func (slog *ScopedLog) Error(message string, err error) {
+	slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
+}
+
+func (slog *ScopedLog) NotImplemented(method string) {
+	log.Fatalf("[%s] [%s] [%s]", FATAL, slog.scope, fmt.Sprintf("%s is not yet implemented", method))
+}
diff --git a/api/bolt/log/log.test.go b/api/bolt/log/log.test.go
new file mode 100644
index 000000000..7330d5405
--- /dev/null
+++ b/api/bolt/log/log.test.go
@@ -0,0 +1 @@
+package log
diff --git a/api/bolt/migrator/migrate_dbversion32.go b/api/bolt/migrator/migrate_dbversion32.go
new file mode 100644
index 000000000..3d800bd36
--- /dev/null
+++ b/api/bolt/migrator/migrate_dbversion32.go
@@ -0,0 +1,124 @@
+package migrator
+
+import (
+	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/bolt/errors"
+)
+
+func (m *Migrator) migrateDBVersionTo32() error {
+	err := m.updateRegistriesToDB32()
+	if err != nil {
+		return err
+	}
+
+	err = m.updateDockerhubToDB32()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *Migrator) updateRegistriesToDB32() error {
+	registries, err := m.registryService.Registries()
+	if err != nil {
+		return err
+	}
+
+	endpoints, err := m.endpointService.Endpoints()
+	if err != nil {
+		return err
+	}
+
+	for _, registry := range registries {
+
+		registry.RegistryAccesses = portainer.RegistryAccesses{}
+
+		for _, endpoint := range endpoints {
+
+			filteredUserAccessPolicies := portainer.UserAccessPolicies{}
+			for userId, registryPolicy := range registry.UserAccessPolicies {
+				if _, found := endpoint.UserAccessPolicies[userId]; found {
+					filteredUserAccessPolicies[userId] = registryPolicy
+				}
+			}
+
+			filteredTeamAccessPolicies := portainer.TeamAccessPolicies{}
+			for teamId, registryPolicy := range registry.TeamAccessPolicies {
+				if _, found := endpoint.TeamAccessPolicies[teamId]; found {
+					filteredTeamAccessPolicies[teamId] = registryPolicy
+				}
+			}
+
+			registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
+				UserAccessPolicies: filteredUserAccessPolicies,
+				TeamAccessPolicies: filteredTeamAccessPolicies,
+				Namespaces:         []string{},
+			}
+		}
+		m.registryService.UpdateRegistry(registry.ID, &registry)
+	}
+	return nil
+}
+
+func (m *Migrator) updateDockerhubToDB32() error {
+	dockerhub, err := m.dockerhubService.DockerHub()
+	if err == errors.ErrObjectNotFound {
+		return nil
+	} else if err != nil {
+		return err
+	}
+
+	if !dockerhub.Authentication {
+		return nil
+	}
+
+	registry := &portainer.Registry{
+		Type:             portainer.DockerHubRegistry,
+		Name:             "Dockerhub (authenticated - migrated)",
+		URL:              "docker.io",
+		Authentication:   true,
+		Username:         dockerhub.Username,
+		Password:         dockerhub.Password,
+		RegistryAccesses: portainer.RegistryAccesses{},
+	}
+
+	endpoints, err := m.endpointService.Endpoints()
+	if err != nil {
+		return err
+	}
+
+	for _, endpoint := range endpoints {
+
+		if endpoint.Type != portainer.KubernetesLocalEnvironment &&
+			endpoint.Type != portainer.AgentOnKubernetesEnvironment &&
+			endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
+
+			userAccessPolicies := portainer.UserAccessPolicies{}
+			for userId := range endpoint.UserAccessPolicies {
+				if _, found := endpoint.UserAccessPolicies[userId]; found {
+					userAccessPolicies[userId] = portainer.AccessPolicy{
+						RoleID: 0,
+					}
+				}
+			}
+
+			teamAccessPolicies := portainer.TeamAccessPolicies{}
+			for teamId := range endpoint.TeamAccessPolicies {
+				if _, found := endpoint.TeamAccessPolicies[teamId]; found {
+					teamAccessPolicies[teamId] = portainer.AccessPolicy{
+						RoleID: 0,
+					}
+				}
+			}
+
+			registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
+				UserAccessPolicies: userAccessPolicies,
+				TeamAccessPolicies: teamAccessPolicies,
+				Namespaces:         []string{},
+			}
+		}
+	}
+
+	return m.registryService.CreateRegistry(registry)
+}
diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go
index 8d99b5bfa..daae8b184 100644
--- a/api/bolt/migrator/migrator.go
+++ b/api/bolt/migrator/migrator.go
@@ -3,10 +3,12 @@ package migrator
 import (
 	"github.com/boltdb/bolt"
 	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/bolt/dockerhub"
 	"github.com/portainer/portainer/api/bolt/endpoint"
 	"github.com/portainer/portainer/api/bolt/endpointgroup"
 	"github.com/portainer/portainer/api/bolt/endpointrelation"
 	"github.com/portainer/portainer/api/bolt/extension"
+	plog "github.com/portainer/portainer/api/bolt/log"
 	"github.com/portainer/portainer/api/bolt/registry"
 	"github.com/portainer/portainer/api/bolt/resourcecontrol"
 	"github.com/portainer/portainer/api/bolt/role"
@@ -20,6 +22,8 @@ import (
 	"github.com/portainer/portainer/api/internal/authorization"
 )
 
+var migrateLog = plog.NewScopedLog("bolt, migrate")
+
 type (
 	// Migrator defines a service to migrate data after a Portainer version update.
 	Migrator struct {
@@ -41,6 +45,7 @@ type (
 		versionService          *version.Service
 		fileService             portainer.FileService
 		authorizationService    *authorization.Service
+		dockerhubService        *dockerhub.Service
 	}
 
 	// Parameters represents the required parameters to create a new Migrator instance.
@@ -63,6 +68,7 @@ type (
 		VersionService          *version.Service
 		FileService             portainer.FileService
 		AuthorizationService    *authorization.Service
+		DockerhubService        *dockerhub.Service
 	}
 )
 
@@ -87,6 +93,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
 		versionService:          parameters.VersionService,
 		fileService:             parameters.FileService,
 		authorizationService:    parameters.AuthorizationService,
+		dockerhubService:        parameters.DockerhubService,
 	}
 }
 
@@ -366,5 +373,13 @@ func (m *Migrator) Migrate() error {
 		}
 	}
 
+	// Portainer 2.9.0
+	if m.currentDBVersion < 32 {
+		err := m.migrateDBVersionTo32()
+		if err != nil {
+			return err
+		}
+	}
+
 	return m.versionService.StoreDBVersion(portainer.DBVersion)
 }
diff --git a/api/bolt/services.go b/api/bolt/services.go
index 4cdc84069..ec4c8ecc6 100644
--- a/api/bolt/services.go
+++ b/api/bolt/services.go
@@ -167,11 +167,6 @@ func (store *Store) CustomTemplate() portainer.CustomTemplateService {
 	return store.CustomTemplateService
 }
 
-// DockerHub gives access to the DockerHub data management layer
-func (store *Store) DockerHub() portainer.DockerHubService {
-	return store.DockerHubService
-}
-
 // EdgeGroup gives access to the EdgeGroup data management layer
 func (store *Store) EdgeGroup() portainer.EdgeGroupService {
 	return store.EdgeGroupService
diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go
index a852abd56..d056f9908 100644
--- a/api/cmd/portainer/main.go
+++ b/api/cmd/portainer/main.go
@@ -134,8 +134,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
 	return docker.NewClientFactory(signatureService, reverseTunnelService)
 }
 
-func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *kubecli.ClientFactory {
-	return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
+func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore portainer.DataStore) *kubecli.ClientFactory {
+	return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
 }
 
 func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
@@ -382,7 +382,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
 	}
 
 	dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
-	kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
+	kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
 
 	snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
 	if err != nil {
diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go
index cf59f7607..faf6cf723 100644
--- a/api/exec/swarm_stack.go
+++ b/api/exec/swarm_stack.go
@@ -10,7 +10,7 @@ import (
 	"path"
 	"runtime"
 
-	"github.com/portainer/portainer/api"
+	portainer "github.com/portainer/portainer/api"
 )
 
 // SwarmStackManager represents a service for managing stacks.
@@ -42,7 +42,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
 }
 
 // Login executes the docker login command against a list of registries (including DockerHub).
-func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
+func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
 	command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
 	for _, registry := range registries {
 		if registry.Authentication {
@@ -50,11 +50,6 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri
 			runCommandAndCaptureStdErr(command, registryArgs, nil, "")
 		}
 	}
-
-	if dockerhub.Authentication {
-		dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
-		runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
-	}
 }
 
 // Logout executes the docker logout command.
diff --git a/api/go.mod b/api/go.mod
index bfafa47fa..2f4e2e7b2 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -34,6 +34,7 @@ require (
 	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
 	gopkg.in/alecthomas/kingpin.v2 v2.2.6
+	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
 	k8s.io/api v0.17.2
 	k8s.io/apimachinery v0.17.2
 	k8s.io/client-go v0.17.2
diff --git a/api/http/handler/dockerhub/dockerhub_inspect.go b/api/http/handler/dockerhub/dockerhub_inspect.go
deleted file mode 100644
index e7dc713f8..000000000
--- a/api/http/handler/dockerhub/dockerhub_inspect.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package dockerhub
-
-import (
-	"net/http"
-
-	httperror "github.com/portainer/libhttp/error"
-	"github.com/portainer/libhttp/response"
-)
-
-// @id DockerHubInspect
-// @summary Retrieve DockerHub information
-// @description Use this endpoint to retrieve the information used to connect to the DockerHub
-// @description **Access policy**: authenticated
-// @tags dockerhub
-// @security jwt
-// @produce json
-// @success 200 {object} portainer.DockerHub
-// @failure 500 "Server error"
-// @router /dockerhub [get]
-func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
-	dockerhub, err := handler.DataStore.DockerHub().DockerHub()
-	if err != nil {
-		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
-	}
-
-	hideFields(dockerhub)
-	return response.JSON(w, dockerhub)
-}
diff --git a/api/http/handler/dockerhub/dockerhub_update.go b/api/http/handler/dockerhub/dockerhub_update.go
deleted file mode 100644
index 536b84420..000000000
--- a/api/http/handler/dockerhub/dockerhub_update.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package dockerhub
-
-import (
-	"errors"
-	"net/http"
-
-	"github.com/asaskevich/govalidator"
-	httperror "github.com/portainer/libhttp/error"
-	"github.com/portainer/libhttp/request"
-	"github.com/portainer/libhttp/response"
-	portainer "github.com/portainer/portainer/api"
-)
-
-type dockerhubUpdatePayload struct {
-	// Enable authentication against DockerHub
-	Authentication bool `validate:"required" example:"false"`
-	// Username used to authenticate against the DockerHub
-	Username string `validate:"required" example:"hub_user"`
-	// Password used to authenticate against the DockerHub
-	Password string `validate:"required" example:"hub_password"`
-}
-
-func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error {
-	if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
-		return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
-	}
-	return nil
-}
-
-// @id DockerHubUpdate
-// @summary Update DockerHub information
-// @description Use this endpoint to update the information used to connect to the DockerHub
-// @description **Access policy**: administrator
-// @tags dockerhub
-// @security jwt
-// @accept json
-// @produce json
-// @param body body dockerhubUpdatePayload true "DockerHub information"
-// @success 204 "Success"
-// @failure 400 "Invalid request"
-// @failure 500 "Server error"
-// @router /dockerhub [put]
-func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
-	var payload dockerhubUpdatePayload
-	err := request.DecodeAndValidateJSONPayload(r, &payload)
-	if err != nil {
-		return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
-	}
-
-	dockerhub := &portainer.DockerHub{
-		Authentication: false,
-		Username:       "",
-		Password:       "",
-	}
-
-	if payload.Authentication {
-		dockerhub.Authentication = true
-		dockerhub.Username = payload.Username
-		dockerhub.Password = payload.Password
-	}
-
-	err = handler.DataStore.DockerHub().UpdateDockerHub(dockerhub)
-	if err != nil {
-		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err}
-	}
-
-	return response.Empty(w)
-}
diff --git a/api/http/handler/dockerhub/handler.go b/api/http/handler/dockerhub/handler.go
deleted file mode 100644
index f1328acb8..000000000
--- a/api/http/handler/dockerhub/handler.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package dockerhub
-
-import (
-	"net/http"
-
-	"github.com/gorilla/mux"
-	httperror "github.com/portainer/libhttp/error"
-	"github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/security"
-)
-
-func hideFields(dockerHub *portainer.DockerHub) {
-	dockerHub.Password = ""
-}
-
-// Handler is the HTTP handler used to handle DockerHub operations.
-type Handler struct {
-	*mux.Router
-	DataStore portainer.DataStore
-}
-
-// NewHandler creates a handler to manage Dockerhub operations.
-func NewHandler(bouncer *security.RequestBouncer) *Handler {
-	h := &Handler{
-		Router: mux.NewRouter(),
-	}
-	h.Handle("/dockerhub",
-		bouncer.RestrictedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
-	h.Handle("/dockerhub",
-		bouncer.AdminAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
-
-	return h
-}
diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go
index 946fa7d7e..50e462869 100644
--- a/api/http/handler/endpoints/endpoint_create.go
+++ b/api/http/handler/endpoints/endpoint_create.go
@@ -322,8 +322,8 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
 		TLSConfig: portainer.TLSConfiguration{
 			TLS: false,
 		},
-		AuthorizedUsers:     []portainer.UserID{},
-		AuthorizedTeams:     []portainer.TeamID{},
+		UserAccessPolicies:  portainer.UserAccessPolicies{},
+		TeamAccessPolicies:  portainer.TeamAccessPolicies{},
 		Extensions:          []portainer.EndpointExtension{},
 		TagIDs:              payload.TagIDs,
 		Status:              portainer.EndpointStatusUp,
diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go
index 9fda9b74c..a2dcf1924 100644
--- a/api/http/handler/endpoints/endpoint_delete.go
+++ b/api/http/handler/endpoints/endpoint_delete.go
@@ -103,6 +103,22 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
 		}
 	}
 
+	registries, err := handler.DataStore.Registry().Registries()
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
+	}
+
+	for idx := range registries {
+		registry := &registries[idx]
+		if _, ok := registry.RegistryAccesses[endpoint.ID]; ok {
+			delete(registry.RegistryAccesses, endpoint.ID)
+			err = handler.DataStore.Registry().UpdateRegistry(registry.ID, registry)
+			if err != nil {
+				return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update registry accesses", Err: err}
+			}
+		}
+	}
+
 	return response.Empty(w)
 }
 
diff --git a/api/http/handler/endpoints/endpoint_dockerhub_status.go b/api/http/handler/endpoints/endpoint_dockerhub_status.go
index 793b85715..92cfbef9c 100644
--- a/api/http/handler/endpoints/endpoint_dockerhub_status.go
+++ b/api/http/handler/endpoints/endpoint_dockerhub_status.go
@@ -22,7 +22,7 @@ type dockerhubStatusResponse struct {
 	Limit     int `json:"limit"`
 }
 
-// GET request on /api/endpoints/{id}/dockerhub/status
+// GET request on /api/endpoints/{id}/dockerhub/{registryId}
 func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
 	endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
 	if err != nil {
@@ -40,13 +40,30 @@ func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.R
 		return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment type", errors.New("Invalid environment type")}
 	}
 
-	dockerhub, err := handler.DataStore.DockerHub().DockerHub()
+	registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
 	if err != nil {
-		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
+		return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
+	}
+
+	var registry *portainer.Registry
+
+	if registryID == 0 {
+		registry = &portainer.Registry{}
+	} else {
+		registry, err = handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
+		if err == bolterrors.ErrObjectNotFound {
+			return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
+		} else if err != nil {
+			return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
+		}
+
+		if registry.Type != portainer.DockerHubRegistry {
+			return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry type", errors.New("Invalid registry type")}
+		}
 	}
 
 	httpClient := client.NewHTTPClient()
-	token, err := getDockerHubToken(httpClient, dockerhub)
+	token, err := getDockerHubToken(httpClient, registry)
 	if err != nil {
 		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub token from DockerHub", err}
 	}
@@ -59,7 +76,7 @@ func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.R
 	return response.JSON(w, resp)
 }
 
-func getDockerHubToken(httpClient *client.HTTPClient, dockerhub *portainer.DockerHub) (string, error) {
+func getDockerHubToken(httpClient *client.HTTPClient, registry *portainer.Registry) (string, error) {
 	type dockerhubTokenResponse struct {
 		Token string `json:"token"`
 	}
@@ -71,8 +88,8 @@ func getDockerHubToken(httpClient *client.HTTPClient, dockerhub *portainer.Docke
 		return "", err
 	}
 
-	if dockerhub.Authentication {
-		req.SetBasicAuth(dockerhub.Username, dockerhub.Password)
+	if registry.Authentication {
+		req.SetBasicAuth(registry.Username, registry.Password)
 	}
 
 	resp, err := httpClient.Do(req)
diff --git a/api/http/handler/endpoints/endpoint_registries_inspect.go b/api/http/handler/endpoints/endpoint_registries_inspect.go
new file mode 100644
index 000000000..405fe9495
--- /dev/null
+++ b/api/http/handler/endpoints/endpoint_registries_inspect.go
@@ -0,0 +1,50 @@
+package endpoints
+
+import (
+	"net/http"
+
+	httperror "github.com/portainer/libhttp/error"
+	"github.com/portainer/libhttp/request"
+	"github.com/portainer/libhttp/response"
+	portainer "github.com/portainer/portainer/api"
+	bolterrors "github.com/portainer/portainer/api/bolt/errors"
+	httperrors "github.com/portainer/portainer/api/http/errors"
+	"github.com/portainer/portainer/api/http/security"
+)
+
+// GET request on /endpoints/{id}/registries/{registryId}
+func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
+	}
+
+	registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid registry identifier route variable", Err: err}
+	}
+
+	registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
+	if err == bolterrors.ErrObjectNotFound {
+		return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a registry with the specified identifier inside the database", Err: err}
+	} else if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a registry with the specified identifier inside the database", Err: err}
+	}
+
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
+	}
+
+	user, err := handler.DataStore.User().User(securityContext.UserID)
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user from the database", Err: err}
+	}
+
+	if !security.AuthorizedRegistryAccess(registry, user, securityContext.UserMemberships, portainer.EndpointID(endpointID)) {
+		return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
+	}
+
+	hideRegistryFields(registry, !securityContext.IsAdmin)
+	return response.JSON(w, registry)
+}
diff --git a/api/http/handler/endpoints/endpoint_registries_list.go b/api/http/handler/endpoints/endpoint_registries_list.go
new file mode 100644
index 000000000..93233fada
--- /dev/null
+++ b/api/http/handler/endpoints/endpoint_registries_list.go
@@ -0,0 +1,130 @@
+package endpoints
+
+import (
+	"net/http"
+
+	"github.com/pkg/errors"
+	httperror "github.com/portainer/libhttp/error"
+	"github.com/portainer/libhttp/request"
+	"github.com/portainer/libhttp/response"
+	portainer "github.com/portainer/portainer/api"
+	bolterrors "github.com/portainer/portainer/api/bolt/errors"
+	"github.com/portainer/portainer/api/http/security"
+	endpointutils "github.com/portainer/portainer/api/internal/endpoint"
+)
+
+// GET request on /endpoints/{id}/registries?namespace
+func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+
+	user, err := handler.DataStore.User().User(securityContext.UserID)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user from the database", err}
+	}
+
+	endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
+	}
+
+	endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
+	if err == bolterrors.ErrObjectNotFound {
+		return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
+	} else if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
+	}
+
+	isAdmin := securityContext.IsAdmin
+
+	registries, err := handler.DataStore.Registry().Registries()
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
+	}
+
+	if endpointutils.IsKubernetesEndpoint(endpoint) {
+		namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
+
+		if namespace == "" && !isAdmin {
+			return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Missing namespace query parameter", Err: errors.New("missing namespace query parameter")}
+		}
+
+		if namespace != "" {
+
+			authorized, err := handler.isNamespaceAuthorized(endpoint, namespace, user.ID, securityContext.UserMemberships, isAdmin)
+			if err != nil {
+				return &httperror.HandlerError{http.StatusNotFound, "Unable to check for namespace authorization", err}
+			}
+
+			if !authorized {
+				return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized to use namespace", Err: errors.New("user is not authorized to use namespace")}
+			}
+
+			registries = filterRegistriesByNamespace(registries, endpoint.ID, namespace)
+		}
+
+	} else if !isAdmin {
+		registries = security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
+	}
+
+	for idx := range registries {
+		hideRegistryFields(&registries[idx], !isAdmin)
+	}
+
+	return response.JSON(w, registries)
+}
+
+func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, namespace string, userId portainer.UserID, memberships []portainer.TeamMembership, isAdmin bool) (bool, error) {
+	if isAdmin || namespace == "" {
+		return true, nil
+	}
+
+	if namespace == "default" {
+		return true, nil
+	}
+
+	kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
+	if err != nil {
+		return false, errors.Wrap(err, "unable to retrieve kubernetes client")
+	}
+
+	accessPolicies, err := kcl.GetNamespaceAccessPolicies()
+	if err != nil {
+		return false, errors.Wrap(err, "unable to retrieve endpoint's namespaces policies")
+	}
+
+	namespacePolicy, ok := accessPolicies[namespace]
+	if !ok {
+		return false, nil
+	}
+
+	return security.AuthorizedAccess(userId, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies), nil
+}
+
+func filterRegistriesByNamespace(registries []portainer.Registry, endpointId portainer.EndpointID, namespace string) []portainer.Registry {
+	if namespace == "" {
+		return registries
+	}
+
+	filteredRegistries := []portainer.Registry{}
+
+	for _, registry := range registries {
+		for _, authorizedNamespace := range registry.RegistryAccesses[endpointId].Namespaces {
+			if authorizedNamespace == namespace {
+				filteredRegistries = append(filteredRegistries, registry)
+			}
+		}
+	}
+
+	return filteredRegistries
+}
+
+func hideRegistryFields(registry *portainer.Registry, hideAccesses bool) {
+	registry.Password = ""
+	registry.ManagementConfiguration = nil
+	if hideAccesses {
+		registry.RegistryAccesses = nil
+	}
+}
diff --git a/api/http/handler/endpoints/endpoint_registry_access.go b/api/http/handler/endpoints/endpoint_registry_access.go
new file mode 100644
index 000000000..8dde41d35
--- /dev/null
+++ b/api/http/handler/endpoints/endpoint_registry_access.go
@@ -0,0 +1,149 @@
+package endpoints
+
+import (
+	"net/http"
+
+	httperror "github.com/portainer/libhttp/error"
+	"github.com/portainer/libhttp/request"
+	"github.com/portainer/libhttp/response"
+	portainer "github.com/portainer/portainer/api"
+	bolterrors "github.com/portainer/portainer/api/bolt/errors"
+	"github.com/portainer/portainer/api/http/security"
+)
+
+type registryAccessPayload struct {
+	UserAccessPolicies portainer.UserAccessPolicies
+	TeamAccessPolicies portainer.TeamAccessPolicies
+	Namespaces         []string
+}
+
+func (payload *registryAccessPayload) Validate(r *http.Request) error {
+	return nil
+}
+
+// PUT request on /endpoints/{id}/registries/{registryId}
+func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid endpoint identifier route variable", Err: err}
+	}
+
+	registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid registry identifier route variable", Err: err}
+	}
+
+	endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
+	if err == bolterrors.ErrObjectNotFound {
+		return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
+	} else if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
+	}
+
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+
+	err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
+	}
+
+	if !securityContext.IsAdmin {
+		return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized", Err: err}
+	}
+
+	registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
+	if err == bolterrors.ErrObjectNotFound {
+		return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
+	} else if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
+	}
+
+	var payload registryAccessPayload
+	err = request.DecodeAndValidateJSONPayload(r, &payload)
+	if err != nil {
+		return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
+	}
+
+	if registry.RegistryAccesses == nil {
+		registry.RegistryAccesses = portainer.RegistryAccesses{}
+	}
+
+	if _, ok := registry.RegistryAccesses[endpoint.ID]; !ok {
+		registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{}
+	}
+
+	registryAccess := registry.RegistryAccesses[endpoint.ID]
+
+	if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
+		err := handler.updateKubeAccess(endpoint, registry, registryAccess.Namespaces, payload.Namespaces)
+		if err != nil {
+			return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update kube access policies", Err: err}
+		}
+
+		registryAccess.Namespaces = payload.Namespaces
+	} else {
+		registryAccess.UserAccessPolicies = payload.UserAccessPolicies
+		registryAccess.TeamAccessPolicies = payload.TeamAccessPolicies
+	}
+
+	registry.RegistryAccesses[portainer.EndpointID(endpointID)] = registryAccess
+
+	handler.DataStore.Registry().UpdateRegistry(registry.ID, registry)
+
+	return response.Empty(w)
+}
+
+func (handler *Handler) updateKubeAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, oldNamespaces, newNamespaces []string) error {
+	oldNamespacesSet := toSet(oldNamespaces)
+	newNamespacesSet := toSet(newNamespaces)
+
+	namespacesToRemove := setDifference(oldNamespacesSet, newNamespacesSet)
+	namespacesToAdd := setDifference(newNamespacesSet, oldNamespacesSet)
+
+	cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
+	if err != nil {
+		return err
+	}
+
+	for namespace := range namespacesToRemove {
+		err := cli.DeleteRegistrySecret(registry, namespace)
+		if err != nil {
+			return err
+		}
+	}
+
+	for namespace := range namespacesToAdd {
+		err := cli.CreateRegistrySecret(registry, namespace)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+type stringSet map[string]bool
+
+func toSet(list []string) stringSet {
+	set := stringSet{}
+	for _, el := range list {
+		set[el] = true
+	}
+	return set
+}
+
+// setDifference returns the set difference tagsA - tagsB
+func setDifference(setA stringSet, setB stringSet) stringSet {
+	set := stringSet{}
+
+	for el := range setA {
+		if !setB[el] {
+			set[el] = true
+		}
+	}
+
+	return set
+}
diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go
index cd82092e7..bee2eb1d6 100644
--- a/api/http/handler/endpoints/handler.go
+++ b/api/http/handler/endpoints/handler.go
@@ -6,6 +6,7 @@ import (
 	"github.com/portainer/portainer/api/http/proxy"
 	"github.com/portainer/portainer/api/http/security"
 	"github.com/portainer/portainer/api/internal/authorization"
+	"github.com/portainer/portainer/api/kubernetes/cli"
 
 	"net/http"
 
@@ -28,6 +29,7 @@ type Handler struct {
 	ProxyManager         *proxy.Manager
 	ReverseTunnelService portainer.ReverseTunnelService
 	SnapshotService      portainer.SnapshotService
+	K8sClientFactory     *cli.ClientFactory
 	ComposeStackManager  portainer.ComposeStackManager
 	AuthorizationService *authorization.Service
 }
@@ -53,7 +55,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
 		bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
 	h.Handle("/endpoints/{id}",
 		bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
-	h.Handle("/endpoints/{id}/dockerhub",
+	h.Handle("/endpoints/{id}/dockerhub/{registryId}",
 		bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
 	h.Handle("/endpoints/{id}/extensions",
 		bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
@@ -63,5 +65,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
 		bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
 	h.Handle("/endpoints/{id}/status",
 		bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
+	h.Handle("/endpoints/{id}/registries",
+		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistriesList))).Methods(http.MethodGet)
+	h.Handle("/endpoints/{id}/registries/{registryId}",
+		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryInspect))).Methods(http.MethodGet)
+	h.Handle("/endpoints/{id}/registries/{registryId}",
+		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryAccess))).Methods(http.MethodPut)
 	return h
 }
diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go
index 2942c3a17..f4d1eceea 100644
--- a/api/http/handler/handler.go
+++ b/api/http/handler/handler.go
@@ -7,7 +7,6 @@ import (
 	"github.com/portainer/portainer/api/http/handler/auth"
 	"github.com/portainer/portainer/api/http/handler/backup"
 	"github.com/portainer/portainer/api/http/handler/customtemplates"
-	"github.com/portainer/portainer/api/http/handler/dockerhub"
 	"github.com/portainer/portainer/api/http/handler/edgegroups"
 	"github.com/portainer/portainer/api/http/handler/edgejobs"
 	"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -39,7 +38,6 @@ type Handler struct {
 	AuthHandler            *auth.Handler
 	BackupHandler          *backup.Handler
 	CustomTemplatesHandler *customtemplates.Handler
-	DockerHubHandler       *dockerhub.Handler
 	EdgeGroupsHandler      *edgegroups.Handler
 	EdgeJobsHandler        *edgejobs.Handler
 	EdgeStacksHandler      *edgestacks.Handler
@@ -88,8 +86,6 @@ type Handler struct {
 // @tag.description Authenticate against Portainer HTTP API
 // @tag.name custom_templates
 // @tag.description Manage Custom Templates
-// @tag.name dockerhub
-// @tag.description Manage how Portainer connects to the DockerHub
 // @tag.name edge_groups
 // @tag.description Manage Edge Groups
 // @tag.name edge_jobs
@@ -146,8 +142,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
 	case strings.HasPrefix(r.URL.Path, "/api/restore"):
 		http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
-	case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
-		http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
 	case strings.HasPrefix(r.URL.Path, "/api/custom_templates"):
 		http.StripPrefix("/api", h.CustomTemplatesHandler).ServeHTTP(w, r)
 	case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go
index 8c921c696..e8dbaeefc 100644
--- a/api/http/handler/registries/handler.go
+++ b/api/http/handler/registries/handler.go
@@ -8,20 +8,25 @@ import (
 	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/http/proxy"
 	"github.com/portainer/portainer/api/http/security"
+	"github.com/portainer/portainer/api/kubernetes/cli"
 )
 
-func hideFields(registry *portainer.Registry) {
+func hideFields(registry *portainer.Registry, hideAccesses bool) {
 	registry.Password = ""
 	registry.ManagementConfiguration = nil
+	if hideAccesses {
+		registry.RegistryAccesses = nil
+	}
 }
 
 // Handler is the HTTP handler used to handle registry operations.
 type Handler struct {
 	*mux.Router
-	requestBouncer       *security.RequestBouncer
-	DataStore         portainer.DataStore
-	FileService       portainer.FileService
-	ProxyManager      *proxy.Manager
+	requestBouncer   *security.RequestBouncer
+	DataStore        portainer.DataStore
+	FileService      portainer.FileService
+	ProxyManager     *proxy.Manager
+	K8sClientFactory *cli.ClientFactory
 }
 
 // NewHandler creates a handler to manage registry operations.
@@ -34,8 +39,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
 
 func newHandler(bouncer *security.RequestBouncer) *Handler {
 	return &Handler{
-		Router:               mux.NewRouter(),
-		requestBouncer:       bouncer,
+		Router:         mux.NewRouter(),
+		requestBouncer: bouncer,
 	}
 }
 
@@ -60,4 +65,15 @@ type accessGuard interface {
 	AdminAccess(h http.Handler) http.Handler
 	RestrictedAccess(h http.Handler) http.Handler
 	AuthenticatedAccess(h http.Handler) http.Handler
-}
\ No newline at end of file
+}
+
+func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Registry) bool {
+	hasSameUrl := r1.URL == r2.URL
+	hasSameCredentials := r1.Authentication == r2.Authentication && (!r1.Authentication || (r1.Authentication && r1.Username == r2.Username))
+
+	if r1.Type != portainer.GitlabRegistry || r2.Type != portainer.GitlabRegistry {
+		return hasSameUrl && hasSameCredentials
+	}
+
+	return hasSameUrl && hasSameCredentials && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath
+}
diff --git a/api/http/handler/registries/registry_configure.go b/api/http/handler/registries/registry_configure.go
index 307101177..cc9398a61 100644
--- a/api/http/handler/registries/registry_configure.go
+++ b/api/http/handler/registries/registry_configure.go
@@ -10,6 +10,8 @@ import (
 	"github.com/portainer/libhttp/response"
 	portainer "github.com/portainer/portainer/api"
 	bolterrors "github.com/portainer/portainer/api/bolt/errors"
+	httperrors "github.com/portainer/portainer/api/http/errors"
+	"github.com/portainer/portainer/api/http/security"
 )
 
 type registryConfigurePayload struct {
@@ -93,9 +95,12 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
 // @failure 500 "Server error"
 // @router /registries/{id}/configure [post]
 func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
-	registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
 	if err != nil {
-		return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+	if !securityContext.IsAdmin {
+		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to configure registry", httperrors.ErrResourceAccessDenied}
 	}
 
 	payload := &registryConfigurePayload{}
@@ -104,6 +109,11 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request
 		return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
 	}
 
+	registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
+	if err != nil {
+		return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
+	}
+
 	registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
 	if err == bolterrors.ErrObjectNotFound {
 		return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go
index f724d7e2e..017121887 100644
--- a/api/http/handler/registries/registry_create.go
+++ b/api/http/handler/registries/registry_create.go
@@ -10,13 +10,15 @@ import (
 	"github.com/portainer/libhttp/request"
 	"github.com/portainer/libhttp/response"
 	portainer "github.com/portainer/portainer/api"
+	httperrors "github.com/portainer/portainer/api/http/errors"
+	"github.com/portainer/portainer/api/http/security"
 )
 
 type registryCreatePayload struct {
 	// Name that will be used to identify this registry
 	Name string `example:"my-registry" validate:"required"`
-	// Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry) or 5 (ProGet registry)
-	Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5"`
+	// Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry) or 6 (DockerHub)
+	Type portainer.RegistryType `example:"1" validate:"required" enums:"1,2,3,4,5,6"`
 	// URL or IP address of the Docker registry
 	URL string `example:"registry.mydomain.tld:2375/feed" validate:"required"`
 	// BaseURL required for ProGet registry
@@ -45,9 +47,9 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
 	}
 
 	switch payload.Type {
-	case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry:
+	case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry, portainer.DockerHubRegistry:
 	default:
-		return errors.New("invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry) or 5 (ProGet registry)")
+		return errors.New("invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry) or 6 (DockerHub)")
 	}
 
 	if payload.Type == portainer.ProGetRegistry && payload.BaseURL == "" {
@@ -71,24 +73,41 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
 // @failure 500 "Server error"
 // @router /registries [post]
 func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+	if !securityContext.IsAdmin {
+		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create registry", httperrors.ErrResourceAccessDenied}
+	}
+
 	var payload registryCreatePayload
-	err := request.DecodeAndValidateJSONPayload(r, &payload)
+	err = request.DecodeAndValidateJSONPayload(r, &payload)
 	if err != nil {
 		return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
 	}
 
 	registry := &portainer.Registry{
-		Type:               portainer.RegistryType(payload.Type),
-		Name:               payload.Name,
-		URL:                payload.URL,
-		BaseURL:            payload.BaseURL,
-		Authentication:     payload.Authentication,
-		Username:           payload.Username,
-		Password:           payload.Password,
-		UserAccessPolicies: portainer.UserAccessPolicies{},
-		TeamAccessPolicies: portainer.TeamAccessPolicies{},
-		Gitlab:             payload.Gitlab,
-		Quay:               payload.Quay,
+		Type:             portainer.RegistryType(payload.Type),
+		Name:             payload.Name,
+		URL:              payload.URL,
+		BaseURL:          payload.BaseURL,
+		Authentication:   payload.Authentication,
+		Username:         payload.Username,
+		Password:         payload.Password,
+		RegistryAccesses: portainer.RegistryAccesses{},
+		Gitlab:           payload.Gitlab,
+		Quay:             payload.Quay,
+	}
+
+	registries, err := handler.DataStore.Registry().Registries()
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
+	}
+	for _, r := range registries {
+		if handler.registriesHaveSameURLAndCredentials(&r, registry) {
+			return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
+		}
 	}
 
 	err = handler.DataStore.Registry().CreateRegistry(registry)
@@ -96,6 +115,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
 		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the registry inside the database", err}
 	}
 
-	hideFields(registry)
+	hideFields(registry, true)
 	return response.JSON(w, registry)
 }
diff --git a/api/http/handler/registries/registry_create_test.go b/api/http/handler/registries/registry_create_test.go
index 2e76bce28..722301164 100644
--- a/api/http/handler/registries/registry_create_test.go
+++ b/api/http/handler/registries/registry_create_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/http/security"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -82,6 +83,14 @@ func TestHandler_registryCreate(t *testing.T) {
 	r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payloadBytes))
 	w := httptest.NewRecorder()
 
+	restrictedContext := &security.RestrictedRequestContext{
+		IsAdmin: true,
+		UserID:  portainer.UserID(1),
+	}
+
+	ctx := security.StoreRestrictedRequestContext(r, restrictedContext)
+	r = r.WithContext(ctx)
+
 	registry := portainer.Registry{}
 	handler := Handler{}
 	handler.DataStore = testDataStore{
diff --git a/api/http/handler/registries/registry_delete.go b/api/http/handler/registries/registry_delete.go
index a5cf8d417..d5db6769a 100644
--- a/api/http/handler/registries/registry_delete.go
+++ b/api/http/handler/registries/registry_delete.go
@@ -8,6 +8,8 @@ import (
 	"github.com/portainer/libhttp/response"
 	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/bolt/errors"
+	httperrors "github.com/portainer/portainer/api/http/errors"
+	"github.com/portainer/portainer/api/http/security"
 )
 
 // @id RegistryDelete
@@ -23,6 +25,14 @@ import (
 // @failure 500 "Server error"
 // @router /registries/{id} [delete]
 func (handler *Handler) registryDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+	if !securityContext.IsAdmin {
+		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete registry", httperrors.ErrResourceAccessDenied}
+	}
+
 	registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
 	if err != nil {
 		return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go
index 7803f420d..29b57fbc2 100644
--- a/api/http/handler/registries/registry_inspect.go
+++ b/api/http/handler/registries/registry_inspect.go
@@ -5,7 +5,6 @@ import (
 
 	portainer "github.com/portainer/portainer/api"
 	bolterrors "github.com/portainer/portainer/api/bolt/errors"
-	"github.com/portainer/portainer/api/http/errors"
 
 	httperror "github.com/portainer/libhttp/error"
 	"github.com/portainer/libhttp/request"
@@ -27,6 +26,7 @@ import (
 // @failure 500 "Server error"
 // @router /registries/{id} [get]
 func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+
 	registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
 	if err != nil {
 		return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
@@ -39,11 +39,6 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
 		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
 	}
 
-	err = handler.requestBouncer.RegistryAccess(r, registry)
-	if err != nil {
-		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied}
-	}
-
-	hideFields(registry)
+	hideFields(registry, false)
 	return response.JSON(w, registry)
 }
diff --git a/api/http/handler/registries/registry_list.go b/api/http/handler/registries/registry_list.go
index a387f7f32..8e9519f68 100644
--- a/api/http/handler/registries/registry_list.go
+++ b/api/http/handler/registries/registry_list.go
@@ -5,6 +5,7 @@ import (
 
 	httperror "github.com/portainer/libhttp/error"
 	"github.com/portainer/libhttp/response"
+	httperrors "github.com/portainer/portainer/api/http/errors"
 	"github.com/portainer/portainer/api/http/security"
 )
 
@@ -21,21 +22,18 @@ import (
 // @failure 500 "Server error"
 // @router /registries [get]
 func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+	if !securityContext.IsAdmin {
+		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list registries, use /endpoints/:endpointId/registries route instead", httperrors.ErrResourceAccessDenied}
+	}
+
 	registries, err := handler.DataStore.Registry().Registries()
 	if err != nil {
 		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
 	}
 
-	securityContext, err := security.RetrieveRestrictedRequestContext(r)
-	if err != nil {
-		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
-	}
-
-	filteredRegistries := security.FilterRegistries(registries, securityContext)
-
-	for idx := range filteredRegistries {
-		hideFields(&filteredRegistries[idx])
-	}
-
-	return response.JSON(w, filteredRegistries)
+	return response.JSON(w, registries)
 }
diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go
index 66c6473d7..bf89fab82 100644
--- a/api/http/handler/registries/registry_update.go
+++ b/api/http/handler/registries/registry_update.go
@@ -9,18 +9,25 @@ import (
 	"github.com/portainer/libhttp/response"
 	portainer "github.com/portainer/portainer/api"
 	bolterrors "github.com/portainer/portainer/api/bolt/errors"
+	httperrors "github.com/portainer/portainer/api/http/errors"
+	"github.com/portainer/portainer/api/http/security"
 )
 
 type registryUpdatePayload struct {
-	Name               *string                      `json:",omitempty" example:"my-registry" validate:"required"`
-	URL                *string                      `json:",omitempty" example:"registry.mydomain.tld:2375/feed" validate:"required"`
-	BaseURL            *string                      `json:",omitempty" example:"registry.mydomain.tld:2375"`
-	Authentication     *bool                        `json:",omitempty" example:"false" validate:"required"`
-	Username           *string                      `json:",omitempty" example:"registry_user"`
-	Password           *string                      `json:",omitempty" example:"registry_password"`
-	UserAccessPolicies portainer.UserAccessPolicies `json:",omitempty"`
-	TeamAccessPolicies portainer.TeamAccessPolicies `json:",omitempty"`
-	Quay               *portainer.QuayRegistryData
+	// Name that will be used to identify this registry
+	Name *string `validate:"required" example:"my-registry"`
+	// URL or IP address of the Docker registry
+	URL *string `validate:"required" example:"registry.mydomain.tld:2375"`
+	// BaseURL is used for quay registry
+	BaseURL *string `json:",omitempty" example:"registry.mydomain.tld:2375"`
+	// Is authentication against this registry enabled
+	Authentication *bool `example:"false" validate:"required"`
+	// Username used to authenticate against this registry. Required when Authentication is true
+	Username *string `example:"registry_user"`
+	// Password used to authenticate against this registry. required when Authentication is true
+	Password         *string `example:"registry_password"`
+	RegistryAccesses *portainer.RegistryAccesses
+	Quay             *portainer.QuayRegistryData
 }
 
 func (payload *registryUpdatePayload) Validate(r *http.Request) error {
@@ -44,17 +51,19 @@ func (payload *registryUpdatePayload) Validate(r *http.Request) error {
 // @failure 500 "Server error"
 // @router /registries/{id} [put]
 func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+	securityContext, err := security.RetrieveRestrictedRequestContext(r)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
+	}
+	if !securityContext.IsAdmin {
+		return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update registry", httperrors.ErrResourceAccessDenied}
+	}
+
 	registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
 	if err != nil {
 		return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
 	}
 
-	var payload registryUpdatePayload
-	err = request.DecodeAndValidateJSONPayload(r, &payload)
-	if err != nil {
-		return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
-	}
-
 	registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
 	if err == bolterrors.ErrObjectNotFound {
 		return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
@@ -62,23 +71,17 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
 		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
 	}
 
+	var payload registryUpdatePayload
+	err = request.DecodeAndValidateJSONPayload(r, &payload)
+	if err != nil {
+		return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
+	}
+
 	if payload.Name != nil {
 		registry.Name = *payload.Name
 	}
 
-	if payload.URL != nil {
-		registries, err := handler.DataStore.Registry().Registries()
-		if err != nil {
-			return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
-		}
-		for _, r := range registries {
-			if r.ID != registry.ID && hasSameURL(&r, registry) {
-				return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", errors.New("A registry is already defined for this URL")}
-			}
-		}
-
-		registry.URL = *payload.URL
-	}
+	shouldUpdateSecrets := false
 
 	if registry.Type == portainer.ProGetRegistry && payload.BaseURL != nil {
 		registry.BaseURL = *payload.BaseURL
@@ -87,6 +90,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
 	if payload.Authentication != nil {
 		if *payload.Authentication {
 			registry.Authentication = true
+			shouldUpdateSecrets = shouldUpdateSecrets || (payload.Username != nil && *payload.Username != registry.Username) || (payload.Password != nil && *payload.Password != registry.Password)
 
 			if payload.Username != nil {
 				registry.Username = *payload.Username
@@ -103,12 +107,35 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
 		}
 	}
 
-	if payload.UserAccessPolicies != nil {
-		registry.UserAccessPolicies = payload.UserAccessPolicies
+	if payload.URL != nil {
+		shouldUpdateSecrets = shouldUpdateSecrets || (*payload.URL != registry.URL)
+
+		registry.URL = *payload.URL
+		registries, err := handler.DataStore.Registry().Registries()
+		if err != nil {
+			return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
+		}
+		for _, r := range registries {
+			if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
+				return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials")}
+			}
+		}
 	}
 
-	if payload.TeamAccessPolicies != nil {
-		registry.TeamAccessPolicies = payload.TeamAccessPolicies
+	if shouldUpdateSecrets {
+		for endpointID, endpointAccess := range registry.RegistryAccesses {
+			endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
+			if err != nil {
+				return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
+			}
+
+			if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
+				err = handler.updateEndpointRegistryAccess(endpoint, registry, endpointAccess)
+				if err != nil {
+					return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update access to registry", err}
+				}
+			}
+		}
 	}
 
 	if payload.Quay != nil {
@@ -123,10 +150,24 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
 	return response.JSON(w, registry)
 }
 
-func hasSameURL(r1, r2 *portainer.Registry) bool {
-	if r1.Type != portainer.GitlabRegistry || r2.Type != portainer.GitlabRegistry {
-		return r1.URL == r2.URL
+func (handler *Handler) updateEndpointRegistryAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, endpointAccess portainer.RegistryAccessPolicies) error {
+
+	cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
+	if err != nil {
+		return err
 	}
 
-	return r1.URL == r2.URL && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath
+	for _, namespace := range endpointAccess.Namespaces {
+		err := cli.DeleteRegistrySecret(registry, namespace)
+		if err != nil {
+			return err
+		}
+
+		err = cli.CreateRegistrySecret(registry, namespace)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
diff --git a/api/http/handler/registries/registry_update_test.go b/api/http/handler/registries/registry_update_test.go
index 8e0fdabc7..d2767ff4f 100644
--- a/api/http/handler/registries/registry_update_test.go
+++ b/api/http/handler/registries/registry_update_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/http/security"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -48,6 +49,14 @@ func TestHandler_registryUpdate(t *testing.T) {
 	r := httptest.NewRequest(http.MethodPut, "/registries/5", bytes.NewReader(payloadBytes))
 	w := httptest.NewRecorder()
 
+	restrictedContext := &security.RestrictedRequestContext{
+		IsAdmin: true,
+		UserID:  portainer.UserID(1),
+	}
+
+	ctx := security.StoreRestrictedRequestContext(r, restrictedContext)
+	r = r.WithContext(ctx)
+
 	updatedRegistry := portainer.Registry{}
 	handler := newHandler(nil)
 	handler.initRouter(TestBouncer{})
diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go
index 0a3b8e526..f66c2572d 100644
--- a/api/http/handler/stacks/create_compose_stack.go
+++ b/api/http/handler/stacks/create_compose_stack.go
@@ -290,7 +290,6 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
 type composeStackDeploymentConfig struct {
 	stack      *portainer.Stack
 	endpoint   *portainer.Endpoint
-	dockerhub  *portainer.DockerHub
 	registries []portainer.Registry
 	isAdmin    bool
 	user       *portainer.User
@@ -302,26 +301,20 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
 		return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
 	}
 
-	dockerhub, err := handler.DataStore.DockerHub().DockerHub()
+	user, err := handler.DataStore.User().User(securityContext.UserID)
 	if err != nil {
-		return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve DockerHub details from the database", Err: err}
+		return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
 	}
 
 	registries, err := handler.DataStore.Registry().Registries()
 	if err != nil {
 		return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve registries from the database", Err: err}
 	}
-	filteredRegistries := security.FilterRegistries(registries, securityContext)
-
-	user, err := handler.DataStore.User().User(securityContext.UserID)
-	if err != nil {
-		return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
-	}
+	filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
 
 	config := &composeStackDeploymentConfig{
 		stack:      stack,
 		endpoint:   endpoint,
-		dockerhub:  dockerhub,
 		registries: filteredRegistries,
 		isAdmin:    securityContext.IsAdmin,
 		user:       user,
@@ -366,7 +359,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
 	handler.stackCreationMutex.Lock()
 	defer handler.stackCreationMutex.Unlock()
 
-	handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
+	handler.SwarmStackManager.Login(config.registries, config.endpoint)
 
 	err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
 	if err != nil {
diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go
index 747f1b456..b439addbf 100644
--- a/api/http/handler/stacks/create_swarm_stack.go
+++ b/api/http/handler/stacks/create_swarm_stack.go
@@ -300,7 +300,6 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
 type swarmStackDeploymentConfig struct {
 	stack      *portainer.Stack
 	endpoint   *portainer.Endpoint
-	dockerhub  *portainer.DockerHub
 	registries []portainer.Registry
 	prune      bool
 	isAdmin    bool
@@ -313,26 +312,20 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
 		return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
 	}
 
-	dockerhub, err := handler.DataStore.DockerHub().DockerHub()
+	user, err := handler.DataStore.User().User(securityContext.UserID)
 	if err != nil {
-		return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
+		return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
 	}
 
 	registries, err := handler.DataStore.Registry().Registries()
 	if err != nil {
 		return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
 	}
-	filteredRegistries := security.FilterRegistries(registries, securityContext)
-
-	user, err := handler.DataStore.User().User(securityContext.UserID)
-	if err != nil {
-		return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
-	}
+	filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
 
 	config := &swarmStackDeploymentConfig{
 		stack:      stack,
 		endpoint:   endpoint,
-		dockerhub:  dockerhub,
 		registries: filteredRegistries,
 		prune:      prune,
 		isAdmin:    securityContext.IsAdmin,
@@ -367,7 +360,7 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
 	handler.stackCreationMutex.Lock()
 	defer handler.stackCreationMutex.Unlock()
 
-	handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
+	handler.SwarmStackManager.Login(config.registries, config.endpoint)
 
 	err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
 	if err != nil {
diff --git a/api/http/proxy/factory/azure/containergroup.go b/api/http/proxy/factory/azure/containergroup.go
index 242968de5..99c7b2a88 100644
--- a/api/http/proxy/factory/azure/containergroup.go
+++ b/api/http/proxy/factory/azure/containergroup.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 )
 
 // proxy for /subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*
@@ -49,7 +49,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
 		errObj := map[string]string{
 			"message": "A container instance with the same name already exists inside the selected resource group",
 		}
-		err = responseutils.RewriteResponse(resp, errObj, http.StatusConflict)
+		err = utils.RewriteResponse(resp, errObj, http.StatusConflict)
 		return resp, err
 	}
 
@@ -58,7 +58,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
 		return response, err
 	}
 
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return response, err
 	}
@@ -80,7 +80,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
 
 	responseObject = decorateObject(responseObject, resourceControl)
 
-	err = responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	err = utils.RewriteResponse(response, responseObject, http.StatusOK)
 	if err != nil {
 		return response, err
 	}
@@ -94,7 +94,7 @@ func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request)
 		return response, err
 	}
 
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return nil, err
 	}
@@ -106,7 +106,7 @@ func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request)
 
 	responseObject = transport.decorateContainerGroup(responseObject, context)
 
-	responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	utils.RewriteResponse(response, responseObject, http.StatusOK)
 
 	return response, nil
 }
@@ -118,7 +118,7 @@ func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Reque
 	}
 
 	if !transport.userCanDeleteContainerGroup(request, context) {
-		return responseutils.WriteAccessDeniedResponse()
+		return utils.WriteAccessDeniedResponse()
 	}
 
 	response, err := http.DefaultTransport.RoundTrip(request)
@@ -126,14 +126,14 @@ func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Reque
 		return response, err
 	}
 
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return nil, err
 	}
 
 	transport.removeResourceControl(responseObject, context)
 
-	responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	utils.RewriteResponse(response, responseObject, http.StatusOK)
 
 	return response, nil
 }
diff --git a/api/http/proxy/factory/azure/containergroups.go b/api/http/proxy/factory/azure/containergroups.go
index ccb441b3b..e567ec5b7 100644
--- a/api/http/proxy/factory/azure/containergroups.go
+++ b/api/http/proxy/factory/azure/containergroups.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 )
 
 // proxy for /subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups
@@ -23,7 +23,7 @@ func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request
 		return nil, err
 	}
 
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return nil, err
 	}
@@ -39,10 +39,10 @@ func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request
 		filteredValue := transport.filterContainerGroups(decoratedValue, context)
 		responseObject["value"] = filteredValue
 
-		responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+		utils.RewriteResponse(response, responseObject, http.StatusOK)
 	} else {
 		return nil, fmt.Errorf("The container groups response has no value property")
 	}
 
 	return response, nil
-}
\ No newline at end of file
+}
diff --git a/api/http/proxy/factory/docker/access_control.go b/api/http/proxy/factory/docker/access_control.go
index 7f1cb4157..8db016ed9 100644
--- a/api/http/proxy/factory/docker/access_control.go
+++ b/api/http/proxy/factory/docker/access_control.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/portainer/portainer/api/internal/stackutils"
 
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/internal/authorization"
 
 	portainer "github.com/portainer/portainer/api"
@@ -162,7 +162,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
 		systemResourceControl := findSystemNetworkResourceControl(responseObject)
 		if systemResourceControl != nil {
 			responseObject = decorateObject(responseObject, systemResourceControl)
-			return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+			return utils.RewriteResponse(response, responseObject, http.StatusOK)
 		}
 	}
 
@@ -175,15 +175,15 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
 	}
 
 	if resourceControl == nil && (executor.operationContext.isAdmin) {
-		return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+		return utils.RewriteResponse(response, responseObject, http.StatusOK)
 	}
 
 	if executor.operationContext.isAdmin || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
 		responseObject = decorateObject(responseObject, resourceControl)
-		return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+		return utils.RewriteResponse(response, responseObject, http.StatusOK)
 	}
 
-	return responseutils.RewriteAccessDeniedResponse(response)
+	return utils.RewriteAccessDeniedResponse(response)
 }
 
 func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) {
diff --git a/api/http/proxy/factory/docker/configs.go b/api/http/proxy/factory/docker/configs.go
index 74d10759d..4820b74c6 100644
--- a/api/http/proxy/factory/docker/configs.go
+++ b/api/http/proxy/factory/docker/configs.go
@@ -7,7 +7,7 @@ import (
 	"github.com/docker/docker/client"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
 
@@ -34,7 +34,7 @@ func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, en
 func (transport *Transport) configListOperation(response *http.Response, executor *operationExecutor) error {
 	// ConfigList response is a JSON array
 	// https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
-	responseArray, err := responseutils.GetResponseAsJSONArray(response)
+	responseArray, err := utils.GetResponseAsJSONArray(response)
 	if err != nil {
 		return err
 	}
@@ -50,7 +50,7 @@ func (transport *Transport) configListOperation(response *http.Response, executo
 		return err
 	}
 
-	return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
+	return utils.RewriteResponse(response, responseArray, http.StatusOK)
 }
 
 // configInspectOperation extracts the response as a JSON object, verify that the user
@@ -58,7 +58,7 @@ func (transport *Transport) configListOperation(response *http.Response, executo
 func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error {
 	// ConfigInspect response is a JSON object
 	// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -78,9 +78,9 @@ func (transport *Transport) configInspectOperation(response *http.Response, exec
 // https://docs.docker.com/engine/api/v1.37/#operation/ConfigList
 // https://docs.docker.com/engine/api/v1.37/#operation/ConfigInspect
 func selectorConfigLabels(responseObject map[string]interface{}) map[string]interface{} {
-	secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
+	secretSpec := utils.GetJSONObject(responseObject, "Spec")
 	if secretSpec != nil {
-		secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
+		secretLabelsObject := utils.GetJSONObject(secretSpec, "Labels")
 		return secretLabelsObject
 	}
 	return nil
diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go
index 97108355e..dc92ae379 100644
--- a/api/http/proxy/factory/docker/containers.go
+++ b/api/http/proxy/factory/docker/containers.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/docker/docker/client"
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/http/security"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
@@ -46,7 +46,7 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
 func (transport *Transport) containerListOperation(response *http.Response, executor *operationExecutor) error {
 	// ContainerList response is a JSON array
 	// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
-	responseArray, err := responseutils.GetResponseAsJSONArray(response)
+	responseArray, err := utils.GetResponseAsJSONArray(response)
 	if err != nil {
 		return err
 	}
@@ -69,7 +69,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec
 		}
 	}
 
-	return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
+	return utils.RewriteResponse(response, responseArray, http.StatusOK)
 }
 
 // containerInspectOperation extracts the response as a JSON object, verify that the user
@@ -77,7 +77,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec
 func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error {
 	//ContainerInspect response is a JSON object
 	// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -96,9 +96,9 @@ func (transport *Transport) containerInspectOperation(response *http.Response, e
 // Labels are available under the "Config.Labels" property.
 // API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
 func selectorContainerLabelsFromContainerInspectOperation(responseObject map[string]interface{}) map[string]interface{} {
-	containerConfigObject := responseutils.GetJSONObject(responseObject, "Config")
+	containerConfigObject := utils.GetJSONObject(responseObject, "Config")
 	if containerConfigObject != nil {
-		containerLabelsObject := responseutils.GetJSONObject(containerConfigObject, "Labels")
+		containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels")
 		return containerLabelsObject
 	}
 	return nil
@@ -109,7 +109,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str
 // Labels are available under the "Labels" property.
 // API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
 func selectorContainerLabelsFromContainerListOperation(responseObject map[string]interface{}) map[string]interface{} {
-	containerLabelsObject := responseutils.GetJSONObject(responseObject, "Labels")
+	containerLabelsObject := utils.GetJSONObject(responseObject, "Labels")
 	return containerLabelsObject
 }
 
diff --git a/api/http/proxy/factory/docker/networks.go b/api/http/proxy/factory/docker/networks.go
index b38ce68ec..05df57589 100644
--- a/api/http/proxy/factory/docker/networks.go
+++ b/api/http/proxy/factory/docker/networks.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/docker/docker/client"
 
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
 
@@ -38,7 +38,7 @@ func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, e
 func (transport *Transport) networkListOperation(response *http.Response, executor *operationExecutor) error {
 	// NetworkList response is a JSON array
 	// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
-	responseArray, err := responseutils.GetResponseAsJSONArray(response)
+	responseArray, err := utils.GetResponseAsJSONArray(response)
 	if err != nil {
 		return err
 	}
@@ -54,7 +54,7 @@ func (transport *Transport) networkListOperation(response *http.Response, execut
 		return err
 	}
 
-	return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
+	return utils.RewriteResponse(response, responseArray, http.StatusOK)
 }
 
 // networkInspectOperation extracts the response as a JSON object, verify that the user
@@ -62,7 +62,7 @@ func (transport *Transport) networkListOperation(response *http.Response, execut
 func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error {
 	// NetworkInspect response is a JSON object
 	// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -99,5 +99,5 @@ func findSystemNetworkResourceControl(networkObject map[string]interface{}) *por
 // https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
 // https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
 func selectorNetworkLabels(responseObject map[string]interface{}) map[string]interface{} {
-	return responseutils.GetJSONObject(responseObject, "Labels")
+	return utils.GetJSONObject(responseObject, "Labels")
 }
diff --git a/api/http/proxy/factory/docker/registry.go b/api/http/proxy/factory/docker/registry.go
index c07ebae3d..38f0bd903 100644
--- a/api/http/proxy/factory/docker/registry.go
+++ b/api/http/proxy/factory/docker/registry.go
@@ -1,39 +1,43 @@
 package docker
 
 import (
-	"github.com/portainer/portainer/api"
+	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/http/security"
 )
 
 type (
 	registryAccessContext struct {
 		isAdmin         bool
-		userID          portainer.UserID
+		user            *portainer.User
+		endpointID      portainer.EndpointID
 		teamMemberships []portainer.TeamMembership
 		registries      []portainer.Registry
-		dockerHub       *portainer.DockerHub
 	}
+
 	registryAuthenticationHeader struct {
 		Username      string `json:"username"`
 		Password      string `json:"password"`
 		Serveraddress string `json:"serveraddress"`
 	}
+
+	portainerRegistryAuthenticationHeader struct {
+		RegistryId portainer.RegistryID `json:"registryId"`
+	}
 )
 
-func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
+func createRegistryAuthenticationHeader(registryId portainer.RegistryID, accessContext *registryAccessContext) *registryAuthenticationHeader {
 	var authenticationHeader *registryAuthenticationHeader
 
-	if serverAddress == "" {
+	if registryId == 0 { // dockerhub (anonymous)
 		authenticationHeader = &registryAuthenticationHeader{
-			Username:      accessContext.dockerHub.Username,
-			Password:      accessContext.dockerHub.Password,
 			Serveraddress: "docker.io",
 		}
-	} else {
+	} else { // any "custom" registry
 		var matchingRegistry *portainer.Registry
 		for _, registry := range accessContext.registries {
-			if registry.URL == serverAddress &&
-				(accessContext.isAdmin || (!accessContext.isAdmin && security.AuthorizedRegistryAccess(&registry, accessContext.userID, accessContext.teamMemberships))) {
+			if registry.ID == registryId &&
+				(accessContext.isAdmin ||
+					security.AuthorizedRegistryAccess(&registry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
 				matchingRegistry = &registry
 				break
 			}
diff --git a/api/http/proxy/factory/docker/secrets.go b/api/http/proxy/factory/docker/secrets.go
index 148073c02..6f7c203f8 100644
--- a/api/http/proxy/factory/docker/secrets.go
+++ b/api/http/proxy/factory/docker/secrets.go
@@ -7,7 +7,7 @@ import (
 	"github.com/docker/docker/client"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
 
@@ -34,7 +34,7 @@ func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, en
 func (transport *Transport) secretListOperation(response *http.Response, executor *operationExecutor) error {
 	// SecretList response is a JSON array
 	// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
-	responseArray, err := responseutils.GetResponseAsJSONArray(response)
+	responseArray, err := utils.GetResponseAsJSONArray(response)
 	if err != nil {
 		return err
 	}
@@ -50,7 +50,7 @@ func (transport *Transport) secretListOperation(response *http.Response, executo
 		return err
 	}
 
-	return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
+	return utils.RewriteResponse(response, responseArray, http.StatusOK)
 }
 
 // secretInspectOperation extracts the response as a JSON object, verify that the user
@@ -58,7 +58,7 @@ func (transport *Transport) secretListOperation(response *http.Response, executo
 func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error {
 	// SecretInspect response is a JSON object
 	// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -78,9 +78,9 @@ func (transport *Transport) secretInspectOperation(response *http.Response, exec
 // https://docs.docker.com/engine/api/v1.37/#operation/SecretList
 // https://docs.docker.com/engine/api/v1.37/#operation/SecretInspect
 func selectorSecretLabels(responseObject map[string]interface{}) map[string]interface{} {
-	secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
+	secretSpec := utils.GetJSONObject(responseObject, "Spec")
 	if secretSpec != nil {
-		secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
+		secretLabelsObject := utils.GetJSONObject(secretSpec, "Labels")
 		return secretLabelsObject
 	}
 	return nil
diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go
index 683859f73..205c48c60 100644
--- a/api/http/proxy/factory/docker/services.go
+++ b/api/http/proxy/factory/docker/services.go
@@ -12,7 +12,7 @@ import (
 	"github.com/docker/docker/client"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
 
@@ -39,7 +39,7 @@ func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, e
 func (transport *Transport) serviceListOperation(response *http.Response, executor *operationExecutor) error {
 	// ServiceList response is a JSON array
 	// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
-	responseArray, err := responseutils.GetResponseAsJSONArray(response)
+	responseArray, err := utils.GetResponseAsJSONArray(response)
 	if err != nil {
 		return err
 	}
@@ -55,7 +55,7 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
 		return err
 	}
 
-	return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
+	return utils.RewriteResponse(response, responseArray, http.StatusOK)
 }
 
 // serviceInspectOperation extracts the response as a JSON object, verify that the user
@@ -63,7 +63,7 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
 func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
 	//ServiceInspect response is a JSON object
 	//https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -83,9 +83,9 @@ func (transport *Transport) serviceInspectOperation(response *http.Response, exe
 // https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
 // https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
 func selectorServiceLabels(responseObject map[string]interface{}) map[string]interface{} {
-	serviceSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
+	serviceSpecObject := utils.GetJSONObject(responseObject, "Spec")
 	if serviceSpecObject != nil {
-		return responseutils.GetJSONObject(serviceSpecObject, "Labels")
+		return utils.GetJSONObject(serviceSpecObject, "Labels")
 	}
 	return nil
 }
diff --git a/api/http/proxy/factory/docker/swarm.go b/api/http/proxy/factory/docker/swarm.go
index bc3ff9c4d..be39a4b0f 100644
--- a/api/http/proxy/factory/docker/swarm.go
+++ b/api/http/proxy/factory/docker/swarm.go
@@ -3,7 +3,7 @@ package docker
 import (
 	"net/http"
 
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 )
 
 // swarmInspectOperation extracts the response as a JSON object and rewrites the response based
@@ -11,7 +11,7 @@ import (
 func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
 	// SwarmInspect response is a JSON object
 	// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -21,5 +21,5 @@ func swarmInspectOperation(response *http.Response, executor *operationExecutor)
 		delete(responseObject, "TLSInfo")
 	}
 
-	return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	return utils.RewriteResponse(response, responseObject, http.StatusOK)
 }
diff --git a/api/http/proxy/factory/docker/tasks.go b/api/http/proxy/factory/docker/tasks.go
index ad13398fd..f91c1a81c 100644
--- a/api/http/proxy/factory/docker/tasks.go
+++ b/api/http/proxy/factory/docker/tasks.go
@@ -4,7 +4,7 @@ import (
 	"net/http"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 )
 
 const (
@@ -16,7 +16,7 @@ const (
 func (transport *Transport) taskListOperation(response *http.Response, executor *operationExecutor) error {
 	// TaskList response is a JSON array
 	// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
-	responseArray, err := responseutils.GetResponseAsJSONArray(response)
+	responseArray, err := utils.GetResponseAsJSONArray(response)
 	if err != nil {
 		return err
 	}
@@ -32,18 +32,18 @@ func (transport *Transport) taskListOperation(response *http.Response, executor
 		return err
 	}
 
-	return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
+	return utils.RewriteResponse(response, responseArray, http.StatusOK)
 }
 
 // selectorServiceLabels retrieve the labels object associated to the task object.
 // Labels are available under the "Spec.ContainerSpec.Labels" property.
 // API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
 func selectorTaskLabels(responseObject map[string]interface{}) map[string]interface{} {
-	taskSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
+	taskSpecObject := utils.GetJSONObject(responseObject, "Spec")
 	if taskSpecObject != nil {
-		containerSpecObject := responseutils.GetJSONObject(taskSpecObject, "ContainerSpec")
+		containerSpecObject := utils.GetJSONObject(taskSpecObject, "ContainerSpec")
 		if containerSpecObject != nil {
-			return responseutils.GetJSONObject(containerSpecObject, "Labels")
+			return utils.GetJSONObject(containerSpecObject, "Labels")
 		}
 	}
 	return nil
diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go
index daeebdc4b..501a521fa 100644
--- a/api/http/proxy/factory/docker/transport.go
+++ b/api/http/proxy/factory/docker/transport.go
@@ -5,17 +5,19 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"log"
 	"net/http"
 	"path"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/docker/docker/client"
 	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/docker"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/http/security"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
@@ -169,12 +171,31 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
 		// volume browser request
 		return transport.restrictedResourceOperation(r, resourceID, volumeName, portainer.VolumeResourceControl, true)
 	case strings.HasPrefix(requestPath, "/dockerhub"):
-		dockerhub, err := transport.dataStore.DockerHub().DockerHub()
+		requestPath, registryIdString := path.Split(r.URL.Path)
+
+		registryID, err := strconv.Atoi(registryIdString)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("missing registry id: %w", err)
 		}
 
-		newBody, err := json.Marshal(dockerhub)
+		r.URL.Path = strings.TrimSuffix(requestPath, "/")
+
+		registry := &portainer.Registry{
+			Type: portainer.DockerHubRegistry,
+		}
+
+		if registryID != 0 {
+			registry, err = transport.dataStore.Registry().Registry(portainer.RegistryID(registryID))
+			if err != nil {
+				return nil, fmt.Errorf("failed fetching registry: %w", err)
+			}
+		}
+
+		if registry.Type != portainer.DockerHubRegistry {
+			return nil, errors.New("Invalid registry type")
+		}
+
+		newBody, err := json.Marshal(registry)
 		if err != nil {
 			return nil, err
 		}
@@ -397,13 +418,13 @@ func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Re
 			return nil, err
 		}
 
-		var originalHeaderData registryAuthenticationHeader
+		var originalHeaderData portainerRegistryAuthenticationHeader
 		err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
 		if err != nil {
 			return nil, err
 		}
 
-		authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext)
+		authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.RegistryId, accessContext)
 
 		headerData, err := json.Marshal(authenticationHeader)
 		if err != nil {
@@ -433,7 +454,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
 			}
 
 			if !securitySettings.AllowVolumeBrowserForRegularUsers {
-				return responseutils.WriteAccessDeniedResponse()
+				return utils.WriteAccessDeniedResponse()
 			}
 		}
 
@@ -468,12 +489,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
 			}
 
 			if inheritedResourceControl == nil || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
-				return responseutils.WriteAccessDeniedResponse()
+				return utils.WriteAccessDeniedResponse()
 			}
 		}
 
 		if resourceControl != nil && !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
-			return responseutils.WriteAccessDeniedResponse()
+			return utils.WriteAccessDeniedResponse()
 		}
 	}
 
@@ -537,7 +558,7 @@ func (transport *Transport) interceptAndRewriteRequest(request *http.Request, op
 // https://docs.docker.com/engine/api/v1.37/#operation/SecretCreate
 // https://docs.docker.com/engine/api/v1.37/#operation/ConfigCreate
 func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -556,7 +577,7 @@ func (transport *Transport) decorateGenericResourceCreationResponse(response *ht
 
 	responseObject = decorateObject(responseObject, resourceControl)
 
-	return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	return utils.RewriteResponse(response, responseObject, http.StatusOK)
 }
 
 func (transport *Transport) decorateGenericResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
@@ -619,7 +640,7 @@ func (transport *Transport) administratorOperation(request *http.Request) (*http
 	}
 
 	if tokenData.Role != portainer.AdministratorRole {
-		return responseutils.WriteAccessDeniedResponse()
+		return utils.WriteAccessDeniedResponse()
 	}
 
 	return transport.executeDockerRequest(request)
@@ -632,15 +653,15 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) (
 	}
 
 	accessContext := &registryAccessContext{
-		isAdmin: true,
-		userID:  tokenData.ID,
+		isAdmin:    true,
+		endpointID: transport.endpoint.ID,
 	}
 
-	hub, err := transport.dataStore.DockerHub().DockerHub()
+	user, err := transport.dataStore.User().User(tokenData.ID)
 	if err != nil {
 		return nil, err
 	}
-	accessContext.dockerHub = hub
+	accessContext.user = user
 
 	registries, err := transport.dataStore.Registry().Registries()
 	if err != nil {
@@ -648,7 +669,7 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) (
 	}
 	accessContext.registries = registries
 
-	if tokenData.Role != portainer.AdministratorRole {
+	if user.Role != portainer.AdministratorRole {
 		accessContext.isAdmin = false
 
 		teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go
index 1f77c4585..2c0b304f7 100644
--- a/api/http/proxy/factory/docker/volumes.go
+++ b/api/http/proxy/factory/docker/volumes.go
@@ -9,7 +9,7 @@ import (
 	"github.com/docker/docker/client"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
 	"github.com/portainer/portainer/api/http/security"
 	"github.com/portainer/portainer/api/internal/authorization"
 )
@@ -37,7 +37,7 @@ func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, en
 func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error {
 	// VolumeList response is a JSON object
 	// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -68,7 +68,7 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
 		responseObject["Volumes"] = volumeData
 	}
 
-	return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	return utils.RewriteResponse(response, responseObject, http.StatusOK)
 }
 
 // volumeInspectOperation extracts the response as a JSON object, verify that the user
@@ -76,7 +76,7 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
 func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
 	// VolumeInspect response is a JSON object
 	// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -101,7 +101,7 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
 // https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
 // https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
 func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
-	return responseutils.GetJSONObject(responseObject, "Labels")
+	return utils.GetJSONObject(responseObject, "Labels")
 }
 
 func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
@@ -142,7 +142,7 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt
 }
 
 func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
-	responseObject, err := responseutils.GetResponseAsJSONObject(response)
+	responseObject, err := utils.GetResponseAsJSONObject(response)
 	if err != nil {
 		return err
 	}
@@ -159,7 +159,7 @@ func (transport *Transport) decorateVolumeCreationResponse(response *http.Respon
 
 	responseObject = decorateObject(responseObject, resourceControl)
 
-	return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
+	return utils.RewriteResponse(response, responseObject, http.StatusOK)
 }
 
 func (transport *Transport) restrictedVolumeOperation(requestPath string, request *http.Request) (*http.Response, error) {
diff --git a/api/http/proxy/factory/kubernetes.go b/api/http/proxy/factory/kubernetes.go
index a1774c0d2..d4aba1769 100644
--- a/api/http/proxy/factory/kubernetes.go
+++ b/api/http/proxy/factory/kubernetes.go
@@ -39,7 +39,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
 		return nil, err
 	}
 
-	transport, err := kubernetes.NewLocalTransport(tokenManager)
+	transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
 	if err != nil {
 		return nil, err
 	}
@@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
 
 	endpointURL.Scheme = "http"
 	proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
-	proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint.ID, tokenManager)
+	proxy.Transport = kubernetes.NewEdgeTransport(factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory, factory.dataStore)
 
 	return proxy, nil
 }
@@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
 	}
 
 	proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
-	proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager)
+	proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
 
 	return proxy, nil
 }
diff --git a/api/http/proxy/factory/kubernetes/agent_transport.go b/api/http/proxy/factory/kubernetes/agent_transport.go
new file mode 100644
index 000000000..ea5af40a1
--- /dev/null
+++ b/api/http/proxy/factory/kubernetes/agent_transport.go
@@ -0,0 +1,60 @@
+package kubernetes
+
+import (
+	"crypto/tls"
+	"net/http"
+	"strings"
+
+	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/kubernetes/cli"
+)
+
+type agentTransport struct {
+	*baseTransport
+	signatureService portainer.DigitalSignatureService
+}
+
+// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
+func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *agentTransport {
+	transport := &agentTransport{
+		baseTransport: newBaseTransport(
+			&http.Transport{
+				TLSClientConfig: tlsConfig,
+			},
+			tokenManager,
+			endpoint,
+			k8sClientFactory,
+			dataStore,
+		),
+		signatureService: signatureService,
+	}
+
+	return transport
+}
+
+// RoundTrip is the implementation of the the http.RoundTripper interface
+func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
+	token, err := getRoundTripToken(request, transport.tokenManager)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
+
+	if strings.HasPrefix(request.URL.Path, "/v2") {
+		err := decorateAgentRequest(request, transport.dataStore)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
+	request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
+
+	return transport.baseTransport.RoundTrip(request)
+}
diff --git a/api/http/proxy/factory/kubernetes/edge_transport.go b/api/http/proxy/factory/kubernetes/edge_transport.go
new file mode 100644
index 000000000..22178fa05
--- /dev/null
+++ b/api/http/proxy/factory/kubernetes/edge_transport.go
@@ -0,0 +1,57 @@
+package kubernetes
+
+import (
+	"net/http"
+	"strings"
+
+	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/kubernetes/cli"
+)
+
+type edgeTransport struct {
+	*baseTransport
+	reverseTunnelService portainer.ReverseTunnelService
+}
+
+// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
+func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *edgeTransport {
+	transport := &edgeTransport{
+		baseTransport: newBaseTransport(
+			&http.Transport{},
+			tokenManager,
+			endpoint,
+			k8sClientFactory,
+			dataStore,
+		),
+		reverseTunnelService: reverseTunnelService,
+	}
+
+	return transport
+}
+
+// RoundTrip is the implementation of the the http.RoundTripper interface
+func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
+	token, err := getRoundTripToken(request, transport.tokenManager)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
+
+	if strings.HasPrefix(request.URL.Path, "/v2") {
+		err := decorateAgentRequest(request, transport.dataStore)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	response, err := transport.baseTransport.RoundTrip(request)
+
+	if err == nil {
+		transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
+	} else {
+		transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
+	}
+
+	return response, err
+}
diff --git a/api/http/proxy/factory/kubernetes/local_transport.go b/api/http/proxy/factory/kubernetes/local_transport.go
new file mode 100644
index 000000000..916d1f6c1
--- /dev/null
+++ b/api/http/proxy/factory/kubernetes/local_transport.go
@@ -0,0 +1,45 @@
+package kubernetes
+
+import (
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/crypto"
+	"github.com/portainer/portainer/api/kubernetes/cli"
+)
+
+type localTransport struct {
+	*baseTransport
+}
+
+// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
+func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) (*localTransport, error) {
+	config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
+	if err != nil {
+		return nil, err
+	}
+
+	transport := &localTransport{
+		baseTransport: newBaseTransport(
+			&http.Transport{
+				TLSClientConfig: config,
+			},
+			tokenManager,
+			endpoint,
+			k8sClientFactory,
+			dataStore,
+		),
+	}
+
+	return transport, nil
+}
+
+// RoundTrip is the implementation of the the http.RoundTripper interface
+func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
+	_, err := transport.prepareRoundTrip(request)
+	if err != nil {
+		return nil, err
+	}
+
+	return transport.baseTransport.RoundTrip(request)
+}
diff --git a/api/http/proxy/factory/kubernetes/namespaces.go b/api/http/proxy/factory/kubernetes/namespaces.go
new file mode 100644
index 000000000..3272649d7
--- /dev/null
+++ b/api/http/proxy/factory/kubernetes/namespaces.go
@@ -0,0 +1,45 @@
+package kubernetes
+
+import (
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (transport *baseTransport) proxyNamespaceDeleteOperation(request *http.Request, namespace string) (*http.Response, error) {
+	registries, err := transport.dataStore.Registry().Registries()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, registry := range registries {
+		for endpointID, registryAccessPolicies := range registry.RegistryAccesses {
+			if endpointID != transport.endpoint.ID {
+				continue
+			}
+
+			namespaces := []string{}
+			for _, ns := range registryAccessPolicies.Namespaces {
+				if ns == namespace {
+					continue
+				}
+				namespaces = append(namespaces, ns)
+			}
+
+			if len(namespaces) != len(registryAccessPolicies.Namespaces) {
+				updatedAccessPolicies := portainer.RegistryAccessPolicies{
+					Namespaces:         namespaces,
+					UserAccessPolicies: registryAccessPolicies.UserAccessPolicies,
+					TeamAccessPolicies: registryAccessPolicies.TeamAccessPolicies,
+				}
+
+				registry.RegistryAccesses[endpointID] = updatedAccessPolicies
+				err := transport.dataStore.Registry().UpdateRegistry(registry.ID, &registry)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+	}
+	return transport.executeKubernetesRequest(request)
+}
diff --git a/api/http/proxy/factory/kubernetes/secrets.go b/api/http/proxy/factory/kubernetes/secrets.go
new file mode 100644
index 000000000..6fc2ed7f6
--- /dev/null
+++ b/api/http/proxy/factory/kubernetes/secrets.go
@@ -0,0 +1,170 @@
+package kubernetes
+
+import (
+	"net/http"
+	"path"
+
+	"github.com/portainer/portainer/api/http/proxy/factory/utils"
+	"github.com/portainer/portainer/api/http/security"
+	"github.com/portainer/portainer/api/kubernetes/privateregistries"
+	v1 "k8s.io/api/core/v1"
+)
+
+func (transport *baseTransport) proxySecretRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
+	switch request.Method {
+	case "POST":
+		return transport.proxySecretCreationOperation(request)
+	case "GET":
+		if path.Base(requestPath) == "secrets" {
+			return transport.proxySecretListOperation(request)
+		}
+		return transport.proxySecretInspectOperation(request)
+	case "PUT":
+		return transport.proxySecretUpdateOperation(request)
+	case "DELETE":
+		return transport.proxySecretDeleteOperation(request, namespace)
+	default:
+		return transport.executeKubernetesRequest(request)
+	}
+}
+
+func (transport *baseTransport) proxySecretCreationOperation(request *http.Request) (*http.Response, error) {
+	body, err := utils.GetRequestAsMap(request)
+	if err != nil {
+		return nil, err
+	}
+
+	if isSecretRepresentPrivateRegistry(body) {
+		return utils.WriteAccessDeniedResponse()
+	}
+
+	err = utils.RewriteRequest(request, body)
+	if err != nil {
+		return nil, err
+	}
+
+	return transport.executeKubernetesRequest(request)
+}
+
+func (transport *baseTransport) proxySecretListOperation(request *http.Request) (*http.Response, error) {
+	response, err := transport.executeKubernetesRequest(request)
+	if err != nil {
+		return nil, err
+	}
+
+	isAdmin, err := security.IsAdmin(request)
+	if err != nil {
+		return nil, err
+	}
+
+	if isAdmin {
+		return response, nil
+	}
+
+	body, err := utils.GetResponseAsJSONObject(response)
+	if err != nil {
+		return nil, err
+	}
+
+	items := utils.GetArrayObject(body, "items")
+
+	if items == nil {
+		utils.RewriteResponse(response, body, response.StatusCode)
+		return response, nil
+	}
+
+	filteredItems := []interface{}{}
+	for _, item := range items {
+		itemObj := item.(map[string]interface{})
+		if !isSecretRepresentPrivateRegistry(itemObj) {
+			filteredItems = append(filteredItems, item)
+		}
+	}
+
+	body["items"] = filteredItems
+
+	utils.RewriteResponse(response, body, response.StatusCode)
+	return response, nil
+}
+
+func (transport *baseTransport) proxySecretInspectOperation(request *http.Request) (*http.Response, error) {
+	response, err := transport.executeKubernetesRequest(request)
+	if err != nil {
+		return nil, err
+	}
+
+	isAdmin, err := security.IsAdmin(request)
+	if err != nil {
+		return nil, err
+	}
+
+	if isAdmin {
+		return response, nil
+	}
+
+	body, err := utils.GetResponseAsJSONObject(response)
+	if err != nil {
+		return nil, err
+	}
+
+	if isSecretRepresentPrivateRegistry(body) {
+		return utils.WriteAccessDeniedResponse()
+	}
+
+	err = utils.RewriteResponse(response, body, response.StatusCode)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, nil
+}
+
+func (transport *baseTransport) proxySecretUpdateOperation(request *http.Request) (*http.Response, error) {
+	body, err := utils.GetRequestAsMap(request)
+	if err != nil {
+		return nil, err
+	}
+
+	if isSecretRepresentPrivateRegistry(body) {
+		return utils.WriteAccessDeniedResponse()
+	}
+
+	err = utils.RewriteRequest(request, body)
+	if err != nil {
+		return nil, err
+	}
+
+	return transport.executeKubernetesRequest(request)
+}
+
+func (transport *baseTransport) proxySecretDeleteOperation(request *http.Request, namespace string) (*http.Response, error) {
+	kcl, err := transport.k8sClientFactory.GetKubeClient(transport.endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	secretName := path.Base(request.RequestURI)
+
+	isRegistry, err := kcl.IsRegistrySecret(namespace, secretName)
+	if err != nil {
+		return nil, err
+	}
+
+	if isRegistry {
+		return utils.WriteAccessDeniedResponse()
+	}
+
+	return transport.executeKubernetesRequest(request)
+}
+
+func isSecretRepresentPrivateRegistry(secret map[string]interface{}) bool {
+	if secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) {
+		return false
+	}
+
+	metadata := utils.GetJSONObject(secret, "metadata")
+	annotations := utils.GetJSONObject(metadata, "annotations")
+	_, ok := annotations[privateregistries.RegistryIDLabel]
+
+	return ok
+}
diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go
index 377c7a3dc..b71ea906c 100644
--- a/api/http/proxy/factory/kubernetes/transport.go
+++ b/api/http/proxy/factory/kubernetes/transport.go
@@ -2,153 +2,107 @@ package kubernetes
 
 import (
 	"bytes"
-	"crypto/tls"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
 	"net/http"
+	"path"
+	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/portainer/portainer/api/http/security"
+	"github.com/portainer/portainer/api/kubernetes/cli"
 
 	portainer "github.com/portainer/portainer/api"
-	"github.com/portainer/portainer/api/crypto"
 )
 
-type (
-	localTransport struct {
-		httpTransport      *http.Transport
-		tokenManager       *tokenManager
-		endpointIdentifier portainer.EndpointID
-	}
-
-	agentTransport struct {
-		dataStore          portainer.DataStore
-		httpTransport      *http.Transport
-		tokenManager       *tokenManager
-		signatureService   portainer.DigitalSignatureService
-		endpointIdentifier portainer.EndpointID
-	}
-
-	edgeTransport struct {
-		dataStore            portainer.DataStore
-		httpTransport        *http.Transport
-		tokenManager         *tokenManager
-		reverseTunnelService portainer.ReverseTunnelService
-		endpointIdentifier   portainer.EndpointID
-	}
-)
-
-// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
-func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
-	config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
-	if err != nil {
-		return nil, err
-	}
-
-	transport := &localTransport{
-		httpTransport: &http.Transport{
-			TLSClientConfig: config,
-		},
-		tokenManager: tokenManager,
-	}
-
-	return transport, nil
+type baseTransport struct {
+	httpTransport    *http.Transport
+	tokenManager     *tokenManager
+	endpoint         *portainer.Endpoint
+	k8sClientFactory *cli.ClientFactory
+	dataStore        portainer.DataStore
 }
 
-// RoundTrip is the implementation of the the http.RoundTripper interface
-func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
-	token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
+func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore portainer.DataStore) *baseTransport {
+	return &baseTransport{
+		httpTransport:    httpTransport,
+		tokenManager:     tokenManager,
+		endpoint:         endpoint,
+		k8sClientFactory: k8sClientFactory,
+		dataStore:        dataStore,
+	}
+}
+
+// #region KUBERNETES PROXY
+
+// proxyKubernetesRequest intercepts a Kubernetes API request and apply logic based
+// on the requested operation.
+func (transport *baseTransport) proxyKubernetesRequest(request *http.Request) (*http.Response, error) {
+	apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/api/v[0-9](\.[0-9])?`)
+	requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
+
+	switch {
+	case strings.EqualFold(requestPath, "/namespaces"):
+		return transport.executeKubernetesRequest(request)
+	case strings.HasPrefix(requestPath, "/namespaces"):
+		return transport.proxyNamespacedRequest(request, requestPath)
+	default:
+		return transport.executeKubernetesRequest(request)
+	}
+}
+
+func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fullRequestPath string) (*http.Response, error) {
+	requestPath := strings.TrimPrefix(fullRequestPath, "/namespaces/")
+	split := strings.SplitN(requestPath, "/", 2)
+	namespace := split[0]
+
+	requestPath = ""
+	if len(split) > 1 {
+		requestPath = split[1]
+	}
+
+	switch {
+	case strings.HasPrefix(requestPath, "secrets"):
+		return transport.proxySecretRequest(request, namespace, requestPath)
+	case requestPath == "" && request.Method == "DELETE":
+		return transport.proxyNamespaceDeleteOperation(request, namespace)
+	default:
+		return transport.executeKubernetesRequest(request)
+	}
+}
+
+func (transport *baseTransport) executeKubernetesRequest(request *http.Request) (*http.Response, error) {
+
+	resp, err := transport.httpTransport.RoundTrip(request)
+
+	return resp, err
+}
+
+// #endregion
+
+// #region ROUND TRIP
+
+func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) {
+	token, err := getRoundTripToken(request, transport.tokenManager)
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 
 	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 
-	return transport.httpTransport.RoundTrip(request)
-}
-
-// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
-func NewAgentTransport(datastore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport {
-	transport := &agentTransport{
-		dataStore: datastore,
-		httpTransport: &http.Transport{
-			TLSClientConfig: tlsConfig,
-		},
-		tokenManager:     tokenManager,
-		signatureService: signatureService,
-	}
-
-	return transport
+	return token, nil
 }
 
 // RoundTrip is the implementation of the the http.RoundTripper interface
-func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
-	token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
-	if err != nil {
-		return nil, err
-	}
-
-	request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
-
-	if strings.HasPrefix(request.URL.Path, "/v2") {
-		decorateAgentRequest(request, transport.dataStore)
-	}
-
-	signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
-	if err != nil {
-		return nil, err
-	}
-
-	request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
-	request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
-
-	return transport.httpTransport.RoundTrip(request)
+func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response, error) {
+	return transport.proxyKubernetesRequest(request)
 }
 
-// NewEdgeTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
-func NewEdgeTransport(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport {
-	transport := &edgeTransport{
-		dataStore:            datastore,
-		httpTransport:        &http.Transport{},
-		tokenManager:         tokenManager,
-		reverseTunnelService: reverseTunnelService,
-		endpointIdentifier:   endpointIdentifier,
-	}
-
-	return transport
-}
-
-// RoundTrip is the implementation of the the http.RoundTripper interface
-func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
-	token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
-	if err != nil {
-		return nil, err
-	}
-
-	request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
-
-	if strings.HasPrefix(request.URL.Path, "/v2") {
-		decorateAgentRequest(request, transport.dataStore)
-	}
-
-	response, err := transport.httpTransport.RoundTrip(request)
-
-	if err == nil {
-		transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier)
-	} else {
-		transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier)
-	}
-
-	return response, err
-}
-
-func getRoundTripToken(
-	request *http.Request,
-	tokenManager *tokenManager,
-	endpointIdentifier portainer.EndpointID,
-) (string, error) {
+func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) {
 	tokenData, err := security.RetrieveTokenData(request)
 	if err != nil {
 		return "", err
@@ -168,32 +122,56 @@ func getRoundTripToken(
 	return token, nil
 }
 
+// #endregion
+
+// #region DECORATE FUNCTIONS
+
 func decorateAgentRequest(r *http.Request, dataStore portainer.DataStore) error {
 	requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
 
 	switch {
 	case strings.HasPrefix(requestPath, "/dockerhub"):
-		decorateAgentDockerHubRequest(r, dataStore)
+		return decorateAgentDockerHubRequest(r, dataStore)
 	}
 
 	return nil
 }
 
 func decorateAgentDockerHubRequest(r *http.Request, dataStore portainer.DataStore) error {
-	dockerhub, err := dataStore.DockerHub().DockerHub()
+	requestPath, registryIdString := path.Split(r.URL.Path)
+
+	registryID, err := strconv.Atoi(registryIdString)
 	if err != nil {
-		return err
+		return fmt.Errorf("missing registry id: %w", err)
 	}
 
-	newBody, err := json.Marshal(dockerhub)
+	r.URL.Path = strings.TrimSuffix(requestPath, "/")
+
+	registry := &portainer.Registry{
+		Type: portainer.DockerHubRegistry,
+	}
+
+	if registryID != 0 {
+		registry, err = dataStore.Registry().Registry(portainer.RegistryID(registryID))
+		if err != nil {
+			return fmt.Errorf("failed fetching registry: %w", err)
+		}
+	}
+
+	if registry.Type != portainer.DockerHubRegistry {
+		return errors.New("invalid registry type")
+	}
+
+	newBody, err := json.Marshal(registry)
 	if err != nil {
-		return err
+		return fmt.Errorf("failed marshaling registry: %w", err)
 	}
 
 	r.Method = http.MethodPost
-
 	r.Body = ioutil.NopCloser(bytes.NewReader(newBody))
 	r.ContentLength = int64(len(newBody))
 
 	return nil
 }
+
+// #endregion
diff --git a/api/http/proxy/factory/responseutils/json.go b/api/http/proxy/factory/responseutils/json.go
deleted file mode 100644
index 15af94c60..000000000
--- a/api/http/proxy/factory/responseutils/json.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package responseutils
-
-// GetJSONObject will extract an object from a specific property of another JSON object.
-// Returns nil if nothing is associated to the specified key.
-func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} {
-	object := jsonObject[property]
-	if object != nil {
-		return object.(map[string]interface{})
-	}
-	return nil
-}
diff --git a/api/http/proxy/factory/utils/json.go b/api/http/proxy/factory/utils/json.go
new file mode 100644
index 000000000..2d44e17f4
--- /dev/null
+++ b/api/http/proxy/factory/utils/json.go
@@ -0,0 +1,91 @@
+package utils
+
+import (
+	"compress/gzip"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+
+	"gopkg.in/yaml.v3"
+)
+
+// GetJSONObject will extract an object from a specific property of another JSON object.
+// Returns nil if nothing is associated to the specified key.
+func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} {
+	object := jsonObject[property]
+	if object != nil {
+		return object.(map[string]interface{})
+	}
+	return nil
+}
+
+// GetArrayObject will extract an array from a specific property of another JSON object.
+// Returns nil if nothing is associated to the specified key.
+func GetArrayObject(jsonObject map[string]interface{}, property string) []interface{} {
+	object := jsonObject[property]
+	if object != nil {
+		return object.([]interface{})
+	}
+	return nil
+}
+
+func getBody(body io.ReadCloser, contentType string, isGzip bool) (interface{}, error) {
+	if body == nil {
+		return nil, errors.New("unable to parse response: empty response body")
+	}
+
+	reader := body
+
+	if isGzip {
+		gzipReader, err := gzip.NewReader(reader)
+		if err != nil {
+			return nil, err
+		}
+
+		reader = gzipReader
+	}
+
+	defer reader.Close()
+
+	bodyBytes, err := ioutil.ReadAll(reader)
+	if err != nil {
+		return nil, err
+	}
+
+	err = body.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	var data interface{}
+	err = unmarshal(contentType, bodyBytes, &data)
+	if err != nil {
+		return nil, err
+	}
+
+	return data, nil
+}
+
+func marshal(contentType string, data interface{}) ([]byte, error) {
+	switch contentType {
+	case "application/yaml":
+		return yaml.Marshal(data)
+	case "application/json", "":
+		return json.Marshal(data)
+	}
+
+	return nil, fmt.Errorf("content type is not supported for marshaling: %s", contentType)
+}
+
+func unmarshal(contentType string, body []byte, returnBody interface{}) error {
+	switch contentType {
+	case "application/yaml":
+		return yaml.Unmarshal(body, returnBody)
+	case "application/json", "":
+		return json.Unmarshal(body, returnBody)
+	}
+
+	return fmt.Errorf("content type is not supported for unmarshaling: %s", contentType)
+}
diff --git a/api/http/proxy/factory/utils/request.go b/api/http/proxy/factory/utils/request.go
new file mode 100644
index 000000000..92724b7fa
--- /dev/null
+++ b/api/http/proxy/factory/utils/request.go
@@ -0,0 +1,45 @@
+package utils
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+	"strconv"
+)
+
+// GetRequestAsMap returns the response content as a generic JSON object
+func GetRequestAsMap(request *http.Request) (map[string]interface{}, error) {
+	data, err := getRequestBody(request)
+	if err != nil {
+		return nil, err
+	}
+
+	return data.(map[string]interface{}), nil
+}
+
+// RewriteRequest will replace the existing request body with the one specified
+// in parameters
+func RewriteRequest(request *http.Request, newData interface{}) error {
+	data, err := marshal(getContentType(request.Header), newData)
+	if err != nil {
+		return err
+	}
+
+	body := ioutil.NopCloser(bytes.NewReader(data))
+
+	request.Body = body
+	request.ContentLength = int64(len(data))
+
+	if request.Header == nil {
+		request.Header = make(http.Header)
+	}
+	request.Header.Set("Content-Length", strconv.Itoa(len(data)))
+
+	return nil
+}
+
+func getRequestBody(request *http.Request) (interface{}, error) {
+	isGzip := request.Header.Get("Content-Encoding") == "gzip"
+
+	return getBody(request.Body, getContentType(request.Header), isGzip)
+}
diff --git a/api/http/proxy/factory/responseutils/response.go b/api/http/proxy/factory/utils/response.go
similarity index 62%
rename from api/http/proxy/factory/responseutils/response.go
rename to api/http/proxy/factory/utils/response.go
index a32cd3252..dc73e5618 100644
--- a/api/http/proxy/factory/responseutils/response.go
+++ b/api/http/proxy/factory/utils/response.go
@@ -1,9 +1,7 @@
-package responseutils
+package utils
 
 import (
 	"bytes"
-	"compress/gzip"
-	"encoding/json"
 	"errors"
 	"io/ioutil"
 	"log"
@@ -13,7 +11,7 @@ import (
 
 // GetResponseAsJSONObject returns the response content as a generic JSON object
 func GetResponseAsJSONObject(response *http.Response) (map[string]interface{}, error) {
-	responseData, err := getResponseBodyAsGenericJSON(response)
+	responseData, err := getResponseBody(response)
 	if err != nil {
 		return nil, err
 	}
@@ -24,7 +22,7 @@ func GetResponseAsJSONObject(response *http.Response) (map[string]interface{}, e
 
 // GetResponseAsJSONArray returns the response content as an array of generic JSON object
 func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
-	responseData, err := getResponseBodyAsGenericJSON(response)
+	responseData, err := getResponseBody(response)
 	if err != nil {
 		return nil, err
 	}
@@ -44,72 +42,54 @@ func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
 	}
 }
 
-func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) {
-	if response.Body == nil {
-		return nil, errors.New("unable to parse response: empty response body")
-	}
-
-	reader := response.Body
-
-	if response.Header.Get("Content-Encoding") == "gzip" {
-		response.Header.Del("Content-Encoding")
-		gzipReader, err := gzip.NewReader(response.Body)
-		if err != nil {
-			return nil, err
-		}
-		reader = gzipReader
-	}
-
-	defer reader.Close()
-
-	var data interface{}
-	body, err := ioutil.ReadAll(reader)
-	if err != nil {
-		return nil, err
-	}
-
-	err = json.Unmarshal(body, &data)
-	if err != nil {
-		return nil, err
-	}
-
-	return data, nil
-}
-
-type dockerErrorResponse struct {
+type errorResponse struct {
 	Message string `json:"message,omitempty"`
 }
 
 // WriteAccessDeniedResponse will create a new access denied response
 func WriteAccessDeniedResponse() (*http.Response, error) {
 	response := &http.Response{}
-	err := RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
+	err := RewriteResponse(response, errorResponse{Message: "access denied to resource"}, http.StatusForbidden)
 	return response, err
 }
 
 // RewriteAccessDeniedResponse will overwrite the existing response with an access denied response
 func RewriteAccessDeniedResponse(response *http.Response) error {
-	return RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
+	return RewriteResponse(response, errorResponse{Message: "access denied to resource"}, http.StatusForbidden)
 }
 
 // RewriteResponse will replace the existing response body and status code with the one specified
 // in parameters
 func RewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
-	jsonData, err := json.Marshal(newResponseData)
+	data, err := marshal(getContentType(response.Header), newResponseData)
 	if err != nil {
 		return err
 	}
 
-	body := ioutil.NopCloser(bytes.NewReader(jsonData))
+	body := ioutil.NopCloser(bytes.NewReader(data))
 
 	response.StatusCode = statusCode
 	response.Body = body
-	response.ContentLength = int64(len(jsonData))
+	response.ContentLength = int64(len(data))
 
 	if response.Header == nil {
 		response.Header = make(http.Header)
 	}
-	response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
+	response.Header.Set("Content-Length", strconv.Itoa(len(data)))
 
 	return nil
 }
+
+func getResponseBody(response *http.Response) (interface{}, error) {
+	isGzip := response.Header.Get("Content-Encoding") == "gzip"
+
+	if isGzip {
+		response.Header.Del("Content-Encoding")
+	}
+
+	return getBody(response.Body, getContentType(response.Header), isGzip)
+}
+
+func getContentType(headers http.Header) string {
+	return headers.Get("Content-type")
+}
diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go
index 2ba3e8743..f205a6493 100644
--- a/api/http/security/authorization.go
+++ b/api/http/security/authorization.go
@@ -1,9 +1,21 @@
 package security
 
 import (
-	"github.com/portainer/portainer/api"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
 )
 
+// IsAdmin returns true if the logged-in user is an admin
+func IsAdmin(request *http.Request) (bool, error) {
+	tokenData, err := RetrieveTokenData(request)
+	if err != nil {
+		return false, err
+	}
+
+	return tokenData.Role == portainer.AdministratorRole, nil
+}
+
 // AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
 func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
 	if context.IsAdmin || resourceControl.Public {
@@ -95,9 +107,9 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques
 // It will check if the user is part of the authorized users or part of a team that is
 // listed in the authorized teams of the endpoint and the associated group.
 func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
-	groupAccess := authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
+	groupAccess := AuthorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
 	if !groupAccess {
-		return authorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies)
+		return AuthorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies)
 	}
 	return true
 }
@@ -106,17 +118,24 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
 // It will check if the user is part of the authorized users or part of a team that is
 // listed in the authorized teams.
 func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
-	return authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
+	return AuthorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
 }
 
 // AuthorizedRegistryAccess ensure that the user can access the specified registry.
 // It will check if the user is part of the authorized users or part of a team that is
-// listed in the authorized teams.
-func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
-	return authorizedAccess(userID, memberships, registry.UserAccessPolicies, registry.TeamAccessPolicies)
+// listed in the authorized teams for a specified endpoint,
+func AuthorizedRegistryAccess(registry *portainer.Registry, user *portainer.User, teamMemberships []portainer.TeamMembership, endpointID portainer.EndpointID) bool {
+	if user.Role == portainer.AdministratorRole {
+		return true
+	}
+
+	registryEndpointAccesses := registry.RegistryAccesses[endpointID]
+
+	return AuthorizedAccess(user.ID, teamMemberships, registryEndpointAccesses.UserAccessPolicies, registryEndpointAccesses.TeamAccessPolicies)
 }
 
-func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool {
+// AuthorizedAccess verifies the userID or memberships are authorized to use an object per the supplied access policies
+func AuthorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool {
 	_, userAccess := userAccessPolicies[userID]
 	if userAccess {
 		return true
diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go
index 5c57314af..dee955e50 100644
--- a/api/http/security/bouncer.go
+++ b/api/http/security/bouncer.go
@@ -128,31 +128,6 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
 	return nil
 }
 
-// RegistryAccess retrieves the JWT token from the request context and verifies
-// that the user can access the specified registry.
-// An error is returned when access is denied.
-func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portainer.Registry) error {
-	tokenData, err := RetrieveTokenData(r)
-	if err != nil {
-		return err
-	}
-
-	if tokenData.Role == portainer.AdministratorRole {
-		return nil
-	}
-
-	memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
-	if err != nil {
-		return err
-	}
-
-	if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) {
-		return httperrors.ErrEndpointAccessDenied
-	}
-
-	return nil
-}
-
 // handlers are applied backwards to the incoming request:
 // - add secure handlers to the response
 // - parse the JWT token and put it into the http context.
@@ -213,7 +188,7 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
 			return
 		}
 
-		ctx := storeRestrictedRequestContext(r, requestContext)
+		ctx := StoreRestrictedRequestContext(r, requestContext)
 		next.ServeHTTP(w, r.WithContext(ctx))
 	})
 }
diff --git a/api/http/security/context.go b/api/http/security/context.go
index 8350fa56f..1601f61ac 100644
--- a/api/http/security/context.go
+++ b/api/http/security/context.go
@@ -5,7 +5,7 @@ import (
 	"errors"
 	"net/http"
 
-	"github.com/portainer/portainer/api"
+	portainer "github.com/portainer/portainer/api"
 )
 
 type (
@@ -33,9 +33,9 @@ func RetrieveTokenData(request *http.Request) (*portainer.TokenData, error) {
 	return tokenData, nil
 }
 
-// storeRestrictedRequestContext stores a RestrictedRequestContext object inside the request context
+// StoreRestrictedRequestContext stores a RestrictedRequestContext object inside the request context
 // and returns the enhanced context.
-func storeRestrictedRequestContext(request *http.Request, requestContext *RestrictedRequestContext) context.Context {
+func StoreRestrictedRequestContext(request *http.Request, requestContext *RestrictedRequestContext) context.Context {
 	return context.WithValue(request.Context(), contextRestrictedRequest, requestContext)
 }
 
diff --git a/api/http/security/filter.go b/api/http/security/filter.go
index 1716b043e..be0106447 100644
--- a/api/http/security/filter.go
+++ b/api/http/security/filter.go
@@ -1,7 +1,7 @@
 package security
 
 import (
-	"github.com/portainer/portainer/api"
+	portainer "github.com/portainer/portainer/api"
 )
 
 // FilterUserTeams filters teams based on user role.
@@ -64,15 +64,16 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po
 
 // FilterRegistries filters registries based on user role and team memberships.
 // Non administrator users only have access to authorized registries.
-func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) []portainer.Registry {
-	filteredRegistries := registries
-	if !context.IsAdmin {
-		filteredRegistries = make([]portainer.Registry, 0)
+func FilterRegistries(registries []portainer.Registry, user *portainer.User, teamMemberships []portainer.TeamMembership, endpointID portainer.EndpointID) []portainer.Registry {
+	if user.Role == portainer.AdministratorRole {
+		return registries
+	}
 
-		for _, registry := range registries {
-			if AuthorizedRegistryAccess(&registry, context.UserID, context.UserMemberships) {
-				filteredRegistries = append(filteredRegistries, registry)
-			}
+	filteredRegistries := []portainer.Registry{}
+
+	for _, registry := range registries {
+		if AuthorizedRegistryAccess(&registry, user, teamMemberships, endpointID) {
+			filteredRegistries = append(filteredRegistries, registry)
 		}
 	}
 
diff --git a/api/http/server.go b/api/http/server.go
index 8f399e7a9..ac5fe0e1b 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -16,7 +16,6 @@ import (
 	"github.com/portainer/portainer/api/http/handler/auth"
 	"github.com/portainer/portainer/api/http/handler/backup"
 	"github.com/portainer/portainer/api/http/handler/customtemplates"
-	"github.com/portainer/portainer/api/http/handler/dockerhub"
 	"github.com/portainer/portainer/api/http/handler/edgegroups"
 	"github.com/portainer/portainer/api/http/handler/edgejobs"
 	"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -111,9 +110,6 @@ func (server *Server) Start() error {
 	customTemplatesHandler.FileService = server.FileService
 	customTemplatesHandler.GitService = server.GitService
 
-	var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
-	dockerHubHandler.DataStore = server.DataStore
-
 	var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer)
 	edgeGroupsHandler.DataStore = server.DataStore
 
@@ -135,6 +131,7 @@ func (server *Server) Start() error {
 	endpointHandler.FileService = server.FileService
 	endpointHandler.ProxyManager = server.ProxyManager
 	endpointHandler.SnapshotService = server.SnapshotService
+	endpointHandler.K8sClientFactory = server.KubernetesClientFactory
 	endpointHandler.ReverseTunnelService = server.ReverseTunnelService
 	endpointHandler.ComposeStackManager = server.ComposeStackManager
 	endpointHandler.AuthorizationService = server.AuthorizationService
@@ -161,6 +158,7 @@ func (server *Server) Start() error {
 	registryHandler.DataStore = server.DataStore
 	registryHandler.FileService = server.FileService
 	registryHandler.ProxyManager = server.ProxyManager
+	registryHandler.K8sClientFactory = server.KubernetesClientFactory
 
 	var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
 	resourceControlHandler.DataStore = server.DataStore
@@ -219,7 +217,6 @@ func (server *Server) Start() error {
 		AuthHandler:            authHandler,
 		BackupHandler:          backupHandler,
 		CustomTemplatesHandler: customTemplatesHandler,
-		DockerHubHandler:       dockerHubHandler,
 		EdgeGroupsHandler:      edgeGroupsHandler,
 		EdgeJobsHandler:        edgeJobsHandler,
 		EdgeStacksHandler:      edgeStacksHandler,
diff --git a/api/internal/authorization/authorizations.go b/api/internal/authorization/authorizations.go
index 805c2e38f..92eae802d 100644
--- a/api/internal/authorization/authorizations.go
+++ b/api/internal/authorization/authorizations.go
@@ -1,15 +1,15 @@
 package authorization
 
 import (
-	"github.com/portainer/portainer/api"
+	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/kubernetes/cli"
 )
 
 // Service represents a service used to
 // update authorizations associated to a user or team.
 type Service struct {
-	dataStore portainer.DataStore
-	K8sClientFactory  *cli.ClientFactory
+	dataStore        portainer.DataStore
+	K8sClientFactory *cli.ClientFactory
 }
 
 // NewService returns a point to a new Service instance.
@@ -140,6 +140,7 @@ func DefaultEndpointAuthorizationsForEndpointAdministratorRole() portainer.Autho
 		portainer.OperationDockerAgentUndefined:               true,
 		portainer.OperationPortainerResourceControlCreate:     true,
 		portainer.OperationPortainerResourceControlUpdate:     true,
+		portainer.OperationPortainerRegistryUpdateAccess:      true,
 		portainer.OperationPortainerStackList:                 true,
 		portainer.OperationPortainerStackInspect:              true,
 		portainer.OperationPortainerStackFile:                 true,
diff --git a/api/internal/endpoint/endpoint.go b/api/internal/endpoint/endpoint.go
index 378ca70e5..079474d11 100644
--- a/api/internal/endpoint/endpoint.go
+++ b/api/internal/endpoint/endpoint.go
@@ -9,8 +9,8 @@ func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
 		endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
 }
 
-// IsDocketEndpoint returns true if this is a docker endpoint
-func IsDocketEndpoint(endpoint *portainer.Endpoint) bool {
+// IsDockerEndpoint returns true if this is a docker endpoint
+func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
 	return endpoint.Type == portainer.DockerEnvironment ||
 		endpoint.Type == portainer.AgentOnDockerEnvironment ||
 		endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
diff --git a/api/internal/testhelpers/datastore.go b/api/internal/testhelpers/datastore.go
index a58da5c7c..970184d86 100644
--- a/api/internal/testhelpers/datastore.go
+++ b/api/internal/testhelpers/datastore.go
@@ -7,7 +7,6 @@ import (
 )
 
 type datastore struct {
-	dockerHub        portainer.DockerHubService
 	customTemplate   portainer.CustomTemplateService
 	edgeGroup        portainer.EdgeGroupService
 	edgeJob          portainer.EdgeJobService
@@ -37,7 +36,6 @@ func (d *datastore) CheckCurrentEdition() error                          { retur
 func (d *datastore) IsNew() bool                                         { return false }
 func (d *datastore) MigrateData(force bool) error                        { return nil }
 func (d *datastore) RollbackToCE() error                                 { return nil }
-func (d *datastore) DockerHub() portainer.DockerHubService               { return d.dockerHub }
 func (d *datastore) CustomTemplate() portainer.CustomTemplateService     { return d.customTemplate }
 func (d *datastore) EdgeGroup() portainer.EdgeGroupService               { return d.edgeGroup }
 func (d *datastore) EdgeJob() portainer.EdgeJobService                   { return d.edgeJob }
diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go
index e4b4729c6..af5fa887a 100644
--- a/api/kubernetes/cli/access.go
+++ b/api/kubernetes/cli/access.go
@@ -12,6 +12,28 @@ type (
 	namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
 )
 
+// GetNamespaceAccessPolicies gets the namespace access policies
+// from config maps in the portainer namespace
+func (kcl *KubeClient) GetNamespaceAccessPolicies() (
+	map[string]portainer.K8sNamespaceAccessPolicy, error,
+) {
+	configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
+	if k8serrors.IsNotFound(err) {
+		return nil, nil
+	} else if err != nil {
+		return nil, err
+	}
+
+	accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
+
+	var policies map[string]portainer.K8sNamespaceAccessPolicy
+	err = json.Unmarshal([]byte(accessData), &policies)
+	if err != nil {
+		return nil, err
+	}
+	return policies, nil
+}
+
 func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
 	configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
 	if k8serrors.IsNotFound(err) {
@@ -80,28 +102,6 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN
 	return false
 }
 
-// GetNamespaceAccessPolicies gets the namespace access policies
-// from config maps in the portainer namespace
-func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
-	configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
-	if k8serrors.IsNotFound(err) {
-		return nil, nil
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
-
-	var policies map[string]portainer.K8sNamespaceAccessPolicy
-	err = json.Unmarshal([]byte(accessData), &policies)
-	if err != nil {
-		return nil, err
-	}
-	return policies, nil
-}
-
 // UpdateNamespaceAccessPolicies updates the namespace access policies
 func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]portainer.K8sNamespaceAccessPolicy) error {
 	data, err := json.Marshal(accessPolicies)
diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go
index a268150c9..c3e687fd9 100644
--- a/api/kubernetes/cli/client.go
+++ b/api/kubernetes/cli/client.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
+	"time"
 
 	cmap "github.com/orcaman/concurrent-map"
 
@@ -17,6 +18,7 @@ import (
 type (
 	// ClientFactory is used to create Kubernetes clients
 	ClientFactory struct {
+		dataStore            portainer.DataStore
 		reverseTunnelService portainer.ReverseTunnelService
 		signatureService     portainer.DigitalSignatureService
 		instanceID           string
@@ -31,8 +33,9 @@ type (
 )
 
 // NewClientFactory returns a new instance of a ClientFactory
-func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *ClientFactory {
+func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore portainer.DataStore) *ClientFactory {
 	return &ClientFactory{
+		dataStore:            dataStore,
 		signatureService:     signatureService,
 		reverseTunnelService: reverseTunnelService,
 		instanceID:           instanceID,
@@ -133,7 +136,29 @@ func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*k
 
 func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
 	tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
-	endpointURL := fmt.Sprintf("http://localhost:%d/kubernetes", tunnel.Port)
+
+	if tunnel.Status == portainer.EdgeAgentIdle {
+		err := factory.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
+		if err != nil {
+			return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
+		}
+
+		if endpoint.EdgeCheckinInterval == 0 {
+			settings, err := factory.dataStore.Settings().Settings()
+			if err != nil {
+				return nil, fmt.Errorf("failed fetching settings from db: %w", err)
+			}
+
+			endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
+		}
+
+		waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
+		time.Sleep(waitForAgentToConnect * 2)
+
+		tunnel = factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
+	}
+
+	endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
 
 	config, err := clientcmd.BuildConfigFromFlags(endpointURL, "")
 	if err != nil {
diff --git a/api/kubernetes/cli/registries.go b/api/kubernetes/cli/registries.go
new file mode 100644
index 000000000..122741a9b
--- /dev/null
+++ b/api/kubernetes/cli/registries.go
@@ -0,0 +1,96 @@
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	"github.com/pkg/errors"
+	portainer "github.com/portainer/portainer/api"
+	v1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	secretDockerConfigKey = ".dockerconfigjson"
+)
+
+type (
+	dockerConfig struct {
+		Auths map[string]registryDockerConfig `json:"auths"`
+	}
+
+	registryDockerConfig struct {
+		Username string `json:"username"`
+		Password string `json:"password"`
+		Email    string `json:"email"`
+	}
+)
+
+func (kcl *KubeClient) DeleteRegistrySecret(registry *portainer.Registry, namespace string) error {
+	err := kcl.cli.CoreV1().Secrets(namespace).Delete(registrySecretName(registry), &metav1.DeleteOptions{})
+	if err != nil && !k8serrors.IsNotFound(err) {
+		return errors.Wrap(err, "failed removing secret")
+	}
+
+	return nil
+}
+
+func (kcl *KubeClient) CreateRegistrySecret(registry *portainer.Registry, namespace string) error {
+	config := dockerConfig{
+		Auths: map[string]registryDockerConfig{
+			registry.URL: {
+				Username: registry.Username,
+				Password: registry.Password,
+			},
+		},
+	}
+
+	configByte, err := json.Marshal(config)
+	if err != nil {
+		return errors.Wrap(err, "failed marshal config")
+	}
+
+	secret := &v1.Secret{
+		TypeMeta: metav1.TypeMeta{},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: registrySecretName(registry),
+			Annotations: map[string]string{
+				"portainer.io/registry.id": strconv.Itoa(int(registry.ID)),
+			},
+		},
+		Data: map[string][]byte{
+			secretDockerConfigKey: configByte,
+		},
+		Type: v1.SecretTypeDockerConfigJson,
+	}
+
+	_, err = kcl.cli.CoreV1().Secrets(namespace).Create(secret)
+	if err != nil && !k8serrors.IsAlreadyExists(err) {
+		return errors.Wrap(err, "failed saving secret")
+	}
+
+	return nil
+
+}
+
+func (cli *KubeClient) IsRegistrySecret(namespace, secretName string) (bool, error) {
+	secret, err := cli.cli.CoreV1().Secrets(namespace).Get(secretName, metav1.GetOptions{})
+	if err != nil {
+		if k8serrors.IsNotFound(err) {
+			return false, nil
+		}
+
+		return false, err
+	}
+
+	isSecret := secret.Type == v1.SecretTypeDockerConfigJson
+
+	return isSecret, nil
+
+}
+
+func registrySecretName(registry *portainer.Registry) string {
+	return fmt.Sprintf("registry-%d", registry.ID)
+}
diff --git a/api/kubernetes/privateregistries/labels.go b/api/kubernetes/privateregistries/labels.go
new file mode 100644
index 000000000..dc780814c
--- /dev/null
+++ b/api/kubernetes/privateregistries/labels.go
@@ -0,0 +1,5 @@
+package privateregistries
+
+const (
+	RegistryIDLabel = "portainer.io/registry.id"
+)
diff --git a/api/portainer.go b/api/portainer.go
index ccfeeea7a..5458fca4b 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -511,8 +511,8 @@ type (
 	Registry struct {
 		// Registry Identifier
 		ID RegistryID `json:"Id" example:"1"`
-		// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet)
-		Type RegistryType `json:"Type" enums:"1,2,3,4,5"`
+		// Registry Type (1 - Quay, 2 - Azure, 3 - Custom, 4 - Gitlab, 5 - ProGet, 6 - DockerHub)
+		Type RegistryType `json:"Type" enums:"1,2,3,4,5,6"`
 		// Registry Name
 		Name string `json:"Name" example:"my-registry"`
 		// URL or IP address of the Docker registry
@@ -528,15 +528,28 @@ type (
 		ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
 		Gitlab                  GitlabRegistryData               `json:"Gitlab"`
 		Quay                    QuayRegistryData                 `json:"Quay"`
-		UserAccessPolicies      UserAccessPolicies               `json:"UserAccessPolicies"`
-		TeamAccessPolicies      TeamAccessPolicies               `json:"TeamAccessPolicies"`
+		RegistryAccesses        RegistryAccesses                 `json:"RegistryAccesses"`
 
 		// Deprecated fields
+		// Deprecated in DBVersion == 31
+		UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
+		// Deprecated in DBVersion == 31
+		TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
+
 		// Deprecated in DBVersion == 18
 		AuthorizedUsers []UserID `json:"AuthorizedUsers"`
+		// Deprecated in DBVersion == 18
 		AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
 	}
 
+	RegistryAccesses map[EndpointID]RegistryAccessPolicies
+
+	RegistryAccessPolicies struct {
+		UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
+		TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
+		Namespaces         []string           `json:"Namespaces"`
+	}
+
 	// RegistryID represents a registry identifier
 	RegistryID int
 
@@ -1019,7 +1032,6 @@ type (
 		CheckCurrentEdition() error
 		BackupTo(w io.Writer) error
 
-		DockerHub() DockerHubService
 		CustomTemplate() CustomTemplateService
 		EdgeGroup() EdgeGroupService
 		EdgeJob() EdgeJobService
@@ -1050,12 +1062,6 @@ type (
 		CreateSignature(message string) (string, error)
 	}
 
-	// DockerHubService represents a service for managing the DockerHub object
-	DockerHubService interface {
-		DockerHub() (*DockerHub, error)
-		UpdateDockerHub(registry *DockerHub) error
-	}
-
 	// DockerSnapshotter represents a service used to create Docker endpoint snapshots
 	DockerSnapshotter interface {
 		CreateSnapshot(endpoint *Endpoint) (*DockerSnapshot, error)
@@ -1169,6 +1175,9 @@ type (
 		StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
 		GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
 		UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
+		DeleteRegistrySecret(registry *Registry, namespace string) error
+		CreateRegistrySecret(registry *Registry, namespace string) error
+		IsRegistrySecret(namespace, secretName string) (bool, error)
 	}
 
 	// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
@@ -1266,7 +1275,7 @@ type (
 
 	// SwarmStackManager represents a service to manage Swarm stacks
 	SwarmStackManager interface {
-		Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint)
+		Login(registries []Registry, endpoint *Endpoint)
 		Logout(endpoint *Endpoint) error
 		Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
 		Remove(stack *Stack, endpoint *Endpoint) error
@@ -1345,7 +1354,7 @@ const (
 	// APIVersion is the version number of the Portainer API
 	APIVersion = "2.6.0"
 	// DBVersion is the version number of the Portainer database
-	DBVersion = 30
+	DBVersion = 32
 	// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
 	ComposeSyntaxMaxVersion = "3.9"
 	// AssetsServerURL represents the URL of the Portainer asset server
@@ -1493,6 +1502,8 @@ const (
 	GitlabRegistry
 	// ProGetRegistry represents a proget registry
 	ProGetRegistry
+	// DockerHubRegistry represents a dockerhub registry
+	DockerHubRegistry
 )
 
 const (
diff --git a/app/agent/rest/dockerhub.js b/app/agent/rest/dockerhub.js
index b48481e8a..eb901b494 100644
--- a/app/agent/rest/dockerhub.js
+++ b/app/agent/rest/dockerhub.js
@@ -4,10 +4,10 @@ angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
 
 function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
   return $resource(
-    `${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub`,
+    `${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub/:registryId`,
     {},
     {
-      limits: { method: 'GET' },
+      limits: { method: 'GET', params: { registryId: '@registryId' } },
     }
   );
 }
diff --git a/app/constants.js b/app/constants.js
index febc848e8..57d1c7c41 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -1,7 +1,6 @@
 angular
   .module('portainer')
   .constant('API_ENDPOINT_AUTH', 'api/auth')
-  .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub')
   .constant('API_ENDPOINT_CUSTOM_TEMPLATES', 'api/custom_templates')
   .constant('API_ENDPOINT_EDGE_GROUPS', 'api/edge_groups')
   .constant('API_ENDPOINT_EDGE_JOBS', 'api/edge_jobs')
diff --git a/app/docker/__module.js b/app/docker/__module.js
index 40cf82c24..94e7e969c 100644
--- a/app/docker/__module.js
+++ b/app/docker/__module.js
@@ -591,6 +591,26 @@ angular.module('portainer.docker', ['portainer.app']).config([
       },
     };
 
+    const registries = {
+      name: 'docker.registries',
+      url: '/registries',
+      views: {
+        'content@': {
+          component: 'endpointRegistriesView',
+        },
+      },
+    };
+
+    const registryAccess = {
+      name: 'docker.registries.access',
+      url: '/:id/access',
+      views: {
+        'content@': {
+          component: 'dockerRegistryAccessView',
+        },
+      },
+    };
+
     $stateRegistryProvider.register(configs);
     $stateRegistryProvider.register(config);
     $stateRegistryProvider.register(configCreation);
@@ -641,5 +661,7 @@ angular.module('portainer.docker', ['portainer.app']).config([
     $stateRegistryProvider.register(volumeBrowse);
     $stateRegistryProvider.register(volumeCreation);
     $stateRegistryProvider.register(dockerFeaturesConfiguration);
+    $stateRegistryProvider.register(registries);
+    $stateRegistryProvider.register(registryAccess);
   },
 ]);
diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html
index 484451142..086047756 100644
--- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html
+++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html
@@ -35,17 +35,19 @@
 <li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess && !$ctrl.offlineMode">
   <a ui-sref="docker.events({endpointId: $ctrl.endpointId})" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
 </li>
-<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
-  <a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
+<li class="sidebar-list">
+  <a ng-if="$ctrl.standaloneManagement" ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
+  <a ng-if="$ctrl.swarmManagement" ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
 
-  <div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
-    <a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
-  </div>
-</li>
-<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
-  <a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
-
-  <div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
-    <a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
+  <div
+    ng-if="$ctrl.adminAccess && ['docker.swarm', 'docker.host', 'docker.registries', 'docker.registries.access', 'docker.featuresConfiguration'].includes($ctrl.currentRouteName)"
+  >
+    <div class="sidebar-sublist">
+      <a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
+    </div>
+
+    <div class="sidebar-sublist">
+      <a ui-sref="docker.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
+    </div>
   </div>
 </li>
diff --git a/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js b/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js
index d823c848d..0c23eba56 100644
--- a/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js
+++ b/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js
@@ -1,31 +1,41 @@
+import EndpointHelper from '@/portainer/helpers/endpointHelper';
+
 export default class porImageRegistryContainerController {
   /* @ngInject */
-  constructor(EndpointHelper, DockerHubService, Notifications) {
-    this.EndpointHelper = EndpointHelper;
+  constructor(DockerHubService, Notifications) {
     this.DockerHubService = DockerHubService;
     this.Notifications = Notifications;
 
     this.pullRateLimits = null;
   }
 
-  $onChanges({ isDockerHubRegistry }) {
-    if (isDockerHubRegistry && isDockerHubRegistry.currentValue) {
+  $onChanges({ registryId }) {
+    if (registryId) {
       this.fetchRateLimits();
     }
   }
 
+  $onInit() {
+    this.setValidity =
+      this.setValidity ||
+      (() => {
+        /* noop */
+      });
+  }
+
   async fetchRateLimits() {
     this.pullRateLimits = null;
-    if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
-      try {
-        this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
-        this.setValidity(this.pullRateLimits.remaining >= 0);
-      } catch (e) {
-        // eslint-disable-next-line no-console
-        console.error('Failed loading DockerHub pull rate limits', e);
-        this.setValidity(true);
-      }
-    } else {
+    if (!EndpointHelper.isAgentEndpoint(this.endpoint) && !EndpointHelper.isLocalEndpoint(this.endpoint)) {
+      this.setValidity(true);
+      return;
+    }
+
+    try {
+      this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint, this.registryId || 0);
+      this.setValidity(this.pullRateLimits.remaining >= 0);
+    } catch (e) {
+      // eslint-disable-next-line no-console
+      console.error('Failed loading DockerHub pull rate limits', e);
       this.setValidity(true);
     }
   }
diff --git a/app/docker/components/imageRegistry/por-image-registry-rate-limits.html b/app/docker/components/imageRegistry/por-image-registry-rate-limits.html
index 31d7eaa82..7097fa66c 100644
--- a/app/docker/components/imageRegistry/por-image-registry-rate-limits.html
+++ b/app/docker/components/imageRegistry/por-image-registry-rate-limits.html
@@ -1,4 +1,4 @@
-<div class="form-group" ng-if="$ctrl.isDockerHubRegistry && $ctrl.pullRateLimits">
+<div class="form-group" ng-if="$ctrl.pullRateLimits">
   <div class="col-sm-12 small">
     <div ng-if="$ctrl.pullRateLimits.remaining > 0" class="text-muted">
       <i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
diff --git a/app/docker/components/imageRegistry/por-image-registry-rate-limits.js b/app/docker/components/imageRegistry/por-image-registry-rate-limits.js
index 3418054f6..b87835a9d 100644
--- a/app/docker/components/imageRegistry/por-image-registry-rate-limits.js
+++ b/app/docker/components/imageRegistry/por-image-registry-rate-limits.js
@@ -5,10 +5,12 @@ import controller from './por-image-registry-rate-limits.controller';
 angular.module('portainer.docker').component('porImageRegistryRateLimits', {
   bindings: {
     endpoint: '<',
+    registry: '<',
     setValidity: '<',
     isAdmin: '<',
     isDockerHubRegistry: '<',
     isAuthenticated: '<',
+    registryId: '<',
   },
   controller,
   transclude: {
diff --git a/app/docker/components/imageRegistry/por-image-registry.controller.js b/app/docker/components/imageRegistry/por-image-registry.controller.js
index edf570f8b..5cf3fc709 100644
--- a/app/docker/components/imageRegistry/por-image-registry.controller.js
+++ b/app/docker/components/imageRegistry/por-image-registry.controller.js
@@ -5,18 +5,21 @@ import { RegistryTypes } from '@/portainer/models/registryTypes';
 
 class porImageRegistryController {
   /* @ngInject */
-  constructor($async, $scope, ImageHelper, RegistryService, DockerHubService, ImageService, Notifications) {
+  constructor($async, $scope, ImageHelper, RegistryService, EndpointService, ImageService, Notifications) {
     this.$async = $async;
     this.$scope = $scope;
     this.ImageHelper = ImageHelper;
     this.RegistryService = RegistryService;
-    this.DockerHubService = DockerHubService;
+    this.EndpointService = EndpointService;
     this.ImageService = ImageService;
     this.Notifications = Notifications;
 
-    this.onInit = this.onInit.bind(this);
     this.onRegistryChange = this.onRegistryChange.bind(this);
 
+    this.registries = [];
+    this.images = [];
+    this.defaultRegistry = new DockerHubViewModel();
+
     this.$scope.$watch(() => this.model.Registry, this.onRegistryChange);
   }
 
@@ -40,7 +43,7 @@ class porImageRegistryController {
       const registryImages = _.filter(this.images, (image) => _.includes(image, url));
       images = _.map(registryImages, (image) => _.replace(image, new RegExp(url + '/?'), ''));
     } else {
-      const registries = _.filter(this.availableRegistries, (reg) => this.isKnownRegistry(reg));
+      const registries = _.filter(this.registries, (reg) => this.isKnownRegistry(reg));
       const registryImages = _.flatMap(registries, (registry) => _.filter(this.images, (image) => _.includes(image, registry.URL)));
       const imagesWithoutKnown = _.difference(this.images, registryImages);
       images = _.filter(imagesWithoutKnown, (image) => !this.ImageHelper.imageContainsURL(image));
@@ -49,7 +52,7 @@ class porImageRegistryController {
   }
 
   isDockerHubRegistry() {
-    return this.model.UseRegistry && this.model.Registry.Name === 'DockerHub';
+    return this.model.UseRegistry && (this.model.Registry.Type === RegistryTypes.DOCKERHUB || this.model.Registry.Type === RegistryTypes.ANONYMOUS);
   }
 
   async onRegistryChange() {
@@ -63,29 +66,49 @@ class porImageRegistryController {
     return this.getRegistryURL(this.model.Registry) || 'docker.io';
   }
 
-  async onInit() {
-    try {
-      const [registries, dockerhub, images] = await Promise.all([
-        this.RegistryService.registries(),
-        this.DockerHubService.dockerhub(),
-        this.autoComplete ? this.ImageService.images() : [],
-      ]);
-      this.images = this.ImageService.getUniqueTagListFromImages(images);
-      this.availableRegistries = _.concat(dockerhub, registries);
+  async reloadRegistries() {
+    return this.$async(async () => {
+      try {
+        const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
+        this.registries = _.concat(this.defaultRegistry, registries);
 
-      const id = this.model.Registry.Id;
-      if (!id) {
-        this.model.Registry = dockerhub;
-      } else {
-        this.model.Registry = _.find(this.availableRegistries, { Id: id });
+        const id = this.model.Registry.Id;
+        const registry = _.find(this.registries, { Id: id });
+        if (!registry) {
+          this.model.Registry = this.defaultRegistry;
+        }
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registries');
       }
-    } catch (err) {
-      this.Notifications.error('Failure', err, 'Unable to retrieve registries');
+    });
+  }
+
+  async loadImages() {
+    return this.$async(async () => {
+      try {
+        if (!this.autoComplete) {
+          this.images = [];
+          return;
+        }
+
+        const images = await this.ImageService.images();
+        this.images = this.ImageService.getUniqueTagListFromImages(images);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve images');
+      }
+    });
+  }
+
+  $onChanges({ namespace, endpoint }) {
+    if ((namespace || endpoint) && this.endpoint.Id) {
+      this.reloadRegistries();
     }
   }
 
   $onInit() {
-    return this.$async(this.onInit);
+    return this.$async(async () => {
+      await this.loadImages();
+    });
   }
 }
 
diff --git a/app/docker/components/imageRegistry/por-image-registry.html b/app/docker/components/imageRegistry/por-image-registry.html
index 97b5c07e4..e332a9f4d 100644
--- a/app/docker/components/imageRegistry/por-image-registry.html
+++ b/app/docker/components/imageRegistry/por-image-registry.html
@@ -6,10 +6,9 @@
     </label>
     <div ng-class="$ctrl.inputClass">
       <select
-        ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
+        ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
         ng-model="$ctrl.model.Registry"
         id="image_registry"
-        selected-item-id="ctrl.selectedItemId"
         class="form-control"
       ></select>
     </div>
@@ -86,12 +85,13 @@
   <div ng-transclude></div>
 
   <por-image-registry-rate-limits
-    ng-show="$ctrl.checkRateLimits"
-    is-docker-hub-registry="$ctrl.isDockerHubRegistry()"
+    ng-if="$ctrl.checkRateLimits && $ctrl.isDockerHubRegistry()"
     endpoint="$ctrl.endpoint"
+    registry="$ctrl.model.Registry"
     set-validity="$ctrl.setValidity"
     is-authenticated="$ctrl.model.Registry.Authentication"
     is-admin="$ctrl.isAdmin"
+    registry-id="$ctrl.model.Registry.Id"
   >
   </por-image-registry-rate-limits>
 </div>
diff --git a/app/docker/components/imageRegistry/por-image-registry.js b/app/docker/components/imageRegistry/por-image-registry.js
index 52b001db0..5c397045a 100644
--- a/app/docker/components/imageRegistry/por-image-registry.js
+++ b/app/docker/components/imageRegistry/por-image-registry.js
@@ -3,7 +3,6 @@ angular.module('portainer.docker').component('porImageRegistry', {
   controller: 'porImageRegistryController',
   bindings: {
     model: '=', // must be of type PorImageRegistryModel
-    pullWarning: '<',
     autoComplete: '<',
     labelClass: '@',
     inputClass: '@',
@@ -12,6 +11,7 @@ angular.module('portainer.docker').component('porImageRegistry', {
     checkRateLimits: '<',
     onImageChange: '&',
     setValidity: '<',
+    namespace: '<',
   },
   require: {
     form: '^form',
diff --git a/app/docker/helpers/imageHelper.js b/app/docker/helpers/imageHelper.js
index af50cea4b..67529a10e 100644
--- a/app/docker/helpers/imageHelper.js
+++ b/app/docker/helpers/imageHelper.js
@@ -1,77 +1,85 @@
 import _ from 'lodash-es';
-import { RegistryTypes } from '@/portainer/models/registryTypes';
+import { RegistryTypes } from 'Portainer/models/registryTypes';
 
-angular.module('portainer.docker').factory('ImageHelper', [
-  function ImageHelperFactory() {
-    'use strict';
+angular.module('portainer.docker').factory('ImageHelper', ImageHelperFactory);
+function ImageHelperFactory() {
+  return {
+    isValidTag,
+    createImageConfigForContainer,
+    getImagesNamesForDownload,
+    removeDigestFromRepository,
+    imageContainsURL,
+  };
 
-    var helper = {};
+  function isValidTag(tag) {
+    return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
+  }
 
-    helper.isValidTag = isValidTag;
-    helper.createImageConfigForContainer = createImageConfigForContainer;
-    helper.getImagesNamesForDownload = getImagesNamesForDownload;
-    helper.removeDigestFromRepository = removeDigestFromRepository;
-    helper.imageContainsURL = imageContainsURL;
+  function getImagesNamesForDownload(images) {
+    var names = images.map(function (image) {
+      return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
+    });
+    return {
+      names: names,
+    };
+  }
 
-    function isValidTag(tag) {
-      return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
+  /**
+   *
+   * @param {PorImageRegistryModel} registry
+   */
+  function createImageConfigForContainer(imageModel) {
+    return {
+      fromImage: buildImageFullURI(imageModel),
+    };
+  }
+
+  function imageContainsURL(image) {
+    const split = _.split(image, '/');
+    const url = split[0];
+    if (split.length > 1) {
+      return _.includes(url, '.') || _.includes(url, ':');
     }
+    return false;
+  }
 
-    function getImagesNamesForDownload(images) {
-      var names = images.map(function (image) {
-        return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
-      });
-      return {
-        names: names,
-      };
-    }
+  function removeDigestFromRepository(repository) {
+    return repository.split('@sha')[0];
+  }
+}
+/**
+ * builds the complete uri for an image based on its registry
+ * @param {PorImageRegistryModel} imageModel
+ */
+export function buildImageFullURI(imageModel) {
+  if (!imageModel.UseRegistry) {
+    return imageModel.Image;
+  }
 
-    /**
-     *
-     * @param {PorImageRegistryModel} registry
-     */
-    function createImageConfigForContainer(registry) {
-      const data = {
-        fromImage: '',
-      };
-      let fullImageName = '';
+  let fullImageName = '';
 
-      if (registry.UseRegistry) {
-        if (registry.Registry.Type === RegistryTypes.GITLAB) {
-          const slash = _.startsWith(registry.Image, ':') ? '' : '/';
-          fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image;
-        } else if (registry.Registry.Type === RegistryTypes.QUAY) {
-          const name = registry.Registry.Quay.UseOrganisation ? registry.Registry.Quay.OrganisationName : registry.Registry.Username;
-          const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
-          fullImageName = url + name + '/' + registry.Image;
-        } else {
-          const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
-          fullImageName = url + registry.Image;
-        }
-        if (!_.includes(registry.Image, ':')) {
-          fullImageName += ':latest';
-        }
-      } else {
-        fullImageName = registry.Image;
-      }
+  switch (imageModel.Registry.Type) {
+    case RegistryTypes.GITLAB:
+      fullImageName = imageModel.Registry.URL + '/' + imageModel.Registry.Gitlab.ProjectPath + (imageModel.Image.startsWith(':') ? '' : '/') + imageModel.Image;
+      break;
+    case RegistryTypes.ANONYMOUS:
+      fullImageName = imageModel.Image;
+      break;
+    case RegistryTypes.QUAY:
+      fullImageName =
+        (imageModel.Registry.URL ? imageModel.Registry.URL + '/' : '') +
+        (imageModel.Registry.Quay.UseOrganisation ? imageModel.Registry.Quay.OrganisationName : imageModel.Registry.Username) +
+        '/' +
+        imageModel.Image;
+      break;
+    default:
+      fullImageName = imageModel.Registry.URL + '/' + imageModel.Image;
+      break;
+  }
 
-      data.fromImage = fullImageName;
-      return data;
-    }
+  if (!imageModel.Image.includes(':')) {
+    fullImageName += ':latest';
+  }
 
-    function imageContainsURL(image) {
-      const split = _.split(image, '/');
-      const url = split[0];
-      if (split.length > 1) {
-        return _.includes(url, '.') || _.includes(url, ':');
-      }
-      return false;
-    }
-
-    function removeDigestFromRepository(repository) {
-      return repository.split('@sha')[0];
-    }
-
-    return helper;
-  },
-]);
+  return fullImageName;
+}
diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js
index f75b000ae..8b7c062d6 100644
--- a/app/docker/views/containers/create/createContainerController.js
+++ b/app/docker/views/containers/create/createContainerController.js
@@ -575,7 +575,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
     }
 
     function loadFromContainerImageConfig() {
-      RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
+      RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image, endpoint.Id)
         .then((model) => {
           $scope.formValues.RegistryModel = model;
         })
diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html
index 6f2aaa9cd..0ce6d50b1 100644
--- a/app/docker/views/containers/create/createcontainer.html
+++ b/app/docker/views/containers/create/createcontainer.html
@@ -40,15 +40,14 @@
             <!-- image-and-registry -->
             <por-image-registry
               model="formValues.RegistryModel"
-              pull-warning="formValues.alwaysPull"
               ng-if="formValues.RegistryModel.Registry"
               auto-complete="true"
               label-class="col-sm-1"
               input-class="col-sm-11"
+              on-image-change="onImageNameChange()"
               endpoint="endpoint"
               is-admin="isAdmin"
               check-rate-limits="formValues.alwaysPull"
-              on-image-change="onImageNameChange()"
               set-validity="setPullImageValidity"
             >
               <!-- always-pull -->
diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html
index 097162ce0..52076b7e6 100644
--- a/app/docker/views/containers/edit/container.html
+++ b/app/docker/views/containers/edit/container.html
@@ -190,7 +190,7 @@
           </div>
           <!-- !tag-description -->
           <!-- image-and-registry -->
-          <por-image-registry model="config.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
+          <por-image-registry model="config.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" endpoint="endpoint"></por-image-registry>
           <!-- !image-and-registry -->
           <!-- tag-note -->
           <div class="form-group">
diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js
index 83db4e944..c828853f0 100644
--- a/app/docker/views/containers/edit/containerController.js
+++ b/app/docker/views/containers/edit/containerController.js
@@ -21,7 +21,6 @@ angular.module('portainer.docker').controller('ContainerController', [
   'ImageService',
   'HttpRequestHelper',
   'Authentication',
-  'StateManager',
   'endpoint',
   function (
     $q,
@@ -42,9 +41,9 @@ angular.module('portainer.docker').controller('ContainerController', [
     ImageService,
     HttpRequestHelper,
     Authentication,
-    StateManager,
     endpoint
   ) {
+    $scope.endpoint = endpoint;
     $scope.activityTime = 0;
     $scope.portBindings = [];
     $scope.displayRecreateButton = false;
@@ -295,7 +294,7 @@ angular.module('portainer.docker').controller('ContainerController', [
         if (!pullImage) {
           return $q.when();
         }
-        return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image).then(function pullImage(registryModel) {
+        return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image, endpoint.Id).then((registryModel) => {
           return ImageService.pullImage(registryModel, true);
         });
       }
diff --git a/app/docker/views/images/edit/image.html b/app/docker/views/images/edit/image.html
index b8a686edb..e117000c5 100644
--- a/app/docker/views/images/edit/image.html
+++ b/app/docker/views/images/edit/image.html
@@ -63,7 +63,7 @@
       <rd-widget-body>
         <form class="form-horizontal">
           <!-- image-and-registry -->
-          <por-image-registry model="formValues.RegistryModel" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
+          <por-image-registry model="formValues.RegistryModel" endpoint="endpoint" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
           <!-- !image-and-registry -->
           <!-- tag-note -->
           <div class="form-group">
diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js
index 597cdb7a0..e7408de7d 100644
--- a/app/docker/views/images/edit/imageController.js
+++ b/app/docker/views/images/edit/imageController.js
@@ -2,11 +2,12 @@ import _ from 'lodash-es';
 import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
 
 angular.module('portainer.docker').controller('ImageController', [
+  '$async',
   '$q',
   '$scope',
   '$transition$',
   '$state',
-  '$timeout',
+  'endpoint',
   'ImageService',
   'ImageHelper',
   'RegistryService',
@@ -15,7 +16,8 @@ angular.module('portainer.docker').controller('ImageController', [
   'ModalService',
   'FileSaver',
   'Blob',
-  function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
+  function ($async, $q, $scope, $transition$, $state, endpoint, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) {
+    $scope.endpoint = endpoint;
     $scope.formValues = {
       RegistryModel: new PorImageRegistryModel(),
     };
@@ -53,39 +55,38 @@ angular.module('portainer.docker').controller('ImageController', [
         });
     };
 
-    $scope.pushTag = function (repository) {
-      $('#uploadResourceHint').show();
-      RegistryService.retrievePorRegistryModelFromRepository(repository)
-        .then(function success(registryModel) {
-          return ImageService.pushImage(registryModel);
-        })
-        .then(function success() {
-          Notifications.success('Image successfully pushed', repository);
-        })
-        .catch(function error(err) {
-          Notifications.error('Failure', err, 'Unable to push image to repository');
-        })
-        .finally(function final() {
-          $('#uploadResourceHint').hide();
-        });
-    };
+    $scope.pushTag = pushTag;
 
-    $scope.pullTag = function (repository) {
-      $('#downloadResourceHint').show();
-      RegistryService.retrievePorRegistryModelFromRepository(repository)
-        .then(function success(registryModel) {
-          return ImageService.pullImage(registryModel, false);
-        })
-        .then(function success() {
-          Notifications.success('Image successfully pulled', repository);
-        })
-        .catch(function error(err) {
-          Notifications.error('Failure', err, 'Unable to pull image');
-        })
-        .finally(function final() {
+    async function pushTag(repository) {
+      return $async(async () => {
+        $('#uploadResourceHint').show();
+        try {
+          const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
+          await ImageService.pushImage(registryModel);
+          Notifications.success('Image successfully pushed', repository);
+        } catch (err) {
+          Notifications.error('Failure', err, 'Unable to push image to repository');
+        } finally {
+          $('#uploadResourceHint').hide();
+        }
+      });
+    }
+
+    $scope.pullTag = pullTag;
+    async function pullTag(repository) {
+      return $async(async () => {
+        $('#downloadResourceHint').show();
+        try {
+          const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
+          await ImageService.pullImage(registryModel);
+          Notifications.success('Image successfully pushed', repository);
+        } catch (err) {
+          Notifications.error('Failure', err, 'Unable to push image to repository');
+        } finally {
           $('#downloadResourceHint').hide();
-        });
-    };
+        }
+      });
+    }
 
     $scope.removeTag = function (repository) {
       ImageService.deleteImage(repository, false)
diff --git a/app/docker/views/images/images.html b/app/docker/views/images/images.html
index a2f3034c3..7130a8bd1 100644
--- a/app/docker/views/images/images.html
+++ b/app/docker/views/images/images.html
@@ -17,7 +17,6 @@
           <por-image-registry
             model="formValues.RegistryModel"
             auto-complete="true"
-            pull-warning="true"
             label-class="col-sm-1"
             input-class="col-sm-11"
             endpoint="endpoint"
diff --git a/app/docker/views/registries/access/registryAccess.html b/app/docker/views/registries/access/registryAccess.html
new file mode 100644
index 000000000..b139ba1d3
--- /dev/null
+++ b/app/docker/views/registries/access/registryAccess.html
@@ -0,0 +1,16 @@
+<rd-header>
+  <rd-header-title title-text="Registry access"></rd-header-title>
+  <rd-header-content> <a ui-sref="docker.registries">Registries</a> &gt; {{ $ctrl.registry.Name }} &gt; Access management </rd-header-content>
+</rd-header>
+
+<registry-details registry="$ctrl.registry" ng-if="$ctrl.registry"></registry-details>
+
+<por-access-management
+  ng-if="$ctrl.registry && $ctrl.endpointGroup"
+  access-controlled-entity="$ctrl.registryEndpointAccesses"
+  entity-type="registry"
+  action-in-progress="$ctrl.state.actionInProgress"
+  update-access="$ctrl.updateAccess"
+  filter-users="$ctrl.filterUsers"
+>
+</por-access-management>
diff --git a/app/docker/views/registries/access/registryAccess.js b/app/docker/views/registries/access/registryAccess.js
new file mode 100644
index 000000000..7ee0814dd
--- /dev/null
+++ b/app/docker/views/registries/access/registryAccess.js
@@ -0,0 +1,7 @@
+angular.module('portainer.docker').component('dockerRegistryAccessView', {
+  templateUrl: './registryAccess.html',
+  controller: 'DockerRegistryAccessController',
+  bindings: {
+    endpoint: '<',
+  },
+});
diff --git a/app/docker/views/registries/access/registryAccessController.js b/app/docker/views/registries/access/registryAccessController.js
new file mode 100644
index 000000000..c30d25dd4
--- /dev/null
+++ b/app/docker/views/registries/access/registryAccessController.js
@@ -0,0 +1,67 @@
+import { TeamAccessViewModel, UserAccessViewModel } from 'Portainer/models/access';
+
+class DockerRegistryAccessController {
+  /* @ngInject */
+  constructor($async, $state, Notifications, EndpointService, GroupService) {
+    this.$async = $async;
+    this.$state = $state;
+    this.Notifications = Notifications;
+    this.EndpointService = EndpointService;
+    this.GroupService = GroupService;
+
+    this.updateAccess = this.updateAccess.bind(this);
+    this.filterUsers = this.filterUsers.bind(this);
+  }
+
+  updateAccess() {
+    return this.$async(async () => {
+      this.state.actionInProgress = true;
+      try {
+        await this.EndpointService.updateRegistryAccess(this.state.endpointId, this.state.registryId, this.registryEndpointAccesses);
+        this.Notifications.success('Access successfully updated');
+        this.$state.reload();
+      } catch (err) {
+        this.state.actionInProgress = false;
+        this.Notifications.error('Failure', err, 'Unable to update accesses');
+      }
+    });
+  }
+
+  filterUsers(users) {
+    const endpointUsers = this.endpoint.UserAccessPolicies;
+    const endpointTeams = this.endpoint.TeamAccessPolicies;
+
+    const endpointGroupUsers = this.endpointGroup.UserAccessPolicies;
+    const endpointGroupTeams = this.endpointGroup.TeamAccessPolicies;
+
+    return users.filter((userOrTeam) => {
+      const userRole = userOrTeam instanceof UserAccessViewModel && (endpointUsers[userOrTeam.Id] || endpointGroupUsers[userOrTeam.Id]);
+      const teamRole = userOrTeam instanceof TeamAccessViewModel && (endpointTeams[userOrTeam.Id] || endpointGroupTeams[userOrTeam.Id]);
+
+      return userRole || teamRole;
+    });
+  }
+
+  $onInit() {
+    return this.$async(async () => {
+      try {
+        this.state = {
+          viewReady: false,
+          actionInProgress: false,
+          endpointId: this.$state.params.endpointId,
+          registryId: this.$state.params.id,
+        };
+        this.registry = await this.EndpointService.registry(this.state.endpointId, this.state.registryId);
+        this.registryEndpointAccesses = this.registry.RegistryAccesses[this.state.endpointId] || {};
+        this.endpointGroup = await this.GroupService.group(this.endpoint.GroupId);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registry details');
+      } finally {
+        this.state.viewReady = true;
+      }
+    });
+  }
+}
+
+export default DockerRegistryAccessController;
+angular.module('portainer.docker').controller('DockerRegistryAccessController', DockerRegistryAccessController);
diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js
index f1aef75a4..73a4f70be 100644
--- a/app/docker/views/services/edit/serviceController.js
+++ b/app/docker/views/services/edit/serviceController.js
@@ -50,7 +50,6 @@ angular.module('portainer.docker').controller('ServiceController', [
   'VolumeService',
   'ImageHelper',
   'WebhookService',
-  'EndpointProvider',
   'clipboard',
   'WebhookHelper',
   'NetworkService',
@@ -82,7 +81,6 @@ angular.module('portainer.docker').controller('ServiceController', [
     VolumeService,
     ImageHelper,
     WebhookService,
-    EndpointProvider,
     clipboard,
     WebhookHelper,
     NetworkService,
@@ -337,7 +335,7 @@ angular.module('portainer.docker').controller('ServiceController', [
             Notifications.error('Failure', err, 'Unable to delete webhook');
           });
       } else {
-        WebhookService.createServiceWebhook(service.Id, EndpointProvider.endpointID())
+        WebhookService.createServiceWebhook(service.Id, endpoint.Id)
           .then(function success(data) {
             $scope.WebhookExists = true;
             $scope.webhookID = data.Id;
@@ -688,7 +686,7 @@ angular.module('portainer.docker').controller('ServiceController', [
             availableImages: ImageService.images(),
             availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
             availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
-            webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
+            webhooks: WebhookService.webhooks(service.Id, endpoint.Id),
           });
         })
         .then(async function success(data) {
diff --git a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js
index c3c6bc3f4..196993d66 100644
--- a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js
+++ b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js
@@ -1,6 +1,6 @@
 export class EditEdgeGroupController {
   /* @ngInject */
-  constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService, EndpointHelper) {
+  constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService) {
     this.EdgeGroupService = EdgeGroupService;
     this.GroupService = GroupService;
     this.TagService = TagService;
@@ -8,7 +8,6 @@ export class EditEdgeGroupController {
     this.$state = $state;
     this.$async = $async;
     this.EndpointService = EndpointService;
-    this.EndpointHelper = EndpointHelper;
 
     this.state = {
       actionInProgress: false,
diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js
index d867a288f..acdde4334 100644
--- a/app/kubernetes/__module.js
+++ b/app/kubernetes/__module.js
@@ -1,4 +1,6 @@
-angular.module('portainer.kubernetes', ['portainer.app']).config([
+import registriesModule from './registries';
+
+angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).config([
   '$stateRegistryProvider',
   function ($stateRegistryProvider) {
     'use strict';
@@ -272,6 +274,26 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
       },
     };
 
+    const registries = {
+      name: 'kubernetes.registries',
+      url: '/registries',
+      views: {
+        'content@': {
+          component: 'endpointRegistriesView',
+        },
+      },
+    };
+
+    const registriesAccess = {
+      name: 'kubernetes.registries.access',
+      url: '/:id/access',
+      views: {
+        'content@': {
+          component: 'kubernetesRegistryAccessView',
+        },
+      },
+    };
+
     $stateRegistryProvider.register(kubernetes);
     $stateRegistryProvider.register(applications);
     $stateRegistryProvider.register(applicationCreation);
@@ -297,5 +319,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
     $stateRegistryProvider.register(resourcePoolAccess);
     $stateRegistryProvider.register(volumes);
     $stateRegistryProvider.register(volume);
+    $stateRegistryProvider.register(registries);
+    $stateRegistryProvider.register(registriesAccess);
   },
 ]);
diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js
index 18417d951..c9459ffba 100644
--- a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js
+++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js
@@ -28,7 +28,7 @@ angular.module('portainer.docker').controller('KubernetesConfigurationsDatatable
     };
 
     this.isSystemConfig = function (item) {
-      return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item);
+      return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item) || item.IsRegistrySecret;
     };
 
     this.isExternalConfiguration = function (item) {
diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html
index f640ce220..f7439c9a6 100644
--- a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html
+++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html
@@ -15,7 +15,17 @@
 </li>
 <li class="sidebar-list">
   <a ui-sref="kubernetes.cluster({endpointId: $ctrl.endpointId})" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
-  <div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ($ctrl.currentState === 'kubernetes.cluster' || $ctrl.currentState === 'portainer.endpoints.endpoint.kubernetesConfig')">
-    <a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
+  <div
+    ng-if="
+      $ctrl.adminAccess &&
+      ['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access'].includes($ctrl.currentState)
+    "
+  >
+    <div class="sidebar-sublist">
+      <a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
+    </div>
+    <div class="sidebar-sublist">
+      <a ui-sref="kubernetes.registries({endpointId: $ctrl.endpointId})" ui-sref-active="active">Registries</a>
+    </div>
   </div>
 </li>
diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js
index a4b3406cf..881e98ece 100644
--- a/app/kubernetes/converters/application.js
+++ b/app/kubernetes/converters/application.js
@@ -62,6 +62,9 @@ class KubernetesApplicationConverter {
     if (containers.length) {
       res.Image = containers[0].image;
     }
+    if (data.spec.template && data.spec.template.spec && data.spec.template.spec.imagePullSecrets && data.spec.template.spec.imagePullSecrets.length) {
+      res.RegistryId = parseInt(data.spec.template.spec.imagePullSecrets[0].name.replace('registry-', ''), 10);
+    }
     res.CreationDate = data.metadata.creationTimestamp;
     res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
     res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];
@@ -268,7 +271,8 @@ class KubernetesApplicationConverter {
     res.Name = app.Name;
     res.StackName = app.StackName;
     res.ApplicationOwner = app.ApplicationOwner;
-    res.Image = app.Image;
+    res.ImageModel.Image = app.Image;
+    res.ImageModel.Registry.Id = app.RegistryId;
     res.ReplicaCount = app.TotalPodsCount;
     res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory);
     res.CpuLimit = app.Limits.Cpu;
@@ -292,7 +296,10 @@ class KubernetesApplicationConverter {
       res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
     }
 
-    KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
+    if (app.Pods && app.Pods.length) {
+      KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
+    }
+
     return res;
   }
 
diff --git a/app/kubernetes/converters/configuration.js b/app/kubernetes/converters/configuration.js
index e034fc2c6..c5e4c32fb 100644
--- a/app/kubernetes/converters/configuration.js
+++ b/app/kubernetes/converters/configuration.js
@@ -14,6 +14,7 @@ class KubernetesConfigurationConverter {
       res.Data[entry.Key] = entry.Value;
     });
     res.ConfigurationOwner = secret.ConfigurationOwner;
+    res.IsRegistrySecret = secret.IsRegistrySecret;
     return res;
   }
 
diff --git a/app/kubernetes/converters/daemonSet.js b/app/kubernetes/converters/daemonSet.js
index 063005593..8f95eba1c 100644
--- a/app/kubernetes/converters/daemonSet.js
+++ b/app/kubernetes/converters/daemonSet.js
@@ -10,10 +10,11 @@ import {
 import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
 import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
 import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
+import { buildImageFullURI } from 'Docker/helpers/imageHelper';
 
 class KubernetesDaemonSetConverter {
   /**
-   * Generate KubernetesDaemonSet from KubenetesApplicationFormValues
+   * Generate KubernetesDaemonSet from KubernetesApplicationFormValues
    * @param {KubernetesApplicationFormValues} formValues
    */
   static applicationFormValuesToDaemonSet(formValues, volumeClaims) {
@@ -23,7 +24,7 @@ class KubernetesDaemonSetConverter {
     res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
     res.ApplicationOwner = formValues.ApplicationOwner;
     res.ApplicationName = formValues.Name;
-    res.Image = formValues.Image;
+    res.ImageModel = formValues.ImageModel;
     res.CpuLimit = formValues.CpuLimit;
     res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
     res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@@ -35,7 +36,7 @@ class KubernetesDaemonSetConverter {
 
   /**
    * Generate CREATE payload from DaemonSet
-   * @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from
+   * @param {KubernetesDaemonSetPayload} model DaemonSet to generate payload from
    */
   static createPayload(daemonSet) {
     const payload = new KubernetesDaemonSetCreatePayload();
@@ -50,7 +51,10 @@ class KubernetesDaemonSetConverter {
     payload.spec.template.metadata.labels.app = daemonSet.Name;
     payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
     payload.spec.template.spec.containers[0].name = daemonSet.Name;
-    payload.spec.template.spec.containers[0].image = daemonSet.Image;
+    payload.spec.template.spec.containers[0].image = buildImageFullURI(daemonSet.ImageModel);
+    if (daemonSet.ImageModel.Registry && daemonSet.ImageModel.Registry.Authentication) {
+      payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${daemonSet.ImageModel.Registry.Id}` }];
+    }
     payload.spec.template.spec.affinity = daemonSet.Affinity;
     KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env);
     KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);
diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js
index 471faa179..080644641 100644
--- a/app/kubernetes/converters/deployment.js
+++ b/app/kubernetes/converters/deployment.js
@@ -11,6 +11,7 @@ import {
 import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
 import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
 import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
+import { buildImageFullURI } from 'Docker/helpers/imageHelper';
 
 class KubernetesDeploymentConverter {
   /**
@@ -25,7 +26,7 @@ class KubernetesDeploymentConverter {
     res.ApplicationOwner = formValues.ApplicationOwner;
     res.ApplicationName = formValues.Name;
     res.ReplicaCount = formValues.ReplicaCount;
-    res.Image = formValues.Image;
+    res.ImageModel = formValues.ImageModel;
     res.CpuLimit = formValues.CpuLimit;
     res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
     res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@@ -53,7 +54,10 @@ class KubernetesDeploymentConverter {
     payload.spec.template.metadata.labels.app = deployment.Name;
     payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
     payload.spec.template.spec.containers[0].name = deployment.Name;
-    payload.spec.template.spec.containers[0].image = deployment.Image;
+    payload.spec.template.spec.containers[0].image = buildImageFullURI(deployment.ImageModel);
+    if (deployment.ImageModel.Registry && deployment.ImageModel.Registry.Authentication) {
+      payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${deployment.ImageModel.Registry.Id}` }];
+    }
     payload.spec.template.spec.affinity = deployment.Affinity;
     KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env);
     KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);
diff --git a/app/kubernetes/converters/resourcePool.js b/app/kubernetes/converters/resourcePool.js
index 234d13d27..eb20ce5e4 100644
--- a/app/kubernetes/converters/resourcePool.js
+++ b/app/kubernetes/converters/resourcePool.js
@@ -28,7 +28,16 @@ class KubernetesResourcePoolConverter {
       }
     });
     const ingresses = _.without(ingMap, undefined);
-    return [namespace, quota, ingresses];
+    const registries = _.map(formValues.Registries, (r) => {
+      if (!r.RegistryAccesses[formValues.EndpointId]) {
+        r.RegistryAccesses[formValues.EndpointId] = { Namespaces: [] };
+      }
+      if (!_.includes(r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name)) {
+        r.RegistryAccesses[formValues.EndpointId].Namespaces = [...r.RegistryAccesses[formValues.EndpointId].Namespaces, formValues.Name];
+      }
+      return r;
+    });
+    return [namespace, quota, ingresses, registries];
   }
 }
 
diff --git a/app/kubernetes/converters/secret.js b/app/kubernetes/converters/secret.js
index 72d37f070..16920f784 100644
--- a/app/kubernetes/converters/secret.js
+++ b/app/kubernetes/converters/secret.js
@@ -56,6 +56,9 @@ class KubernetesSecretConverter {
     res.Namespace = payload.metadata.namespace;
     res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
     res.CreationDate = payload.metadata.creationTimestamp;
+
+    res.IsRegistrySecret = payload.metadata.annotations && !!payload.metadata.annotations['portainer.io/registry.id'];
+
     res.Yaml = yaml ? yaml.data : '';
 
     res.Data = _.map(payload.data, (value, key) => {
diff --git a/app/kubernetes/converters/statefulSet.js b/app/kubernetes/converters/statefulSet.js
index 26cffd4ac..a7693513b 100644
--- a/app/kubernetes/converters/statefulSet.js
+++ b/app/kubernetes/converters/statefulSet.js
@@ -12,6 +12,7 @@ import {
 import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
 import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
 import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
+import { buildImageFullURI } from 'Docker/helpers/imageHelper';
 import KubernetesPersistentVolumeClaimConverter from './persistentVolumeClaim';
 
 class KubernetesStatefulSetConverter {
@@ -27,7 +28,7 @@ class KubernetesStatefulSetConverter {
     res.ApplicationOwner = formValues.ApplicationOwner;
     res.ApplicationName = formValues.Name;
     res.ReplicaCount = formValues.ReplicaCount;
-    res.Image = formValues.Image;
+    res.ImageModel = formValues.ImageModel;
     res.CpuLimit = formValues.CpuLimit;
     res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
     res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
@@ -56,7 +57,12 @@ class KubernetesStatefulSetConverter {
     payload.spec.template.metadata.labels.app = statefulSet.Name;
     payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
     payload.spec.template.spec.containers[0].name = statefulSet.Name;
-    payload.spec.template.spec.containers[0].image = statefulSet.Image;
+    if (statefulSet.ImageModel.Image) {
+      payload.spec.template.spec.containers[0].image = buildImageFullURI(statefulSet.ImageModel);
+      if (statefulSet.ImageModel.Registry && statefulSet.ImageModel.Registry.Authentication) {
+        payload.spec.template.spec.imagePullSecrets = [{ name: `registry-${statefulSet.ImageModel.Registry.Id}` }];
+      }
+    }
     payload.spec.template.spec.affinity = statefulSet.Affinity;
     KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env);
     KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts);
diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js
index 6faffaeb6..83dd05499 100644
--- a/app/kubernetes/models/application/formValues.js
+++ b/app/kubernetes/models/application/formValues.js
@@ -1,37 +1,34 @@
+import { PorImageRegistryModel } from '@/docker/models/porImageRegistry';
 import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationPlacementTypes } from './models';
 
 /**
  * KubernetesApplicationFormValues Model
  */
-const _KubernetesApplicationFormValues = Object.freeze({
-  ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation)
-  ResourcePool: {},
-  Name: '',
-  StackName: '',
-  ApplicationOwner: '',
-  Image: '',
-  Note: '',
-  MemoryLimit: 0,
-  CpuLimit: 0,
-  DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
-  ReplicaCount: 1,
-  AutoScaler: {},
-  Containers: [],
-  EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
-  DataAccessPolicy: KubernetesApplicationDataAccessPolicies.ISOLATED,
-  PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
-  Configurations: [], // KubernetesApplicationConfigurationFormValue list
-  PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
-  PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
-  PlacementType: KubernetesApplicationPlacementTypes.MANDATORY,
-  Placements: [], // KubernetesApplicationPlacementFormValue list
-  OriginalIngresses: undefined,
-});
-
-export class KubernetesApplicationFormValues {
-  constructor() {
-    Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationFormValues)));
-  }
+export function KubernetesApplicationFormValues() {
+  return {
+    ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation)
+    ResourcePool: {},
+    Name: '',
+    StackName: '',
+    ApplicationOwner: '',
+    ImageModel: new PorImageRegistryModel(),
+    Note: '',
+    MemoryLimit: 0,
+    CpuLimit: 0,
+    DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
+    ReplicaCount: 1,
+    AutoScaler: {},
+    Containers: [],
+    EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
+    DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
+    PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
+    Configurations: [], // KubernetesApplicationConfigurationFormValue list
+    PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
+    PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
+    PlacementType: KubernetesApplicationPlacementTypes.PREFERRED,
+    Placements: [], // KubernetesApplicationPlacementFormValue list
+    OriginalIngresses: undefined,
+  };
 }
 
 export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({
diff --git a/app/kubernetes/models/daemon-set/models.js b/app/kubernetes/models/daemon-set/models.js
index a4c4fc411..4f2f82ed1 100644
--- a/app/kubernetes/models/daemon-set/models.js
+++ b/app/kubernetes/models/daemon-set/models.js
@@ -5,7 +5,7 @@ const _KubernetesDaemonSet = Object.freeze({
   Namespace: '',
   Name: '',
   StackName: '',
-  Image: '',
+  ImageModel: null,
   Env: [],
   CpuLimit: 0,
   MemoryLimit: 0,
diff --git a/app/kubernetes/models/deployment/models.js b/app/kubernetes/models/deployment/models.js
index df8a8d79c..05a6963df 100644
--- a/app/kubernetes/models/deployment/models.js
+++ b/app/kubernetes/models/deployment/models.js
@@ -6,7 +6,7 @@ const _KubernetesDeployment = Object.freeze({
   Name: '',
   StackName: '',
   ReplicaCount: 0,
-  Image: '',
+  ImageModel: null,
   Env: [],
   CpuLimit: 0,
   MemoryLimit: 0,
diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js
index 55efbfbba..e4e9f95f1 100644
--- a/app/kubernetes/models/resource-pool/formValues.js
+++ b/app/kubernetes/models/resource-pool/formValues.js
@@ -1,9 +1,13 @@
 export function KubernetesResourcePoolFormValues(defaults) {
-  this.Name = '';
-  this.MemoryLimit = defaults.MemoryLimit;
-  this.CpuLimit = defaults.CpuLimit;
-  this.HasQuota = false;
-  this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue
+  return {
+    Name: '',
+    MemoryLimit: defaults.MemoryLimit,
+    CpuLimit: defaults.CpuLimit,
+    HasQuota: false,
+    IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
+    Registries: [], // RegistryViewModel
+    EndpointId: 0,
+  };
 }
 
 /**
diff --git a/app/kubernetes/models/stateful-set/models.js b/app/kubernetes/models/stateful-set/models.js
index 28609416c..fa445799f 100644
--- a/app/kubernetes/models/stateful-set/models.js
+++ b/app/kubernetes/models/stateful-set/models.js
@@ -6,7 +6,7 @@ const _KubernetesStatefulSet = Object.freeze({
   Name: '',
   StackName: '',
   ReplicaCount: 0,
-  Image: '',
+  ImageModel: null,
   Env: [],
   CpuLimit: '',
   MemoryLimit: '',
diff --git a/app/kubernetes/registries/index.js b/app/kubernetes/registries/index.js
new file mode 100644
index 000000000..54373821c
--- /dev/null
+++ b/app/kubernetes/registries/index.js
@@ -0,0 +1,5 @@
+import angular from 'angular';
+
+import { kubernetesRegistryAccessView } from './kube-registry-access-view';
+
+export default angular.module('portainer.kubernetes.registries', []).component('kubernetesRegistryAccessView', kubernetesRegistryAccessView).name;
diff --git a/app/kubernetes/registries/kube-registry-access-view/index.js b/app/kubernetes/registries/kube-registry-access-view/index.js
new file mode 100644
index 000000000..bf51890e0
--- /dev/null
+++ b/app/kubernetes/registries/kube-registry-access-view/index.js
@@ -0,0 +1,9 @@
+import controller from './kube-registry-access-view.controller';
+
+export const kubernetesRegistryAccessView = {
+  templateUrl: './kube-registry-access-view.html',
+  controller,
+  bindings: {
+    endpoint: '<',
+  },
+};
diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js
new file mode 100644
index 000000000..e2703bd2c
--- /dev/null
+++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.controller.js
@@ -0,0 +1,70 @@
+export default class KubernetesRegistryAccessController {
+  /* @ngInject */
+  constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService, KubernetesNamespaceHelper) {
+    this.$async = $async;
+    this.$state = $state;
+    this.Notifications = Notifications;
+    this.KubernetesResourcePoolService = KubernetesResourcePoolService;
+    this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
+    this.EndpointService = EndpointService;
+
+    this.state = {
+      actionInProgress: false,
+    };
+
+    this.selectedResourcePools = [];
+    this.resourcePools = [];
+    this.savedResourcePools = [];
+
+    this.handleRemove = this.handleRemove.bind(this);
+  }
+
+  async submit() {
+    return this.updateNamespaces([...this.savedResourcePools.map(({ value }) => value), ...this.selectedResourcePools.map((pool) => pool.name)]);
+  }
+
+  handleRemove(namespaces) {
+    const removeNamespaces = namespaces.map(({ value }) => value);
+
+    return this.updateNamespaces(this.savedResourcePools.map(({ value }) => value).filter((value) => !removeNamespaces.includes(value)));
+  }
+
+  updateNamespaces(namespaces) {
+    return this.$async(async () => {
+      try {
+        await this.EndpointService.updateRegistryAccess(this.endpoint.Id, this.registry.Id, {
+          namespaces,
+        });
+        this.$state.reload();
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Failed saving registry access');
+      }
+    });
+  }
+
+  $onInit() {
+    return this.$async(async () => {
+      try {
+        this.state = {
+          registryId: this.$state.params.id,
+        };
+        this.registry = await this.EndpointService.registry(this.endpoint.Id, this.state.registryId);
+        if (this.registry.RegistryAccesses && this.registry.RegistryAccesses[this.endpoint.Id]) {
+          this.savedResourcePools = this.registry.RegistryAccesses[this.endpoint.Id].Namespaces.map((value) => ({ value }));
+        }
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registry details');
+      }
+
+      try {
+        const resourcePools = await this.KubernetesResourcePoolService.get();
+
+        this.resourcePools = resourcePools
+          .filter((pool) => !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name))
+          .map((pool) => ({ name: pool.Namespace.Name, id: pool.Namespace.Id }));
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
+      }
+    });
+  }
+}
diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html
new file mode 100644
index 000000000..969d3d3a1
--- /dev/null
+++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html
@@ -0,0 +1,72 @@
+<rd-header>
+  <rd-header-title title-text="Registry access"></rd-header-title>
+  <rd-header-content> <a ui-sref="kubernetes.registries">Registries</a> &gt; {{ $ctrl.registry.Name }} &gt; Access management </rd-header-content>
+</rd-header>
+
+<registry-details registry="$ctrl.registry" ng-if="$ctrl.registry"></registry-details>
+
+<div class="row">
+  <div class="col-sm-12">
+    <rd-widget>
+      <rd-widget-header icon="fa-user-lock" title-text="Create access"></rd-widget-header>
+      <rd-widget-body>
+        <form class="form-horizontal">
+          <div class="form-group">
+            <label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
+              Select namespaces
+            </label>
+            <div class="col-sm-9 col-lg-4">
+              <span class="small text-muted" ng-if="!$ctrl.resourcePools.length">
+                No namespaces available.
+              </span>
+              <span
+                isteven-multi-select
+                ng-if="$ctrl.resourcePools.length"
+                input-model="$ctrl.resourcePools"
+                output-model="$ctrl.selectedResourcePools"
+                button-label="name"
+                item-label="name"
+                tick-property="ticked"
+                helper-elements="filter"
+                search-property="name"
+                translation="{nothingSelected: 'Select one or more namespaces', search: 'Search...'}"
+              >
+              </span>
+            </div>
+          </div>
+
+          <!-- actions -->
+          <div class="form-group">
+            <div class="col-sm-12">
+              <button
+                type="submit"
+                class="btn btn-primary btn-sm"
+                ng-disabled="$ctrl.selectedResourcePools.length === 0 || $ctrl.state.actionInProgress"
+                ng-click="$ctrl.submit()"
+                button-spinner="$ctrl.state.actionInProgress"
+              >
+                <span ng-hide="$ctrl.state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create access</span>
+                <span ng-show="$ctrl.state.actionInProgress">Creating access...</span>
+              </button>
+            </div>
+          </div>
+          <!-- !actions -->
+        </form>
+      </rd-widget-body>
+    </rd-widget>
+  </div>
+</div>
+<div class="row">
+  <div class="col-sm-12">
+    <strings-datatable
+      title-text="Access"
+      title-icon="fa-user-lock"
+      table-key="access_registry_resourcepools"
+      dataset="$ctrl.savedResourcePools"
+      empty-dataset-message="No namespace has been authorized yet."
+      on-remove="($ctrl.handleRemove)"
+      column-header="Namespace"
+    >
+    </strings-datatable>
+  </div>
+</div>
diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js
index ac38e2d4f..e89069191 100644
--- a/app/kubernetes/services/resourcePoolService.js
+++ b/app/kubernetes/services/resourcePoolService.js
@@ -5,7 +5,7 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'
 import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
 
 /* @ngInject */
-export function KubernetesResourcePoolService($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
+export function KubernetesResourcePoolService($async, EndpointService, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
   return {
     get,
     create,
@@ -59,7 +59,7 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
   function create(formValues) {
     return $async(async () => {
       try {
-        const [namespace, quota, ingresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
+        const [namespace, quota, ingresses, registries] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues);
         await KubernetesNamespaceService.create(namespace);
 
         if (quota) {
@@ -67,6 +67,10 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
         }
         const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i));
         await Promise.all(ingressPromises);
+
+        const endpointId = formValues.EndpointId;
+        const registriesPromises = _.map(registries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
+        await Promise.all(registriesPromises);
       } catch (err) {
         throw err;
       }
@@ -76,8 +80,8 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
   function patch(oldFormValues, newFormValues) {
     return $async(async () => {
       try {
-        const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
-        const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
+        const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
+        const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
         void oldNamespace, newNamespace;
 
         if (oldQuota && newQuota) {
@@ -103,6 +107,18 @@ export function KubernetesResourcePoolService($async, KubernetesNamespaceService
 
         const promises = _.flatten([createPromises, delPromises, patchPromises]);
         await Promise.all(promises);
+
+        const endpointId = newFormValues.EndpointId;
+        const keptRegistries = _.intersectionBy(oldRegistries, newRegistries, 'Id');
+        const removedRegistries = _.without(oldRegistries, ...keptRegistries);
+
+        const newRegistriesPromises = _.map(newRegistries, (r) => EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]));
+        const removedRegistriesPromises = _.map(removedRegistries, (r) => {
+          _.pull(r.RegistryAccesses[endpointId].Namespaces, newFormValues.Name);
+          return EndpointService.updateRegistryAccess(endpointId, r.Id, r.RegistryAccesses[endpointId]);
+        });
+
+        await Promise.all(_.concat(newRegistriesPromises, removedRegistriesPromises));
       } catch (err) {
         throw err;
       }
diff --git a/app/kubernetes/services/statefulSetService.js b/app/kubernetes/services/statefulSetService.js
index 60cdea44c..5a14c4190 100644
--- a/app/kubernetes/services/statefulSetService.js
+++ b/app/kubernetes/services/statefulSetService.js
@@ -93,6 +93,7 @@ class KubernetesStatefulSetService {
       if (!payload.length) {
         return;
       }
+
       const data = await this.KubernetesStatefulSets(namespace).patch(params, payload).$promise;
       return data;
     } catch (err) {
diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html
index 152bbc87a..c256ec3e0 100644
--- a/app/kubernetes/views/applications/create/createApplication.html
+++ b/app/kubernetes/views/applications/create/createApplication.html
@@ -16,6 +16,40 @@
       <rd-widget>
         <rd-widget-body>
           <form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
+            <div class="col-sm-12 form-section-title">
+              Namespace
+            </div>
+            <!-- #region NAMESPACE -->
+            <div class="form-group" ng-if="ctrl.formValues.ResourcePool">
+              <label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
+              <div class="col-sm-11">
+                <select
+                  class="form-control"
+                  id="resource-pool-selector"
+                  ng-model="ctrl.formValues.ResourcePool"
+                  ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
+                  ng-change="ctrl.onResourcePoolSelectionChange()"
+                  ng-disabled="ctrl.state.isEdit"
+                ></select>
+              </div>
+            </div>
+            <div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
+              <div class="col-sm-12 small text-danger">
+                <i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
+                This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
+                namespace.
+              </div>
+            </div>
+            <div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
+              <div class="col-sm-12 small text-muted">
+                <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
+                You do not have access to any namespace. Contact your administrator to get access to a namespace.
+              </div>
+            </div>
+            <!-- #endregion -->
+            <div class="col-sm-12 form-section-title">
+              Application
+            </div>
             <!-- #region NAME FIELD -->
             <div class="form-group">
               <label for="application_name" class="col-sm-1 control-label text-left">Name</label>
@@ -53,61 +87,20 @@
             <!-- #region IMAGE FIELD -->
 
             <div class="form-group">
-              <label for="container_image" class="col-sm-1 control-label text-left">Image</label>
-              <div class="col-sm-11">
-                <input
-                  type="text"
-                  class="form-control"
-                  name="container_image"
-                  ng-model="ctrl.formValues.Image"
-                  placeholder="nginx:latest"
-                  required
-                  ng-disabled="ctrl.formValues.Containers.length > 1"
-                />
+              <div class="col-sm-12">
+                <por-image-registry
+                  model="ctrl.formValues.ImageModel"
+                  auto-complete="false"
+                  label-class="col-sm-1"
+                  input-class="col-sm-11"
+                  namespace="ctrl.formValues.ResourcePool.Namespace.Name"
+                  endpoint="ctrl.endpoint"
+                  is-admin="ctrl.isAdmin"
+                  check-rate-limits="true"
+                  set-validity="ctrl.setPullImageValidity"
+                ></por-image-registry>
               </div>
             </div>
-            <div class="form-group" ng-show="kubernetesApplicationCreationForm.container_image.$invalid">
-              <div class="col-sm-12 small text-warning">
-                <div ng-messages="kubernetesApplicationCreationForm.container_image.$error">
-                  <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
-                </div>
-              </div>
-            </div>
-            <por-image-registry-rate-limits
-              is-docker-hub-registry="true"
-              endpoint="ctrl.endpoint"
-              is-authenticated="ctrl.state.isDockerAuthenticated"
-              is-admin="ctrl.isAdmin"
-              set-validity="ctrl.setPullImageValidity"
-            >
-            </por-image-registry-rate-limits>
-            <!-- #endregion -->
-
-            <div class="col-sm-12 form-section-title">
-              Namespace
-            </div>
-            <!-- #region NAMESPACE -->
-            <div class="form-group">
-              <label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
-              <div class="col-sm-11">
-                <select
-                  class="form-control"
-                  id="resource-pool-selector"
-                  ng-model="ctrl.formValues.ResourcePool"
-                  ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
-                  ng-change="ctrl.onResourcePoolSelectionChange()"
-                  ng-disabled="ctrl.state.isEdit"
-                ></select>
-              </div>
-            </div>
-            <div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
-              <div class="col-sm-12 small text-danger">
-                <i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
-                This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
-                namespace.
-              </div>
-            </div>
-            <!-- #endregion -->
 
             <div class="col-sm-12 form-section-title">
               Stack
diff --git a/app/kubernetes/views/applications/create/createApplication.js b/app/kubernetes/views/applications/create/createApplication.js
index 9feaabc76..c5c8df16b 100644
--- a/app/kubernetes/views/applications/create/createApplication.js
+++ b/app/kubernetes/views/applications/create/createApplication.js
@@ -3,7 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesCreateApplicationVie
   controller: 'KubernetesCreateApplicationController',
   controllerAs: 'ctrl',
   bindings: {
-    $transition$: '<',
     endpoint: '<',
   },
 });
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js
index 09b25384f..aca7d8dcc 100644
--- a/app/kubernetes/views/applications/create/createApplicationController.js
+++ b/app/kubernetes/views/applications/create/createApplicationController.js
@@ -38,9 +38,7 @@ class KubernetesCreateApplicationController {
     $async,
     $state,
     Notifications,
-    EndpointProvider,
     Authentication,
-    DockerHubService,
     ModalService,
     KubernetesResourcePoolService,
     KubernetesApplicationService,
@@ -50,14 +48,13 @@ class KubernetesCreateApplicationController {
     KubernetesIngressService,
     KubernetesPersistentVolumeClaimService,
     KubernetesNamespaceHelper,
-    KubernetesVolumeService
+    KubernetesVolumeService,
+    RegistryService
   ) {
     this.$async = $async;
     this.$state = $state;
     this.Notifications = Notifications;
-    this.EndpointProvider = EndpointProvider;
     this.Authentication = Authentication;
-    this.DockerHubService = DockerHubService;
     this.ModalService = ModalService;
     this.KubernetesResourcePoolService = KubernetesResourcePoolService;
     this.KubernetesApplicationService = KubernetesApplicationService;
@@ -68,6 +65,7 @@ class KubernetesCreateApplicationController {
     this.KubernetesIngressService = KubernetesIngressService;
     this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
     this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
+    this.RegistryService = RegistryService;
 
     this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
     this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
@@ -77,6 +75,56 @@ class KubernetesCreateApplicationController {
     this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
     this.ServiceTypes = KubernetesServiceTypes;
 
+    this.state = {
+      actionInProgress: false,
+      useLoadBalancer: false,
+      useServerMetrics: false,
+      sliders: {
+        cpu: {
+          min: 0,
+          max: 0,
+        },
+        memory: {
+          min: 0,
+          max: 0,
+        },
+      },
+      nodes: {
+        memory: 0,
+        cpu: 0,
+      },
+      resourcePoolHasQuota: false,
+      viewReady: false,
+      availableSizeUnits: ['MB', 'GB', 'TB'],
+      alreadyExists: false,
+      duplicates: {
+        environmentVariables: new KubernetesFormValidationReferences(),
+        persistedFolders: new KubernetesFormValidationReferences(),
+        configurationPaths: new KubernetesFormValidationReferences(),
+        existingVolumes: new KubernetesFormValidationReferences(),
+        publishedPorts: {
+          containerPorts: new KubernetesFormValidationReferences(),
+          nodePorts: new KubernetesFormValidationReferences(),
+          ingressRoutes: new KubernetesFormValidationReferences(),
+          loadBalancerPorts: new KubernetesFormValidationReferences(),
+        },
+        placements: new KubernetesFormValidationReferences(),
+      },
+      isEdit: this.$state.params.namespace && this.$state.params.name,
+      persistedFoldersUseExistingVolumes: false,
+      pullImageValidity: false,
+    };
+
+    this.isAdmin = this.Authentication.isAdmin();
+
+    this.editChanges = [];
+
+    this.storageClasses = [];
+    this.state.useLoadBalancer = false;
+    this.state.useServerMetrics = false;
+
+    this.formValues = new KubernetesApplicationFormValues();
+
     this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
     this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
     this.setPullImageValidity = this.setPullImageValidity.bind(this);
@@ -869,9 +917,9 @@ class KubernetesCreateApplicationController {
   getApplication() {
     return this.$async(async () => {
       try {
-        const namespace = this.state.params.namespace;
+        const namespace = this.$state.params.namespace;
         [this.application, this.persistentVolumeClaims] = await Promise.all([
-          this.KubernetesApplicationService.get(namespace, this.state.params.name),
+          this.KubernetesApplicationService.get(namespace, this.$state.params.name),
           this.KubernetesPersistentVolumeClaimService.get(namespace),
         ]);
       } catch (err) {
@@ -879,71 +927,26 @@ class KubernetesCreateApplicationController {
       }
     });
   }
+
+  async parseImageConfiguration(imageModel) {
+    return this.$async(async () => {
+      try {
+        return await this.RegistryService.retrievePorRegistryModelFromRepository(imageModel.Image, this.endpoint.Id, imageModel.Registry.Id, this.$state.params.namespace);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registry');
+        return imageModel;
+      }
+    });
+  }
   /* #endregion */
 
   /* #region  ON INIT */
   $onInit() {
     return this.$async(async () => {
       try {
-        this.state = {
-          actionInProgress: false,
-          useLoadBalancer: false,
-          useServerMetrics: false,
-          sliders: {
-            cpu: {
-              min: 0,
-              max: 0,
-            },
-            memory: {
-              min: 0,
-              max: 0,
-            },
-          },
-          nodes: {
-            memory: 0,
-            cpu: 0,
-          },
-          resourcePoolHasQuota: false,
-          viewReady: false,
-          availableSizeUnits: ['MB', 'GB', 'TB'],
-          alreadyExists: false,
-          duplicates: {
-            environmentVariables: new KubernetesFormValidationReferences(),
-            persistedFolders: new KubernetesFormValidationReferences(),
-            configurationPaths: new KubernetesFormValidationReferences(),
-            existingVolumes: new KubernetesFormValidationReferences(),
-            publishedPorts: {
-              containerPorts: new KubernetesFormValidationReferences(),
-              nodePorts: new KubernetesFormValidationReferences(),
-              ingressRoutes: new KubernetesFormValidationReferences(),
-              loadBalancerPorts: new KubernetesFormValidationReferences(),
-            },
-            placements: new KubernetesFormValidationReferences(),
-          },
-          isEdit: false,
-          params: {
-            namespace: this.$transition$.params().namespace,
-            name: this.$transition$.params().name,
-          },
-          persistedFoldersUseExistingVolumes: false,
-          pullImageValidity: false,
-        };
-
-        this.isAdmin = this.Authentication.isAdmin();
-
-        this.editChanges = [];
-
-        if (this.state.params.namespace && this.state.params.name) {
-          this.state.isEdit = true;
-        }
-
-        const endpoint = this.EndpointProvider.currentEndpoint();
-        this.endpoint = endpoint;
-        this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
-        this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
-        this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics;
-
-        this.formValues = new KubernetesApplicationFormValues();
+        this.storageClasses = this.endpoint.Kubernetes.Configuration.StorageClasses;
+        this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
+        this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
 
         const [resourcePools, nodes, ingresses] = await Promise.all([
           this.KubernetesResourcePoolService.get(),
@@ -964,7 +967,7 @@ class KubernetesCreateApplicationController {
         });
         this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
 
-        const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
+        const namespace = this.state.isEdit ? this.$state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
         await this.refreshNamespaceData(namespace);
 
         if (this.state.isEdit) {
@@ -978,6 +981,7 @@ class KubernetesCreateApplicationController {
             this.filteredIngresses
           );
           this.formValues.OriginalIngresses = this.filteredIngresses;
+          this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel);
           this.savedFormValues = angular.copy(this.formValues);
           delete this.formValues.ApplicationType;
 
@@ -995,11 +999,7 @@ class KubernetesCreateApplicationController {
           this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
           this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
         }
-
         this.updateSliders();
-
-        const dockerHub = await this.DockerHubService.dockerhub();
-        this.state.isDockerAuthenticated = dockerHub.Authentication;
       } catch (err) {
         this.Notifications.error('Failure', err, 'Unable to load view data');
       } finally {
diff --git a/app/kubernetes/views/configurations/edit/configuration.html b/app/kubernetes/views/configurations/edit/configuration.html
index 09b07e13e..1ccc77ed3 100644
--- a/app/kubernetes/views/configurations/edit/configuration.html
+++ b/app/kubernetes/views/configurations/edit/configuration.html
@@ -21,6 +21,7 @@
                       <td>Name</td>
                       <td>
                         {{ ctrl.configuration.Name }}
+                        <span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.configuration.IsRegistrySecret">system</span>
                       </td>
                     </tr>
                     <tr>
@@ -76,7 +77,7 @@
     <div class="col-sm-12">
       <rd-widget>
         <rd-widget-body>
-          <form ng-if="!ctrl.isSystemNamespace()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
+          <form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
             <kubernetes-configuration-data
               ng-if="ctrl.formValues"
               form-values="ctrl.formValues"
@@ -111,7 +112,7 @@
             </div>
             <!-- !actions -->
           </form>
-          <div ng-if="ctrl.isSystemNamespace()">
+          <div ng-if="ctrl.isSystemConfig()">
             <div class="col-sm-12 form-section-title" style="margin-top: 10px;">
               Data
             </div>
diff --git a/app/kubernetes/views/configurations/edit/configurationController.js b/app/kubernetes/views/configurations/edit/configurationController.js
index 57928dbb1..0bf9f2366 100644
--- a/app/kubernetes/views/configurations/edit/configurationController.js
+++ b/app/kubernetes/views/configurations/edit/configurationController.js
@@ -55,6 +55,10 @@ class KubernetesConfigurationController {
     return this.KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace);
   }
 
+  isSystemConfig() {
+    return this.isSystemNamespace() || this.configuration.IsRegistrySecret;
+  }
+
   selectTab(index) {
     this.LocalStorage.storeActiveTab('configuration', index);
   }
@@ -134,6 +138,10 @@ class KubernetesConfigurationController {
       const name = this.$transition$.params().name;
       const namespace = this.$transition$.params().namespace;
       const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]);
+      if (secret.status === 'rejected' && secret.reason.err.status === 403) {
+        this.$state.go('kubernetes.configurations');
+        throw new Error('Not authorized to edit secret');
+      }
       if (secret.status === 'fulfilled') {
         this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
         this.formValues.Data = secret.value.Data;
@@ -146,6 +154,8 @@ class KubernetesConfigurationController {
       this.formValues.Name = this.configuration.Name;
       this.formValues.Type = this.configuration.Type;
       this.oldDataYaml = this.formValues.DataYaml;
+
+      return this.configuration;
     } catch (err) {
       this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
     } finally {
@@ -251,11 +261,12 @@ class KubernetesConfigurationController {
       this.formValues = new KubernetesConfigurationFormValues();
 
       this.resourcePools = await this.KubernetesResourcePoolService.get();
-      await this.getConfiguration();
-      await this.getApplications(this.configuration.Namespace);
-      await this.getEvents(this.configuration.Namespace);
-      await this.getConfigurations();
-
+      const configuration = await this.getConfiguration();
+      if (configuration) {
+        await this.getApplications(this.configuration.Namespace);
+        await this.getEvents(this.configuration.Namespace);
+        await this.getConfigurations();
+      }
       this.tagUsedDataKeys();
     } catch (err) {
       this.Notifications.error('Failure', err, 'Unable to load view data');
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js
index a1c5db285..10184786c 100644
--- a/app/kubernetes/views/configure/configureController.js
+++ b/app/kubernetes/views/configure/configureController.js
@@ -9,18 +9,10 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
 class KubernetesConfigureController {
   /* #region  CONSTRUCTOR */
 
-  // TODO: technical debt
-  // $transition$ cannot be injected as bindings: { $transition$: '<' } inside app/portainer/__module.js
-  // because this view is not using a component (https://ui-router.github.io/guide/ng1/route-to-component#accessing-transition)
-  // and will cause
-  // >> Error: Cannot combine: component|bindings|componentProvider
-  // >> with: templateProvider|templateUrl|template|notify|async|controller|controllerProvider|controllerAs|resolveAs
-  // >> in stateview: 'content@@portainer.endpoints.endpoint.kubernetesConfig'
   /* @ngInject */
   constructor(
     $async,
     $state,
-    $transition$,
     Notifications,
     KubernetesStorageService,
     EndpointService,
@@ -33,7 +25,6 @@ class KubernetesConfigureController {
   ) {
     this.$async = $async;
     this.$state = $state;
-    this.$transition$ = $transition$;
     this.Notifications = Notifications;
     this.KubernetesStorageService = KubernetesStorageService;
     this.EndpointService = EndpointService;
@@ -253,7 +244,7 @@ class KubernetesConfigureController {
       actionInProgress: false,
       displayConfigureClassPanel: {},
       viewReady: false,
-      endpointId: this.$transition$.params().id,
+      endpointId: this.$state.params.id,
       duplicates: {
         ingressClasses: new KubernetesFormValidationReferences(),
       },
diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html
index 1b2bb4555..6cf535d1d 100644
--- a/app/kubernetes/views/resource-pools/create/createResourcePool.html
+++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html
@@ -2,9 +2,9 @@
   <a ui-sref="kubernetes.resourcePools">Namespaces</a> &gt; Create a namespace
 </kubernetes-view-header>
 
-<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
+<kubernetes-view-loading view-ready="$ctrl.state.viewReady"></kubernetes-view-loading>
 
-<div ng-if="ctrl.state.viewReady">
+<div ng-if="$ctrl.state.viewReady">
   <div class="row">
     <div class="col-lg-12 col-md-12 col-xs-12">
       <rd-widget>
@@ -18,16 +18,16 @@
                   type="text"
                   class="form-control"
                   name="pool_name"
-                  ng-model="ctrl.formValues.Name"
+                  ng-model="$ctrl.formValues.Name"
                   ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/"
-                  ng-change="ctrl.onChangeName()"
+                  ng-change="$ctrl.onChangeName()"
                   placeholder="my-project"
                   required
                   auto-focus
                 />
               </div>
             </div>
-            <div class="form-group" ng-show="resourcePoolCreationForm.pool_name.$invalid || ctrl.state.isAlreadyExist">
+            <div class="form-group" ng-show="resourcePoolCreationForm.pool_name.$invalid || $ctrl.state.isAlreadyExist">
               <div class="col-sm-12 small text-warning">
                 <div ng-messages="resourcePoolCreationForm.pool_name.$error">
                   <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
@@ -36,7 +36,7 @@
                     with an alphanumeric character.</p
                   >
                 </div>
-                <p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A namespace with the same name already exists.</p>
+                <p ng-if="$ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A namespace with the same name already exists.</p>
               </div>
             </div>
             <!-- #endregion -->
@@ -58,16 +58,16 @@
                 <label class="control-label text-left">
                   Resource assignment
                 </label>
-                <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.HasQuota" /><i></i> </label>
+                <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.formValues.HasQuota" /><i></i> </label>
               </div>
             </div>
-            <div class="form-group" ng-if="ctrl.formValues.HasQuota && !ctrl.isQuotaValid()">
+            <div class="form-group" ng-if="$ctrl.formValues.HasQuota && !$ctrl.isQuotaValid()">
               <span class="col-sm-12 text-warning small">
                 <p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
               </span>
             </div>
             <!-- !quotas-switch -->
-            <div ng-if="ctrl.formValues.HasQuota">
+            <div ng-if="$ctrl.formValues.HasQuota">
               <div class="col-sm-12 form-section-title">
                 Resource limits
               </div>
@@ -78,17 +78,23 @@
                     Memory
                   </label>
                   <div class="col-sm-3">
-                    <slider model="ctrl.formValues.MemoryLimit" floor="ctrl.defaults.MemoryLimit" ceil="ctrl.state.sliderMaxMemory" step="128" ng-if="ctrl.state.sliderMaxMemory">
+                    <slider
+                      model="$ctrl.formValues.MemoryLimit"
+                      floor="$ctrl.defaults.MemoryLimit"
+                      ceil="$ctrl.state.sliderMaxMemory"
+                      step="128"
+                      ng-if="$ctrl.state.sliderMaxMemory"
+                    >
                     </slider>
                   </div>
                   <div class="col-sm-2">
                     <input
                       name="memory_limit"
                       type="number"
-                      min="{{ ctrl.defaults.MemoryLimit }}"
-                      max="{{ ctrl.state.sliderMaxMemory }}"
+                      min="{{ $ctrl.defaults.MemoryLimit }}"
+                      max="{{ $ctrl.state.sliderMaxMemory }}"
                       class="form-control"
-                      ng-model="ctrl.formValues.MemoryLimit"
+                      ng-model="$ctrl.formValues.MemoryLimit"
                       id="memory-limit"
                       required
                     />
@@ -103,7 +109,7 @@
                   <div class="col-sm-12 small text-warning">
                     <div ng-messages="resourcePoolCreationForm.pool_name.$error">
                       <p
-                        ><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}
+                        ><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ $ctrl.defaults.MemoryLimit }} and {{ $ctrl.state.sliderMaxMemory }}
                       </p>
                     </div>
                   </div>
@@ -115,7 +121,14 @@
                     CPU
                   </label>
                   <div class="col-sm-5">
-                    <slider model="ctrl.formValues.CpuLimit" floor="ctrl.defaults.CpuLimit" ceil="ctrl.state.sliderMaxCpu" step="0.1" precision="2" ng-if="ctrl.state.sliderMaxCpu">
+                    <slider
+                      model="$ctrl.formValues.CpuLimit"
+                      floor="$ctrl.defaults.CpuLimit"
+                      ceil="$ctrl.state.sliderMaxCpu"
+                      step="0.1"
+                      precision="2"
+                      ng-if="$ctrl.state.sliderMaxCpu"
+                    >
                     </slider>
                   </div>
                   <div class="col-sm-4" style="margin-top: 20px;">
@@ -186,20 +199,20 @@
             </div>
             <!-- #endregion -->
 
-            <div ng-if="ctrl.state.canUseIngress">
+            <div ng-if="$ctrl.state.canUseIngress">
               <div class="col-sm-12 form-section-title">
                 Ingresses
               </div>
               <!-- #region INGRESSES -->
-              <div class="form-group" ng-if="ctrl.formValues.IngressClasses.length === 0">
+              <div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length === 0">
                 <div class="col-sm-12 small text-muted">
                   The ingress feature must be enabled in the
-                  <a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside this
+                  <a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside this
                   namespace.
                 </div>
               </div>
 
-              <div class="form-group" ng-if="ctrl.formValues.IngressClasses.length > 0">
+              <div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length > 0">
                 <div class="col-sm-12 small text-muted">
                   <p>
                     <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@@ -208,7 +221,7 @@
                 </div>
               </div>
 
-              <div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
+              <div class="form-group" ng-repeat-start="ic in $ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
                 <div class="text-muted col-sm-12" style="width: 100%;">
                   <div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
                     <i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
@@ -234,7 +247,7 @@
                       >
                       </portainer-tooltip>
                     </label>
-                    <span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
+                    <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addHostname(ic)">
                       <i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
                     </span>
                   </div>
@@ -248,13 +261,13 @@
                             class="form-control"
                             name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
                             ng-model="item.Host"
-                            ng-change="ctrl.onChangeIngressHostname()"
+                            ng-change="$ctrl.onChangeIngressHostname()"
                             placeholder="foo"
                             required
                           />
                         </div>
                         <div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
-                          <button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeHostname(ic, $index)">
+                          <button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeHostname(ic, $index)">
                             <i class="fa fa-trash-alt" aria-hidden="true"></i>
                           </button>
                         </div>
@@ -264,20 +277,20 @@
                         style="margin-top: 5px;"
                         ng-show="
                           resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid ||
-                          ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
+                          $ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
                         "
                       >
                         <ng-messages for="resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
                           <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
                         </ng-messages>
-                        <p ng-if="ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
+                        <p ng-if="$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
                           <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This hostname is already used.
                         </p>
                       </div>
                     </div>
                   </div>
                 </div>
-                <div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
+                <div class="form-group" ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX">
                   <div class="col-sm-12">
                     <label class="control-label text-left">
                       Redirect published routes to / in application
@@ -312,7 +325,7 @@
 
                 <div class="col-sm-12" ng-if="ic.AdvancedConfig">
                   <label class="control-label text-left">Annotations</label>
-                  <span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addAnnotation(ic)">
+                  <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addAnnotation(ic)">
                     <i class="fa fa-plus-circle" aria-hidden="true"></i> add annotation
                   </span>
                 </div>
@@ -328,7 +341,7 @@
                       <input type="text" class="form-control" ng-model="annotation.Value" placeholder="/$1" required />
                     </div>
                     <div class="col-sm-1 input-group input-group-sm">
-                      <button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeAnnotation(ic, $index)">
+                      <button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeAnnotation(ic, $index)">
                         <i class="fa fa-trash-alt" aria-hidden="true"></i>
                       </button>
                     </div>
@@ -338,8 +351,49 @@
               <!-- #endregion -->
             </div>
 
+            <!-- #region REGISTRIES -->
+            <div class="col-sm-12 form-section-title">
+              Registries
+            </div>
+            <div class="form-group">
+              <div class="col-sm-12 small text-muted">
+                <p>
+                  <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
+                  Define which registry can be used by users who have access to this namespace.
+                </p>
+              </div>
+            </div>
+
+            <div class="form-group">
+              <label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
+                Select registries
+              </label>
+              <div class="col-sm-9 col-lg-4">
+                <span class="small text-muted" ng-if="!$ctrl.registries.length && $ctrl.state.isAdmin">
+                  No registries available. Head over <a ui-sref="portainer.registries">registry view</a> to define container registry.
+                </span>
+                <span class="small text-muted" ng-if="!$ctrl.registries.length && !$ctrl.state.isAdmin">
+                  No registries available. Contact your administrator to create a container registry.
+                </span>
+                <span
+                  isteven-multi-select
+                  ng-if="$ctrl.registries.length"
+                  input-model="$ctrl.registries"
+                  output-model="$ctrl.formValues.Registries"
+                  button-label="Name"
+                  item-label="Name"
+                  tick-property="Checked"
+                  helper-elements="filter"
+                  search-property="Name"
+                  translation="{nothingSelected: 'Select one or more registry', search: 'Search...'}"
+                >
+                </span>
+              </div>
+            </div>
+            <!-- #endregion -->
+
             <!-- summary -->
-            <kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !ctrl.isCreateButtonDisabled()" form-values="ctrl.formValues"></kubernetes-summary-view>
+            <kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !$ctrl.isCreateButtonDisabled()" form-values="$ctrl.formValues"></kubernetes-summary-view>
             <!-- !summary -->
 
             <div class="col-sm-12 form-section-title">
@@ -351,12 +405,12 @@
                 <button
                   type="button"
                   class="btn btn-primary btn-sm"
-                  ng-disabled="!resourcePoolCreationForm.$valid || ctrl.isCreateButtonDisabled()"
-                  ng-click="ctrl.createResourcePool()"
-                  button-spinner="ctrl.state.actionInProgress"
+                  ng-disabled="!resourcePoolCreationForm.$valid || $ctrl.isCreateButtonDisabled()"
+                  ng-click="$ctrl.createResourcePool()"
+                  button-spinner="$ctrl.state.actionInProgress"
                 >
-                  <span ng-hide="ctrl.state.actionInProgress">Create namespace</span>
-                  <span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
+                  <span ng-hide="$ctrl.state.actionInProgress">Create namespace</span>
+                  <span ng-show="$ctrl.state.actionInProgress">Creation in progress...</span>
                 </button>
               </div>
             </div>
diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.js b/app/kubernetes/views/resource-pools/create/createResourcePool.js
index daf67bd9c..a192da090 100644
--- a/app/kubernetes/views/resource-pools/create/createResourcePool.js
+++ b/app/kubernetes/views/resource-pools/create/createResourcePool.js
@@ -1,5 +1,10 @@
+import angular from 'angular';
+import KubernetesCreateResourcePoolController from './createResourcePoolController';
+
 angular.module('portainer.kubernetes').component('kubernetesCreateResourcePoolView', {
   templateUrl: './createResourcePool.html',
-  controller: 'KubernetesCreateResourcePoolController',
-  controllerAs: 'ctrl',
+  controller: KubernetesCreateResourcePoolController,
+  bindings: {
+    endpoint: '<',
+  },
 });
diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js
index 21d070bcb..5cdc8accc 100644
--- a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js
+++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js
@@ -1,4 +1,3 @@
-import angular from 'angular';
 import _ from 'lodash-es';
 import filesizeParser from 'filesize-parser';
 import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
@@ -16,23 +15,19 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
 class KubernetesCreateResourcePoolController {
   /* #region  CONSTRUCTOR */
   /* @ngInject */
-  constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointProvider) {
-    this.$async = $async;
-    this.$state = $state;
-    this.Notifications = Notifications;
-    this.Authentication = Authentication;
-    this.EndpointProvider = EndpointProvider;
-
-    this.KubernetesNodeService = KubernetesNodeService;
-    this.KubernetesResourcePoolService = KubernetesResourcePoolService;
-    this.KubernetesIngressService = KubernetesIngressService;
+  constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointService) {
+    Object.assign(this, {
+      $async,
+      $state,
+      Notifications,
+      KubernetesNodeService,
+      KubernetesResourcePoolService,
+      KubernetesIngressService,
+      Authentication,
+      EndpointService,
+    });
 
     this.IngressClassTypes = KubernetesIngressClassTypes;
-
-    this.onInit = this.onInit.bind(this);
-    this.createResourcePoolAsync = this.createResourcePoolAsync.bind(this);
-    this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this);
-    this.getIngressesAsync = this.getIngressesAsync.bind(this);
   }
   /* #endregion */
 
@@ -116,106 +111,111 @@ class KubernetesCreateResourcePoolController {
   }
 
   /* #region  CREATE NAMESPACE */
-  async createResourcePoolAsync() {
-    this.state.actionInProgress = true;
-    try {
-      this.checkDefaults();
-      const owner = this.Authentication.getUserDetails().username;
-      this.formValues.Owner = owner;
-      await this.KubernetesResourcePoolService.create(this.formValues);
-      this.Notifications.success('Namespace successfully created', this.formValues.Name);
-      this.$state.go('kubernetes.resourcePools');
-    } catch (err) {
-      this.Notifications.error('Failure', err, 'Unable to create namespace');
-    } finally {
-      this.state.actionInProgress = false;
-    }
-  }
-
   createResourcePool() {
-    return this.$async(this.createResourcePoolAsync);
+    return this.$async(async () => {
+      this.state.actionInProgress = true;
+      try {
+        this.checkDefaults();
+        this.formValues.Owner = this.Authentication.getUserDetails().username;
+        await this.KubernetesResourcePoolService.create(this.formValues);
+        this.Notifications.success('Namespace successfully created', this.formValues.Name);
+        this.$state.go('kubernetes.resourcePools');
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to create namespace');
+      } finally {
+        this.state.actionInProgress = false;
+      }
+    });
   }
   /* #endregion */
 
   /* #region  GET INGRESSES */
-  async getIngressesAsync() {
-    try {
-      this.allIngresses = await this.KubernetesIngressService.get();
-    } catch (err) {
-      this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
-    }
-  }
-
   getIngresses() {
-    return this.$async(this.getIngressesAsync);
+    return this.$async(async () => {
+      try {
+        this.allIngresses = await this.KubernetesIngressService.get();
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
+      }
+    });
   }
   /* #endregion */
 
   /* #region  GET NAMESPACES */
-  async getResourcePoolsAsync() {
-    try {
-      this.resourcePools = await this.KubernetesResourcePoolService.get();
-    } catch (err) {
-      this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
-    }
-  }
-
   getResourcePools() {
-    return this.$async(this.getResourcePoolsAsync);
+    return this.$async(async () => {
+      try {
+        this.resourcePools = await this.KubernetesResourcePoolService.get();
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');
+      }
+    });
+  }
+  /* #endregion */
+
+  /* #region  GET REGISTRIES */
+  getRegistries() {
+    return this.$async(async () => {
+      try {
+        this.registries = await this.EndpointService.registries(this.endpoint.Id);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registries');
+      }
+    });
   }
   /* #endregion */
 
   /* #region  ON INIT */
-  async onInit() {
-    try {
-      const endpoint = this.EndpointProvider.currentEndpoint();
-      this.endpoint = endpoint;
-      this.defaults = KubernetesResourceQuotaDefaults;
-      this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
-      this.formValues.HasQuota = true;
-
-      this.state = {
-        actionInProgress: false,
-        sliderMaxMemory: 0,
-        sliderMaxCpu: 0,
-        viewReady: false,
-        isAlreadyExist: false,
-        canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
-        duplicates: {
-          ingressHosts: new KubernetesFormValidationReferences(),
-        },
-      };
-
-      const nodes = await this.KubernetesNodeService.get();
-
-      _.forEach(nodes, (item) => {
-        this.state.sliderMaxMemory += filesizeParser(item.Memory);
-        this.state.sliderMaxCpu += item.CPU;
-      });
-      this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
-      await this.getResourcePools();
-      if (this.state.canUseIngress) {
-        await this.getIngresses();
-        const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
-        this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
-      }
-      _.forEach(this.formValues.IngressClasses, (ic) => {
-        if (ic.Hosts.length === 0) {
-          ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
-        }
-      });
-    } catch (err) {
-      this.Notifications.error('Failure', err, 'Unable to load view data');
-    } finally {
-      this.state.viewReady = true;
-    }
-  }
-
   $onInit() {
-    return this.$async(this.onInit);
+    return this.$async(async () => {
+      try {
+        const endpoint = this.endpoint;
+        this.defaults = KubernetesResourceQuotaDefaults;
+        this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
+        this.formValues.EndpointId = this.endpoint.Id;
+        this.formValues.HasQuota = true;
+
+        this.state = {
+          actionInProgress: false,
+          sliderMaxMemory: 0,
+          sliderMaxCpu: 0,
+          viewReady: false,
+          isAlreadyExist: false,
+          canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
+          duplicates: {
+            ingressHosts: new KubernetesFormValidationReferences(),
+          },
+          isAdmin: this.Authentication.isAdmin(),
+        };
+
+        const nodes = await this.KubernetesNodeService.get();
+
+        _.forEach(nodes, (item) => {
+          this.state.sliderMaxMemory += filesizeParser(item.Memory);
+          this.state.sliderMaxCpu += item.CPU;
+        });
+        this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
+        await this.getResourcePools();
+        if (this.state.canUseIngress) {
+          await this.getIngresses();
+          const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
+          this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
+        }
+        _.forEach(this.formValues.IngressClasses, (ic) => {
+          if (ic.Hosts.length === 0) {
+            ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
+          }
+        });
+
+        await this.getRegistries();
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to load view data');
+      } finally {
+        this.state.viewReady = true;
+      }
+    });
   }
   /* #endregion */
 }
 
 export default KubernetesCreateResourcePoolController;
-angular.module('portainer.kubernetes').controller('KubernetesCreateResourcePoolController', KubernetesCreateResourcePoolController);
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html
index 78c180980..2a7ece853 100644
--- a/app/kubernetes/views/resource-pools/edit/resourcePool.html
+++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html
@@ -298,6 +298,61 @@
                   </div>
                   <!-- #endregion -->
                 </div>
+
+                <!-- #region REGISTRIES -->
+                <div>
+                  <div class="col-sm-12 form-section-title">
+                    Registries
+                  </div>
+
+                  <div class="form-group" ng-if="!ctrl.isAdmin">
+                    <label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
+                      Selected registries
+                    </label>
+                    <div class="col-sm-9 col-lg-4">
+                      {{ ctrl.selectedRegistries }}
+                    </div>
+                  </div>
+
+                  <div ng-if="ctrl.isAdmin">
+                    <div class="form-group">
+                      <div class="col-sm-12 small text-muted">
+                        <p>
+                          <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
+                          Define which registry can be used by users who have access to this namespace.
+                        </p>
+                      </div>
+                    </div>
+                    <div class="form-group">
+                      <label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0;">
+                        Select registries
+                      </label>
+                      <div class="col-sm-9 col-lg-4">
+                        <span class="small text-muted" ng-if="!ctrl.registries.length && ctrl.state.isAdmin">
+                          No registries available. Head over <a ui-sref="portainer.registries">registry view</a> to define container registry.
+                        </span>
+                        <span class="small text-muted" ng-if="!ctrl.registries.length && !ctrl.state.isAdmin">
+                          No registries available. Contact your administrator to create a container registry.
+                        </span>
+                        <span
+                          isteven-multi-select
+                          ng-if="ctrl.registries.length"
+                          input-model="ctrl.registries"
+                          output-model="ctrl.formValues.Registries"
+                          button-label="Name"
+                          item-label="Name"
+                          tick-property="Checked"
+                          helper-elements="filter"
+                          search-property="Name"
+                          translation="{nothingSelected: 'Select one or more registry', search: 'Search...'}"
+                        >
+                        </span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                <!-- #endregion -->
+
                 <!-- #region STORAGES -->
                 <div class="col-sm-12 form-section-title">
                   Storages
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.js b/app/kubernetes/views/resource-pools/edit/resourcePool.js
index 3b6012a2d..d612e9f6a 100644
--- a/app/kubernetes/views/resource-pools/edit/resourcePool.js
+++ b/app/kubernetes/views/resource-pools/edit/resourcePool.js
@@ -3,6 +3,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', {
   controller: 'KubernetesResourcePoolController',
   controllerAs: 'ctrl',
   bindings: {
-    $transition$: '<',
+    endpoint: '<',
   },
 });
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js
index ee12da091..31c5ff6f3 100644
--- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js
+++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js
@@ -1,7 +1,7 @@
 import angular from 'angular';
 import _ from 'lodash-es';
 import filesizeParser from 'filesize-parser';
-import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
+import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
 import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
 import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
 import {
@@ -24,7 +24,7 @@ class KubernetesResourcePoolController {
     Authentication,
     Notifications,
     LocalStorage,
-    EndpointProvider,
+    EndpointService,
     ModalService,
     KubernetesNodeService,
     KubernetesResourceQuotaService,
@@ -36,33 +36,30 @@ class KubernetesResourcePoolController {
     KubernetesIngressService,
     KubernetesVolumeService
   ) {
-    this.$async = $async;
-    this.$state = $state;
-    this.Notifications = Notifications;
-    this.Authentication = Authentication;
-    this.LocalStorage = LocalStorage;
-    this.EndpointProvider = EndpointProvider;
-    this.ModalService = ModalService;
-
-    this.KubernetesNodeService = KubernetesNodeService;
-    this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
-    this.KubernetesResourcePoolService = KubernetesResourcePoolService;
-    this.KubernetesEventService = KubernetesEventService;
-    this.KubernetesPodService = KubernetesPodService;
-    this.KubernetesApplicationService = KubernetesApplicationService;
-    this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
-    this.KubernetesIngressService = KubernetesIngressService;
-    this.KubernetesVolumeService = KubernetesVolumeService;
+    Object.assign(this, {
+      $async,
+      $state,
+      Authentication,
+      Notifications,
+      LocalStorage,
+      EndpointService,
+      ModalService,
+      KubernetesNodeService,
+      KubernetesResourceQuotaService,
+      KubernetesResourcePoolService,
+      KubernetesEventService,
+      KubernetesPodService,
+      KubernetesApplicationService,
+      KubernetesNamespaceHelper,
+      KubernetesIngressService,
+      KubernetesVolumeService,
+    });
 
     this.IngressClassTypes = KubernetesIngressClassTypes;
     this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
 
-    this.onInit = this.onInit.bind(this);
-    this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
     this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
     this.getEvents = this.getEvents.bind(this);
-    this.getApplications = this.getApplications.bind(this);
-    this.getIngresses = this.getIngresses.bind(this);
   }
   /* #endregion */
 
@@ -159,15 +156,6 @@ class KubernetesResourcePoolController {
     this.selectTab(2);
   }
 
-  async createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit) {
-    const quota = new KubernetesResourceQuota(namespace);
-    quota.CpuLimit = cpuLimit;
-    quota.MemoryLimit = memoryLimit;
-    quota.ResourcePoolName = namespace;
-    quota.ResourcePoolOwner = owner;
-    await this.KubernetesResourceQuotaService.create(quota);
-  }
-
   hasResourceQuotaBeenReduced() {
     if (this.formValues.HasQuota && this.oldQuota) {
       const cpuLimit = this.formValues.CpuLimit;
@@ -285,88 +273,119 @@ class KubernetesResourcePoolController {
   }
   /* #endregion */
 
+  /* #region  GET REGISTRIES */
+  getRegistries() {
+    return this.$async(async () => {
+      try {
+        const namespace = this.$state.params.id;
+
+        if (this.isAdmin) {
+          this.registries = await this.EndpointService.registries(this.endpoint.Id);
+          this.registries.forEach((reg) => {
+            if (reg.RegistryAccesses && reg.RegistryAccesses[this.endpoint.Id] && reg.RegistryAccesses[this.endpoint.Id].Namespaces.includes(namespace)) {
+              reg.Checked = true;
+              this.formValues.Registries.push(reg);
+            }
+          });
+
+          return;
+        }
+
+        const registries = await this.EndpointService.registries(this.endpoint.Id, namespace);
+        this.selectedRegistries = registries.map((r) => r.Name).join(', ');
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registries');
+      }
+    });
+  }
+  /* #endregion */
+
   /* #region  ON INIT */
-  async onInit() {
-    try {
-      const endpoint = this.EndpointProvider.currentEndpoint();
-      this.endpoint = endpoint;
-      this.isAdmin = this.Authentication.isAdmin();
-
-      this.state = {
-        actionInProgress: false,
-        sliderMaxMemory: 0,
-        sliderMaxCpu: 0,
-        cpuUsage: 0,
-        cpuUsed: 0,
-        memoryUsage: 0,
-        memoryUsed: 0,
-        activeTab: 0,
-        currentName: this.$state.$current.name,
-        showEditorTab: false,
-        eventsLoading: true,
-        applicationsLoading: true,
-        ingressesLoading: true,
-        viewReady: false,
-        eventWarningCount: 0,
-        canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
-        duplicates: {
-          ingressHosts: new KubernetesFormValidationReferences(),
-        },
-      };
-
-      this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
-
-      const name = this.$transition$.params().id;
-
-      const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
-
-      this.pool = _.find(pools, { Namespace: { Name: name } });
-      this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
-      this.formValues.Name = this.pool.Namespace.Name;
-
-      _.forEach(nodes, (item) => {
-        this.state.sliderMaxMemory += filesizeParser(item.Memory);
-        this.state.sliderMaxCpu += item.CPU;
-      });
-      this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
-
-      const quota = this.pool.Quota;
-      if (quota) {
-        this.oldQuota = angular.copy(quota);
-        this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota);
-        this.state.cpuUsed = quota.CpuLimitUsed;
-        this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
-      }
-
-      this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name);
-      if (this.pool.Namespace.Name === 'default') {
-        this.isEditable = false;
-      }
-
-      await this.getEvents();
-      await this.getApplications();
-
-      if (this.state.canUseIngress) {
-        await this.getIngresses();
-        const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
-        this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
-        _.forEach(this.formValues.IngressClasses, (ic) => {
-          if (ic.Hosts.length === 0) {
-            ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
-          }
-        });
-      }
-      this.savedFormValues = angular.copy(this.formValues);
-    } catch (err) {
-      this.Notifications.error('Failure', err, 'Unable to load view data');
-    } finally {
-      this.state.viewReady = true;
-    }
-  }
-
   $onInit() {
-    return this.$async(this.onInit);
+    return this.$async(async () => {
+      try {
+        const endpoint = this.endpoint;
+        this.isAdmin = this.Authentication.isAdmin();
+
+        this.state = {
+          actionInProgress: false,
+          sliderMaxMemory: 0,
+          sliderMaxCpu: 0,
+          cpuUsage: 0,
+          cpuUsed: 0,
+          memoryUsage: 0,
+          memoryUsed: 0,
+          activeTab: 0,
+          currentName: this.$state.$current.name,
+          showEditorTab: false,
+          eventsLoading: true,
+          applicationsLoading: true,
+          ingressesLoading: true,
+          viewReady: false,
+          eventWarningCount: 0,
+          canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
+          duplicates: {
+            ingressHosts: new KubernetesFormValidationReferences(),
+          },
+        };
+
+        this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
+
+        const name = this.$state.params.id;
+
+        const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
+
+        this.pool = _.find(pools, { Namespace: { Name: name } });
+        this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
+        this.formValues.Name = this.pool.Namespace.Name;
+        this.formValues.EndpointId = this.endpoint.Id;
+
+        _.forEach(nodes, (item) => {
+          this.state.sliderMaxMemory += filesizeParser(item.Memory);
+          this.state.sliderMaxCpu += item.CPU;
+        });
+        this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
+
+        const quota = this.pool.Quota;
+        if (quota) {
+          this.oldQuota = angular.copy(quota);
+          this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota);
+          this.formValues.EndpointId = this.endpoint.Id;
+
+          this.state.cpuUsed = quota.CpuLimitUsed;
+          this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
+        }
+
+        this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name);
+        if (this.pool.Namespace.Name === 'default') {
+          this.isEditable = false;
+        }
+
+        await this.getEvents();
+        await this.getApplications();
+
+        if (this.state.canUseIngress) {
+          await this.getIngresses();
+          const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
+          this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
+          _.forEach(this.formValues.IngressClasses, (ic) => {
+            if (ic.Hosts.length === 0) {
+              ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
+            }
+          });
+        }
+
+        await this.getRegistries();
+
+        this.savedFormValues = angular.copy(this.formValues);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to load view data');
+      } finally {
+        this.state.viewReady = true;
+      }
+    });
   }
+
   /* #endregion */
 
   $onDestroy() {
diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index 21e14120b..2724c8d6d 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -295,24 +295,12 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
       },
     };
 
-    var registryCreation = {
+    const registryCreation = {
       name: 'portainer.registries.new',
       url: '/new',
       views: {
         'content@': {
-          templateUrl: './views/registries/create/createregistry.html',
-          controller: 'CreateRegistryController',
-        },
-      },
-    };
-
-    var registryAccess = {
-      name: 'portainer.registries.registry.access',
-      url: '/access',
-      views: {
-        'content@': {
-          templateUrl: './views/registries/access/registryAccess.html',
-          controller: 'RegistryAccessController',
+          component: 'createRegistry',
         },
       },
     };
@@ -425,7 +413,6 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
     $stateRegistryProvider.register(initAdmin);
     $stateRegistryProvider.register(registries);
     $stateRegistryProvider.register(registry);
-    $stateRegistryProvider.register(registryAccess);
     $stateRegistryProvider.register(registryCreation);
     $stateRegistryProvider.register(settings);
     $stateRegistryProvider.register(settingsAuthentication);
diff --git a/app/portainer/components/accessManagement/index.js b/app/portainer/components/accessManagement/index.js
new file mode 100644
index 000000000..bfbb96001
--- /dev/null
+++ b/app/portainer/components/accessManagement/index.js
@@ -0,0 +1,9 @@
+import angular from 'angular';
+
+import { porAccessManagement } from './por-access-management';
+import { porAccessManagementUsersSelector } from './por-access-management-users-selector';
+
+export default angular
+  .module('portainer.app.component.access-management', [])
+  .component('porAccessManagement', porAccessManagement)
+  .component('porAccessManagementUsersSelector', porAccessManagementUsersSelector).name;
diff --git a/app/portainer/components/accessManagement/por-access-management-users-selector/index.js b/app/portainer/components/accessManagement/por-access-management-users-selector/index.js
new file mode 100644
index 000000000..51c4de45c
--- /dev/null
+++ b/app/portainer/components/accessManagement/por-access-management-users-selector/index.js
@@ -0,0 +1,7 @@
+export const porAccessManagementUsersSelector = {
+  templateUrl: './por-access-management-users-selector.html',
+  bindings: {
+    options: '<',
+    value: '=',
+  },
+};
diff --git a/app/portainer/components/accessManagement/por-access-management-users-selector/por-access-management-users-selector.html b/app/portainer/components/accessManagement/por-access-management-users-selector/por-access-management-users-selector.html
new file mode 100644
index 000000000..e73eb3bc9
--- /dev/null
+++ b/app/portainer/components/accessManagement/por-access-management-users-selector/por-access-management-users-selector.html
@@ -0,0 +1,23 @@
+<div class="form-group">
+  <label class="col-sm-3 col-lg-2 control-label text-left">
+    Select user(s) and/or team(s)
+  </label>
+  <div class="col-sm-9 col-lg-4">
+    <span class="small text-muted" ng-if="$ctrl.options.length === 0">
+      No users or teams available.
+    </span>
+    <span
+      isteven-multi-select
+      ng-if="$ctrl.options.length > 0"
+      input-model="$ctrl.options"
+      output-model="$ctrl.value"
+      button-label="icon '-' Name"
+      item-label="icon '-' Name"
+      tick-property="ticked"
+      helper-elements="filter"
+      search-property="Name"
+      translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
+    >
+    </span>
+  </div>
+</div>
diff --git a/app/portainer/components/accessManagement/por-access-management.js b/app/portainer/components/accessManagement/por-access-management.js
index 491f795c0..7c8cf34ab 100644
--- a/app/portainer/components/accessManagement/por-access-management.js
+++ b/app/portainer/components/accessManagement/por-access-management.js
@@ -1,4 +1,4 @@
-angular.module('portainer.app').component('porAccessManagement', {
+export const porAccessManagement = {
   templateUrl: './porAccessManagement.html',
   controller: 'porAccessManagementController',
   controllerAs: 'ctrl',
@@ -8,5 +8,6 @@ angular.module('portainer.app').component('porAccessManagement', {
     entityType: '@',
     updateAccess: '<',
     actionInProgress: '<',
+    filterUsers: '<',
   },
-});
+};
diff --git a/app/portainer/components/accessManagement/porAccessManagement.html b/app/portainer/components/accessManagement/porAccessManagement.html
index 011394d5f..182bc6097 100644
--- a/app/portainer/components/accessManagement/porAccessManagement.html
+++ b/app/portainer/components/accessManagement/porAccessManagement.html
@@ -4,31 +4,9 @@
       <rd-widget-header icon="fa-user-lock" title-text="Create access"></rd-widget-header>
       <rd-widget-body>
         <form class="form-horizontal">
-          <div class="form-group">
-            <label class="col-sm-3 col-lg-2 control-label text-left">
-              Select user(s) and/or team(s)
-            </label>
-            <div class="col-sm-9 col-lg-4">
-              <span class="small text-muted" ng-if="ctrl.availableUsersAndTeams.length === 0">
-                No users or teams available.
-              </span>
-              <span
-                isteven-multi-select
-                ng-if="ctrl.availableUsersAndTeams.length > 0"
-                input-model="ctrl.availableUsersAndTeams"
-                output-model="ctrl.formValues.multiselectOutput"
-                button-label="icon '-' Name"
-                item-label="icon '-' Name"
-                tick-property="ticked"
-                helper-elements="filter"
-                search-property="Name"
-                translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
-              >
-              </span>
-            </div>
-          </div>
+          <por-access-management-users-selector options="ctrl.availableUsersAndTeams" value="ctrl.formValues.multiselectOutput"></por-access-management-users-selector>
 
-          <div class="form-group">
+          <div class="form-group" ng-if="ctrl.entityType != 'registry'">
             <label class="col-sm-3 col-lg-2 control-label text-left">
               Role
             </label>
diff --git a/app/portainer/components/accessManagement/porAccessManagementController.js b/app/portainer/components/accessManagement/porAccessManagementController.js
index 460c302d9..8c1e5d524 100644
--- a/app/portainer/components/accessManagement/porAccessManagementController.js
+++ b/app/portainer/components/accessManagement/porAccessManagementController.js
@@ -44,6 +44,7 @@ class PorAccessManagementController {
     const teamAccessPolicies = entity.TeamAccessPolicies;
     const selectedUserAccesses = _.filter(selectedAccesses, (access) => access.Type === 'user');
     const selectedTeamAccesses = _.filter(selectedAccesses, (access) => access.Type === 'team');
+
     _.forEach(selectedUserAccesses, (access) => delete userAccessPolicies[access.Id]);
     _.forEach(selectedTeamAccesses, (access) => delete teamAccessPolicies[access.Id]);
     this.updateAccess();
@@ -55,6 +56,11 @@ class PorAccessManagementController {
       const parent = this.inheritFrom;
 
       const data = await this.AccessService.accesses(entity, parent, this.roles);
+
+      if (this.filterUsers) {
+        data.availableUsersAndTeams = this.filterUsers(data.availableUsersAndTeams);
+      }
+
       this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc');
       this.authorizedUsersAndTeams = data.authorizedUsersAndTeams;
     } catch (err) {
diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html
index 697e7fd47..16d09365a 100644
--- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html
+++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html
@@ -4,8 +4,14 @@
       <div class="toolBar">
         <div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
       </div>
-      <div class="actionBar" ng-if="$ctrl.accessManagement">
-        <button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
+      <div class="actionBar" ng-if="$ctrl.isAdmin">
+        <button
+          ng-if="!$ctrl.endpointType"
+          type="button"
+          class="btn btn-sm btn-danger"
+          ng-disabled="$ctrl.state.selectedItemCount === 0"
+          ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
+        >
           <i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
         </button>
         <button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.registries.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add registry </button>
@@ -27,7 +33,7 @@
           <thead>
             <tr>
               <th>
-                <span class="md-checkbox" ng-if="$ctrl.accessManagement">
+                <span class="md-checkbox" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
                   <input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
                   <label for="select_all"></label>
                 </span>
@@ -53,22 +59,29 @@
               ng-class="{ active: item.Checked }"
             >
               <td>
-                <span class="md-checkbox" ng-if="$ctrl.accessManagement">
-                  <input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
+                <span class="md-checkbox" ng-if="$ctrl.isAdmin && !$ctrl.endpointType">
+                  <input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
                   <label for="select_{{ $index }}"></label>
                 </span>
-                <a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.accessManagement">{{ item.Name }}</a>
-                <span ng-if="!$ctrl.accessManagement">{{ item.Name }}</span>
+                <a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.enableGoToLink(item)">{{ item.Name }}</a>
+                <span ng-if="!$ctrl.enableGoToLink(item)">{{ item.Name }}</span>
                 <span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
               </td>
               <td>
                 {{ item.URL }}
               </td>
               <td>
-                <a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
-                <span class="text-muted space-left" style="cursor: pointer;" data-toggle="tooltip" title="This feature is available in Portainer Business Edition">
+                <a ng-if="$ctrl.canManageAccess(item)" ng-click="$ctrl.redirectToManageAccess(item)"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
+                <span
+                  ng-if="$ctrl.canBrowse(item)"
+                  class="text-muted space-left"
+                  style="cursor: pointer;"
+                  data-toggle="tooltip"
+                  title="This feature is available in Portainer Business Edition"
+                >
                   <i class="fa fa-search" aria-hidden="true"></i> Browse</span
                 >
+                <span ng-if="!$ctrl.canBrowse(item) && !$ctrl.canManageAccess(item)"> - </span>
               </td>
             </tr>
             <tr ng-if="!$ctrl.dataset">
diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js
index f4df3c5a8..e5acb6907 100644
--- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js
+++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js
@@ -1,6 +1,6 @@
 angular.module('portainer.app').component('registriesDatatable', {
   templateUrl: './registriesDatatable.html',
-  controller: 'GenericDatatableController',
+  controller: 'RegistriesDatatableController',
   bindings: {
     titleText: '@',
     titleIcon: '@',
@@ -8,8 +8,9 @@ angular.module('portainer.app').component('registriesDatatable', {
     tableKey: '@',
     orderBy: '@',
     reverseOrder: '<',
-    accessManagement: '<',
     removeAction: '<',
     canBrowse: '<',
+    endpointType: '<',
+    canManageAccess: '<',
   },
 });
diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js b/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js
new file mode 100644
index 000000000..1b2f86e35
--- /dev/null
+++ b/app/portainer/components/datatables/registries-datatable/registriesDatatableController.js
@@ -0,0 +1,85 @@
+import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
+
+angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController);
+
+/* @ngInject */
+function RegistriesDatatableController($scope, $controller, $state, Authentication, DatatableService) {
+  angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
+
+  this.allowSelection = function (item) {
+    return item.Id;
+  };
+
+  this.enableGoToLink = (item) => {
+    return this.isAdmin && item.Id && !this.endpointType;
+  };
+
+  this.goToRegistry = function (item) {
+    if (
+      this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment ||
+      this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
+      this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
+    ) {
+      $state.go('kubernetes.registries.registry', { id: item.Id });
+    } else if (
+      this.endpointType === PortainerEndpointTypes.DockerEnvironment ||
+      this.endpointType === PortainerEndpointTypes.AgentOnDockerEnvironment ||
+      this.endpointType === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment
+    ) {
+      $state.go('docker.registries.registry', { id: item.Id });
+    } else {
+      $state.go('portainer.registries.registry', { id: item.Id });
+    }
+  };
+
+  this.redirectToManageAccess = function (item) {
+    if (
+      this.endpointType === PortainerEndpointTypes.KubernetesLocalEnvironment ||
+      this.endpointType === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
+      this.endpointType === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
+    ) {
+      $state.go('kubernetes.registries.access', { id: item.Id });
+    } else {
+      $state.go('docker.registries.access', { id: item.Id });
+    }
+  };
+
+  this.$onInit = function () {
+    this.isAdmin = Authentication.isAdmin();
+    this.setDefaults();
+    this.prepareTableFromDataset();
+
+    this.state.orderBy = this.orderBy;
+    var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
+    if (storedOrder !== null) {
+      this.state.reverseOrder = storedOrder.reverse;
+      this.state.orderBy = storedOrder.orderBy;
+    }
+
+    var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
+    if (textFilter !== null) {
+      this.state.textFilter = textFilter;
+      this.onTextFilterChange();
+    }
+
+    var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
+    if (storedFilters !== null) {
+      this.filters = storedFilters;
+    }
+    if (this.filters && this.filters.state) {
+      this.filters.state.open = false;
+    }
+
+    var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
+    if (storedSettings !== null) {
+      this.settings = storedSettings;
+      this.settings.open = false;
+    }
+    this.onSettingsRepeaterChange();
+
+    var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
+    if (storedColumnVisibility !== null) {
+      this.columnVisibility = storedColumnVisibility;
+    }
+  };
+}
diff --git a/app/portainer/components/datatables/strings-datatable/index.js b/app/portainer/components/datatables/strings-datatable/index.js
new file mode 100644
index 000000000..dda9fa5fd
--- /dev/null
+++ b/app/portainer/components/datatables/strings-datatable/index.js
@@ -0,0 +1,20 @@
+import angular from 'angular';
+// import controller from './strings-datatable.controller.js';
+
+export const stringsDatatable = {
+  templateUrl: './strings-datatable.html',
+  controller: 'GenericDatatableController',
+  bindings: {
+    titleText: '@',
+    titleIcon: '@',
+    dataset: '<',
+    emptyDatasetMessage: '@',
+
+    columnHeader: '@',
+    tableKey: '@',
+
+    onRemove: '<',
+  },
+};
+
+angular.module('portainer.app').component('stringsDatatable', stringsDatatable);
diff --git a/app/portainer/components/datatables/strings-datatable/strings-datatable.html b/app/portainer/components/datatables/strings-datatable/strings-datatable.html
new file mode 100644
index 000000000..0b6faf0ec
--- /dev/null
+++ b/app/portainer/components/datatables/strings-datatable/strings-datatable.html
@@ -0,0 +1,65 @@
+<div class="datatable">
+  <rd-widget>
+    <rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}"> </rd-widget-header>
+    <rd-widget-body classes="no-padding">
+      <div class="actionBar">
+        <button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.onRemove($ctrl.state.selectedItems)">
+          <i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
+        </button>
+      </div>
+      <div class="searchBar">
+        <i class="fa fa-search searchIcon" aria-hidden="true"></i>
+        <input
+          type="text"
+          class="searchInput"
+          ng-model="$ctrl.state.textFilter"
+          ng-change="$ctrl.onTextFilterChange()"
+          placeholder="Search..."
+          ng-model-options="{ debounce: 300 }"
+        />
+      </div>
+      <div class="table-responsive">
+        <table class="table table-hover nowrap-cells">
+          <thead>
+            <tr>
+              <th>
+                <span class="md-checkbox">
+                  <input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
+                  <label for="select_all"></label>
+                </span>
+                <a ng-click="$ctrl.changeOrderBy('Name')">
+                  {{ $ctrl.columnHeader }}
+                  <i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
+                  <i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
+                </a>
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr
+              dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit)) track by $index"
+              ng-class="{ active: $ctrl.state.selectedItems.includes(item) }"
+            >
+              <td>
+                <span class="md-checkbox">
+                  <input
+                    id="select_{{ $index }}"
+                    type="checkbox"
+                    ng-checked="$ctrl.state.selectedItems.includes(item)"
+                    ng-disabled="$ctrl.disableRemove(item)"
+                    ng-click="$ctrl.selectItem(item, $event)"
+                  />
+                  <label for="select_{{ $index }}"></label>
+                </span>
+                {{ item.value }}
+              </td>
+            </tr>
+            <tr ng-if="$ctrl.state.filteredDataSet.length === 0">
+              <td class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </rd-widget-body>
+  </rd-widget>
+</div>
diff --git a/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html
new file mode 100644
index 000000000..ddf26592e
--- /dev/null
+++ b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.html
@@ -0,0 +1,64 @@
+<form class="form-horizontal" name="registryFormDockerhub" ng-submit="$ctrl.formAction()">
+  <div class="col-sm-12 form-section-title">
+    DockerHub account details
+  </div>
+  <!-- name-input -->
+  <div class="form-group">
+    <label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
+    <div class="col-sm-9 col-lg-10">
+      <input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="dockerhub-prod-us" required />
+    </div>
+  </div>
+  <div class="form-group" ng-show="registryFormDockerhub.registry_name.$invalid">
+    <div class="col-sm-12 small text-warning">
+      <div ng-messages="registryFormDockerhub.registry_name.$error">
+        <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
+      </div>
+    </div>
+  </div>
+  <!-- !name-input -->
+  <!-- credentials-user -->
+  <div class="form-group">
+    <label for="registry_username" class="col-sm-3 col-lg-2 control-label text-left">DockerHub username</label>
+    <div class="col-sm-9 col-lg-10">
+      <input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required />
+    </div>
+  </div>
+  <div class="form-group" ng-show="registryFormDockerhub.registry_username.$invalid">
+    <div class="col-sm-12 small text-warning">
+      <div ng-messages="registryFormDockerhub.registry_username.$error">
+        <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
+      </div>
+    </div>
+  </div>
+  <!-- !credentials-user -->
+  <!-- credentials-password -->
+  <div class="form-group">
+    <label for="registry_password" class="col-sm-3 col-lg-2 control-label text-left">DockerHub password</label>
+    <div class="col-sm-9 col-lg-10">
+      <input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required />
+    </div>
+  </div>
+  <div class="form-group" ng-show="registryFormDockerhub.registry_password.$invalid">
+    <div class="col-sm-12 small text-warning">
+      <div ng-messages="registryFormDockerhub.registry_password.$error">
+        <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
+      </div>
+    </div>
+  </div>
+  <!-- !credentials-password -->
+
+  <!-- actions -->
+  <div class="col-sm-12 form-section-title">
+    Actions
+  </div>
+  <div class="form-group">
+    <div class="col-sm-12">
+      <button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.actionInProgress || !registryFormDockerhub.$valid" button-spinner="$ctrl.actionInProgress">
+        <span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
+        <span ng-show="$ctrl.actionInProgress">In progress...</span>
+      </button>
+    </div>
+  </div>
+  <!-- !actions -->
+</form>
diff --git a/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js
new file mode 100644
index 000000000..adf60fcd5
--- /dev/null
+++ b/app/portainer/components/forms/registry-form-dockerhub/registry-form-dockerhub.js
@@ -0,0 +1,9 @@
+angular.module('portainer.app').component('registryFormDockerhub', {
+  templateUrl: './registry-form-dockerhub.html',
+  bindings: {
+    model: '=',
+    formAction: '<',
+    formActionLabel: '@',
+    actionInProgress: '<',
+  },
+});
diff --git a/app/portainer/components/forms/template-form/template-form.js b/app/portainer/components/forms/template-form/template-form.js
deleted file mode 100644
index 8aacd5839..000000000
--- a/app/portainer/components/forms/template-form/template-form.js
+++ /dev/null
@@ -1,13 +0,0 @@
-angular.module('portainer.app').component('templateForm', {
-  templateUrl: './templateForm.html',
-  controller: 'TemplateFormController',
-  bindings: {
-    model: '=',
-    categories: '<',
-    networks: '<',
-    formAction: '<',
-    formActionLabel: '@',
-    actionInProgress: '<',
-    showTypeSelector: '<',
-  },
-});
diff --git a/app/portainer/components/forms/template-form/templateForm.html b/app/portainer/components/forms/template-form/templateForm.html
deleted file mode 100644
index 12bff3e7b..000000000
--- a/app/portainer/components/forms/template-form/templateForm.html
+++ /dev/null
@@ -1,580 +0,0 @@
-<form class="form-horizontal" name="templateForm">
-  <!-- title-input -->
-  <div class="form-group" ng-class="{ 'has-error': templateForm.template_title.$invalid }">
-    <label for="template_title" class="col-sm-3 col-lg-2 control-label text-left">Title</label>
-    <div class="col-sm-9 col-lg-10">
-      <input type="text" class="form-control" name="template_title" ng-model="$ctrl.model.Title" placeholder="e.g. my-template" required auto-focus />
-    </div>
-  </div>
-  <div class="form-group" ng-show="templateForm.template_title.$invalid">
-    <div class="col-sm-12 small text-warning">
-      <div ng-messages="templateForm.template_title.$error">
-        <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
-      </div>
-    </div>
-  </div>
-  <!-- !title-input -->
-  <!-- description-input -->
-  <div class="form-group" ng-class="{ 'has-error': templateForm.template_description.$invalid }">
-    <label for="template_description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
-    <div class="col-sm-9 col-lg-10">
-      <input type="text" class="form-control" name="template_description" ng-model="$ctrl.model.Description" placeholder="e.g. template description..." required />
-    </div>
-  </div>
-  <div class="form-group" ng-show="templateForm.template_description.$invalid">
-    <div class="col-sm-12 small text-warning">
-      <div ng-messages="templateForm.template_description.$error">
-        <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
-      </div>
-    </div>
-  </div>
-  <!-- !description-input -->
-  <div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseTemplate = !$ctrl.state.collapseTemplate">
-    Template
-    <span class="small space-left">
-      <a ng-if="$ctrl.state.collapseTemplate"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
-      <a ng-if="!$ctrl.state.collapseTemplate"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
-    </span>
-  </div>
-  <!-- template-details -->
-  <div uib-collapse="$ctrl.state.collapseTemplate">
-    <div ng-if="$ctrl.showTypeSelector">
-      <div class="form-group"></div>
-      <div class="form-group" style="margin-bottom: 0;">
-        <div class="boxselector_wrapper">
-          <div>
-            <input type="radio" id="template_container" ng-model="$ctrl.model.Type" ng-value="1" />
-            <label for="template_container">
-              <div class="boxselector_header">
-                <i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
-                Container
-              </div>
-              <p>Container template</p>
-            </label>
-          </div>
-          <div>
-            <input type="radio" id="template_swarm_stack" ng-model="$ctrl.model.Type" ng-value="2" />
-            <label for="template_swarm_stack">
-              <div class="boxselector_header">
-                <i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
-                Swarm stack
-              </div>
-              <p>Stack template (Swarm)</p>
-            </label>
-          </div>
-          <div>
-            <input type="radio" id="template_compose_stack" ng-model="$ctrl.model.Type" ng-value="3" />
-            <label for="template_compose_stack">
-              <div class="boxselector_header">
-                <i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
-                Compose stack
-              </div>
-              <p>Stack template (Compose)</p>
-            </label>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- name -->
-    <div class="form-group">
-      <label for="template_name" class="col-sm-3 col-lg-2 control-label text-left">
-        Name
-        <portainer-tooltip position="bottom" message="Default name that will be associated to the template"></portainer-tooltip>
-      </label>
-      <div class="col-sm-9 col-lg-10">
-        <input type="text" class="form-control" name="template_name" ng-model="$ctrl.model.Name" placeholder="e.g. myApp" />
-      </div>
-    </div>
-    <!-- !name -->
-    <!-- logo -->
-    <div class="form-group">
-      <label for="template_logo" class="col-sm-3 col-lg-2 control-label text-left">
-        Logo URL
-        <portainer-tooltip position="bottom" message="Recommended size: 60x60"></portainer-tooltip>
-      </label>
-      <div class="col-sm-9 col-lg-10">
-        <input type="text" class="form-control" name="template_logo" ng-model="$ctrl.model.Logo" placeholder="e.g. https://portainer.io/images/logos/nginx.png" />
-      </div>
-    </div>
-    <!-- !logo -->
-    <!-- note -->
-    <div class="form-group">
-      <label for="template_note" class="col-sm-3 col-lg-2 control-label text-left">
-        Note
-        <portainer-tooltip position="bottom" message="Usage/extra information about the template. Supports HTML."></portainer-tooltip>
-      </label>
-      <div class="col-sm-9 col-lg-10">
-        <textarea
-          class="form-control"
-          name="template_note"
-          ng-model="$ctrl.model.Note"
-          placeholder="You can use this field to specify extra information. <br/> It supports <b>HTML</b>."
-        ></textarea>
-      </div>
-    </div>
-    <!-- !note -->
-    <!-- platform -->
-    <div class="form-group">
-      <label for="template_platform" class="col-sm-3 col-lg-2 control-label text-left">
-        Platform
-      </label>
-      <div class="col-sm-9 col-lg-10">
-        <select class="form-control" name="template_platform" ng-model="$ctrl.model.Platform">
-          <option value="">Multi-platform</option>
-          <option value="linux">Linux</option>
-          <option value="windows">Windows</option>
-        </select>
-      </div>
-    </div>
-    <!-- !platform -->
-    <!-- categories -->
-    <div class="form-group">
-      <label for="template_categories" class="col-sm-3 col-lg-2 control-label text-left">
-        Categories
-      </label>
-      <div class="col-sm-9 col-lg-10">
-        <ui-select multiple tagging tagging-label="(new category)" ng-model="$ctrl.model.Categories" sortable="true" style="width: 300px;" title="Choose a category">
-          <ui-select-match placeholder="Select categories...">{{ $item }}</ui-select-match>
-          <ui-select-choices repeat="category in $ctrl.categories | filter:$select.search">
-            {{ category }}
-          </ui-select-choices>
-        </ui-select>
-      </div>
-    </div>
-    <!-- !categories -->
-    <!-- administrator-only -->
-    <div class="form-group">
-      <div class="col-sm-12">
-        <label for="tls" class="control-label text-left">
-          Administrator template
-          <portainer-tooltip position="bottom" message="This template will only be available to administrator users."></portainer-tooltip>
-        </label>
-        <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.AdministratorOnly" /><i></i> </label>
-      </div>
-    </div>
-    <!-- administrator-only -->
-  </div>
-  <!-- !template-details -->
-  <div ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
-    <div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseStack = !$ctrl.state.collapseStack">
-      Stack
-      <span class="small space-left">
-        <a ng-if="$ctrl.state.collapseStack"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
-        <a ng-if="!$ctrl.state.collapseStack"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
-      </span>
-    </div>
-    <!-- stack-details -->
-    <div uib-collapse="$ctrl.state.collapseStack">
-      <!-- repository-url -->
-      <div class="form-group" ng-class="{ 'has-error': templateForm.template_repository_url.$invalid }">
-        <label for="template_repository_url" class="col-sm-3 col-lg-2 control-label text-left">Repository URL</label>
-        <div class="col-sm-9 col-lg-10">
-          <input
-            type="text"
-            class="form-control"
-            name="template_repository_url"
-            ng-model="$ctrl.model.Repository.url"
-            placeholder="https://github.com/portainer/portainer-compose"
-            required
-          />
-        </div>
-      </div>
-      <div class="form-group" ng-show="templateForm.template_repository_url.$invalid">
-        <div class="col-sm-12 small text-warning">
-          <div ng-messages="templateForm.template_repository_url.$error">
-            <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
-          </div>
-        </div>
-      </div>
-      <!-- !repository-url -->
-      <!-- composefile-path -->
-      <div class="form-group">
-        <label for="template_repository_path" class="col-sm-3 col-lg-2 control-label text-left">
-          Compose file path
-        </label>
-        <div class="col-sm-9 col-lg-10">
-          <input type="text" class="form-control" name="template_repository_path" ng-model="$ctrl.model.Repository.stackfile" placeholder="docker-compose.yml" />
-        </div>
-      </div>
-      <!-- !composefile-path -->
-    </div>
-    <!-- !stack-details -->
-  </div>
-  <div ng-if="$ctrl.model.Type === 1">
-    <div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseContainer = !$ctrl.state.collapseContainer">
-      Container
-      <span class="small space-left">
-        <a ng-if="$ctrl.state.collapseContainer"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
-        <a ng-if="!$ctrl.state.collapseContainer"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
-      </span>
-    </div>
-    <!-- container-details -->
-    <div uib-collapse="$ctrl.state.collapseContainer">
-      <por-image-registry model="$ctrl.model.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
-      <!-- command -->
-      <div class="form-group">
-        <label for="template_command" class="col-sm-3 col-lg-2 control-label text-left">
-          Command
-          <portainer-tooltip
-            position="bottom"
-            message="The command to run in the container. If not specified, the container will use the default command specified in its Dockerfile."
-          ></portainer-tooltip>
-        </label>
-        <div class="col-sm-9 col-lg-10">
-          <input type="text" class="form-control" name="template_command" ng-model="$ctrl.model.Command" placeholder='/bin/bash -c \"echo hello\" && exit 777' />
-        </div>
-      </div>
-      <!-- !command -->
-      <!-- hostname -->
-      <div class="form-group">
-        <label for="template_hostname" class="col-sm-3 col-lg-2 control-label text-left">
-          Hostname
-          <portainer-tooltip position="bottom" message="Set the hostname of the container. Will use Docker default if not specified."></portainer-tooltip>
-        </label>
-        <div class="col-sm-9 col-lg-10">
-          <input type="text" class="form-control" name="template_hostname" ng-model="$ctrl.model.Hostname" placeholder="mycontainername" />
-        </div>
-      </div>
-      <!-- !hostname -->
-      <!-- network -->
-      <div class="form-group">
-        <label for="template_network" class="col-sm-3 col-lg-2 control-label text-left">
-          Network
-        </label>
-        <div class="col-sm-10">
-          <select class="form-control" ng-options="net.Name for net in $ctrl.networks" ng-model="$ctrl.model.Network">
-            <option disabled hidden value="">Select a network</option>
-          </select>
-        </div>
-      </div>
-      <!-- !network -->
-      <!-- port-mapping -->
-      <div class="form-group">
-        <div class="col-sm-12" style="margin-top: 5px;">
-          <label class="control-label text-left">Port mapping</label>
-          <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addPortBinding()">
-            <i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
-          </span>
-        </div>
-        <div class="col-sm-12" style="margin-top: 10px;" ng-if="$ctrl.model.Ports.length > 0">
-          <span class="small text-muted">Portainer will automatically assign a port if you leave the host port empty.</span>
-        </div>
-        <!-- port-mapping-input-list -->
-        <div class="col-sm-12">
-          <div class="col-sm-12 form-inline" style="margin-top: 10px;">
-            <div ng-repeat="portBinding in $ctrl.model.Ports" style="margin-top: 2px;">
-              <!-- host-port -->
-              <div class="input-group col-sm-4 input-group-sm">
-                <span class="input-group-addon">host</span>
-                <input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)" />
-              </div>
-              <!-- !host-port -->
-              <span style="margin: 0 10px 0 10px;">
-                <i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
-              </span>
-              <!-- container-port -->
-              <div class="input-group col-sm-4 input-group-sm">
-                <span class="input-group-addon">container</span>
-                <input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80" />
-              </div>
-              <!-- !container-port -->
-              <!-- protocol-actions -->
-              <div class="input-group col-sm-3 input-group-sm">
-                <div class="btn-group btn-group-sm">
-                  <label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
-                  <label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
-                </div>
-                <button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removePortBinding($index)">
-                  <i class="fa fa-trash" aria-hidden="true"></i>
-                </button>
-              </div>
-              <!-- !protocol-actions -->
-            </div>
-          </div>
-        </div>
-        <!-- !port-mapping-input-list -->
-      </div>
-      <!-- !port-mapping -->
-      <!-- volumes -->
-      <div class="form-group">
-        <div class="col-sm-12" style="margin-top: 5px;">
-          <label class="control-label text-left">Volume mapping</label>
-          <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addVolume()">
-            <i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
-          </span>
-        </div>
-        <div class="col-sm-12" style="margin-top: 10px;" ng-if="$ctrl.model.Volumes.length > 0">
-          <span class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</span>
-        </div>
-        <div ng-repeat="volume in $ctrl.model.Volumes">
-          <div class="col-sm-12" style="margin-top: 10px;">
-            <!-- volume-line1 -->
-            <div class="col-sm-12 form-inline">
-              <!-- container-path -->
-              <div class="input-group input-group-sm col-sm-6">
-                <span class="input-group-addon">container</span>
-                <input type="text" class="form-control" ng-model="volume.container" placeholder="e.g. /path/in/container" />
-              </div>
-              <!-- !container-path -->
-              <!-- volume-type -->
-              <div class="input-group col-sm-5" style="margin-left: 5px;">
-                <div class="btn-group btn-group-sm">
-                  <label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
-                  <label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''">Bind</label>
-                </div>
-                <button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeVolume($index)">
-                  <i class="fa fa-trash" aria-hidden="true"></i>
-                </button>
-              </div>
-              <!-- !volume-type -->
-            </div>
-            <!-- !volume-line1 -->
-            <!-- volume-line2 -->
-            <div class="col-sm-12 form-inline" style="margin-top: 5px;" ng-if="volume.type !== 'auto'">
-              <i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
-              <!-- bind -->
-              <div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'bind'">
-                <span class="input-group-addon">host</span>
-                <input type="text" class="form-control" ng-model="volume.bind" placeholder="e.g. /path/on/host" />
-              </div>
-              <!-- !bind -->
-              <!-- read-only -->
-              <div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
-                <div class="btn-group btn-group-sm">
-                  <label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
-                  <label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
-                </div>
-              </div>
-              <!-- !read-only -->
-            </div>
-            <!-- !volume-line2 -->
-          </div>
-        </div>
-      </div>
-      <!-- !volumes -->
-      <!-- labels -->
-      <div class="form-group">
-        <div class="col-sm-12" style="margin-top: 5px;">
-          <label class="control-label text-left">Labels</label>
-          <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabel()">
-            <i class="fa fa-plus-circle" aria-hidden="true"></i> add label
-          </span>
-        </div>
-        <!-- labels-input-list -->
-        <div class="col-sm-12">
-          <div class="col-sm-12 form-inline" style="margin-top: 10px;">
-            <div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
-              <div class="input-group col-sm-5 input-group-sm">
-                <span class="input-group-addon">name</span>
-                <input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo" />
-              </div>
-              <div class="input-group col-sm-5 input-group-sm">
-                <span class="input-group-addon">value</span>
-                <input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" />
-              </div>
-              <button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabel($index)">
-                <i class="fa fa-trash" aria-hidden="true"></i>
-              </button>
-            </div>
-          </div>
-        </div>
-        <!-- !labels-input-list -->
-      </div>
-      <!-- !labels -->
-      <!-- restart_policy -->
-      <div class="form-group">
-        <label for="template_restart_policy" class="col-sm-3 col-lg-2 control-label text-left">
-          Restart policy
-        </label>
-        <div class="col-sm-9 col-lg-10">
-          <select class="form-control" name="template_platform" ng-model="$ctrl.model.RestartPolicy">
-            <option value="always">Always</option>
-            <option value="unless-stopped">Unless stopped</option>
-            <option value="on-failure">On failure</option>
-            <option value="no">None</option>
-          </select>
-        </div>
-      </div>
-      <!-- !restart_policy -->
-      <!-- privileged-mode -->
-      <div class="form-group">
-        <div class="col-sm-12">
-          <label for="tls" class="control-label text-left">
-            Privileged mode
-            <portainer-tooltip position="bottom" message="Start the container in privileged mode."></portainer-tooltip>
-          </label>
-          <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.Privileged" /><i></i> </label>
-        </div>
-      </div>
-      <!-- !privileged-mode -->
-      <!-- interactive-mode -->
-      <div class="form-group">
-        <div class="col-sm-12">
-          <label for="tls" class="control-label text-left">
-            Interactive mode
-            <portainer-tooltip position="bottom" message="Start the container in foreground (equivalent of -i -t flags)."></portainer-tooltip>
-          </label>
-          <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.model.Interactive" /><i></i> </label>
-        </div>
-      </div>
-      <!-- !interactive-mode -->
-    </div>
-    <!-- !container-details -->
-  </div>
-  <div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseEnv = !$ctrl.state.collapseEnv">
-    Environment
-    <span class="small space-left">
-      <a ng-if="$ctrl.state.collapseEnv"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
-      <a ng-if="!$ctrl.state.collapseEnv"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
-    </span>
-  </div>
-  <!-- environment-details -->
-  <div uib-collapse="$ctrl.state.collapseEnv">
-    <!-- env -->
-    <div class="form-group">
-      <div class="col-sm-12" style="margin-top: 5px;">
-        <label class="control-label text-left">Environment variables</label>
-        <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVar()">
-          <i class="fa fa-plus-circle" aria-hidden="true"></i> add variable
-        </span>
-      </div>
-      <!-- env-var-list -->
-      <div style="margin-top: 10px;">
-        <div class="col-sm-12 template-envvar" ng-repeat="var in $ctrl.model.Env" style="margin-top: 10px;">
-          <div class="form-group"></div>
-          <div class="form-group" style="margin-bottom: 0;">
-            <div class="boxselector_wrapper">
-              <div>
-                <input type="radio" id="preset_var_{{ $index }}" ng-model="var.type" ng-value="1" ng-change="$ctrl.changeEnvVarType(var)" />
-                <label for="preset_var_{{ $index }}">
-                  <div class="boxselector_header">
-                    <i class="fa fa-user-slash" aria-hidden="true" style="margin-right: 2px;"></i>
-                    Preset
-                  </div>
-                  <p>Preset variable</p>
-                </label>
-              </div>
-              <div>
-                <input type="radio" id="text_var_{{ $index }}" ng-model="var.type" ng-value="2" ng-change="$ctrl.changeEnvVarType(var)" />
-                <label for="text_var_{{ $index }}">
-                  <div class="boxselector_header">
-                    <i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
-                    Text
-                  </div>
-                  <p>Free text value</p>
-                </label>
-              </div>
-              <div>
-                <input type="radio" id="select_var_{{ $index }}" ng-model="var.type" ng-value="3" />
-                <label for="select_var_{{ $index }}">
-                  <div class="boxselector_header">
-                    <i class="fa fa-list-ol" aria-hidden="true" style="margin-right: 2px;"></i>
-                    Select
-                  </div>
-                  <p>Choose value from list</p>
-                </label>
-              </div>
-            </div>
-          </div>
-          <div class="form-group">
-            <label class="col-sm-2 control-label text-left">
-              Name
-            </label>
-            <div class="col-sm-8">
-              <input type="text" class="form-control" ng-model="var.name" placeholder="env_var" />
-            </div>
-            <div class="col-sm-2">
-              <button class="btn btn-sm btn-danger space-left" type="button" ng-click="$ctrl.removeEnvVar($index)">
-                <i class="fa fa-trash" aria-hidden="true"></i>
-              </button>
-            </div>
-          </div>
-          <div ng-if="var.type == 2 || var.type == 3">
-            <div class="form-group">
-              <label class="col-sm-2 control-label text-left">
-                Label
-              </label>
-              <div class="col-sm-10">
-                <input type="text" class="form-control" ng-model="var.label" placeholder="Choose a label" />
-              </div>
-            </div>
-            <div class="form-group">
-              <label class="col-sm-2 control-label text-left" style="margin-top: 2px;">
-                Description
-              </label>
-              <div class="col-sm-10" style="margin-top: 2px;">
-                <input type="text" class="form-control" ng-model="var.description" placeholder="Tooltip" />
-              </div>
-            </div>
-          </div>
-          <div class="form-group" ng-if="var.type === 1 || var.type === 2">
-            <label class="col-sm-2 control-label text-left">
-              Default value
-            </label>
-            <div class="col-sm-10">
-              <input type="text" class="form-control" ng-model="var.default" placeholder="default_value" />
-            </div>
-          </div>
-          <div ng-if="var.type === 3" style="margin-bottom: 5px;" class="form-group">
-            <div class="col-sm-12" style="margin-top: 5px;">
-              <label class="control-label text-left">Values</label>
-              <span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVarValue(var)">
-                <i class="fa fa-plus-circle" aria-hidden="true"></i> add allowed value
-              </span>
-            </div>
-            <!-- envvar-values-list -->
-            <div class="col-sm-12">
-              <div class="col-sm-12 form-inline" style="margin-top: 10px;">
-                <div ng-repeat="val in var.select" style="margin-top: 2px;">
-                  <div class="input-group col-sm-5 input-group-sm">
-                    <span class="input-group-addon">name</span>
-                    <input type="text" class="form-control" ng-model="val.text" placeholder="Yes, I agree" />
-                  </div>
-                  <div class="input-group col-sm-5 input-group-sm">
-                    <span class="input-group-addon">value</span>
-                    <input type="text" class="form-control" ng-model="val.value" placeholder="Y" />
-                  </div>
-                  <div class="input-group col-sm-1 input-group-sm">
-                    <button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeEnvVarValue(var, $index)">
-                      <i class="fa fa-trash" aria-hidden="true"></i>
-                    </button>
-                    <input style="margin-left: 5px;" type="checkbox" ng-model="val.default" id="val_default_{{ $index }}" /><label for="val_default_{{ $index }}" class="space-left"
-                      >Default</label
-                    >
-                  </div>
-                </div>
-              </div>
-            </div>
-            <!-- envvar-values-list -->
-          </div>
-          <div class="col-sm-12" ng-show="$ctrl.model.Env.length > 1">
-            <div class="line-separator"></div>
-          </div>
-        </div>
-      </div>
-      <!-- !env-var-list -->
-    </div>
-    <!-- !env -->
-  </div>
-  <!-- !environment-details -->
-  <!-- actions -->
-  <div class="col-sm-12 form-section-title">
-    Actions
-  </div>
-  <div class="form-group">
-    <div class="col-sm-12">
-      <button
-        type="button"
-        class="btn btn-primary btn-sm"
-        ng-click="$ctrl.formAction()"
-        ng-disabled="$ctrl.actionInProgress || !templateForm.$valid"
-        button-spinner="$ctrl.actionInProgress"
-      >
-        <span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
-        <span ng-show="$ctrl.actionInProgress">In progress...</span>
-      </button>
-    </div>
-  </div>
-  <!-- !actions -->
-</form>
diff --git a/app/portainer/components/forms/template-form/templateFormController.js b/app/portainer/components/forms/template-form/templateFormController.js
deleted file mode 100644
index c03c11b60..000000000
--- a/app/portainer/components/forms/template-form/templateFormController.js
+++ /dev/null
@@ -1,55 +0,0 @@
-angular.module('portainer.app').controller('TemplateFormController', [
-  function () {
-    this.state = {
-      collapseTemplate: false,
-      collapseContainer: false,
-      collapseStack: false,
-      collapseEnv: false,
-    };
-
-    this.addPortBinding = function () {
-      this.model.Ports.push({ containerPort: '', protocol: 'tcp' });
-    };
-
-    this.removePortBinding = function (index) {
-      this.model.Ports.splice(index, 1);
-    };
-
-    this.addVolume = function () {
-      this.model.Volumes.push({ container: '', bind: '', readonly: false, type: 'auto' });
-    };
-
-    this.removeVolume = function (index) {
-      this.model.Volumes.splice(index, 1);
-    };
-
-    this.addLabel = function () {
-      this.model.Labels.push({ name: '', value: '' });
-    };
-
-    this.removeLabel = function (index) {
-      this.model.Labels.splice(index, 1);
-    };
-
-    this.addEnvVar = function () {
-      this.model.Env.push({ type: 1, name: '', label: '', description: '', default: '', preset: true, select: [] });
-    };
-
-    this.removeEnvVar = function (index) {
-      this.model.Env.splice(index, 1);
-    };
-
-    this.addEnvVarValue = function (env) {
-      env.select = env.select || [];
-      env.select.push({ name: '', value: '' });
-    };
-
-    this.removeEnvVarValue = function (env, index) {
-      env.select.splice(index, 1);
-    };
-
-    this.changeEnvVarType = function (env) {
-      env.preset = env.type === 1;
-    };
-  },
-]);
diff --git a/app/portainer/components/index.js b/app/portainer/components/index.js
index 8fa3030a0..6291cb0b5 100644
--- a/app/portainer/components/index.js
+++ b/app/portainer/components/index.js
@@ -1,5 +1,6 @@
 import angular from 'angular';
 
 import gitFormModule from './forms/git-form';
+import porAccessManagementModule from './accessManagement';
 
-export default angular.module('portainer.app.components', [gitFormModule]).name;
+export default angular.module('portainer.app.components', [gitFormModule, porAccessManagementModule]).name;
diff --git a/app/portainer/components/registry-details/index.js b/app/portainer/components/registry-details/index.js
new file mode 100644
index 000000000..418c48781
--- /dev/null
+++ b/app/portainer/components/registry-details/index.js
@@ -0,0 +1,10 @@
+import angular from 'angular';
+
+export const registryDetails = {
+  templateUrl: './registry-details.html',
+  bindings: {
+    registry: '<',
+  },
+};
+
+angular.module('portainer.app').component('registryDetails', registryDetails);
diff --git a/app/portainer/components/registry-details/registry-details.html b/app/portainer/components/registry-details/registry-details.html
new file mode 100644
index 000000000..b67e69b03
--- /dev/null
+++ b/app/portainer/components/registry-details/registry-details.html
@@ -0,0 +1,25 @@
+<div class="row" ng-if="$ctrl.registry">
+  <div class="col-lg-12 col-md-12 col-xs-12">
+    <rd-widget>
+      <rd-widget-header icon="fa-plug" title-text="Registry"></rd-widget-header>
+      <rd-widget-body classes="no-padding">
+        <table class="table">
+          <tbody>
+            <tr>
+              <td>Name</td>
+              <td>
+                {{ $ctrl.registry.Name }}
+              </td>
+            </tr>
+            <tr>
+              <td>URL</td>
+              <td>
+                {{ $ctrl.registry.URL }}
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </rd-widget-body>
+    </rd-widget>
+  </div>
+</div>
diff --git a/app/portainer/helpers/endpointHelper.js b/app/portainer/helpers/endpointHelper.js
index 609d31f19..005127859 100644
--- a/app/portainer/helpers/endpointHelper.js
+++ b/app/portainer/helpers/endpointHelper.js
@@ -1,36 +1,33 @@
 import _ from 'lodash-es';
+import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
 
-angular.module('portainer.app').factory('EndpointHelper', [
-  function EndpointHelperFactory() {
-    'use strict';
-    var helper = {};
+function findAssociatedGroup(endpoint, groups) {
+  return _.find(groups, function (group) {
+    return group.Id === endpoint.GroupId;
+  });
+}
 
-    function findAssociatedGroup(endpoint, groups) {
-      return _.find(groups, function (group) {
-        return group.Id === endpoint.GroupId;
-      });
-    }
+export default class EndpointHelper {
+  static isLocalEndpoint(endpoint) {
+    return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment;
+  }
 
-    helper.isLocalEndpoint = isLocalEndpoint;
-    function isLocalEndpoint(endpoint) {
-      return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === 5;
-    }
+  static isAgentEndpoint(endpoint) {
+    return [
+      PortainerEndpointTypes.AgentOnDockerEnvironment,
+      PortainerEndpointTypes.EdgeAgentOnDockerEnvironment,
+      PortainerEndpointTypes.AgentOnKubernetesEnvironment,
+      PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment,
+    ].includes(endpoint.Type);
+  }
 
-    helper.isAgentEndpoint = isAgentEndpoint;
-    function isAgentEndpoint(endpoint) {
-      return [2, 4, 6, 7].includes(endpoint.Type);
-    }
-
-    helper.mapGroupNameToEndpoint = function (endpoints, groups) {
-      for (var i = 0; i < endpoints.length; i++) {
-        var endpoint = endpoints[i];
-        var group = findAssociatedGroup(endpoint, groups);
-        if (group) {
-          endpoint.GroupName = group.Name;
-        }
+  static mapGroupNameToEndpoint(endpoints, groups) {
+    for (var i = 0; i < endpoints.length; i++) {
+      var endpoint = endpoints[i];
+      var group = findAssociatedGroup(endpoint, groups);
+      if (group) {
+        endpoint.GroupName = group.Name;
       }
-    };
-
-    return helper;
-  },
-]);
+    }
+  }
+}
diff --git a/app/portainer/models/dockerhub.js b/app/portainer/models/dockerhub.js
index 880a89f5b..27b67e130 100644
--- a/app/portainer/models/dockerhub.js
+++ b/app/portainer/models/dockerhub.js
@@ -1,7 +1,8 @@
-export function DockerHubViewModel(data) {
-  this.Name = 'DockerHub';
-  this.URL = '';
-  this.Authentication = data.Authentication;
-  this.Username = data.Username;
-  this.Password = data.Password;
+import { RegistryTypes } from './registryTypes';
+
+export function DockerHubViewModel() {
+  this.Id = 0;
+  this.Type = RegistryTypes.ANONYMOUS;
+  this.Name = 'DockerHub (anonymous)';
+  this.URL = 'docker.io';
 }
diff --git a/app/portainer/models/registry.js b/app/portainer/models/registry.js
index a7e2ac26e..a9effdc33 100644
--- a/app/portainer/models/registry.js
+++ b/app/portainer/models/registry.js
@@ -10,10 +10,7 @@ export function RegistryViewModel(data) {
   this.Authentication = data.Authentication;
   this.Username = data.Username;
   this.Password = data.Password;
-  this.AuthorizedUsers = data.AuthorizedUsers;
-  this.AuthorizedTeams = data.AuthorizedTeams;
-  this.UserAccessPolicies = data.UserAccessPolicies;
-  this.TeamAccessPolicies = data.TeamAccessPolicies;
+  this.RegistryAccesses = data.RegistryAccesses; // map[EndpointID]{UserAccessPolicies, TeamAccessPolicies, NamespaceAccessPolicies}
   this.Checked = false;
   this.Gitlab = data.Gitlab;
   this.Quay = data.Quay;
@@ -40,7 +37,7 @@ export function RegistryManagementConfigurationDefaultModel(registry) {
   }
 }
 
-export function RegistryDefaultModel() {
+export function RegistryCreateFormValues() {
   this.Type = RegistryTypes.CUSTOM;
   this.URL = '';
   this.Name = '';
diff --git a/app/portainer/models/registryTypes.js b/app/portainer/models/registryTypes.js
index a427a5954..ef5ded0ae 100644
--- a/app/portainer/models/registryTypes.js
+++ b/app/portainer/models/registryTypes.js
@@ -1,7 +1,9 @@
 export const RegistryTypes = Object.freeze({
+  ANONYMOUS: 0, // not backend replicated, only for frontend
   QUAY: 1,
   AZURE: 2,
   CUSTOM: 3,
   GITLAB: 4,
   PROGET: 5,
+  DOCKERHUB: 6,
 });
diff --git a/app/portainer/rest/dockerhub.js b/app/portainer/rest/dockerhub.js
deleted file mode 100644
index 35b891076..000000000
--- a/app/portainer/rest/dockerhub.js
+++ /dev/null
@@ -1,15 +0,0 @@
-angular.module('portainer.app').factory('DockerHub', [
-  '$resource',
-  'API_ENDPOINT_DOCKERHUB',
-  function DockerHubFactory($resource, API_ENDPOINT_DOCKERHUB) {
-    'use strict';
-    return $resource(
-      API_ENDPOINT_DOCKERHUB,
-      {},
-      {
-        get: { method: 'GET' },
-        update: { method: 'PUT' },
-      }
-    );
-  },
-]);
diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js
index d88e4e243..52bc57f92 100644
--- a/app/portainer/rest/endpoint.js
+++ b/app/portainer/rest/endpoint.js
@@ -22,7 +22,26 @@ angular.module('portainer.app').factory('Endpoints', [
         snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
         status: { method: 'GET', params: { id: '@id', action: 'status' } },
         updateSecuritySettings: { method: 'PUT', params: { id: '@id', action: 'settings' } },
-        dockerhubLimits: { method: 'GET', params: { id: '@id', action: 'dockerhub' } },
+        dockerhubLimits: {
+          method: 'GET',
+          url: `${API_ENDPOINT_ENDPOINTS}/:id/dockerhub/:registryId`,
+        },
+        registries: {
+          method: 'GET',
+          url: `${API_ENDPOINT_ENDPOINTS}/:id/registries`,
+          params: { id: '@id', namespace: '@namespace' },
+          isArray: true,
+        },
+        registry: {
+          url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`,
+          method: 'GET',
+          params: { id: '@id', namespace: '@namespace', registryId: '@registryId' },
+        },
+        updateRegistryAccess: {
+          method: 'PUT',
+          url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`,
+          params: { id: '@id', registryId: '@registryId' },
+        },
       }
     );
   },
diff --git a/app/portainer/rest/registry.js b/app/portainer/rest/registry.js
index 345ee6d5b..706c56ae1 100644
--- a/app/portainer/rest/registry.js
+++ b/app/portainer/rest/registry.js
@@ -9,9 +9,8 @@ angular.module('portainer.app').factory('Registries', [
       {
         create: { method: 'POST', ignoreLoadingBar: true },
         query: { method: 'GET', isArray: true },
-        get: { method: 'GET', params: { id: '@id' } },
+        get: { method: 'GET', params: { id: '@id', action: '', endpointId: '@endpointId' } },
         update: { method: 'PUT', params: { id: '@id' } },
-        updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
         remove: { method: 'DELETE', params: { id: '@id' } },
         configure: { method: 'POST', params: { id: '@id', action: 'configure' } },
       }
diff --git a/app/portainer/services/api/accessService.js b/app/portainer/services/api/accessService.js
index d09f4bdfe..40018afd3 100644
--- a/app/portainer/services/api/accessService.js
+++ b/app/portainer/services/api/accessService.js
@@ -9,7 +9,10 @@ angular.module('portainer.app').factory('AccessService', [
   'TeamService',
   function AccessServiceFactory($q, $async, UserService, TeamService) {
     'use strict';
-    var service = {};
+    return {
+      accesses,
+      generateAccessPolicies,
+    };
 
     function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) {
       var availableAccesses = [];
@@ -76,7 +79,7 @@ angular.module('portainer.app').factory('AccessService', [
     async function accessesAsync(entity, parent) {
       try {
         if (!entity) {
-          throw { msg: 'Unable to retrieve accesses' };
+          throw new Error('Unable to retrieve accesses');
         }
         if (!entity.UserAccessPolicies) {
           entity.UserAccessPolicies = {};
@@ -100,9 +103,7 @@ angular.module('portainer.app').factory('AccessService', [
       return $async(accessesAsync, entity, parent);
     }
 
-    service.accesses = accesses;
-
-    service.generateAccessPolicies = function (userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) {
+    function generateAccessPolicies(userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) {
       const newUserPolicies = _.clone(userAccessPolicies);
       const newTeamPolicies = _.clone(teamAccessPolicies);
 
@@ -115,8 +116,6 @@ angular.module('portainer.app').factory('AccessService', [
       };
 
       return accessPolicies;
-    };
-
-    return service;
+    }
   },
 ]);
diff --git a/app/portainer/services/api/dockerhubService.js b/app/portainer/services/api/dockerhubService.js
index aa079ae40..6326fa9a8 100644
--- a/app/portainer/services/api/dockerhubService.js
+++ b/app/portainer/services/api/dockerhubService.js
@@ -1,51 +1,27 @@
-import { DockerHubViewModel } from '../../models/dockerhub';
+import EndpointHelper from 'Portainer/helpers/endpointHelper';
+import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
 
-angular.module('portainer.app').factory('DockerHubService', [
-  '$q',
-  'DockerHub',
-  'Endpoints',
-  'AgentDockerhub',
-  'EndpointHelper',
-  function DockerHubServiceFactory($q, DockerHub, Endpoints, AgentDockerhub, EndpointHelper) {
-    'use strict';
-    var service = {};
+angular.module('portainer.app').factory('DockerHubService', DockerHubService);
 
-    service.dockerhub = function () {
-      var deferred = $q.defer();
+/* @ngInject */
+function DockerHubService(Endpoints, AgentDockerhub) {
+  return {
+    checkRateLimits,
+  };
 
-      DockerHub.get()
-        .$promise.then(function success(data) {
-          var dockerhub = new DockerHubViewModel(data);
-          deferred.resolve(dockerhub);
-        })
-        .catch(function error(err) {
-          deferred.reject({ msg: 'Unable to retrieve DockerHub details', err: err });
-        });
-
-      return deferred.promise;
-    };
-
-    service.update = function (dockerhub) {
-      return DockerHub.update({}, dockerhub).$promise;
-    };
-
-    service.checkRateLimits = checkRateLimits;
-    function checkRateLimits(endpoint) {
-      if (EndpointHelper.isLocalEndpoint(endpoint)) {
-        return Endpoints.dockerhubLimits({ id: endpoint.Id }).$promise;
-      }
-
-      switch (endpoint.Type) {
-        case 2: //AgentOnDockerEnvironment
-        case 4: //EdgeAgentOnDockerEnvironment
-          return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'docker' }).$promise;
-
-        case 6: //AgentOnKubernetesEnvironment
-        case 7: //EdgeAgentOnKubernetesEnvironment
-          return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'kubernetes' }).$promise;
-      }
+  function checkRateLimits(endpoint, registryId) {
+    if (EndpointHelper.isLocalEndpoint(endpoint)) {
+      return Endpoints.dockerhubLimits({ id: endpoint.Id, registryId }).$promise;
     }
 
-    return service;
-  },
-]);
+    switch (endpoint.Type) {
+      case PortainerEndpointTypes.AgentOnDockerEnvironment:
+      case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment:
+        return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'docker', registryId }).$promise;
+
+      case PortainerEndpointTypes.AgentOnKubernetesEnvironment:
+      case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment:
+        return AgentDockerhub.limits({ endpointId: endpoint.Id, endpointType: 'kubernetes', registryId }).$promise;
+    }
+  }
+}
diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js
index 870075156..ef7291343 100644
--- a/app/portainer/services/api/endpointService.js
+++ b/app/portainer/services/api/endpointService.js
@@ -8,6 +8,9 @@ angular.module('portainer.app').factory('EndpointService', [
     'use strict';
     var service = {
       updateSecuritySettings,
+      registries,
+      registry,
+      updateRegistryAccess,
     };
 
     service.endpoint = function (endpointID) {
@@ -157,10 +160,22 @@ angular.module('portainer.app').factory('EndpointService', [
       return deferred.promise;
     };
 
+    function updateRegistryAccess(id, registryId, endpointAccesses) {
+      return Endpoints.updateRegistryAccess({ registryId, id }, endpointAccesses).$promise;
+    }
+
+    function registries(id, namespace) {
+      return Endpoints.registries({ namespace, id }).$promise;
+    }
+
     return service;
 
     function updateSecuritySettings(id, securitySettings) {
       return Endpoints.updateSecuritySettings({ id }, securitySettings).$promise;
     }
+
+    function registry(endpointId, registryId) {
+      return Endpoints.registry({ registryId, id: endpointId }).$promise;
+    }
   },
 ]);
diff --git a/app/portainer/services/api/registryService.js b/app/portainer/services/api/registryService.js
index 36c46ad77..34b8e882f 100644
--- a/app/portainer/services/api/registryService.js
+++ b/app/portainer/services/api/registryService.js
@@ -1,20 +1,30 @@
 import _ from 'lodash-es';
 import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
-import { RegistryTypes } from '@/portainer/models/registryTypes';
-import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry';
+import { RegistryTypes } from 'Portainer/models/registryTypes';
+import { RegistryCreateRequest, RegistryViewModel } from 'Portainer/models/registry';
+import { DockerHubViewModel } from 'Portainer/models/dockerhub';
 
 angular.module('portainer.app').factory('RegistryService', [
   '$q',
   '$async',
+  'EndpointService',
   'Registries',
-  'DockerHubService',
   'ImageHelper',
   'FileUploadService',
-  function RegistryServiceFactory($q, $async, Registries, DockerHubService, ImageHelper, FileUploadService) {
-    'use strict';
-    var service = {};
+  function RegistryServiceFactory($q, $async, EndpointService, Registries, ImageHelper, FileUploadService) {
+    return {
+      registries,
+      registry,
+      encodedCredentials,
+      deleteRegistry,
+      updateRegistry,
+      configureRegistry,
+      createRegistry,
+      createGitlabRegistries,
+      retrievePorRegistryModelFromRepository,
+    };
 
-    service.registries = function () {
+    function registries() {
       var deferred = $q.defer();
 
       Registries.query()
@@ -29,12 +39,12 @@ angular.module('portainer.app').factory('RegistryService', [
         });
 
       return deferred.promise;
-    };
+    }
 
-    service.registry = function (id) {
+    function registry(id, endpointId) {
       var deferred = $q.defer();
 
-      Registries.get({ id: id })
+      Registries.get({ id, endpointId })
         .$promise.then(function success(data) {
           var registry = new RegistryViewModel(data);
           deferred.resolve(registry);
@@ -44,39 +54,35 @@ angular.module('portainer.app').factory('RegistryService', [
         });
 
       return deferred.promise;
-    };
+    }
 
-    service.encodedCredentials = function (registry) {
+    function encodedCredentials(registry) {
       var credentials = {
-        serveraddress: registry.URL,
+        registryId: registry.Id,
       };
       return btoa(JSON.stringify(credentials));
-    };
+    }
 
-    service.updateAccess = function (id, userAccessPolicies, teamAccessPolicies) {
-      return Registries.updateAccess({ id: id }, { UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies }).$promise;
-    };
-
-    service.deleteRegistry = function (id) {
+    function deleteRegistry(id) {
       return Registries.remove({ id: id }).$promise;
-    };
+    }
 
-    service.updateRegistry = function (registry) {
+    function updateRegistry(registry) {
       registry.URL = _.replace(registry.URL, /^https?\:\/\//i, '');
       registry.URL = _.replace(registry.URL, /\/$/, '');
       return Registries.update({ id: registry.Id }, registry).$promise;
-    };
+    }
 
-    service.configureRegistry = function (id, registryManagementConfigurationModel) {
+    function configureRegistry(id, registryManagementConfigurationModel) {
       return FileUploadService.configureRegistry(id, registryManagementConfigurationModel);
-    };
+    }
 
-    service.createRegistry = function (model) {
+    function createRegistry(model) {
       var payload = new RegistryCreateRequest(model);
       return Registries.create(payload).$promise;
-    };
+    }
 
-    service.createGitlabRegistries = function (model, projects) {
+    function createGitlabRegistries(model, projects) {
       const promises = [];
       _.forEach(projects, (p) => {
         const m = model;
@@ -88,9 +94,7 @@ angular.module('portainer.app').factory('RegistryService', [
         promises.push(Registries.create(payload).$promise);
       });
       return $q.all(promises);
-    };
-
-    service.retrievePorRegistryModelFromRepositoryWithRegistries = retrievePorRegistryModelFromRepositoryWithRegistries;
+    }
 
     function getURL(reg) {
       let url = reg.URL;
@@ -103,14 +107,23 @@ angular.module('portainer.app').factory('RegistryService', [
       return url;
     }
 
-    function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub) {
+    function retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, registryId) {
       const model = new PorImageRegistryModel();
-      const registry = _.find(registries, (reg) => _.includes(repository, getURL(reg)));
+      const registry = registries.find((reg) => {
+        if (registryId) {
+          return reg.Id === registryId;
+        }
+        if (reg.Type === RegistryTypes.DOCKERHUB) {
+          return _.includes(repository, reg.Username);
+        }
+        return _.includes(repository, getURL(reg));
+      });
       if (registry) {
         const url = getURL(registry);
-        const lastIndex = repository.lastIndexOf(url) + url.length;
+        let lastIndex = repository.lastIndexOf(url);
+        lastIndex = lastIndex === -1 ? 0 : lastIndex + url.length;
         let image = repository.substring(lastIndex);
-        if (!_.startsWith(image, ':')) {
+        if (_.startsWith(image, '/')) {
           image = image.substring(1);
         }
         model.Registry = registry;
@@ -119,25 +132,21 @@ angular.module('portainer.app').factory('RegistryService', [
         if (ImageHelper.imageContainsURL(repository)) {
           model.UseRegistry = false;
         }
-        model.Registry = dockerhub;
+        model.Registry = new DockerHubViewModel();
         model.Image = repository;
       }
       return model;
     }
 
-    async function retrievePorRegistryModelFromRepositoryAsync(repository) {
-      try {
-        let [registries, dockerhub] = await Promise.all([service.registries(), DockerHubService.dockerhub()]);
-        return retrievePorRegistryModelFromRepositoryWithRegistries(repository, registries, dockerhub);
-      } catch (err) {
-        throw { msg: 'Unable to retrieve the registry associated to the repository', err: err };
-      }
+    function retrievePorRegistryModelFromRepository(repository, endpointId, registryId, namespace) {
+      return $async(async () => {
+        try {
+          const regs = await EndpointService.registries(endpointId, namespace);
+          return retrievePorRegistryModelFromRepositoryWithRegistries(repository, regs, registryId);
+        } catch (err) {
+          throw { msg: 'Unable to retrieve the registry associated to the repository', err: err };
+        }
+      });
     }
-
-    service.retrievePorRegistryModelFromRepository = function (repository) {
-      return $async(retrievePorRegistryModelFromRepositoryAsync, repository);
-    };
-
-    return service;
   },
 ]);
diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js
index 82fb486e9..04265c85b 100644
--- a/app/portainer/services/api/templateService.js
+++ b/app/portainer/services/api/templateService.js
@@ -1,91 +1,85 @@
-import _ from 'lodash-es';
+import { DockerHubViewModel } from 'Portainer/models/dockerhub';
 import { TemplateViewModel } from '../../models/template';
 
-angular.module('portainer.app').factory('TemplateService', [
-  '$q',
-  'Templates',
-  'TemplateHelper',
-  'RegistryService',
-  'DockerHubService',
-  'ImageHelper',
-  'ContainerHelper',
-  function TemplateServiceFactory($q, Templates, TemplateHelper, RegistryService, DockerHubService, ImageHelper, ContainerHelper) {
-    'use strict';
-    var service = {};
+angular.module('portainer.app').factory('TemplateService', TemplateServiceFactory);
 
-    service.templates = function () {
-      const deferred = $q.defer();
+/* @ngInject */
+function TemplateServiceFactory($q, Templates, TemplateHelper, EndpointProvider, ImageHelper, ContainerHelper, EndpointService) {
+  var service = {};
 
-      $q.all({
-        templates: Templates.query().$promise,
-        registries: RegistryService.registries(),
-        dockerhub: DockerHubService.dockerhub(),
-      })
-        .then(function success(data) {
-          const version = data.templates.version;
-          const templates = _.map(data.templates.templates, (item) => {
+  service.templates = function () {
+    const deferred = $q.defer();
+    const endpointId = EndpointProvider.currentEndpoint().Id;
+
+    $q.all({
+      templates: Templates.query().$promise,
+      registries: EndpointService.registries(endpointId),
+    })
+      .then(function success({ templates, registries }) {
+        const version = templates.version;
+        deferred.resolve(
+          templates.templates.map((item) => {
             try {
               const template = new TemplateViewModel(item, version);
-              const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(template.RegistryModel.Registry.URL, data.registries, data.dockerhub);
-              registry.Image = template.RegistryModel.Image;
-              template.RegistryModel = registry;
+              const registryURL = template.RegistryModel.Registry.URL;
+              const registry = registryURL ? registries.find((reg) => reg.URL === registryURL) : new DockerHubViewModel();
+              template.RegistryModel.Registry = registry;
               return template;
             } catch (err) {
               deferred.reject({ msg: 'Unable to retrieve templates', err: err });
             }
-          });
-          deferred.resolve(templates);
-        })
-        .catch(function error(err) {
-          deferred.reject({ msg: 'Unable to retrieve templates', err: err });
-        });
-
-      return deferred.promise;
-    };
-
-    service.templateFile = templateFile;
-    function templateFile(repositoryUrl, composeFilePathInRepository) {
-      return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise;
-    }
-
-    service.createTemplateConfiguration = function (template, containerName, network) {
-      var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
-      var containerConfiguration = createContainerConfiguration(template, containerName, network);
-      containerConfiguration.Image = imageConfiguration.fromImage;
-      return containerConfiguration;
-    };
-
-    function createContainerConfiguration(template, containerName, network) {
-      var configuration = TemplateHelper.getDefaultContainerConfiguration();
-      configuration.HostConfig.NetworkMode = network.Name;
-      configuration.HostConfig.Privileged = template.Privileged;
-      configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy };
-      configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
-      configuration.name = containerName;
-      configuration.Hostname = template.Hostname;
-      configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
-      configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
-      var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
-      configuration.HostConfig.PortBindings = portConfiguration.bindings;
-      configuration.ExposedPorts = portConfiguration.exposedPorts;
-      var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive);
-      configuration.OpenStdin = consoleConfiguration.openStdin;
-      configuration.Tty = consoleConfiguration.tty;
-      configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
-      return configuration;
-    }
-
-    service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) {
-      var volumes = template.Volumes;
-      TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
-      volumes.forEach(function (volume) {
-        if (volume.binding) {
-          configuration.Volumes[volume.container] = {};
-          configuration.HostConfig.Binds.push(volume.binding);
-        }
+          })
+        );
+      })
+      .catch(function error(err) {
+        deferred.reject({ msg: 'Unable to retrieve templates', err: err });
       });
-    };
 
-    return service;
-  },
-]);
+    return deferred.promise;
+  };
+
+  service.templateFile = templateFile;
+  function templateFile(repositoryUrl, composeFilePathInRepository) {
+    return Templates.file({ repositoryUrl, composeFilePathInRepository }).$promise;
+  }
+
+  service.createTemplateConfiguration = function (template, containerName, network) {
+    var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel);
+    var containerConfiguration = createContainerConfiguration(template, containerName, network);
+    containerConfiguration.Image = imageConfiguration.fromImage;
+    return containerConfiguration;
+  };
+
+  function createContainerConfiguration(template, containerName, network) {
+    var configuration = TemplateHelper.getDefaultContainerConfiguration();
+    configuration.HostConfig.NetworkMode = network.Name;
+    configuration.HostConfig.Privileged = template.Privileged;
+    configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy };
+    configuration.HostConfig.ExtraHosts = template.Hosts ? template.Hosts : [];
+    configuration.name = containerName;
+    configuration.Hostname = template.Hostname;
+    configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
+    configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
+    var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
+    configuration.HostConfig.PortBindings = portConfiguration.bindings;
+    configuration.ExposedPorts = portConfiguration.exposedPorts;
+    var consoleConfiguration = TemplateHelper.getConsoleConfiguration(template.Interactive);
+    configuration.OpenStdin = consoleConfiguration.openStdin;
+    configuration.Tty = consoleConfiguration.tty;
+    configuration.Labels = TemplateHelper.updateContainerConfigurationWithLabels(template.Labels);
+    return configuration;
+  }
+
+  service.updateContainerConfigurationWithVolumes = function (configuration, template, generatedVolumesPile) {
+    var volumes = template.Volumes;
+    TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
+    volumes.forEach(function (volume) {
+      if (volume.binding) {
+        configuration.Volumes[volume.container] = {};
+        configuration.HostConfig.Binds.push(volume.binding);
+      }
+    });
+  };
+
+  return service;
+}
diff --git a/app/portainer/views/endpoint-registries/registries.html b/app/portainer/views/endpoint-registries/registries.html
new file mode 100644
index 000000000..5ed4f74b3
--- /dev/null
+++ b/app/portainer/views/endpoint-registries/registries.html
@@ -0,0 +1,21 @@
+<rd-header>
+  <rd-header-title title-text="Environment registries">
+    <a data-toggle="tooltip" title="Refresh" ui-sref="docker.registries" ui-sref-opts="{reload: true}">
+      <i class="fa fa-sync" aria-hidden="true"></i>
+    </a>
+  </rd-header-title>
+  <rd-header-content>Manage registry access inside this environment</rd-header-content>
+</rd-header>
+<div class="row">
+  <div class="col-sm-12">
+    <registries-datatable
+      title-text="Registries"
+      title-icon="fa-database"
+      dataset="$ctrl.registries"
+      table-key="endpointRegistries"
+      order-by="Name"
+      endpoint-type="$ctrl.endpointType"
+      can-manage-access="$ctrl.canManageAccess"
+    ></registries-datatable>
+  </div>
+</div>
diff --git a/app/portainer/views/endpoint-registries/registries.js b/app/portainer/views/endpoint-registries/registries.js
new file mode 100644
index 000000000..61dfc3b42
--- /dev/null
+++ b/app/portainer/views/endpoint-registries/registries.js
@@ -0,0 +1,7 @@
+angular.module('portainer.app').component('endpointRegistriesView', {
+  templateUrl: './registries.html',
+  controller: 'EndpointRegistriesController',
+  bindings: {
+    endpoint: '<',
+  },
+});
diff --git a/app/portainer/views/endpoint-registries/registriesController.js b/app/portainer/views/endpoint-registries/registriesController.js
new file mode 100644
index 000000000..2805fa433
--- /dev/null
+++ b/app/portainer/views/endpoint-registries/registriesController.js
@@ -0,0 +1,51 @@
+import _ from 'lodash-es';
+import { DockerHubViewModel } from 'Portainer/models/dockerhub';
+import { RegistryTypes } from 'Portainer/models/registryTypes';
+
+class EndpointRegistriesController {
+  /* @ngInject */
+  constructor($async, Notifications, EndpointService) {
+    this.$async = $async;
+    this.Notifications = Notifications;
+    this.EndpointService = EndpointService;
+
+    this.canManageAccess = this.canManageAccess.bind(this);
+  }
+
+  canManageAccess(item) {
+    return item.Type !== RegistryTypes.ANONYMOUS;
+  }
+
+  getRegistries() {
+    return this.$async(async () => {
+      try {
+        const dockerhub = new DockerHubViewModel();
+        const registries = await this.EndpointService.registries(this.endpointId);
+        this.registries = _.concat(dockerhub, registries);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registries');
+      }
+    });
+  }
+
+  $onInit() {
+    return this.$async(async () => {
+      this.state = {
+        viewReady: false,
+      };
+
+      try {
+        this.endpointType = this.endpoint.Type;
+        this.endpointId = this.endpoint.Id;
+        await this.getRegistries();
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve registries');
+      } finally {
+        this.state.viewReady = true;
+      }
+    });
+  }
+}
+
+export default EndpointRegistriesController;
+angular.module('portainer.app').controller('EndpointRegistriesController', EndpointRegistriesController);
diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js
index df3c72781..83f9bfd0a 100644
--- a/app/portainer/views/endpoints/endpointsController.js
+++ b/app/portainer/views/endpoints/endpointsController.js
@@ -1,8 +1,9 @@
 import angular from 'angular';
+import EndpointHelper from 'Portainer/helpers/endpointHelper';
 
 angular.module('portainer.app').controller('EndpointsController', EndpointsController);
 
-function EndpointsController($q, $scope, $state, $async, EndpointService, GroupService, EndpointHelper, Notifications) {
+function EndpointsController($q, $scope, $state, $async, EndpointService, GroupService, Notifications) {
   $scope.removeAction = removeAction;
 
   function removeAction(endpoints) {
diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js
index a3fe0ddcc..b629a6bea 100644
--- a/app/portainer/views/home/homeController.js
+++ b/app/portainer/views/home/homeController.js
@@ -1,3 +1,5 @@
+import EndpointHelper from 'Portainer/helpers/endpointHelper';
+
 angular
   .module('portainer.app')
   .controller('HomeController', function (
@@ -7,7 +9,6 @@ angular
     TagService,
     Authentication,
     EndpointService,
-    EndpointHelper,
     GroupService,
     Notifications,
     EndpointProvider,
diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js
index 8872bcfe1..1432b8ecc 100644
--- a/app/portainer/views/init/admin/initAdminController.js
+++ b/app/portainer/views/init/admin/initAdminController.js
@@ -1,5 +1,4 @@
 angular.module('portainer.app').controller('InitAdminController', [
-  '$async',
   '$scope',
   '$state',
   'Notifications',
@@ -10,7 +9,7 @@ angular.module('portainer.app').controller('InitAdminController', [
   'EndpointService',
   'BackupService',
   'StatusService',
-  function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
+  function ($scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
     $scope.logo = StateManager.getState().application.logo;
 
     $scope.formValues = {
@@ -85,7 +84,9 @@ angular.module('portainer.app').controller('InitAdminController', [
           if (status && status.Version) {
             return;
           }
-        } catch (e) {}
+        } catch (e) {
+          // pass
+        }
       }
       throw 'Timeout to wait for Portainer restarting';
     }
diff --git a/app/portainer/views/registries/access/registryAccess.html b/app/portainer/views/registries/access/registryAccess.html
deleted file mode 100644
index bab5d7391..000000000
--- a/app/portainer/views/registries/access/registryAccess.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<rd-header>
-  <rd-header-title title-text="Registry access"></rd-header-title>
-  <rd-header-content>
-    <a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> &gt; Access management
-  </rd-header-content>
-</rd-header>
-
-<div class="row" ng-if="registry">
-  <div class="col-lg-12 col-md-12 col-xs-12">
-    <rd-widget>
-      <rd-widget-header icon="fa-plug" title-text="Registry"></rd-widget-header>
-      <rd-widget-body classes="no-padding">
-        <table class="table">
-          <tbody>
-            <tr>
-              <td>Name</td>
-              <td>
-                {{ registry.Name }}
-              </td>
-            </tr>
-            <tr>
-              <td>URL</td>
-              <td>
-                {{ registry.URL }}
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </rd-widget-body>
-    </rd-widget>
-  </div>
-</div>
-
-<por-access-management ng-if="registry" access-controlled-entity="registry" entity-type="registry" action-in-progress="state.actionInProgress" update-access="updateAccess">
-</por-access-management>
diff --git a/app/portainer/views/registries/access/registryAccessController.js b/app/portainer/views/registries/access/registryAccessController.js
deleted file mode 100644
index a31372368..000000000
--- a/app/portainer/views/registries/access/registryAccessController.js
+++ /dev/null
@@ -1,34 +0,0 @@
-angular.module('portainer.app').controller('RegistryAccessController', [
-  '$scope',
-  '$state',
-  '$transition$',
-  'RegistryService',
-  'Notifications',
-  function ($scope, $state, $transition$, RegistryService, Notifications) {
-    $scope.updateAccess = function () {
-      $scope.state.actionInProgress = true;
-      RegistryService.updateRegistry($scope.registry)
-        .then(() => {
-          Notifications.success('Access successfully updated');
-          $state.reload();
-        })
-        .catch((err) => {
-          $scope.state.actionInProgress = false;
-          Notifications.error('Failure', err, 'Unable to update accesses');
-        });
-    };
-
-    function initView() {
-      $scope.state = { actionInProgress: false };
-      RegistryService.registry($transition$.params().id)
-        .then(function success(data) {
-          $scope.registry = data;
-        })
-        .catch(function error(err) {
-          Notifications.error('Failure', err, 'Unable to retrieve registry details');
-        });
-    }
-
-    initView();
-  },
-]);
diff --git a/app/portainer/views/registries/create/createregistry.html b/app/portainer/views/registries/create/createRegistry.html
similarity index 52%
rename from app/portainer/views/registries/create/createregistry.html
rename to app/portainer/views/registries/create/createRegistry.html
index cecca19d3..aab5b0bdd 100644
--- a/app/portainer/views/registries/create/createregistry.html
+++ b/app/portainer/views/registries/create/createRegistry.html
@@ -17,8 +17,18 @@
           <div class="form-group" style="margin-bottom: 0;">
             <div class="boxselector_wrapper">
               <div>
-                <input type="radio" id="registry_quay" ng-model="model.Type" ng-value="RegistryTypes.QUAY" />
-                <label for="registry_quay" ng-click="selectQuayRegistry()">
+                <input type="radio" id="registry_dockerhub" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.DOCKERHUB" />
+                <label for="registry_dockerhub" ng-click="$ctrl.selectDockerHub()">
+                  <div class="boxselector_header">
+                    <i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
+                    DockerHub
+                  </div>
+                  <p>DockerHub authenticated account</p>
+                </label>
+              </div>
+              <div>
+                <input type="radio" id="registry_quay" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.QUAY" />
+                <label for="registry_quay" ng-click="$ctrl.selectQuayRegistry()">
                   <div class="boxselector_header">
                     <i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
                     Quay.io
@@ -27,8 +37,8 @@
                 </label>
               </div>
               <div>
-                <input type="radio" id="registry_proget" ng-model="model.Type" ng-value="RegistryTypes.PROGET" />
-                <label for="registry_proget" ng-click="selectProGetRegistry()">
+                <input type="radio" id="registry_proget" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.PROGET" />
+                <label for="registry_proget" ng-click="$ctrl.selectProGetRegistry()">
                   <div class="boxselector_header">
                     <i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
                     ProGet
@@ -37,8 +47,8 @@
                 </label>
               </div>
               <div>
-                <input type="radio" id="registry_azure" ng-model="model.Type" ng-value="RegistryTypes.AZURE" />
-                <label for="registry_azure" ng-click="selectAzureRegistry()">
+                <input type="radio" id="registry_azure" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.AZURE" />
+                <label for="registry_azure" ng-click="$ctrl.selectAzureRegistry()">
                   <div class="boxselector_header">
                     <i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
                     Azure
@@ -47,8 +57,8 @@
                 </label>
               </div>
               <div>
-                <input type="radio" id="registry_gitlab" ng-model="model.Type" ng-value="RegistryTypes.GITLAB" />
-                <label for="registry_gitlab" ng-click="selectGitlabRegistry()">
+                <input type="radio" id="registry_gitlab" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.GITLAB" />
+                <label for="registry_gitlab" ng-click="$ctrl.selectGitlabRegistry()">
                   <div class="boxselector_header">
                     <i class="fab fa-gitlab" aria-hidden="true" style="margin-right: 2px;"></i>
                     Gitlab
@@ -57,8 +67,8 @@
                 </label>
               </div>
               <div>
-                <input type="radio" id="registry_custom" ng-model="model.Type" ng-value="RegistryTypes.CUSTOM" />
-                <label for="registry_custom" ng-click="selectCustomRegistry()">
+                <input type="radio" id="registry_custom" ng-model="$ctrl.model.Type" ng-value="$ctrl.RegistryTypes.CUSTOM" />
+                <label for="registry_custom" ng-click="$ctrl.selectCustomRegistry()">
                   <div class="boxselector_header">
                     <i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
                     Custom registry
@@ -70,47 +80,55 @@
           </div>
 
           <registry-form-quay
-            ng-if="model.Type === RegistryTypes.QUAY"
-            model="model"
-            form-action="create"
+            ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.QUAY"
+            model="$ctrl.model"
+            form-action="$ctrl.createRegistry"
             form-action-label="Add registry"
-            action-in-progress="state.actionInProgress"
+            action-in-progress="$ctrl.state.actionInProgress"
           ></registry-form-quay>
 
           <registry-form-azure
-            ng-if="model.Type === RegistryTypes.AZURE"
-            model="model"
-            form-action="create"
+            ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.AZURE"
+            model="$ctrl.model"
+            form-action="$ctrl.createRegistry"
             form-action-label="Add registry"
-            action-in-progress="state.actionInProgress"
+            action-in-progress="$ctrl.state.actionInProgress"
           ></registry-form-azure>
 
           <registry-form-custom
-            ng-if="model.Type === RegistryTypes.CUSTOM"
-            model="model"
-            form-action="create"
+            ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.CUSTOM"
+            model="$ctrl.model"
+            form-action="$ctrl.createRegistry"
             form-action-label="Add registry"
-            action-in-progress="state.actionInProgress"
+            action-in-progress="$ctrl.state.actionInProgress"
           ></registry-form-custom>
 
           <registry-form-proget
-            ng-if="model.Type === RegistryTypes.PROGET"
-            model="model"
-            form-action="create"
+            ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.PROGET"
+            model="$ctrl.model"
+            form-action="$ctrl.createRegistry"
             form-action-label="Add registry"
-            action-in-progress="state.actionInProgress"
+            action-in-progress="$ctrl.state.actionInProgress"
           ></registry-form-proget>
 
           <registry-form-gitlab
-            ng-if="model.Type === RegistryTypes.GITLAB"
-            model="model"
-            retrieve-registries="retrieveGitlabRegistries"
-            create-registries="createGitlabRegistries"
-            projects="gitlabProjects"
-            state="state"
-            action-in-progress="state.actionInProgress"
-            reset-defaults="useDefaultGitlabConfiguration"
+            ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.GITLAB"
+            model="$ctrl.model"
+            retrieve-registries="$ctrl.retrieveGitlabRegistries"
+            create-registries="$ctrl.createGitlabRegistries"
+            projects="$ctrl.gitlabProjects"
+            state="$ctrl.state"
+            action-in-progress="$ctrl.state.actionInProgress"
+            reset-defaults="$ctrl.useDefaultGitlabConfiguration"
           ></registry-form-gitlab>
+
+          <registry-form-dockerhub
+            ng-if="$ctrl.model.Type === $ctrl.RegistryTypes.DOCKERHUB"
+            model="$ctrl.model"
+            form-action="$ctrl.createRegistry"
+            form-action-label="Add registry"
+            action-in-progress="$ctrl.state.actionInProgress"
+          ></registry-form-dockerhub>
         </form>
       </rd-widget-body>
     </rd-widget>
diff --git a/app/portainer/views/registries/create/createRegistry.js b/app/portainer/views/registries/create/createRegistry.js
new file mode 100644
index 000000000..e935feea6
--- /dev/null
+++ b/app/portainer/views/registries/create/createRegistry.js
@@ -0,0 +1,10 @@
+import angular from 'angular';
+import CreateRegistryController from './createRegistryController';
+
+angular.module('portainer.app').component('createRegistry', {
+  templateUrl: './createRegistry.html',
+  controller: CreateRegistryController,
+  bindings: {
+    $transition$: '<',
+  },
+});
diff --git a/app/portainer/views/registries/create/createRegistryController.js b/app/portainer/views/registries/create/createRegistryController.js
index 5d4b0acc2..7198ee3b5 100644
--- a/app/portainer/views/registries/create/createRegistryController.js
+++ b/app/portainer/views/registries/create/createRegistryController.js
@@ -1,24 +1,13 @@
-import { RegistryTypes } from '@/portainer/models/registryTypes';
-import { RegistryDefaultModel } from '@/portainer/models/registry';
+import { RegistryTypes } from 'Portainer/models/registryTypes';
+import { RegistryCreateFormValues } from 'Portainer/models/registry';
 
-angular.module('portainer.app').controller('CreateRegistryController', [
-  '$scope',
-  '$state',
-  'RegistryService',
-  'Notifications',
-  'RegistryGitlabService',
-  function ($scope, $state, RegistryService, Notifications, RegistryGitlabService) {
-    $scope.selectQuayRegistry = selectQuayRegistry;
-    $scope.selectAzureRegistry = selectAzureRegistry;
-    $scope.selectCustomRegistry = selectCustomRegistry;
-    $scope.selectProGetRegistry = selectProGetRegistry;
-    $scope.selectGitlabRegistry = selectGitlabRegistry;
-    $scope.create = createRegistry;
-    $scope.useDefaultGitlabConfiguration = useDefaultGitlabConfiguration;
-    $scope.retrieveGitlabRegistries = retrieveGitlabRegistries;
-    $scope.createGitlabRegistries = createGitlabRegistries;
+class CreateRegistryController {
+  /* @ngInject */
+  constructor($async, $state, EndpointProvider, Notifications, RegistryService, RegistryGitlabService) {
+    Object.assign(this, { $async, $state, EndpointProvider, Notifications, RegistryService, RegistryGitlabService });
 
-    $scope.state = {
+    this.RegistryTypes = RegistryTypes;
+    this.state = {
       actionInProgress: false,
       overrideConfiguration: false,
       gitlab: {
@@ -27,101 +16,113 @@ angular.module('portainer.app').controller('CreateRegistryController', [
         },
         selectedItems: [],
       },
+      originViewReference: 'portainer.registries',
     };
 
-    function useDefaultQuayConfiguration() {
-      $scope.model.Quay.useOrganisation = false;
-      $scope.model.Quay.organisationName = '';
-    }
+    this.createRegistry = this.createRegistry.bind(this);
+    this.retrieveGitlabRegistries = this.retrieveGitlabRegistries.bind(this);
+    this.createGitlabRegistries = this.createGitlabRegistries.bind(this);
+  }
 
-    function selectQuayRegistry() {
-      $scope.model.Name = 'Quay';
-      $scope.model.URL = 'quay.io';
-      $scope.model.Authentication = true;
-      $scope.model.Quay = {};
-      useDefaultQuayConfiguration();
-    }
+  useDefaultQuayConfiguration() {
+    this.model.Quay.useOrganisation = false;
+    this.model.Quay.organisationName = '';
+  }
 
-    function useDefaultGitlabConfiguration() {
-      $scope.model.URL = 'https://registry.gitlab.com';
-      $scope.model.Gitlab.InstanceURL = 'https://gitlab.com';
-    }
+  selectQuayRegistry() {
+    this.model.Name = 'Quay';
+    this.model.URL = 'quay.io';
+    this.model.Authentication = true;
+    this.model.Quay = {};
+    this.useDefaultQuayConfiguration();
+  }
 
-    function selectGitlabRegistry() {
-      $scope.model.Name = '';
-      $scope.model.Authentication = true;
-      $scope.model.Gitlab = {};
-      useDefaultGitlabConfiguration();
-    }
+  useDefaultGitlabConfiguration() {
+    this.model.URL = 'https://registry.gitlab.com';
+    this.model.Gitlab.InstanceURL = 'https://gitlab.com';
+  }
 
-    function selectAzureRegistry() {
-      $scope.model.Name = '';
-      $scope.model.URL = '';
-      $scope.model.Authentication = true;
-    }
+  selectGitlabRegistry() {
+    this.model.Name = '';
+    this.model.Authentication = true;
+    this.model.Gitlab = {};
+    this.useDefaultGitlabConfiguration();
+  }
 
-    function selectCustomRegistry() {
-      $scope.model.Name = '';
-      $scope.model.URL = '';
-      $scope.model.Authentication = false;
-    }
+  selectAzureRegistry() {
+    this.model.Name = '';
+    this.model.URL = '';
+    this.model.Authentication = true;
+  }
 
-    function selectProGetRegistry() {
-      $scope.model.Name = '';
-      $scope.model.URL = '';
-      $scope.model.BaseURL = '';
-      $scope.model.Authentication = true;
-    }
+  selectProGetRegistry() {
+    this.model.Name = '';
+    this.model.URL = '';
+    this.model.BaseURL = '';
+    this.model.Authentication = true;
+  }
 
-    function retrieveGitlabRegistries() {
-      $scope.state.actionInProgress = true;
-      RegistryGitlabService.projects($scope.model.Gitlab.InstanceURL, $scope.model.Token)
-        .then((data) => {
-          $scope.gitlabProjects = data;
-        })
-        .catch((err) => {
-          Notifications.error('Failure', err, 'Unable to retrieve projects');
-        })
-        .finally(() => {
-          $scope.state.actionInProgress = false;
-        });
-    }
+  selectCustomRegistry() {
+    this.model.Name = '';
+    this.model.URL = '';
+    this.model.Authentication = false;
+  }
 
-    function createGitlabRegistries() {
-      $scope.state.actionInProgress = true;
-      RegistryService.createGitlabRegistries($scope.model, $scope.state.gitlab.selectedItems)
-        .then(() => {
-          Notifications.success('Registries successfully created');
-          $state.go('portainer.registries');
-        })
-        .catch((err) => {
-          Notifications.error('Failure', err, 'Unable to create registries');
-        })
-        .finally(() => {
-          $scope.state.actionInProgress = false;
-        });
-    }
+  selectDockerHub() {
+    this.model.Name = '';
+    this.model.URL = 'docker.io';
+    this.model.Authentication = true;
+  }
 
-    function createRegistry() {
-      $scope.state.actionInProgress = true;
-      RegistryService.createRegistry($scope.model)
-        .then(function success() {
-          Notifications.success('Registry successfully created');
-          $state.go('portainer.registries');
-        })
-        .catch(function error(err) {
-          Notifications.error('Failure', err, 'Unable to create registry');
-        })
-        .finally(function final() {
-          $scope.state.actionInProgress = false;
-        });
-    }
+  retrieveGitlabRegistries() {
+    return this.$async(async () => {
+      this.state.actionInProgress = true;
+      try {
+        this.gitlabProjects = await this.RegistryGitlabService.projects(this.model.Gitlab.InstanceURL, this.model.Token);
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to retrieve projects');
+      } finally {
+        this.state.actionInProgress = false;
+      }
+    });
+  }
 
-    function initView() {
-      $scope.RegistryTypes = RegistryTypes;
-      $scope.model = new RegistryDefaultModel();
-    }
+  createGitlabRegistries() {
+    return this.$async(async () => {
+      try {
+        this.state.actionInProgress = true;
+        await this.RegistryService.createGitlabRegistries(this.model, this.state.gitlab.selectedItems);
+        this.Notifications.success('Registries successfully created');
+        this.$state.go(this.state.originViewReference, { endpointId: this.EndpointProvider.endpointID() });
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to create registries');
+        this.state.actionInProgress = false;
+      }
+    });
+  }
 
-    initView();
-  },
-]);
+  createRegistry() {
+    return this.$async(async () => {
+      try {
+        this.state.actionInProgress = true;
+        await this.RegistryService.createRegistry(this.model);
+        this.Notifications.success('Registry successfully created');
+        this.$state.go(this.state.originViewReference, { endpointId: this.EndpointProvider.endpointID() });
+      } catch (err) {
+        this.Notifications.error('Failure', err, 'Unable to create registry');
+        this.state.actionInProgress = false;
+      }
+    });
+  }
+
+  $onInit() {
+    this.model = new RegistryCreateFormValues();
+
+    const origin = this.$transition$.originalTransition().from();
+    if (origin.name && /^[a-z]+\.registries$/.test(origin.name)) {
+      this.state.originViewReference = origin;
+    }
+  }
+}
+
+export default CreateRegistryController;
diff --git a/app/portainer/views/registries/edit/registryController.js b/app/portainer/views/registries/edit/registryController.js
index 52bb669ae..2ea6d5784 100644
--- a/app/portainer/views/registries/edit/registryController.js
+++ b/app/portainer/views/registries/edit/registryController.js
@@ -3,11 +3,9 @@ import { RegistryTypes } from '@/portainer/models/registryTypes';
 angular.module('portainer.app').controller('RegistryController', [
   '$scope',
   '$state',
-  '$transition$',
-  '$filter',
   'RegistryService',
   'Notifications',
-  function ($scope, $state, $transition$, $filter, RegistryService, Notifications) {
+  function ($scope, $state, RegistryService, Notifications) {
     $scope.state = {
       actionInProgress: false,
     };
@@ -36,7 +34,7 @@ angular.module('portainer.app').controller('RegistryController', [
     };
 
     function initView() {
-      var registryID = $transition$.params().id;
+      var registryID = $state.params.id;
       RegistryService.registry(registryID)
         .then(function success(data) {
           $scope.registry = data;
diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html
index 420f06612..219a1886f 100644
--- a/app/portainer/views/registries/registries.html
+++ b/app/portainer/views/registries/registries.html
@@ -7,79 +7,6 @@
   <rd-header-content>Registry management</rd-header-content>
 </rd-header>
 
-<information-panel title-text="Registry usage">
-  <span class="small">
-    <p class="text-muted">
-      <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
-      DockerHub credentials and registries can only be used with Docker endpoints at the time.
-    </p>
-  </span>
-</information-panel>
-
-<div class="row" ng-if="dockerhub && isAdmin">
-  <div class="col-sm-12">
-    <rd-widget>
-      <rd-widget-header icon="fa-database" title-text="DockerHub"> </rd-widget-header>
-      <rd-widget-body>
-        <form class="form-horizontal">
-          <!-- note -->
-          <div class="form-group">
-            <span class="col-sm-12 text-muted small">
-              The DockerHub registry can be used by any user. You can specify the credentials that will be used to push &amp; pull images here.
-            </span>
-          </div>
-          <!-- !note -->
-          <!-- authentication-checkbox -->
-          <div class="form-group">
-            <div class="col-sm-12">
-              <label for="registry_auth" class="control-label text-left">
-                Authentication
-                <portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to push/pull private images."></portainer-tooltip>
-              </label>
-              <label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="dockerhub.Authentication" /><i></i> </label>
-            </div>
-          </div>
-          <!-- !authentication-checkbox -->
-          <!-- authentication-credentials -->
-          <div ng-if="dockerhub.Authentication">
-            <!-- credentials-user -->
-            <div class="form-group">
-              <label for="hub_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
-              <div class="col-sm-9 col-lg-10">
-                <input type="text" class="form-control" id="hub_username" ng-model="dockerhub.Username" />
-              </div>
-            </div>
-            <!-- !credentials-user -->
-            <!-- credentials-password -->
-            <div class="form-group">
-              <label for="hub_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
-              <div class="col-sm-9 col-lg-10">
-                <input type="password" class="form-control" id="hub_password" ng-model="formValues.dockerHubPassword" placeholder="*******" />
-              </div>
-            </div>
-            <!-- !credentials-password -->
-          </div>
-          <!-- !authentication-credentials -->
-          <div class="form-group">
-            <div class="col-sm-12">
-              <button
-                type="button"
-                class="btn btn-primary btn-sm"
-                ng-disabled="state.actionInProgress || dockerhub.Authentication && (!dockerhub.Username || !formValues.dockerHubPassword)"
-                ng-click="updateDockerHub()"
-                button-spinner="state.actionInProgress"
-              >
-                <span ng-hide="state.actionInProgress">Update</span>
-                <span ng-show="state.actionInProgress">Updating DockerHub settings...</span>
-              </button>
-            </div>
-          </div>
-        </form>
-      </rd-widget-body>
-    </rd-widget>
-  </div>
-</div>
-
 <div class="row">
   <div class="col-sm-12">
     <registries-datatable
@@ -88,7 +15,6 @@
       dataset="registries"
       table-key="registries"
       order-by="Name"
-      access-management="isAdmin"
       remove-action="removeAction"
       can-browse="canBrowse"
     ></registries-datatable>
diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js
index c0e40acf8..83e3cbafc 100644
--- a/app/portainer/views/registries/registriesController.js
+++ b/app/portainer/views/registries/registriesController.js
@@ -1,43 +1,23 @@
 import _ from 'lodash-es';
+import { RegistryTypes } from 'Portainer/models/registryTypes';
+import { DockerHubViewModel } from 'Portainer/models/dockerhub';
 
 angular.module('portainer.app').controller('RegistriesController', [
   '$q',
   '$scope',
   '$state',
   'RegistryService',
-  'DockerHubService',
   'ModalService',
   'Notifications',
-  'Authentication',
-  function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Authentication) {
+  function ($q, $scope, $state, RegistryService, ModalService, Notifications) {
     $scope.state = {
       actionInProgress: false,
     };
 
-    $scope.formValues = {
-      dockerHubPassword: '',
-    };
-
-    const nonBrowsableUrls = ['quay.io'];
+    const nonBrowsableTypes = [RegistryTypes.ANONYMOUS, RegistryTypes.DOCKERHUB, RegistryTypes.QUAY];
 
     $scope.canBrowse = function (item) {
-      return !_.includes(nonBrowsableUrls, item.URL);
-    };
-
-    $scope.updateDockerHub = function () {
-      var dockerhub = $scope.dockerhub;
-      dockerhub.Password = $scope.formValues.dockerHubPassword;
-      $scope.state.actionInProgress = true;
-      DockerHubService.update(dockerhub)
-        .then(function success() {
-          Notifications.success('DockerHub registry updated');
-        })
-        .catch(function error(err) {
-          Notifications.error('Failure', err, 'Unable to update DockerHub details');
-        })
-        .finally(function final() {
-          $scope.state.actionInProgress = false;
-        });
+      return !_.includes(nonBrowsableTypes, item.Type);
     };
 
     $scope.removeAction = function (selectedItems) {
@@ -73,12 +53,9 @@ angular.module('portainer.app').controller('RegistriesController', [
     function initView() {
       $q.all({
         registries: RegistryService.registries(),
-        dockerhub: DockerHubService.dockerhub(),
       })
         .then(function success(data) {
-          $scope.registries = data.registries;
-          $scope.dockerhub = data.dockerhub;
-          $scope.isAdmin = Authentication.isAdmin();
+          $scope.registries = _.concat(new DockerHubViewModel(), data.registries);
         })
         .catch(function error(err) {
           $scope.registries = [];
diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html
index d1d181d92..c1be6661b 100644
--- a/app/portainer/views/sidebar/sidebar.html
+++ b/app/portainer/views/sidebar/sidebar.html
@@ -108,7 +108,7 @@
       <li class="sidebar-list" ng-if="isAdmin && applicationState.application.enableEdgeComputeFeatures">
         <a ui-sref="edge.jobs" ui-sref-active="active">Edge Jobs <span class="menu-icon fa fa-clock fa-fw"></span></a>
       </li>
-      <li class="sidebar-title">
+      <li class="sidebar-title" ng-if="isAdmin || isTeamLeader">
         <span>Settings</span>
       </li>
       <li class="sidebar-list" ng-if="isAdmin || isTeamLeader">
@@ -181,7 +181,7 @@
           <a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
         </div>
       </li>
-      <li class="sidebar-list">
+      <li class="sidebar-list" ng-if="isAdmin">
         <a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
       </li>
       <li class="sidebar-list" ng-if="isAdmin">
diff --git a/app/portainer/views/sidebar/sidebarController.js b/app/portainer/views/sidebar/sidebarController.js
index 6e1d8ecef..29c12a27f 100644
--- a/app/portainer/views/sidebar/sidebarController.js
+++ b/app/portainer/views/sidebar/sidebarController.js
@@ -19,15 +19,20 @@ angular.module('portainer.app').controller('SidebarController', [
       $scope.isTeamLeader = isLeader;
     }
 
+    function isClusterAdmin() {
+      return Authentication.isAdmin();
+    }
+
     async function initView() {
       $scope.uiVersion = StateManager.getState().application.version;
       $scope.logo = StateManager.getState().application.logo;
-      $scope.showStacks = await shouldShowStacks();
+
+      $scope.endpointId = EndpointProvider.endpointID();
+      $scope.showStacks = shouldShowStacks();
 
       let userDetails = Authentication.getUserDetails();
-      let isAdmin = Authentication.isAdmin();
+      const isAdmin = isClusterAdmin();
       $scope.isAdmin = isAdmin;
-      $scope.endpointId = EndpointProvider.endpointID();
 
       $q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : [])
         .then(function success(data) {
@@ -36,18 +41,12 @@ angular.module('portainer.app').controller('SidebarController', [
         .catch(function error(err) {
           Notifications.error('Failure', err, 'Unable to retrieve user memberships');
         });
-
-      $transitions.onEnter({}, () => {
-        $scope.endpointId = EndpointProvider.endpointID();
-      });
     }
 
     initView();
 
-    async function shouldShowStacks() {
-      const isAdmin = Authentication.isAdmin();
-
-      if (isAdmin) {
+    function shouldShowStacks() {
+      if (isClusterAdmin()) {
         return true;
       }
 
@@ -60,7 +59,9 @@ angular.module('portainer.app').controller('SidebarController', [
     }
 
     $transitions.onEnter({}, async () => {
-      $scope.showStacks = await shouldShowStacks();
+      $scope.endpointId = EndpointProvider.endpointID();
+      $scope.showStacks = shouldShowStacks();
+      $scope.isAdmin = isClusterAdmin();
 
       if ($scope.applicationState.endpoint.name) {
         document.title = `${$rootScope.defaultTitle} | ${$scope.applicationState.endpoint.name}`;
diff --git a/jsconfig.json b/jsconfig.json
index 6e5e7a1af..a09392634 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -9,7 +9,8 @@
       "Azure/*": ["azure/*"],
       "Docker/*": ["docker/*"],
       "Kubernetes/*": ["kubernetes/*"],
-      "Portainer/*": ["portainer/*"]
+      "Portainer/*": ["portainer/*"],
+      "@/*": ["../app/*"],
     }
   },
   "exclude": ["api", "build", "dist", "distribution", "node_modules", "test", "webpack"]
diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js
index 585692e93..3b8dcaac5 100644
--- a/webpack/webpack.common.js
+++ b/webpack/webpack.common.js
@@ -27,8 +27,8 @@ module.exports = {
             loader: 'source-map-loader',
             options: {
               filterSourceMappingUrl: (_, resourcePath) => {
-                // ignores `chardet` missing sourcemaps
-                return !/node_modules\/chardet/i.test(resourcePath);
+                // ignores pkgs missing sourcemaps
+                return ['chardet', 'tokenize-ansi'].every((pkg) => !resourcePath.includes(pkg));
               },
             },
           },