Skip to content

Powered by Grav

lshell + manage_ltd_users

lshell + manage_ltd_users

The per-tenant restricted-shell layer is the boundary that keeps Octopus tenants isolated from each other and from the host system. It is driven by lshell (Limited Shell) plus BOA's manage_ltd_users.sh orchestrator (aegir/tools/system/manage_ltd_users.sh, deployed to /var/xdrago/manage_ltd_users.sh).

The two-account model per Octopus instance

Each Octopus instance gets two Unix accounts:

Account Shell Purpose
oN bash Quick file inspection; minimal scripting
oN.ftp MySecureShell → lshell All drush, vdrush, Composer, PHP-CLI work

oN.ftp's login shell in /etc/passwd is /usr/bin/mysecureshell in the normal BOA state (MySecureShell installed and /etc/ssh/sftp_config present).

Interactive SSH still lands in lshell: manage_ltd_users.sh adds the account to the lshellg group, and sftp_config's <Group lshellg> block sets Shell /usr/bin/lshell for that group. Only when MySecureShell is absent does the passwd shell become /usr/bin/lshell directly.

The rest of this page refers to that effective restricted shell simply as "lshell".

The lshell wrapper layer is only active under oN.ftp. The two accounts have distinct identities and homes: oN is the instance owner of /data/disk/oN/, while the lshell/SFTP account is oN.ftp, homed at /home/oN.ftp/ with a static symlink back to the instance tree.

What lshell restricts

A tenant in lshell can:

  • cd into directories under their home.
  • ls, cat, less, grep, find files they own.
  • Run BOA's curated whitelist of commands (drush, vdrush, mybackup, composer, gem, npm if enabled, git, …).
  • Exit / logout.

A tenant in lshell cannot:

  • Run arbitrary binaries (bash, sh, python, perl as general-purpose shells).
  • Use shell metacharacters like &&, ||, ; for command chaining beyond what lshell explicitly permits.
  • Read files outside their home directory.
  • Escape the chroot/jail boundary.

The exact command whitelist is in lshell's config under /etc/lshell.conf plus the per-user overrides BOA writes.

du is off by default. BOA regenerates /etc/lshell.conf from its own template (/var/xdrago/conf/lshell.conf) on every manage_ltd_users.sh pass, and unless du is explicitly allowed it strips du out of that copy first — both the entry in the allowed command list and the dudu -s -h alias. The supported switch is _ALLOW_DU=YES in /root/.barracuda.cnf, with the legacy marker /etc/boa/.allow.du.cnf honoured for one release beside it. The test is an AND of both negations, so either one is enough to keep du for every lshell account on the box, and the strip happens only when the key is not YES and the file does not exist — to take du away again, set _ALLOW_DU=NO and remove the marker. Either way the change lands on the next pass (within three minutes), and only when the regenerated config actually differs from the installed one — in which case the outgoing /etc/lshell.conf is kept under /var/backups/ltd/old/. The marker is read from /etc/boa/ only; a legacy /root/.allow.du.cnf is copied across once by BOA during an upgrade and ignored afterwards. Note that /root/.allow.mc.cnf, tested a few lines earlier in the same block, has not moved — it is still read from /root/, and by design it has no variable form either: the lshell shell-escape markers are kept file-only, because a cnf line is copyable box-to-box in a way a root-owned marker is not.

Single commands over SSH — the overssh allow-list

The whitelist above is for an interactive lshell session. A command sent non-interactivelyssh oN.ftp@server "<command>" — is governed by a separate, narrower allow-list in the same config, lshell's overssh set.

overssh permits drush (and drush8/drush10/drush11), mysql/mysqldump/mydumper/myloader, rsync, scp, git, composer and cp/mv/rm/ls/grep — enough to drive remote dumps, file syncs and Drush work. It does not permit tar, cat, vdrush or the site-local vendor/drush/drush/drush.php, even though the interactive whitelist allows all four. So a command that runs fine when typed into an interactive session (a raw cat or tar, say) can still be refused as a one-shot SSH command — a common surprise when scripting against a tenant account from outside.

Node / NPM security gate

Node/NPM is not enabled by default because Node can be used to bypass lshell. node, npm, npx (and scp, which rides the same gate) are stripped from the lshell allowed command list unless one of two conditions is met:

  • The Octopus instance type is PHANTOM, CLUSTER, ULTRA or MONSTER — for these instance types Node/NPM/npx and scp are auto-enabled with no control file required (BOA greps the instance type out of /root/.<USER>.octopus.cnf).
  • The control file exists:

    SH
    touch /root/.allow.node.lshell.cnf
    

