Drush extension deny-filter (*.drush.inc)

Classic Drush 8 auto-discovers and loads any *.drush.inc command file it finds in a site's modules and themes. On a shared Aegir server that is a privilege-escalation hole: a tenant who can write to their site's codebase can plant a *.drush.inc that Drush then executes as the Aegir backend user (aegir or oN) during a routine task — verify, migrate, cron — with authority over every site on that Octopus instance. This is the long-standing upstream provision issue #762138.

BOA's patched Drush 8 closes it at the Drush layer with a default-deny filter (includes/boa_extension_filter.inc in the bundled Drush, the single source of truth for all three of Drush's command-file discovery call sites): when Drush runs as a privileged backend identity, contributed *.drush.inc files under tenant-writable paths are refused. Core Drush commands, BOA's own extensions, and all hosting tasks are unaffected.

What is denied — and what never is

The deny set is anchored on BOA's canonical roots (case-insensitive):

Path Verdict
/data/disk/<oN>/static/…, /data/disk/<oN>/distro/…, /data/disk/<oN>/platforms/… Denied — tenant codebase territory
/data/disk/<oN>/.drush/… Denied, except the carve-outs below
/data/disk/<oN>/.drush/{sys,usr,xts}/… Allowed — BOA-managed Provision/Aegir extensions
/data/disk/<oN>/aegir/distro/… Allowed — Drush's own install; does not match the <oN>/{static,distro,platforms} anchor
/var/aegir/… Allowed — the Aegir Master hosts no tenant sites under BOA
Anything else Allowed

The trust boundary this encodes: /data/disk/<oN>/aegir/ is owned by the Octopus system user and is not tenant-writable; only static/distro/platforms and the non-{sys,usr,xts} parts of .drush/ are reachable by a site code contributor.

Candidate paths are canonicalised with realpath() before matching — Drush hands the discovery scanners paths that are relative to the Drupal root during site bootstrap, which would otherwise dodge every ^/-anchored pattern. When realpath() fails on a relative path, it is absolutised lexically against the cwd — fail toward deny, never silent allow. Stream-wrapper paths (phar:// — Drush ships phar-packaged command files) pass through untouched.

Who is filtered — the backend identity gate

On current BOA (Drush 8.5.4), the filter applies only to privileged backend identities:

  • the Aegir master — effective uid whose passwd entry is aegir with home /var/aegir;
  • an Octopus instance user oN — passwd home /data/disk/<oN>, dot-free name matching the directory segment.

oN.ftp limited-shell sessions — the account clients are meant to use for all CLI work (see lshell + ltd users) — are exempt: their own contributed drush commands load normally. Running as oN.ftp is not an escalation; the user already owns that uid.

Identity is derived from posix_geteuid() → the root-owned /etc/passwd dir field, deliberately not $HOME: several BOA backend calls run su <user> without -, leaving HOME=/root while the euid is correctly oN. A client cannot forge the euid. Fail-closed: if the posix extension is unavailable, or the effective uid is root, the filter stays on.

Per-instance backend opt-ins

Some contributed modules legitimately provide Drush commands the backend itself needs. These are enabled per Octopus instance with presence-only control files under /data/conf/ — content irrelevant, create as root (same convention as BOA's other opt-in control files):

Control file Allows (backend only)
/data/conf/<oN>_civicrm.txt civicrm.drush.inc, cv.drush.inc, civicrm_drush.drush.inc
/data/conf/<oN>_elysia_cron.txt elysia_cron.drush.inc, so backend-mode cron (hosting_cron_use_backend) keeps Elysia Cron's fine-grained schedule instead of degrading to core cron
touch /data/conf/o1_civicrm.txt

Backend Drush cron is a legacy path — BOA does not use Drush-based cron on Drupal 8+ — so the elysia_cron opt-in matters only for older sites that both run Elysia Cron and have hosting_cron_use_backend enabled. Neither opt-in is ever created automatically. Everything else on a deny-matched path stays denied for the backend regardless of opt-ins.

Global kill switch

touch /data/conf/drush_extension_filter_disabled.txt

Turns the filter off for every identity, restoring stock Drush *.drush.inc loading — and re-opening #762138 on that server. For single-tenant servers only; never use it on a box hosting sites for more than one party. Remove the file to re-enable the protection.

Rollout history

The filter shipped in two stages; which behaviour a server has follows from its bundled Drush 8 version:

  1. BOA 5.10.3 (Drush 8.5.3) — the first build where the filter actually denies (earlier wiring was inert: two of the three discovery sites matched class names instead of paths, and relative bootstrap paths dodged the anchors). In this build the filter applied to every Drush 8 invocation regardless of caller — including oN.ftp client sessions — and the only opt-in was the (then $HOME-derived) CiviCRM control file. Net effect on 5.10.3 boxes: contributed drush commands stop loading for all callers.
  2. Drush 8.5.4 (current: _DRUSH_EIGHT_VRN/_DRUSH_EIGHT_TEST_VRN=8.5.4 in BARRACUDA.sh.txt, _DRUSH_VERSION=8.5.4 in OCTOPUS.sh.txt) — added the backend identity gate (so oN.ftp sessions are exempt and their contributed commands load normally), switched opt-in lookup to the euid-derived instance user, and added the elysia_cron opt-in and the global kill switch.

Related mechanism: D11 codebase cleanup

Defence in depth at the filesystem layer: the bundled fix-drupal-platform-permissions.sh (hosting_tasks_extra fix_permissions) deletes all *.drush.inc files under <root>/modules/contrib and <root>/sites/*/modules on Drupal 11 codebases (gate: core/modules/workspaces_ui exists) during every platform permission fix. They are dead weight there — no Drush that can run D11 loads them — but expect contrib .drush.inc files to vanish from D11 platforms after permission-fix runs.

Troubleshooting signature

Core commands work (drush @site cc all) but a contributed command is "not recognized": the session is running as the oN bash user, which is a backend identity subject to the filter. Reconnect as oN.ftp — clients should always use oN.ftp for CLI work. If a contributed command is needed inside an actual backend task (e.g. backend-mode cron), use the per-instance opt-in above instead. See task failures for the CiviCRM variant of this symptom.

To verify on a real box that the filter holds both properties — backend identities deny tenant *.drush.inc while oN.ftp sessions are unaffected — follow the maintainer runbook docs/DRUSH-FILTER-TESTING.md in the BOA repository.

Related