How do I prevent a controller from creating unwanted routes? I have a custom module that is adding sections to the CP, like this:

// Register CP routes
Event::on(
    UrlManager::class,
    UrlManager::EVENT_REGISTER_CP_URL_RULES,
    function(craft\events\RegisterUrlRulesEvent $event) {
        $event-&gt;rules['&lt;controller:(subscriptions|purchases)&gt;/&lt;action:&gt;'] = [
            'route' =&gt; 'module/&lt;controller&gt;/&lt;action&gt;', 
            'defaults' =&gt; [
                'action' =&gt; 'index',
            ],
        ];
    }
);

// Add CP nav items
Event::on(
    Cp::class,
    Cp::EVENT_REGISTER_CP_NAV_ITEMS,
    function(craft\events\RegisterCpNavItemsEvent $event) {
        $event-&gt;navItems[] = [
            'url' =&gt; 'subscriptions',
            'label' =&gt; 'Subscriptions',
        ];
        $event-&gt;navItems[] = [
            'url' =&gt; 'purchases',
            'label' =&gt; 'Purchases',
        ];
    }
);


I have a SubscriptionsController and a PurchasesController, inside of which there is an actionIndex() method that computes some stuff and renders the appropriate template like this:

return $this-&gt;renderTemplate('module/cp/subscriptions/index', [
    'stuff' =&gt; $stuff,
]);


All this works fine, but I realized that the above registers some URLs that I do not want:

1) Controllers automatically register URLs in the format admin/actions/&lt;module&gt;/&lt;controller&gt;/&lt;action&gt; AND actions/&lt;module&gt;/&lt;controller/&lt;action&gt;. I'm fine with the first one even though it causes duplicate URLs due to my custom &lt;controller:(subscriptions|purchases)&gt;/&lt;action:&gt; rule, but the second one is problematic because they're not even CP URLs, yet they invoke a controller that is only meant to be used in the CP. It's causing an error when trying to render the template, because the module template root is only registered for the CP with the code below:

// Set the base CP template directory
Event::on(
    View::class,
    View::EVENT_REGISTER_CP_TEMPLATE_ROOTS,
    function (craft\events\RegisterTemplateRootsEvent $event) {
        $event-&gt;roots['module'] = $this-&gt;getBasePath() . DIRECTORY_SEPARATOR . 'templates';
    }
);


2) Craft automatically creates routes based on my template structure, meaning I can render the module/subscriptions/index.twig template directly from this URL: admin/module/subscriptions. This is also causing an error because the template requires variables that are passed to it in the controller action, but they're not set when rendering the template directly. I usually work around this issue on the front-end by setting the general.php config setting privateTemplateTrigger to an empty string '', but it doesn't seem to have any effect on the CP.

Any help with these two issues would be greatly appreciated. :)
After too much time spent on this, I think I found a solution to both issues.

1) To prevent controller actions meant for the CP from being accessible on the front-end (via actions/&lt;module&gt;/&lt;controller&gt;/&lt;action&gt; URLs), simply add this line of code at the top of the action's method:

$this-&gt;requireCpRequest();


The non-CP action URLs will now return a 400 error.

2) Although it doesn't seem possible to change the private template trigger on the CP to prevent any template from being directly accessed with a URL, using an underscore at the beginning of any file or folder in the path works. But even better -- the template root itself can be private:

// Set the base CP template directory
Event::on(
    View::class,
    View::EVENT_REGISTER_CP_TEMPLATE_ROOTS,
    function(craft\events\RegisterTemplateRootsEvent $event) {
        $event-&gt;roots['_module'] = $this-&gt;getBasePath() . DIRECTORY_SEPARATOR . 'templates';
    }
);


Notice the underscore in front of _module. This ensures that the module's templates will only render programmatically.