Only create the control file on hosts with trusted tenants. Shared / public hosting hosts should keep this disabled.

Even where Node/NPM is enabled, only the main oN.ftp account gets it. Per-Client shell subaccounts (oN.<client>) receive Ruby Gems access only — never Node/NPM. Both are initialised from the same ~/static/control/compass.info control file, but the NPM half is skipped for every account other than the main one, which keeps the lshell-bypass surface on a single main-tenant account rather than spreading it across every client subaccount.

manage_ltd_users.sh — the orchestrator

The BOA script that:

  1. Provisions new tenant accounts (when a new Octopus instance is created).
  2. Rotates passwords every 90 days per BOA security policy.
  3. Configures lshell per user with the BOA command whitelist.
  4. Manages SFTP/FTPS via MySecureShell / Pure-FTPd.
  5. Manages SSH authorized_keys.
  6. Grants extra subaccounts (per Ægir Client — see shell extras).
  7. Enforces filesystem permissions under each tenant home (including the per-user pool disable_functions applied to the FPM pool). Late in every pass it also runs one wildcard chmod 700 /home/*, which resets every entry directly under /home — tenant oN.ftp homes, client subaccount homes, and anything else parked there — to owner-only. _HOME_NO_WILDCARD_CHMOD=YES in /root/.barracuda.cnf suppresses that single wildcard chmod, with the legacy marker /etc/boa/.home.no.wildcard.chmod.cnf honoured for one release beside it: the test is an AND of both negations, so the chmod runs every three minutes only while neither is in force. Nothing else changes — the same pass still re-asserts 0600 on /var/log/lsh/*, the 0400/0710 modes on the /var/aegir/.drush alias files, and the server_master config tree modes. Set it when a non-tenant directory under /home has to keep modes of its own; the marker is read from /etc/boa/ only, a legacy /root/ copy having been relocated there once by BOA.
  8. Reaps ghost Drush aliases (opt-in). On each pass it detects stale per-site Drush alias copies in the oN.ftp tree — "ghosts" whose site directory no longer has a drushrc.php — and, when _GHOST_ALIASES_CLEANUP=YES is set (read inline from /root/.<account>.octopus.cnf or /root/.barracuda.cnf; the default NO only detects and logs), moves the account-side alias to <account>/undo/ (recoverable) and removes the .ftp copy. Safeguards:

    • Skips while any provision process is running.
    • Skips aliases written under 60 minutes ago (mid-install/clone protection).
    • Keeps any alias whose site_path parses empty or outside /data/disk (fail-closed).
    • Never touches an alias whose site_path lives under aegir/distro — a front-end control-panel companion belongs to the nightly classifier's operator-review path, not to this sweep.
    • Holds every ghost candidate for 48 hours after its first sighting (marker log/ctrl/ghost-ltd-<domain>.seen; any valid sighting clears it, and markers only accumulate while the flag is YES, so a flip never mass-reaps a backlog). The hold is what protects an inbound xoct/xcopy transfer: rsync -a preserves old alias mtimes, so the 60-minute guard alone cannot tell a freshly-arrived alias from a stale one, and a hand-set /etc/boa/.pause_tasks_maint.cnf does not survive on a hosted system (the box automation removes it within minutes outside a barracuda run).
    • Skips the Drush yml alias rebuild on any run that reaped, so a deregistration is never immediately baked in.

    On builds that predate the 48h hold, keep _GHOST_ALIASES_CLEANUP=NO in both control files on any box while it is receiving an account transfer. Where the nightly site reaper is also enabled it usually acts first (with classification and the client notice); this sweep is the between-nights backstop that also cleans the mirrored ltd-shell copy.

    See ghost cleanup for the full reaper-flag family.

It lives at /var/xdrago/manage_ltd_users.sh and runs:

  • Every 3 minutes via cron (state sync) — the */3 root crontab entry (aegir/tools/system/cron/crontabs/root).
  • On octopus install and octopus upgrade (initial provision).
  • On password-change events.

When the wrapper is deactivated

manage_ltd_users.sh defers while a barracuda or octopus run is in progress: at startup it checks for the BOA run-lock files (/run/boa_run.pid, /run/octopus_install_run.pid, plus its own /run/manage_ltd_users.pid) and exits early (exit 0) if any is present, which protects the upgrade procedure from interfering with itself. During this window:

  • Existing tenant sessions keep working.
  • New SSH connections may not respect the latest config changes.
  • Some BOA-specific shell-wrapper features (such as PHP-CLI version switching via cli.info) are bypassed.

