lshell is a Python-based limited shell. It is designed to restrict a user to a defined command set, enforce path and character restrictions, control SSH command behavior (scp, sftp, rsync, ...), and log activity.
pip install limited-shellpython3 -m pip install build --user
python3 -m build
pip install . --break-system-packagespip uninstall limited-shelllshell --config /path/to/lshell.confDefault config location:
- Linux:
/etc/lshell.conf - *BSD:
/usr/{pkg,local}/etc/lshell.conf
You can also override configuration values from CLI:
lshell --config /path/to/lshell.conf --log /var/log/lshell --umask 0077Use the lshell shebang and keep the .lsh extension:
#!/usr/bin/lshell
echo "test"usermod -aG lshell usernameLinux:
chsh -s /usr/bin/lshell user_name*BSD:
chsh -s /usr/{pkg,local}/bin/lshell user_nameMake sure lshell is present in /etc/shells.
The main template is etc/lshell.conf. Full reference is available in the man page.
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
allowed accepts command names and exact command lines.
allowed: ['ls', 'echo asd', 'telnet localhost']lsallowslswith any arguments.echo asdallows only that exact command line.telnet localhostallows onlylocalhostas host.
For local executables, add explicit relative paths (for example ./deploy.sh).
warning_counter is decremented on forbidden command/path/character attempts.
When strict = 1, unknown syntax/commands also decrement warning_counter.
path_noexec: if available, lshell usessudo_noexec.soto reduce command escape vectors.allowed_shell_escape: explicit list of commands allowed to run child programs. Do not set it to'all'.allowed_file_extensions: optional allow-list for file extensions passed in command lines.
Set a persistent session umask in config:
umask : 00020002-> files664, directories7750022-> files644, directories7550077-> files600, directories700
umask must be octal (0000 to 0777).
If you set umask in login_script, it does not persist because login_script runs in a child shell.
Quick check inside an lshell session:
umask
touch test_file
mkdir test_dir
ls -ld test_file test_dirFor 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 ENDRun tests on multiple distributions in parallel:
docker compose up ubuntu_tests debian_tests fedora_testsThis runs pytest, pylint, and flake8 in the configured test services.
man lshell(installed)man ./man/lshell.1(from repository)
Open an issue or pull request: https://github.com/ghantoos/lshell/issues