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.confExplain effective policy resolution for a target user/group set and a command:
lshell policy-show \
--config /path/to/lshell.conf \
--user deploy \
--group ops \
--group release \
--command "sudo systemctl restart nginx"- prints precedence resolution (
default -> groups -> user) - lists included config files (
include_dir) - shows key-level overrides and final merged policy
- returns command decision (
ALLOW/DENY) with reason
Inside an lshell session:
policy-show [<command...>]: show resolved values and optionally check a commandpolicy-path: show allowed/denied pathspolicy-sudo: show allowed sudo commands
Backward-compatible aliases:
lpath->policy-pathlsudo->policy-sudo
You can hide these policy commands from users with:
policy_commands : 0Default 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.
messages is an optional dictionary for customizing user-facing shell messages.
Unsupported keys and unsupported placeholders are rejected during config parsing.
Supported keys and placeholders:
unknown_syntax:{command}forbidden_generic:{messagetype},{command}forbidden_command:{command}forbidden_path:{command}forbidden_character:{command}forbidden_control_char:{command}forbidden_command_over_ssh:{message},{command}forbidden_scp_over_ssh:{message}warning_remaining:{remaining},{violation_label}session_terminated: no placeholdersincident_reported: no placeholders
Example:
messages : {
'unknown_syntax': 'lshell: unknown syntax: {command}',
'forbidden_generic': 'lshell: forbidden {messagetype}: "{command}"',
'forbidden_command': 'lshell: forbidden command: "{command}"',
'forbidden_path': 'lshell: forbidden path: "{command}"',
'forbidden_character': 'lshell: forbidden character: "{command}"',
'forbidden_control_char': 'lshell: forbidden control char: "{command}"',
'forbidden_command_over_ssh': 'lshell: forbidden {message}: "{command}"',
'forbidden_scp_over_ssh': 'lshell: forbidden {message}',
'warning_remaining': '*** You have {remaining} warning(s) left, before getting kicked out.',
'session_terminated': 'lshell: session terminated: warning limit exceeded',
'incident_reported': 'This incident has been reported.'
}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
messages : {
'unknown_syntax': 'lshell: unknown syntax: {command}',
'forbidden_generic': 'lshell: forbidden {messagetype}: "{command}"',
'forbidden_command': 'lshell: forbidden command: "{command}"',
'forbidden_path': 'lshell: forbidden path: "{command}"',
'forbidden_character': 'lshell: forbidden character: "{command}"',
'forbidden_control_char': 'lshell: forbidden control char: "{command}"',
'forbidden_command_over_ssh': 'lshell: forbidden {message}: "{command}"',
'forbidden_scp_over_ssh': 'lshell: forbidden {message}',
'warning_remaining': 'lshell: warning: {remaining} {violation_label} remaining before session termination',
'session_terminated': 'lshell: session terminated: warning limit exceeded',
'incident_reported': 'This incident has been reported.'
}
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.
Run full local validation (including real SSH end-to-end scenarios configured with Ansible):
just test-allRun only real SSH end-to-end checks:
just ssh-e2eman lshell(installed)man ./man/lshell.1(from repository)
Open an issue or pull request: https://github.com/ghantoos/lshell/issues