diff --git a/config/vufind/permissionBehavior.ini b/config/vufind/permissionBehavior.ini
index 550838fb0c9fecda480485c6388b1e286a961c29..3390fd023d82bca3924145c1bd4b97e94b793366 100644
--- a/config/vufind/permissionBehavior.ini
+++ b/config/vufind/permissionBehavior.ini
@@ -66,6 +66,18 @@ defaultDeniedControllerBehavior = "promptLogin"
 ; (False means "use the default behavior defined by the template").
 defaultDeniedTemplateBehavior = false
 
+; This setting can be used to override access permissions for controllers. You can
+; set a controller class name inside the brackets to explicitly override that
+; controller's access permission (and that of any of its children, unless more
+; specific permissions are set for those subclasses), whether or not it has a
+; default defined in the class. You can use * inside the brackets to set a default
+; access permission that is applied to all controllers that do not have an
+; internally-defined default (the MyResearchController is also excluded from
+; this setting to avoid infinite login redirects). This can be useful if you
+; wish to password-protect your entire site.
+;controllerAccess[*] = access.VuFindInterface
+;controllerAccess[VuFind\Controller\SearchController] = access.SearchResults
+
 ; Example configuration for non-standard favorites permission behavior:
 ;[feature.Favorites]
 ;deniedTemplateBehavior = "showMessage:Login for Favorites"
