This is part of a new weekly (or biweekly) installment of happenings on Discord, a community chat staffed by the developers and curated by its users.

Preserving cron while disabling SSH with whiteouts

A cPanelesque feature allows users to use cron while disabling ssh. Doing so still allows the user arbitrary access to SSH into the server by opening a tunnel within a cron task, so the only means to ensure an environment without shell access is to disable all ingress routes. Despite this advice, cron may be enabled while disabling terminal access with a new service layer that utilizes the whiteout feature of OverlayFS:

mkdir -p /home/virtual/FILESYSTEMTEMPLATE/nossh/etc/pam.d
mknod /home/virtual/FILESYSTEMTEMPLATE/nossh/etc/pam.d/sshd c 0 0
touch /home/virtual/site1/info/services/nossh
/etc/systemd/user/fsmount.init mount site1 nossh

Use OverlayFS' whiteout feature to mask the sshd pam service, so you'd have all the affordances of the ssh service layer/bins but without the ability to authenticate via SSH. Layers are left-to-right precedence and layers are mounted lexicographically. To have a service supercede "nossh" name it lower in the alphabet such as "negatednossh".

Any character device with major:minor 0:0 is hidden on upper layers. This a feature consistent with layered filesystems, OverlayFS and aufs notably. apnscp uses OverlayFS presently but used aufs prior to 2016. A corresponding surrogate is created to mount the layer if the package name matches a package which lacks ssh but permits cron:

<?php declare(strict_types=1);

    class Ssh_Module_Surrogate extends Ssh_Module
        const SSHLESS_PLANS = ['basic'];
        public function _create()
            $plan = $this->getServiceValue('siteinfo', 'plan', \Opcenter\Service\Plans::default());
            if (!\in_array($plan, static::SSHLESS_PLANS, true)) {

            return $this->maskSsh();

        public function _edit()
            $newPlan = array_get($this->getNewServices('siteinfo'), 'plan', \Opcenter\Service\Plans::default());
            $oldPlan = array_get($this->getOldServices('siteinfo'), 'plan', \Opcenter\Service\Plans::default());
            if (\in_array($oldPlan, static::SSHLESS_PLANS, true) === ($post = \in_array($newPlan, static::SSHLESS_PLANS, true))) {
            return $post ? $this->maskSsh() : $this->unmaskSsh();

        private function maskSsh(): bool {
            $layer = new \Opcenter\Service\ServiceLayer($this->site);
            if (!$layer->installServiceLayer('nossh') || !$layer->reload()) {
                return error("Failed to mount `nossh' service");
            return true;

        private function unmaskSsh(): bool
            $layer = new \Opcenter\Service\ServiceLayer($this->site);
            if (!$layer->uninstallServiceLayer('nossh') || !$layer->reload()) {
                return error("Failed to unmount `nossh' service");

            return true;

Make sure the plan listed above in SSHLESS_PLANS exists (see artisan opcenter:plan) and you're off to the races!

Module hooks are a very simple way to accomplish this. Building a stout service definition extracts this logic into compatible parcels at the expense of added code. Thus, in summary:

🥉 Above as the quick fix.
🥇 Service definition for extra credit.

Altering API sensitivity

API calls follow a consistent flow with the UI: only unhandled exceptions or fatal() macro calls terminate flow. Errors may be fatal at the API callee's discretion. Sensitivity may be adjusted in API calls by packaging an "Abort-On:" header that corresponds to the Error Reporter types. For example, if using the Util_API client:

$client = \Util_API::create_client(
        'stream_context' => stream_context_create([
             'http' => [
                  'header' => 'Abort-On: info'

SOAP API calls will terminate whenever an info() macro is used. This works great with error() calls that may arise when adding a side. These would otherwise be encoded in SOAP headers that can be difficult to extract.

Configuration precedence

How are we supposed to to know when we should be doing just a config:set apnscp.config or config:set apnscp.bootstrapper?

In general, if there is a specific Scope, use this before using apnscp.config. Scopes may be listed with cpcmd config:list or cpcmd scope:list, which is the new module name in 3.1 (both will be supported for a few years). Examples of which include,

Scope config.ini Description
dns:ip4-proxy [dns] => my_ip4 Set remote IP4 address
apache:dav [dav] => enabled Allow Apache + CP DAV access
system.virus-scanner [antivirus] => installed Enable ClamAV usage
dns.default-provider-key [dns] => provider_key Set default DNS provider key
dns:default-provider [dns] => provider_default Set default DNS provider

apnscp Cheatsheet is a worthwhile collection of common commands to make the process easier.

SSO/fast switch domains

Bridging the gap between reseller and typical hosting accounts, apnscp now supports login to child domains by the parent. For the first domain, set the service parameter billing,invoice=IDENTIFIER. For each child domain parented to this domain, set billing,parent_invoice=IDENTIFIER.  Child domains may not login to the parent unless transitioned into by the parent and only within the session transitioned from which the parent transitioned.

Login association

Domain transitioning is a simple process within the user card dropdown. If no known domains are on the same server as the parent, the domain is presented normally.

SSO domain switching