Skip to content

Powered by Grav

The codebase lock — nightly ownership management

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 with core/lib/Drupal.php next to ../vendor/autoload.php and a composer.json requiring drupal/core or drupal/core-recommended, the shape of drupal/recommended-project and Drupal CMS) the pass operates on the repository root, so vendor/ and composer.json are covered too. Then the permission sweep: directories 0775, files 0664, and the hardened read-only paths (vendor/drush, selected vendor/symfony/console internals) locked to 0400.
  • Platform level (every registered platform, in addition to the pass above): sites/all/{modules,themes,libraries}/* chowned to oN and the account's own group (see the per-instance group); the sites/ skeleton re-asserted (sites 0751, sites/* 0755, sites/*.php 0644; on a tenant-owned Composer codebase under ~/static the two directories Drupal's scaffold plugin writes into take group write instead, sites 02771 and sites/default 02775, so a tenant composer run can refresh its scaffold files there. Others keep execute-only on sites, and every sites/<uri> stays 0755); stray code archives (*.tar, *.tar.gz, *.zip) inside sites/all trees are deleted. A platform whose sites/all/modules, sites/all/themes, sites/all/libraries or sites/all/drush is a symlink has the legs that walk through those names withheld (a SKIP line in the account's nightly log names the link, once per night); the sites/ 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 (a modules link 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 own SKIP line; a Grav or Textpattern platform is withheld and reported the same way, its sites/all/drush not 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 leave sites/all/libraries/tcpdf alone when it or its cache child is a symlink.
  • Built-in platforms (~/distro/NNN/<platform>): the tenant-writable sites/all/{modules,themes,libraries} keep 02775/0664; core, profiles, includes, vendor and the platform root take 0755/0644, no group write (the three Drush-lock dirs keep their lock-state mode).
  • Hostmaster trees (aegir/distro/NNN on an Octopus account, /var/aegir/distro/NNN on the master): no shell user has any business there, so the platform script and the nightly keep them at 0755/0644 with no group write (root dir included), unlike every other platform.
  • Site level: each site's {modules,themes,libraries}/* chowned to oN and the account's group with directories 02775 and files 0664; settings-class files (settings.php, local.settings.php, civicrm.settings.php) kept at oN:www-data, mode 0440/0640; the site's files/ tree chowned to oN:www-data (symlink-safe, chown -h). A site whose modules, themes or libraries is a symlink has only the legs that reach through those names withheld (the archive sweep, the modules/local-allow.info removal and the code-dir ownership pass), with a SKIP line naming the link; the settings-file narrowing and the files/ and private/ 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; a modules link 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 the SKIP line naming <site>/modules is 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; the files/ and private/ 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 target oN.ftp instead of oN — the whole tree for ~/static platforms, and the {modules,themes,libraries} contents at platform and site level; the container directories, settings files and files/ areas stay with the backend user. That hands the owner-only rights to the shell account so in-place composer update and 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 and files//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 for skip.info; the sites/all and 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=YES in /root/.barracuda.cnf (default YES) gates the entire nightly permission/ownership machinery. It is not a durable off switch: a barracuda upgrade run rewrites the line back to _PERMISSIONS_FIX=YES, so a hand-set NO lasts only until the next upgrade. It is also the only operator gate on the separate sweep over the shared /data/all and /data/conf trees, 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.info when 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.txt present to run at all.
  • _SKIP_PERMISSIONS_PASS=YES in /root/.barracuda.cnf (default NO) — 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 to YES. The legacy marker /etc/boa/.dont.touch.permissions.cnf is 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=NO and 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 = FALSE in a platform's active INI file opts that platform out. The nightly worker seeds the variable into each platform INI as a commented-out TRUE default — 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's static/ 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.

© 2026 BOA Documentation. All rights reserved.