Skip to content

Powered by Grav

Per-instance group — instgrp

Per-instance group — instgrp

Every Octopus account on a BOA server owns a private Unix group named after itself: the account o1 has the group o1, and that group is the primary group of the account's backend user (o1), its limited-shell user (o1.ftp) and every per-client sub-account (o1.<client>). The account's files carry that group. Nothing outside the account is a member of it.

Before this, every account's identities shared one box-wide primary group, users, and so did their files. A file that granted read to its group — a site's drushrc.php with the database credentials, a Drush alias — granted it to every other account on the box. The web pool identity was kept out (it runs as www-data), and the limited shell fences the paths a tenant can type, but a tenant's backend identity, or a command run by an allowed tool, could open a neighbour's credential files.

The per-instance group closes that: group read now means this account's identities, nothing wider.

What changes, and what does not

Before After
Primary group of oN, oN.ftp, oN.<client> users (box-wide) oN (the account's own)
Group on the account's files (~/static, ~/.drush, platforms, sites/<uri>/drushrc.php, …) users oN
users on those identities primary kept, supplementary
settings.php, files/, private/ oN:www-data unchanged
FPM pool identities oN.web, oN.<php>.web www-data only unchanged — they never join oN
Shared codebases under /data/all root:users unchanged (see below)

users stays on every identity on purpose. On a BOA server it is not a data group but an execute capability: the system binaries are root:users 0750 (see strict binary permissions), lshell itself included, and the shared-core directories that tenants may write to are group users. An identity outside users cannot run git, composer or even log in. Nothing removes it; the build-time guards on the BOA tree refuse any tool that would.

How an account gets there

Nothing to do. The octopus upgrade converts each account it upgrades, and an account created after this ships is born converted. Each conversion is recorded in the account's log/instance-group.txt.

The conversion is ordered so that no identity ever loses access it had:

  1. the group is created (an old group of the same name is adopted only when nothing else holds it);
  2. users, www-data and the lshell groups are asserted as supplementary groups on every identity of the account;
  3. only then does the primary group move to the account's group;
  4. the account's roots are walked and every path still in group users is re-grouped — a group-only change, one chown -R --from per root, that never follows a symlink and never touches the www-data paths (the immutable control files in the shell homes are handled one by one);
  5. the marker is written.

A shell session that was logged in before the conversion keeps its old group set until it reconnects; its own files still open through the owner bits.

The conversion refuses to start while another BOA run is live, waits for the account's own provision tasks to finish, and skips the account (to the next upgrade) rather than half-convert it. It also refuses when any root-run maintenance tool on the box still carries the old, box-wide form — an old tool would write users back onto the tree on its next pass, and an old websh would lock the tenant out of its shell. Every BOA tool that writes into account trees derives the group from the account it is writing for (users until the account is converted, the account's group after), so a mixed fleet — converted here, not yet there — keeps working throughout.

Reading and steering it: instgrp

instgrp is a root-only operator command (installed in /opt/local/bin, never exposed through sudo or the limited shell):

SH
instgrp status o1        # or: instgrp status all
instgrp convert o1       # idempotent; also what the upgrade arm runs
instgrp revert o1
instgrp check            # are the fetched maintenance tools group-aware?

status prints the marker, the group, each identity's group set, the FPM pool identities (which must not be in the group), the per-group file counts under the account's roots, and one verdict line:

Verdict Meaning
CONVERTED group, identities, files and marker all agree
UNCONVERTED the account is still on the box-wide model, consistently
DRIFT converted, but some paths are back in group users — an import from an unconverted box, a hand chown, a tool that predates the form. Re-run convert; it only touches what drifted
INCONSISTENT something in between (a group without a marker, an identity outside users, an FPM user inside the group) — read the lines above it

A marker whose recorded gid is not the account group's gid on this box is reported as stale and ignored: it travelled with a tree copied from another box (a migration, a restore) and records that box's conversion, not this one's. The same copy can leave paths in an unassigned numeric gid (the source account's group has no name here unless this account is converted too); status counts them as nogroup and convert claims them for the account. xoct transfer runs that group pass on the destination itself, after every transfer.

The exit status follows the verdict (0, 0, 2, 3), so status all is usable from a check script. Every action appends a line to /var/log/boa/instgrp.log.

Opting an account out

_INSTANCE_GROUP=NO in /root/.oN.octopus.cnf (see octopus.cnf) keeps that account on the box-wide group model: the upgrade arm skips it, and a fresh account with that line is born the old way. It does not undo a conversion already made — run instgrp revert oN for that (files first, then identities, the group removed once empty). An explicit instgrp convert ignores the switch; it is an operator's order.

Removing an account

boa cleanup purge oN removes the account's group after its identities, so the gid is not left for a later account to inherit. Removing a single sub-account (through the panel or manage_ltd_users) leaves the group in place — it belongs to the account, not to the sub-account.

What this does not close

Be precise about the boundary, because "per-instance group" is easy to over-read:

  • Within one account, a per-client sub-account can still read the drushrc.php of a sibling site on the same account: the group is account-wide, not site-wide. That is box-wide becoming account-wide — a strict improvement — and the sub-accounts are kept at their own site directories by the limited shell's Landlock rules, but it is not per-site isolation and must not be described as one.
  • Shared codebases (/data/all/…/sites/all/{modules,libraries,themes}) stay root:users and group-writable by every account, by design: they are shared between instances. Cross-tenant write into a shared codebase is not closed by the group change; the honest fix for that is a per-account overlay, not a group.
  • The master (/var/aegir) keeps its own group aegir plus users, as it always had.

Relationship to the limited shell

The limited shell confines every tenant shell process with Landlock, so a command run by an allowed tool cannot open a neighbour's files even when the filesystem would let it. The per-instance group is the complementary, discretionary half: it protects the files themselves, so the backend identity, a cron job, a task running as oN, or any path that is not a tenant shell meets the same boundary. Both are in force; neither replaces the other.

© 2026 BOA Documentation. All rights reserved.