diff --git a/module/VuFind/src/VuFind/Controller/AbstractBase.php b/module/VuFind/src/VuFind/Controller/AbstractBase.php
index 69fdaf2c5c7f20ab3b8b32e6390db5410ece9aac..90e9961f4f173c2174b259b063776552f9b62379 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractBase.php
@@ -51,11 +51,12 @@ class AbstractBase extends AbstractActionController
 {
     /**
      * Permission that must be granted to access this module (false for no
-     * restriction)
+     * restriction, null to use configured default (which is usually the same
+     * as false)).
      *
      * @var string|bool
      */
-    protected $accessPermission = false;
+    protected $accessPermission = null;
 
     /**
      * Behavior when access is denied (used unless overridden through
@@ -105,6 +106,29 @@ class AbstractBase extends AbstractActionController
         }
     }
 
+    /**
+     * Getter for access permission.
+     *
+     * @return string|bool
+     */
+    public function getAccessPermission()
+    {
+        return $this->accessPermission;
+    }
+
+    /**
+     * Getter for access permission.
+     *
+     * @param string $ap Permission to require for access to the controller (false
+     * for no requirement)
+     *
+     * @return void
+     */
+    public function setAccessPermission($ap)
+    {
+        $this->accessPermission = empty($ap) ? false : $ap;
+    }
+
     /**
      * Register the default events for this controller
      *
diff --git a/module/VuFind/src/VuFind/Controller/AbstractBaseFactory.php b/module/VuFind/src/VuFind/Controller/AbstractBaseFactory.php
index 6f3d621cc8a42f64a02a5cc252718a906dde3aa6..f16c84b5708fd901823ff6aa1e924436333d894a 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractBaseFactory.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractBaseFactory.php
@@ -41,6 +41,46 @@ use Zend\ServiceManager\Factory\FactoryInterface;
  */
 class AbstractBaseFactory implements FactoryInterface
 {
+    /**
+     * Apply permission settings to the controller.
+     *
+     * @param ContainerInterface $container  Service manager
+     * @param AbstractBase       $controller Controller to configure
+     *
+     * @return AbstractBase
+     */
+    protected function applyPermissions($container, $controller)
+    {
+        $config = $container->get(\VuFind\Config\PluginManager::class)
+            ->get('permissionBehavior');
+        $permissions = $config->global->controllerAccess ?? [];
+
+        if (!empty($permissions)) {
+            // Iterate through parent classes until we find the most specific
+            // class access permission defined (if any):
+            $class = get_class($controller);
+            do {
+                if (isset($permissions[$class])) {
+                    $controller->setAccessPermission($permissions[$class]);
+                    break;
+                }
+                $class = get_parent_class($class);
+            } while ($class);
+
+            // If the controller's current permission is null (as opposed to false
+            // or a string), that means it has no internally configured default, and
+            // setAccessPermission was not called above; thus, we should apply the
+            // default value:
+            if (isset($permissions['*'])
+                && $controller->getAccessPermission() === null
+            ) {
+                $controller->setAccessPermission($permissions['*']);
+            }
+        }
+
+        return $controller;
+    }
+
     /**
      * Create an object
      *
@@ -61,6 +101,6 @@ class AbstractBaseFactory implements FactoryInterface
         if (!empty($options)) {
             throw new \Exception('Unexpected options sent to factory.');
         }
-        return new $requestedName($container);
+        return $this->applyPermissions($container, new $requestedName($container));
     }
 }
diff --git a/module/VuFind/src/VuFind/Controller/AbstractBaseWithConfigFactory.php b/module/VuFind/src/VuFind/Controller/AbstractBaseWithConfigFactory.php
index b6a6f5c731f912daa49fa3dc1937c378156a34aa..f9efa5a7e22287a3147f7964093ea717cc06d1cf 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractBaseWithConfigFactory.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractBaseWithConfigFactory.php
@@ -28,7 +28,6 @@
 namespace VuFind\Controller;
 
 use Interop\Container\ContainerInterface;
-use Zend\ServiceManager\Factory\FactoryInterface;
 
 /**
  * Generic controller factory (with config injection).
@@ -39,7 +38,7 @@ use Zend\ServiceManager\Factory\FactoryInterface;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class AbstractBaseWithConfigFactory implements FactoryInterface
+class AbstractBaseWithConfigFactory extends AbstractBaseFactory
 {
     /**
      * Create an object
@@ -63,6 +62,8 @@ class AbstractBaseWithConfigFactory implements FactoryInterface
         }
         $config = $container->get(\VuFind\Config\PluginManager::class)
             ->get('config');
-        return new $requestedName($container, $config);
+        return $this->applyPermissions(
+            $container, new $requestedName($container, $config)
+        );
     }
 }
diff --git a/module/VuFind/src/VuFind/Controller/CartControllerFactory.php b/module/VuFind/src/VuFind/Controller/CartControllerFactory.php
index bb56f89a8b958f05151767a0f076a2bed9ace5de..5e5a908056d627b45eba6d87db6aeebbbd31e6cb 100644
--- a/module/VuFind/src/VuFind/Controller/CartControllerFactory.php
+++ b/module/VuFind/src/VuFind/Controller/CartControllerFactory.php
@@ -28,7 +28,6 @@
 namespace VuFind\Controller;
 
 use Interop\Container\ContainerInterface;
-use Zend\ServiceManager\Factory\FactoryInterface;
 
 /**
  * Cart controller factory.
@@ -39,7 +38,7 @@ use Zend\ServiceManager\Factory\FactoryInterface;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class CartControllerFactory implements FactoryInterface
+class CartControllerFactory extends AbstractBaseFactory
 {
     /**
      * Create an object
@@ -64,6 +63,9 @@ class CartControllerFactory implements FactoryInterface
         $session = new \Zend\Session\Container(
             'cart_followup', $container->get(\Zend\Session\SessionManager::class)
         );
-        return new $requestedName($container, $session);
+        return $this->applyPermissions(
+            $container,
+            new $requestedName($container, $session)
+        );
     }
 }
diff --git a/module/VuFind/src/VuFind/Controller/ChannelsControllerFactory.php b/module/VuFind/src/VuFind/Controller/ChannelsControllerFactory.php
index d3ca628582eb65491f5da5918beeacdfe3d2a58d..28e3c9217069161a887681a45b7ca48a117f08ed 100644
--- a/module/VuFind/src/VuFind/Controller/ChannelsControllerFactory.php
+++ b/module/VuFind/src/VuFind/Controller/ChannelsControllerFactory.php
@@ -28,7 +28,6 @@
 namespace VuFind\Controller;
 
 use Interop\Container\ContainerInterface;
-use Zend\ServiceManager\Factory\FactoryInterface;
 
 /**
  * Channels controller factory.
@@ -39,7 +38,7 @@ use Zend\ServiceManager\Factory\FactoryInterface;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class ChannelsControllerFactory implements FactoryInterface
+class ChannelsControllerFactory extends AbstractBaseFactory
 {
     /**
      * Create an object
@@ -62,6 +61,6 @@ class ChannelsControllerFactory implements FactoryInterface
             throw new \Exception('Unexpected options sent to factory.');
         }
         $loader = $container->get(\VuFind\ChannelProvider\ChannelLoader::class);
-        return new $requestedName($loader);
+        return $this->applyPermissions($container, new $requestedName($loader));
     }
 }
diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php
index 0c3dcad453da21776c0fd1ea4866c827a512228c..30d7d1e1f1823a890e0bae5cd9adeb7ccee54011 100644
--- a/module/VuFind/src/VuFind/Controller/MyResearchController.php
+++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php
@@ -49,6 +49,19 @@ use Zend\View\Model\ViewModel;
  */
 class MyResearchController extends AbstractBase
 {
+    /**
+     * Permission that must be granted to access this module (false for no
+     * restriction, null to use configured default (which is usually the same
+     * as false)).
+     *
+     * For this controller, we default to false rather than null because
+     * we don't want a default setting to override the controller's accessibility
+     * and break the login process!
+     *
+     * @var string|bool
+     */
+    protected $accessPermission = false;
+
     /**
      * ILS Pagination Helper
      *
diff --git a/module/VuFind/src/VuFind/Controller/UpgradeControllerFactory.php b/module/VuFind/src/VuFind/Controller/UpgradeControllerFactory.php
index 928b0daeb65aed7d28b26d40be2ecd6e11f3d71f..572b9d0c345211c57be4a982f50a3cfabe7ff521 100644
--- a/module/VuFind/src/VuFind/Controller/UpgradeControllerFactory.php
+++ b/module/VuFind/src/VuFind/Controller/UpgradeControllerFactory.php
@@ -28,7 +28,6 @@
 namespace VuFind\Controller;
 
 use Interop\Container\ContainerInterface;
-use Zend\ServiceManager\Factory\FactoryInterface;
 
 /**
  * Upgrade controller factory.
@@ -39,7 +38,7 @@ use Zend\ServiceManager\Factory\FactoryInterface;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class UpgradeControllerFactory implements FactoryInterface
+class UpgradeControllerFactory extends AbstractBaseFactory
 {
     /**
      * Create an object
@@ -65,6 +64,9 @@ class UpgradeControllerFactory implements FactoryInterface
         $session = new \Zend\Session\Container(
             'upgrade', $container->get(\Zend\Session\SessionManager::class)
         );
-        return new $requestedName($container, $cookieManager, $session);
+        return $this->applyPermissions(
+            $container,
+            new $requestedName($container, $cookieManager, $session)
+        );
     }
 }