After the upgrade finishes, manage_ltd_users.sh is restored and catches up on any pending state.

What an Ægir Client is

Beyond the main oN.ftp account, each Octopus instance can host multiple Ægir Clients — independent customer identities within the instance. Each Client can have:

  • Their own Ægir UI login (non-admin).
  • One extra SSH/SFTP/FTPS shell subaccount (oN.<client>, one per Client).
  • Per-client quotas (hosting_quota module).
  • Per-client sites (only their own sites visible in the UI).

See shell extras for the operator how-to on adding extra shell subaccounts per client.

Per-tenant chroot

Each oN.ftp SFTP/FTPS session is chrooted to the tenant's home via MySecureShell + Pure-FTPd; the tenant cannot cd .. out of /home/oN.ftp/.

For SSH, the chroot is enforced through the lshell config rather than via chroot(). Tenants cannot escape because the only commands they can run do not include shell-spawning functions.

Attached-storage paths in the jail

When an account's static file store lives on attached storage, the oN.ftp jail's allow-list also carries the account's store path on that mount: the internal _MNT_STATIC_FILES path, <mount>/files/<oN>/static/files (with a per-client-domain variant <mount>/files/<oN>/static/files/<domain>/). _MNT_STATIC_FILES is computed at wiring time — it is an internal variable, not an operator cnf knob.

BOA locates the attached mount by what it is, not what it is named: each directory under /mnt is tested with mountpoint -q (falling back to a st_dev-differs-from-/mnt stat check), and the mount-side path is wired only when exactly one real mountpoint exists under /mnt.

Zero or multiple mounts fail closed — no usable attached-storage path is granted (two of the three writers omit the mount entries entirely; the /etc/lshell.conf writer emits a placeholder /files/<oN>/static/files that resolves nowhere).

A plainly named mount such as /mnt/extra works; the old heuristic that required a dot in the mount directory name is retired.

Multiple mounts under /mnt are unsupported fleet-wide — BOA cannot disambiguate which one is the files disk — and the same single-mount invariant is enforced by migratefs (refuses to run, exit 1, even with an explicit --target) and the xoct/xcopy/xmass migration tools.

The identical detection block is carried at all three wiring sites:

  • manage_ltd_users.sh — per-client-domain access paths (per-domain _MNT_STATIC_FILES appended to the allowed-dirs list),
  • manage_ltd_users.sh — the per-user jail path list for oN.ftp,
  • satellite.sh.inc — the [oN.ftp] path : allow-list in /etc/lshell.conf, which lists _MNT_STATIC_FILES alongside /opt/user/gems/oN.ftp and /opt/user/npm/oN.ftp.

See migratefs for the operator tool that relocates file stores onto that mount.

What breaks lshell — common gotchas

Things that bypass the lshell layer (avoid on multi-tenant hosts):

  1. /root/.allow.mc.cnf — opens Midnight Commander to lshell users. MC can spawn sub-shells. Do not enable on untrusted-tenant hosts.
  2. /root/.allow.node.lshell.cnf — allows Node/NPM. Node can bypass lshell. Only enable on trusted-tenant hosts — see the Node / NPM security gate section above for the full gate (instance-type auto-enable, main-account-only scope).
  3. chsh -s /bin/bash oN — temporarily swaps oN.ftp's shell to bash. The legacy remote_import procedure does this and must reverse it (chsh -s /bin/false) afterwards.
  4. mv /var/xdrago/manage_ltd_users.sh /var/backups/ — also from the legacy remote_import procedure. Disables the orchestrator entirely and must be reversed afterwards.

If you do steps 3 or 4 for migration, always reverse them before re-enabling tenant access.

Inspecting + auditing

SH
# List active lshell sessions
ps -fu oN.ftp | grep lshell

# View lshell logs (BOA overrides the lshell default to /var/log/lsh/)
ls /var/log/lsh/

# View per-tenant authorized_keys
# (oN is homed at /data/disk/oN; oN.ftp and client subaccounts live under /home)
cat /data/disk/oN/.ssh/authorized_keys
cat /home/oN.ftp/.ssh/authorized_keys

For SSH-key auditing:

SH
# Find all authorized_keys under both tenant-home roots
find /home /data/disk -name authorized_keys -type f

Password rotation

Every 90 days the tenant's password expires and rotates:

  • A new password is generated automatically.
  • It is emailed to the Octopus owner's address.
  • Old SSH keys still work (key auth bypasses password expiry).

Tenants who use only SSH key auth still get a new password emailed that they do not need to use; tenants who use password auth must update their saved password.

© 2026 BOA Documentation. All rights reserved.