Skip to content

Powered by Grav

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

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 Ægir 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 Ægir 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/Ægir extensions
/data/disk/<oN>/aegir/distro/… Allowed — Drush's own install; does not match the <oN>/{static,distro,platforms} anchor
/var/aegir/… Allowed — the Ægir 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 towards 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 and later), the filter applies only to privileged backend identities:

  • the Ægir 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
SH
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

SH
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 and later (current pins: _DRUSH_EIGHT_VRN/ _DRUSH_EIGHT_TEST_VRN=8.5.7 in BARRACUDA.sh.txt, _DRUSH_VERSION=8.5.7 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.

The files stay on disk

The filter is the only mechanism. BOA does not delete or rename a contributed *.drush.inc and gives it no special treatment (it takes the platform's ordinary codebase mode like any other file): a platform's modules/contrib, sites/*/modules and webform trees keep every command file composer put there, and the backend simply never loads the ones on tenant-writable paths. A composer install or a git checkout on a hosted platform therefore no longer shows deleted files after a Verify or a permission fix, and the per-instance CiviCRM opt-in above allows a file that is actually still there.

Earlier BOA builds swept those files off Drupal 11 codebases (from mid-2025) and out of modules/contrib/webform (from early 2026) at platform-lock and permission-fix time, until September 2026: a workaround from before the filter existed for command files written for Drush 12 that broke Ægir's Drush 8 when it loaded them. That sweep is retired: see Discontinued features.

One consequence to know: a client's own drush8 session on a Drupal 8+ site is unfiltered and will load such a file, and a contributed command file written for Drush 12 can then fail at command discovery. That is one more reason the site-local vdrush is the documented route on Drupal 8+.

Troubleshooting signature

Core commands work (drush @site cc all) but a contributed command is "not recognised": 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 filter verification runbook, which carries the preconditions and pass criteria for both halves.

© 2026 BOA Documentation. All rights reserved.