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:
- the group is created (an old group of the same name is adopted only when nothing else holds it);
users,www-dataand the lshell groups are asserted as supplementary groups on every identity of the account;- only then does the primary group move to the account's group;
- the account's roots are walked and every path still in group
usersis re-grouped — a group-only change, onechown -R --fromper root, that never follows a symlink and never touches thewww-datapaths (the immutable control files in the shell homes are handled one by one); - 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):
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.phpof 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}) stayroot:usersand 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 groupaegirplususers, 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.
Related
- Database credentials — cloaking — what the 0440 files hold and how the web and command-line paths read them.
- Shared codebase permissions — fixrepo —
the group-write repair tool; hand a root-cloned tree to the account with
chown -R oN:$(id -gn oN)before running it. - The codebase lock — the nightly ownership management, which writes the account's group.