Skip to content

Powered by Grav

Platform developer account

Platform developer account

Available on every BOA server, Drupal and Backdrop platforms only, off by default. An operator turns it on per Client, in a root-owned file; nothing in the control panel creates, changes or removes it, and no tenant file can.

The ordinary per-Client sub-account (oN.<client>, see shell extras) reaches its Client's site directories and nothing above them: files and themes, no Composer, no Drush. Some Clients have a developer who maintains the whole codebase — they need the platform root, composer and the platform's own Drush. This account is that: a second login for the same Client, oN.<client>-dev, reaching every platform where every site is that Client's — read there, and writing exactly where the main oN.ftp login writes — and refused on any platform that also carries someone else's site. Root decides from a root-owned setting, and the worker re-checks the decision on every pass.

Before you start

  • The Client exists and owns at least one Drupal or Backdrop site — the Client field on the site's form, in the control panel. That ownership is what creates /data/disk/<oN>/clients/<client>/ with one symlink per site, and it is what gives the Client its ordinary sub-account. The directory name under clients/ is the name you will put in the setting:

    SH
    ls /data/disk/o1/clients/
    
  • Plan the platforms. A platform qualifies only when every site directory on its docroot (every sites/*/settings.php except all and default) is one of this Client's sites. Your own sites, another Client's sites, a site owned by nobody — any one of them keeps the whole platform out. So a developer who should reach a codebase needs that codebase to hold only their Client's sites; on a shared platform, move the other sites elsewhere first, or accept that the platform stays out.

  • Drupal and Backdrop only. Grav and Textpattern platforms are never enumerated, exactly as their sites are never linked into a sub-account.

Step 1 — enable it

Add the Client's directory name to _LTD_PLATFORM_CLIENTS in the instance's Octopus control file:

INI
# /root/.o1.octopus.cnf
_LTD_PLATFORM_CLIENTS="acmecorp"

Several Clients are space-separated: _LTD_PLATFORM_CLIENTS="acmecorp widgetco". Use the directory name exactly as ls shows it. Edit the line that is already there: every instance created since the feature landed carries _LTD_PLATFORM_CLIENTS="" in its control file; an older instance has no such line and needs one added. The file is sourced, so with two lines the last one wins — a second line placed above the empty one is a silent no-op. There is nothing to run: the worker reads the file on every pass, so no octopus or barracuda run is needed, and the line survives upgrades (an upgrade pass rewrites the control file only when it is missing or hand-seeded, and lays the kept lines back).

Step 2 — what the next pass builds

manage_ltd_users.sh runs from cron every three minutes (it defers while a barracuda or octopus run holds the box). On the first pass after the edit it creates, for each name in the list:

  • The account o1.acmecorp-dev: the directory name reduced to lowercase letters and digits, then -dev. The full name must fit in 32 characters; a longer one is reported and nothing is granted (shorten the Client's name).
  • Groups: the instance's own group as primary, plus www-data, users, lshellg and ltd-shell — the same set as the ordinary sub-account. The login shell is MySecureShell, which hands interactive sessions to lshell, exactly as for every tenant login.
  • The password, generated fresh and written to /home/o1.ftp/users/o1.acmecorp-dev — the tenant sees it as ~/users/o1.acmecorp-dev next to the ordinary sub-account's file. Nothing is emailed anywhere; the tenant forwards it.
  • The home /home/o1.acmecorp-dev: ~/sites → the Client's directory (the same view the ordinary sub-account has), ~/.tmp, and a root-owned ~/platforms/ with one link per qualifying codebase, named <revision>-<codebase> (for example 002-drupal-10.4.10-prod), plus a README.txt repeating the Drush-window instructions. The links are placed by root so the SFTP side sees them too; the home is immutable between passes.
  • The lshell section [o1.acmecorp-dev] in /etc/lshell.conf: its path carries the Client's site directories and their file stores as usual and the app root of every qualifying platform (the composer root where vendor/ lives, the docroot otherwise); and, once at least one platform is granted, two extra lines re-add composer, drush, drush8, drush10, drush11, vdrush and vendor/drush/drush/drush.php on allowed and allowed_shell_escape. lshell applies the per-user section last, after the ltd-shell group's removals, so the re-additions win. The overssh list — what a one-shot ssh user@host command may run — is not widened.

A platform that fails the test is simply not granted, and the pass writes why:

TXT
... platform /data/disk/o1/distro/002/drupal-10.4.10-prod refused for o1.acmecorp-dev: not single-client: /data/disk/o1/distro/002/drupal-10.4.10-prod/sites/blog.example.com

in /var/log/boa/manage_ltd.incident.log, naming the site directory that is not the Client's (or one of three fixed reasons: no docroot, no sites directory, not a Drupal or Backdrop platform); the other platforms are still granted. The same text is mailed to _MY_EMAIL once a day per platform unless _INCIDENT_REPORT is OFF (or NO) in /root/.barracuda.cnf. A Client that owns no Drupal or Backdrop site gets no account at all — the ordinary sub-account rule: no site, no account. That case is not silent, though: the sub-account step drops a Client's Grav and Textpattern links and removes the emptied client directory, and the platform step, which runs straight after it in the same pass, then finds no directory for the name and logs platform account for o1: no client directory /data/disk/o1/clients/acmecorp: listed in _LTD_PLATFORM_CLIENTS; nothing granted on every pass (mailed once a day) until the name leaves the list or the Client owns a Drupal or Backdrop site.

Step 3 — verify it

SH
getent passwd o1.acmecorp-dev
id -nG o1.acmecorp-dev
ls -l /home/o1.acmecorp-dev/platforms/
sed -n '/^\[o1.acmecorp-dev\]/,/^\[/p' /etc/lshell.conf
grep acmecorp-dev /var/log/boa/manage_ltd.incident.log

Expect the account in ltd-shell, one link per platform you planned, a path line that ends with those app roots and the two allowed ... : + [...] lines under it, and no refused for line for a platform you expected. Then check the shell itself as root, through the account's own login shell (MySecureShell hands the session to lshell exactly as an SSH login would), without the password:

SH
su - o1.acmecorp-dev -c 'composer -V'
su - o1.acmecorp-dev -c 'cd ~/platforms/<a Drupal 7 or Backdrop one> && drush8 status'
su - o1.acmecorp-dev -c 'cd ~/platforms/<a Drupal 8+ one> && vdrush --version'

The first two answer; the third is refused with lshell: command not found: "vendor/drush/drush/drush.php" until a window is open — next section.

Step 4 — hand it over

The tenant reads ~/users/o1.acmecorp-dev and passes the login on; the developer's own key goes into /home/o1.acmecorp-dev/.ssh/authorized_keys, the same as for any sub-account. The tenant-facing instructions — connecting, what they see, the window, the limits — are on extra accounts for your developers; point them there.

The Drush window

Drupal 8+ platforms are kept locked for Ægir's own Drush 8 between uses (how the lock state is derived): the lock de-types the console classes and overlays the psr/log version Drush 8 needs, and a modern Drush is typed against the current psr/log, so under PHP 8 the two cannot load in one codebase — the developer's vdrush needs the platform unlocked while they work. The account asks for a window and the worker opens and closes it:

  1. The developer runs touch ~/.tmp/drush-window.request. A regular file at that name is a request; a symlink there is ignored.
  2. On its next pass the worker removes the marker, starts a root-owned clock (/var/backups/ltd/window/o1.acmecorp-dev), records each granted composer platform's current state as locked or unlocked in /var/backups/ltd/window/o1.acmecorp-dev.prev, and runs provision-dunlock as the instance user on every granted platform that is locked (Drupal 7 and Backdrop platforms have no lock and need no window). The incident log gets drush window opened for 60 min; the unlock task's own output lands in a dated file under /var/backups/ltd/log/, a directory every BOA upgrade pass clears, so the incident log is the durable record.
  3. Every pass inside the window re-unlocks any granted platform that a task locked again meanwhile (a site Verify re-locks its platform).
  4. Touching the marker again restarts the clock; the recorded states stay.
  5. When the clock is older than 60 minutes the next pass runs provision-dlock on every platform it had recorded as locked, removes the state files and logs drush window closed. A platform an operator had unlocked before the window stays unlocked.

The length is a constant in the worker, not a control-file key. ls /var/backups/ltd/window/ shows which accounts hold an open window (one clock file each, plus its .prev). To close a window early, age the clock — it is root's file, nothing a tenant can reach — and let the next pass do the rest:

SH
touch -d '2 hours ago' /var/backups/ltd/window/o1.acmecorp-dev

While a window is open: control-panel tasks on that platform's sites can fail, because Ægir's Drush 8 cannot drive an unlocked platform; a site Verify re-locks it and the next pass unlocks it again; and a compiled container built while unlocked may need a Verify once the platform is locked again. Tell the tenant to time their panel work around it.

Retiring the account

Remove the name from _LTD_PLATFORM_CLIENTS — or let the Client lose its last site, or delete the Client, which removes its directory — and the next pass:

  • closes an open window (the platforms it had found locked are locked again),
  • deletes the account, backing the home up under /var/backups/zombie/deleted/<timestamp>/ (that is an archival copy, not a reactivatable home),
  • removes the password store file and the lshell section, and logs LTD account o1.acmecorp-dev removed: platform account retired (not in _LTD_PLATFORM_CLIENTS of o1).

Only the first route is complete on its own: after deleting the Client or taking its last site away, the name is still in _LTD_PLATFORM_CLIENTS, and every later pass reports no client directory for it (logged, mailed once a day) until you remove the name as well. Nothing the developer wrote is touched: the code and the sites belong to the tenant account, the login only reached them. An account is retired the moment it leaves the list — deliberately, on the spot: a section vanishing while the login stayed would drop it to the group policy, which is wider than the account ever was.

Password rotation is the sub-account rule: delete /home/o1.ftp/users/o1.acmecorp-dev (the tenant can do it from their own login) and the next pass rebuilds the account with a fresh password in the same file; the home, including ~/.ssh/authorized_keys, goes with the rebuild. The 90-day expiry applies as to every tenant login.

When something is missing

You see Why What to do
No account after a few minutes, incident log says no client directory, and the control panel shows no such Client (or one owning no site) The name is not a directory under clients/: a typo, or a Client that owns no site yet Compare with ls /data/disk/o1/clients/; make the Client the owner of a Drupal or Backdrop site
No account, incident log says no client directory, but the control panel shows the Client with Grav or Textpattern sites only Those links are dropped and the emptied clients/ directory removed by the sub-account step, before the platform step runs in the same pass (the pass log reads Skipping non-Drupal site ..., then Empty ... - deleting now) Give the Client a Drupal or Backdrop site, or take the name out of _LTD_PLATFORM_CLIENTS
No account, nothing logged at all A barracuda or octopus run is holding the box (the worker defers), or the line sits above an existing _LTD_PLATFORM_CLIENTS="" line, which wins Wait for the run to end; edit the existing line instead of adding one
Incident log says longer than 32 characters The account name would not fit Rename the Client (a shorter directory name)
A platform is not under ~/platforms It was refused — the incident log names the site directory that is not the Client's, or one of the fixed reasons (no docroot, no sites directory, not a Drupal or Backdrop platform) Move that site off the platform (onto one of its own, or one of its Client's), or accept the refusal; a non-Drupal, non-Backdrop platform is never granted
vdrush is refused on a Drupal 8+ platform No window is open (the request was never made, or was a symlink), or the platform has no vendor/drush Have the developer touch ~/.tmp/drush-window.request; wait one pass
Incident log says no platform alias found The window cannot unlock a platform whose docroot no platform_* alias of the instance names Verify the platform from the control panel so its alias exists
The developer cannot write to a platform root, or into a site directory itself Platforms installed by the control panel under distro/ are 0755 oN:oN (their vendor/, web/ and web/modules/ 2755), and a site directory with its settings.php and drushrc.php is 0755 oN:oN too — read-only for every tenant login, this one included Expected; the writable places are a site's files/, private/, modules/, themes/ and libraries/ (the 2775 parts, exactly as for the main oN.ftp login) and the whole tree of a platform the tenant deployed under static/ (measured: 2775 oN:oN after its Verify)
A panel task on the platform failed A window was open Re-run after the window closes, or close it early (above)

Security model

  • Authority is root's. The list lives in /root/.<oN>.octopus.cnf; the platform set is enumerated by root from the Client's site links and the platforms' own sites/ directories; the only thing the account can do is drop a request marker, and a marker can only ever ask for a window, never for a path.
  • Fail-closed from the platform side. A site directory is accepted only when its real path is one of the Client's links; a site the operator owns appears in no Client directory at all, so "not in the farm" refuses the platform rather than passing it.
  • The grant is policy, not ownership. lshell's path and the Landlock confinement of the per-instance group decide what the shell may reach; Unix permissions are unchanged, which is why a control-panel platform stays read-only, a site directory takes writes only in its files/, private/, modules/, themes/ and libraries/, and a tenant-deployed platform under static/ takes them throughout — for this login exactly as for the main one. Composer therefore changes code only on the tenant's own platforms; on a control-panel platform it reads (show, outdated, why).
  • One-shot SSH stays narrow. The overssh list is the sub-account's; a script driving the account from outside gets the same short list as before.
  • Refusals are reported, once a day per platform, through the ordinary incident channel (_INCIDENT_REPORT, _MY_EMAIL).

© 2026 BOA Documentation. All rights reserved.