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
aegirwith 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:
- 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.ftpclient 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. - Drush 8.5.4 (current:
_DRUSH_EIGHT_VRN/_DRUSH_EIGHT_TEST_VRN=8.5.4inBARRACUDA.sh.txt,_DRUSH_VERSION=8.5.4inOCTOPUS.sh.txt) — added the backend identity gate (sooN.ftpsessions are exempt and their contributed commands load normally), switched opt-in lookup to the euid-derived instance user, and added theelysia_cronopt-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
- Security model — the broader BOA security model.
- lshell + ltd users — the
oNvsoN.ftptwo-account model the identity gate builds on. - Control files overview — the presence-only control-file convention.
- Task failures — contributed drush command symptoms during hosting tasks.