The codebase lock — nightly ownership management
BOA treats code ownership on tenant platforms as managed state. Once per night, the maintenance worker walks every registered site, resolves its platform root, and re-asserts both ownership and permissions. The historical motivation (post-Drupalgeddon) was to make sure a compromised site cannot rewrite code at all, and a leaked SFTP credential cannot strip the hardening or subvert git's repository-ownership trust (the code trees stay group-writable for the shell pair, so the lock is not a write barrier); the mechanism survives today as the default lock, with a tenant-controlled unlock for in-place maintenance.
What the nightly pass does
For each registered site's platform (per account, once per night per
platform, tracked by marker files under ~/log/ctrl/):
- Platforms under
~/static(all tenant platforms live there): the whole codebase is chowned to the account's backend user (oN). For composer-managed shapes (a docroot withcore/lib/Drupal.phpnext to../vendor/autoload.phpand acomposer.jsonrequiringdrupal/coreordrupal/core-recommended, the shape of drupal/recommended-project and Drupal CMS) the pass operates on the repository root, sovendor/andcomposer.jsonare covered too. Then the permission sweep: directories0775, files0664, and the hardened read-only paths (vendor/drush, selectedvendor/symfony/consoleinternals) locked to0400. - Platform level (every registered platform, in addition to the pass
above):
sites/all/{modules,themes,libraries}/*chowned tooNand the account's own group (see the per-instance group); thesites/skeleton re-asserted (sites0751,sites/*0755,sites/*.php0644; on a tenant-owned Composer codebase under~/staticthe two directories Drupal's scaffold plugin writes into take group write instead,sites02771andsites/default02775, so a tenant composer run can refresh its scaffold files there. Others keep execute-only onsites, and everysites/<uri>stays0755); stray code archives (*.tar,*.tar.gz,*.zip) insidesites/alltrees are deleted. A platform whosesites/all/modules,sites/all/themes,sites/all/librariesorsites/all/drushis a symlink has the legs that walk through those names withheld (aSKIPline in the account's nightly log names the link, once per night); thesites/skeleton modes are still re-asserted and the pass marker still stamped, so the platform ends no wider than an accepted one and is examined again the next night (amoduleslink already in place when the pass starts is refused earlier still, at the per-site control-dir gate, which skips that site's whole iteration with its ownSKIPline; a Grav or Textpattern platform is withheld and reported the same way, itssites/all/drushnot created for that pass either). None of the four is ever legitimately a symlink, and the platform ownership and permission helpers Verify runs refuse the same shape; both also leavesites/all/libraries/tcpdfalone when it or itscachechild is a symlink. - Built-in platforms (
~/distro/NNN/<platform>): the tenant-writablesites/all/{modules,themes,libraries}keep02775/0664; core, profiles, includes, vendor and the platform root take0755/0644, no group write (the three Drush-lock dirs keep their lock-state mode). - Hostmaster trees (
aegir/distro/NNNon an Octopus account,/var/aegir/distro/NNNon the master): no shell user has any business there, so the platform script and the nightly keep them at0755/0644with no group write (root dir included), unlike every other platform. - Site level: each site's
{modules,themes,libraries}/*chowned tooNand the account's group with directories02775and files0664; settings-class files (settings.php,local.settings.php,civicrm.settings.php) kept atoN:www-data, mode0440/0640; the site'sfiles/tree chowned tooN:www-data(symlink-safe,chown -h). A site whosemodules,themesorlibrariesis a symlink has only the legs that reach through those names withheld (the archive sweep, themodules/local-allow.inforemoval and the code-dir ownership pass), with aSKIPline naming the link; the settings-file narrowing and thefiles/andprivate/legs still run, so the site ends no wider than an accepted one. That split covers a link that appears while the pass is already under way; amoduleslink already in place when the site's iteration begins is refused earlier still, at the per-site control-dir gate, which skips that whole iteration, so no leg of the site pass runs for it that night and theSKIPline naming<site>/modulesis the gate's, not the site pass's (on a tenant-built platform the whole-tree pass that widened the tree narrows every site's settings-class files back itself, so that site ends no wider either). The site ownership and permission helpers Verify runs refuse that shape outright; thefiles/andprivate/child entries are handled on the resolved store path only, never through a planted link.
Group-write for the shell pair survives all of this — the lock manages
the ownership axis, and with it the owner-only rights: chmod, git's
repository-ownership trust, and replacing read-only paths. The web
identity (the per-account .web FPM user, a member of www-data only)
can write code in neither state.
The two tenant switches
~/static/control/unlock.info— account-wide direction flip. While it exists, the code-tree chowns above targetoN.ftpinstead ofoN— the whole tree for~/staticplatforms, and the{modules,themes,libraries}contents at platform and site level; the container directories, settings files andfiles/areas stay with the backend user. That hands the owner-only rights to the shell account so in-placecomposer updateand git work function. Takes effect on the next nightly run; removing the file re-locks the next night. The tenant-facing workflow is documented in the Using tree (in-place upgrades).<platform root>/skip.info— per-platform opt-out from the recursive code-tree chowns in both directions: a skipped platform keeps whatever ownership you set on the code trees. The container directories, settings files andfiles//private/areas are still re-chowned to their managed owners, and the permission sweep still runs. The whole-tree pass on composer shapes checks the repository root forskip.info; thesites/alland site-level passes check the docroot — advise tenants to place it in both for composer-managed codebases.
Platforms that carry no registered site are never visited by the nightly pass at all; a platform Verify still applies the ownership and permission map, so only an unregistered, never-verified tree is entirely manual (see shared codebase permissions for the group-write repair tool for such trees).
Operator knobs
_PERMISSIONS_FIX=YESin/root/.barracuda.cnf(defaultYES) gates the entire nightly permission/ownership machinery. It is not a durable off switch: abarracudaupgrade run rewrites the line back to_PERMISSIONS_FIX=YES, so a hand-setNOlasts only until the next upgrade. It is also the only operator gate on the separate sweep over the shared/data/alland/data/conftrees, which the box-wide skip below does not touch. That sweep is not a nightly one: it stamps/data/all/permissions-fix-<serial>-<version>-fixed-dz.infowhen it finishes and skips itself while that file is present, so it runs once per BOA serial and release — in practice once after each upgrade — and needs/opt/tmp/barracuda-release.txtpresent to run at all._SKIP_PERMISSIONS_PASS=YESin/root/.barracuda.cnf(defaultNO) — the box-wide kill switch, and the setting that does survive an upgrade: the line is maintained append-if-absent, so an operator value there is never overwritten, unlike a hand-set_PERMISSIONS_FIX=NO, which an upgrade pass rewrites back toYES. The legacy marker/etc/boa/.dont.touch.permissions.cnfis honoured for one release beside it. While either is in force, the nightly worker forces its per-platform decision to "do not touch" for every registered platform, so the ownership and permission pass is skipped box-wide; the rest of the nightly per-site work still runs. That test is evaluated last, after both the INI opt-out and the Drupal 7 exception below, which is what makes it a kill switch rather than one more vote. To switch the skip off during the transition, set_SKIP_PERMISSIONS_PASS=NOand remove the marker file — while the file exists it wins and is re-asserted on every upgrade pass. The marker is read from/etc/boa/only; a legacy/root/copy is relocated there once by BOA during an upgrade and ignored afterwards.fix_files_permissions_daily = FALSEin a platform's active INI file opts that platform out. The nightly worker seeds the variable into each platform INI as a commented-outTRUEdefault — that seeding happens before either opt-out is evaluated, so a platform INI still gains the commented default on a box carrying the box-wide skip. One exception overrides the INI opt-out: a Drupal 7 platform missing the SA-CORE-2014-005 core patch has its permissions fixed anyway. The patch half of that exception is narrower — the patch helper is only ever reached for platforms living under an account'sstatic/tree, so a D7 platform outside it is re-permissioned but never patched.
That last exception is subordinate to the box-wide skip, not to the INI.
The patch is only ever applied from inside the permission pass, so while the
skip is in force — by _SKIP_PERMISSIONS_PASS=YES or by the marker — an
unpatched Drupal 7 codebase is left both un-permissioned and un-patched,
silently. Prefer the per-platform fix_files_permissions_daily = FALSE opt-out
when a single platform is the problem, and treat the box-wide skip as a
short-lived, whole-box measure.
Interplay with Ægir tasks
Platform Verify (and the install flows that run it) invokes the
sudo-exposed fix-drupal-platform-ownership.sh wrapper with the backend
user as the target (--script-user) — that is, a platform Verify
re-locks ownership immediately, in any lock state. While unlock.info
remains in place the next nightly run restores the unlock; but a tenant
mid-upgrade who runs a platform Verify will find the code handed back to
the backend user on the spot. This is by design: Verify's job is to
converge the platform to the managed state.
The nightly ownership flip and the fixrepo tool
solve different axes: the lock manages who owns registered platforms on
a schedule; fixrepo repairs group-write and setgid on a tree (typically
an unregistered one) once, by hand. They compose without conflict — a
fixrepo-treated registered platform still gets its ownership managed
nightly.