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. - 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).
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.