lshell is a Python-based restricted shell that limits users to a defined set of commands, enforces path and SSH transfer controls (scp, sftp, rsync, ...), logs user activity, supports session/time restrictions, and more.
PyPI project page: https://pypi.org/project/limited-shell/
Install from PyPI:
pip install limited-shellPrepare system resources (run as root once per host):
lshell setup-system --group lshell --log-dir /var/log/lshell --owner root --mode 2770Build/install from source:
python3 -m pip install build --user
python3 -m build
pip install . --break-system-packagesUninstall:
pip uninstall limited-shellmain: stable release branch. Tag stable versions from this branch (for example1.2.3).pre-release: integration branch for tested features before release. Tag release candidates from this branch (for example1.2.4rc1).- PyPI publishing uses one project (limited-shell) and accepts both stable and
rcversions. - CI (
lshell-tests) runs on pushes and PRs targeting bothmainandpre-release.
Run lshell with an explicit config:
lshell --config /path/to/lshell.confDefault config location:
- Linux:
/etc/lshell.conf - *BSD:
/usr/{pkg,local}/etc/lshell.conf
Set lshell as login shell:
chsh -s /usr/bin/lshell user_nameFor automated setup (including /etc/shells registration + user shell assignment):
lshell setup-system --set-shell-user user_name --add-group-user user_nameGenerate a hardened scoped include file for a specific group and user directly from CLI flags:
lshell harden-init \
--profile sftp-only \
--group sftpusers \
--user alice \
--output /etc/lshell.d/sftp-only.confIf --output is omitted, harden-init writes to /etc/lshell.d/<profile>.conf.
Explain the effective policy and decision for a command:
lshell policy-show \
--config /path/to/lshell.conf \
--user deploy \
--group ops \
--group release \
--command "sudo systemctl restart nginx"Inside an interactive session:
policy-show [<command...>]policy-path(lpathalias)policy-sudo(lsudoalias)
Hide these built-ins if needed:
policy_commands : 0harden-init ships secure-by-default templates to bootstrap restricted accounts quickly:
sftp-onlyrsync-backupdeploy-minimalreadonly-support
Examples:
# Show available templates
lshell harden-init --list-templates
# Print generated profile to stdout
lshell harden-init --profile readonly-support --stdout
# Validate rendering and sanity checks without writing files
lshell harden-init --profile rsync-backup --dry-run
# Show rationale for security controls
lshell harden-init --profile deploy-minimal --stdout --explain
# Generate scoped sections (no [default] section)
lshell harden-init --profile sftp-only --group sftpusers --user alice --stdoutPrimary template: etc/lshell.conf
Key settings to review:
allowed/forbiddenpathsudo_commandsoverssh,scp,sftp,scp_upload,scp_downloadallowed_shell_escapeallowed_file_extensionsmessageswarning_counter,strictumask- runtime containment:
max_sessions_per_user,max_background_jobs,command_timeout,max_processes
CLI overrides are supported, for example:
lshell --config /path/to/lshell.conf --log /var/log/lshell --umask 0077Runtime limits are optional and disabled by default when set to 0.
max_sessions_per_user : 2
max_background_jobs : 4
command_timeout : 30
max_processes : 64Operational notes:
max_sessions_per_useris tracked with lock-protected session records; stale entries are cleaned automatically.max_background_jobsdenies new&jobs once the configured active count is reached.command_timeoutenforces a per-command wall-clock timeout (foreground and background commands).max_processesis applied via POSIXRLIMIT_NPROCon spawned command processes.- Best practice: keep
command_timeoutenabled whenevermax_processesis strict (especially1).
- Prefer an explicit
allowedallow-list instead of'all'. - Keep
allowed_shell_escapeshort and audit every entry. Never add tools that execute arbitrary commands (for examplefind,vim,xargs). - Use
allowed_file_extensionswhen users are expected to work with a known set of file types. - Keep
warning_counterenabled (avoid-1unless you intentionally want warning-only behavior). - Use
policy-showduring reviews to validate effective policy before assigning it to users. - For pip installs, do not rely on installation side effects for system setup. Use
lshell setup-system(or distro package post-install hooks) to create groups,/var/log/lshell, and login-shell registration.
Supported section types:
[global]for global lshell settings[default]for all users[username]for a specific user[grp:groupname]for a UNIX group
Precedence order:
- User section
- Group section
- Default section
For users foo and bar in UNIX group users:
# CONFIGURATION START
[global]
logpath : /var/log/lshell/
loglevel : 2
[default]
allowed : ['ls','pwd']
forbidden : [';', '&', '|']
warning_counter : 2
timer : 0
path : ['/etc', '/usr']
env_path : '/sbin:/usr/foo'
scp : 1
sftp : 1
overssh : ['rsync','ls']
aliases : {'ls':'ls --color=auto','ll':'ls -l'}
[grp:users]
warning_counter : 5
overssh : - ['ls']
[foo]
allowed : 'all' - ['su']
path : ['/var', '/usr'] - ['/usr/local']
home_path : '/home/users'
[bar]
allowed : + ['ping'] - ['ls']
path : - ['/usr/local']
strict : 1
scpforce : '/home/bar/uploads/'
# CONFIGURATION ENDFor full option details, use:
man lshellman ./man/lshell.1
Run test services directly:
docker compose up ubuntu_tests debian_tests fedora_testsRun full validation:
just test-allRun only SSH end-to-end checks:
just test-ssh-e2eList commands:
just --listRun distro-specific tests:
just test-debian
just test-ubuntu
just test-fedoraRun sample configs interactively:
just sample-list
just sample-ubuntu 01_baseline_allowlist.confRun Atheris fuzzing in Debian Docker (dependencies installed in-container):
just test-fuzz-security-parser 20000Optional local run (if you want to fuzz outside Docker):
pip install -r requirements-fuzz.txt
python3 fuzz/fuzz_parser_policy.py -runs=20000Open an issue or pull request: https://github.com/ghantoos/lshell/issues