lshell - Limited Shell
Project description
lshell
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.
Installation
Install from PyPI
pip install limited-shell
Build and install from source
python3 -m pip install build --user
python3 -m build
pip install . --break-system-packages
Uninstall
pip uninstall limited-shell
Usage
Run lshell
lshell --config /path/to/lshell.conf
Admin diagnostics (lshell policy-show)
Explain 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
Interactive policy builtins
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 : 0
Default 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 0077
Use lshell in scripts
Use the lshell shebang and keep the .lsh extension:
#!/usr/bin/lshell
echo "test"
System setup
Add user to lshell group (for log access)
usermod -aG lshell username
Set lshell as login shell
Linux:
chsh -s /usr/bin/lshell user_name
*BSD:
chsh -s /usr/{pkg,local}/bin/lshell user_name
Make sure lshell is present in /etc/shells.
Configuration
The main template is etc/lshell.conf. Full reference is available in the man page.
Best practices
- 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.
Section model and precedence
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: exact vs generic commands
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 and strict
warning_counter is decremented on forbidden command/path/character attempts.
When strict = 1, unknown syntax/commands also decrement warning_counter.
strict = 1 is typically preferred for higher-assurance restricted environments.
messages
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.'
}
Security-related settings
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.
Prompt accessibility
- Keep the default prompt text-based and readable in monochrome terminals.
- If you use ANSI colors in
promptor$LPS1, avoid color-only meaning (for example, include separators likeuser@host:path). - Verify contrast and readability over SSH clients commonly used in your environment.
umask
Set a persistent session umask in config:
umask : 0002
0002-> 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_dir
Example configuration
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
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 END
Testing with Docker Compose
Run tests on multiple distributions in parallel:
docker compose up ubuntu_tests debian_tests fedora_tests
This 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-all
Run only real SSH end-to-end checks:
just ssh-e2e
Documentation
man lshell(installed)man ./man/lshell.1(from repository)
Contributing
Open an issue or pull request: https://github.com/ghantoos/lshell/issues
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file limited_shell-0.11.0.tar.gz.
File metadata
- Download URL: limited_shell-0.11.0.tar.gz
- Upload date:
- Size: 119.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb326834a0920f3296a5ac59881aa17d2b4015a9b18dc5aa5f94d30ff8cb90d5
|
|
| MD5 |
2c0b840b69be952d867c9d95720a203f
|
|
| BLAKE2b-256 |
33fd2d38f2a63daeec36afa66d73d4223b2f73ef4e49216cda3d7d5217703736
|
Provenance
The following attestation bundles were made for limited_shell-0.11.0.tar.gz:
Publisher:
pypi-publish.yml on ghantoos/lshell
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
limited_shell-0.11.0.tar.gz -
Subject digest:
cb326834a0920f3296a5ac59881aa17d2b4015a9b18dc5aa5f94d30ff8cb90d5 - Sigstore transparency entry: 1078006131
- Sigstore integration time:
-
Permalink:
ghantoos/lshell@6a5c402882ce5e2e2cf3ef3c123f6d2021996c54 -
Branch / Tag:
refs/tags/0.11.0 - Owner: https://github.com/ghantoos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@6a5c402882ce5e2e2cf3ef3c123f6d2021996c54 -
Trigger Event:
push
-
Statement type:
File details
Details for the file limited_shell-0.11.0-py3-none-any.whl.
File metadata
- Download URL: limited_shell-0.11.0-py3-none-any.whl
- Upload date:
- Size: 97.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d286bac4383d44bd66e61fbdba99315b8adddbdfdfc00a22bc00911eeb99976
|
|
| MD5 |
2454202009d6d3539ec5c121462c5871
|
|
| BLAKE2b-256 |
407874ce599e587fa193c38de9196958f1fcb95bbc0114c0ab6dd7c5a1f9ab28
|
Provenance
The following attestation bundles were made for limited_shell-0.11.0-py3-none-any.whl:
Publisher:
pypi-publish.yml on ghantoos/lshell
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
limited_shell-0.11.0-py3-none-any.whl -
Subject digest:
0d286bac4383d44bd66e61fbdba99315b8adddbdfdfc00a22bc00911eeb99976 - Sigstore transparency entry: 1078006134
- Sigstore integration time:
-
Permalink:
ghantoos/lshell@6a5c402882ce5e2e2cf3ef3c123f6d2021996c54 -
Branch / Tag:
refs/tags/0.11.0 - Owner: https://github.com/ghantoos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@6a5c402882ce5e2e2cf3ef3c123f6d2021996c54 -
Trigger Event:
push
-
Statement type: