lshell is a restricted login shell written in Python. It confines a user to a configurable set of allowed commands, keeps them inside their own directory tree, controls what may run over SSH (scp, sftp, rsync, etc.), and logs their activity.
This is the QuickBox.IO-maintained fork of the (now unmaintained) upstream ghantoos/lshell. It is packaged as a standard Python wheel, supports Python 3.10–3.13, and carries security hardening specific to the QuickBox Pro seedbox platform.
On every QuickBox Pro install, lshell is the login shell for shell-level-3 (restricted) non-admin users. Admin and full-shell users are unaffected. A restricted user who logs in over SSH — or opens a terminal in the dashboard — lands in lshell instead of bash, and can only run the whitelisted commands their seedbox workflow needs (rtorrent, rclone, git, rsync, and so on).
The goal is containment: a restricted user can operate their seedbox but cannot escape into a general-purpose shell, read outside their home tree, or execute arbitrary code on the host.
Each user's environment is resolved from the configuration file in this order of priority:
[username]— a section named for the UNIX user[grp:groupname]— a section named for the user's UNIX group[default]— the fallback applied to everyone
Key controls (see man lshell for the full list):
allowed— the exact command whitelist (or'all'for everything onPATH).forbidden— characters and tokens that are rejected outright (;,&,|, backtick,>,<,$(,${,sudo,./, …).path— the directory tree the user is geographically restricted to (e.g.['/home/%u/']).overssh— the commands permitted to run non-interactively over SSH.strict— when1, any unknown command is treated as forbidden and decrements the user's warning counter.
The fork adds fail-closed exec protection on top of the upstream checks:
- Enforced
sudo_noexec.sobackstop. Before each command runs,lshellprependsLD_PRELOAD=<sudo_noexec.so>, so a "rich" whitelisted binary (an editor,find, etc.) cannotexec()its way into a subshell. path_noexec_strict(QuickBox default). When set, if thesudo_noexec.solibrary cannot be located,lshellrefuses to start rather than launching a restricted shell with no exec protection. A missing backstop is treated as a hard failure, not a warning.- Sanitised environment. Inherited
LD_PRELOAD,LD_LIBRARY_PATH, andGCONV_PATHare stripped andPATHis rebuilt fromenv_path/allowed_cmd_path, so a user cannot pre-load their own library or point the shell at their own binaries. - Traversal-safe path checks. Directory restrictions resolve real paths and reject wildcard- and dotfile-obfuscated parent-directory traversal (
.*/.*/etc/passwd,../../../etc/passwd, and similar), so a user cannot walk out of their permitted tree. - Forbidden-token filtering. Command separators, redirections, and substitution syntax are blocked so a whitelisted command cannot be chained into an un-whitelisted one.
QuickBox installs and updates lshell as a pre-built wheel served by the QuickBox release proxy — there is no build toolchain on a user's box. The installer and updater:
- Request the current release artifact from the release proxy.
- Verify it (non-empty, SHA-256, version-in-filename) before installing.
pip install --no-depsthe verified wheel, creating thelshellconsole entry at/usr/bin/lshell.
This replaces the old git clone + flit build flow: no anonymous clone, no on-box build, and no chance of a failed build leaving a dangling shell symlink that locks restricted users out at login.
lshell is not distributed as a .deb or an RPM, and is not installed from source on user machines. QuickBox Pro is Debian/Ubuntu only.
The canonical QuickBox configuration lives in the v3 repository at src/config/system/lshell/lshell.conf and is deployed to /etc/lshell.conf. Edit configuration there — do not treat any copy bundled in this repository as the source of truth. The configuration is reloaded dynamically: editing /etc/lshell.conf applies to already-connected users on their next command.
lshell targets Python 3.10–3.13. The wheel is py3-none-any (interpreter-agnostic) and depends only on the standard library.
The test suite lives under test/:
test_unit.py— unit coverage of the security and configuration logic; this is the release gate.test_functional.py— spawnsbin/lshellviapexpectand exercises real confinement behaviour end to end.
Both run in CI (see .github/workflows/test.yml) across Python 3.10, 3.11, 3.12, and 3.13 on every push and pull request. To run the unit suite locally:
python -m unittest discover -s test -p 'test_unit.py' -vRelease artifacts (wheel + sdist) are built with flit and published by .github/workflows/release.yml; the release proxy serves the latest stable wheel to QuickBox servers.
Licensed under the GNU General Public License v3 (see COPYING).
Originally written by Ignace Mouzannar (ghantoos). This is the QuickBox.IO-maintained fork, kept current for modern Python and hardened for the QuickBox Pro platform.