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:
cdinto directories under their home.ls,cat,less,grep,findfiles they own.- Run BOA's curated whitelist of commands (
drush,vdrush,mybackup,composer,gem,npmif enabled,git, …). - Exit /
logout.
A tenant in lshell cannot:
- Run arbitrary binaries (
bash,sh,python,perlas 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.
Single commands over SSH — the overssh allow-list
The whitelist above is for an interactive lshell session. A command sent
non-interactively — ssh 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.
manage_ltd_users.sh — the orchestrator
The BOA script that:
- Provisions new tenant accounts (when a new Octopus instance is created).
- Rotates passwords every 90 days per BOA security policy.
- Configures
lshellper user with the BOA command whitelist. - Manages SFTP/FTPS via MySecureShell / Pure-FTPd.
- Manages SSH
authorized_keys. - Grants extra subaccounts (per Ægir Client — see shell extras).
- Enforces filesystem permissions under each tenant home (including the
per-user pool
disable_functionsapplied to the FPM pool). -
Reaps ghost Drush aliases (opt-in). On each pass it detects stale per-site Drush alias copies in the
oN.ftptree — "ghosts" whose site directory no longer has adrushrc.php— and, when_GHOST_ALIASES_CLEANUP=YESis set (read inline from/root/.<account>.octopus.cnfor/root/.barracuda.cnf; the defaultNOonly detects and logs), moves the account-side alias to<account>/undo/(recoverable) and removes the.ftpcopy. Safeguards:- Skips while any
provisionprocess is running. - Skips aliases written under 60 minutes ago (mid-install/clone protection).
- Keeps any alias whose
site_pathparses empty or outside/data/disk(fail-closed). - Skips the Drush yml alias rebuild on any run that reaped, so a deregistration is never immediately baked in.
Unlike the nightly ghost-site reaper, this one has no consecutive-run persistence, candidate classification, or migration interlock, and
rsync -apreserves old alias mtimes — so during anxoct/xcopyaccount transfer its freshness guard does not protect freshly-arrived aliases whose site directories have not landed yet. It therefore stays a per-box decision everywhere (it is not part of the hosted-fleet defaults); if enabled on a box that receives account migrations, hold/etc/boa/.pause_tasks_maint.cnfon the target for the duration of the transfer — it suspends the wholemanage_ltd_users.shpass.See ghost cleanup for the full reaper-flag family.
- Skips while any
It lives at /var/xdrago/manage_ltd_users.sh and runs:
- Every 3 minutes via cron (state sync) — the
*/3root crontab entry (aegir/tools/system/cron/crontabs/root). - On
octopus installandoctopus 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_quotamodule). - 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_FILESappended to the allowed-dirs list),manage_ltd_users.sh— the per-user jail path list foroN.ftp,satellite.sh.inc— the[oN.ftp]path :allow-list in/etc/lshell.conf, which lists_MNT_STATIC_FILESalongside/opt/user/gems/oN.ftpand/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):
/root/.allow.mc.cnf— opens Midnight Commander to lshell users. MC can spawn sub-shells. Do not enable on untrusted-tenant hosts./root/.allow.node.lshell.cnf— allows Node/NPM. Node can bypass lshell. Only enable on trusted-tenant hosts.chsh -s /bin/bash oN— temporarily swapsoN.ftp's shell to bash. The legacyremote_importprocedure does this and must reverse it (chsh -s /bin/false) afterwards.mv /var/xdrago/manage_ltd_users.sh /var/backups/— also from the legacyremote_importprocedure. 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
# 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:
# 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.
Related
- Security model — the broader security model.
- SSH + SFTP hardening — SSH/SFTP server-side config.
- Shell extras — extra accounts per client.
- Migration & cloning — the
remote_importprocedure that temporarily disables the lshell layer.