![PyPI - Version](https://img.shields.io/pypi/v/limited-shell?link=https%3A%2F%2Fpypi.org%2Fproject%2Flimited-shell%2F)
![PyPI - Downloads](https://img.shields.io/pypi/dm/limited-shell)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ghantoos/lshell/lshell-tests.yml?label=tests&link=https%3A%2F%2Fgithub.com%2Fghantoos%2Flshell%2Factions%2Fworkflows%2Flshell-tests.yml)

# lshell

`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/

## Installation

Install from PyPI:

```bash
pip install limited-shell
```

Prepare system resources (run as root once per host):

```bash
lshell setup-system --group lshell --log-dir /var/log/lshell --owner root --mode 2770
```

Build/install from source:

```bash
python3 -m pip install build --user
python3 -m build
pip install . --break-system-packages
```

Uninstall:

```bash
pip uninstall limited-shell
```

## Branch and release workflow

- `main`: stable release branch. Tag stable versions from this branch (for example `1.2.3`).
- `pre-release`: integration branch for tested features before release. Tag release candidates from this branch (for example `1.2.4rc1`).
- PyPI publishing uses one project ([limited-shell](https://pypi.org/project/limited-shell/)) and accepts both stable and `rc` versions.
- CI (`lshell-tests`) runs on pushes and PRs targeting both `main` and `pre-release`.

## Quick start

Run `lshell` with an explicit config:

```bash
lshell --config /path/to/lshell.conf
```

Default config location:

- Linux: `/etc/lshell.conf`
- *BSD: `/usr/{pkg,local}/etc/lshell.conf`

Set `lshell` as login shell:

```bash
chsh -s /usr/bin/lshell user_name
```

For automated setup (including `/etc/shells` registration + user shell assignment):

```bash
lshell setup-system --set-shell-user user_name --add-group-user user_name
```

Generate a hardened scoped include file for a specific group and user directly from CLI flags:

```bash
lshell harden-init \
  --profile sftp-only \
  --group sftpusers \
  --user alice \
  --output /etc/lshell.d/sftp-only.conf
```

If `--output` is omitted, `harden-init` writes to `/etc/lshell.d/<profile>.conf`.

## Policy diagnostics

Explain the effective policy and decision for a command:

```bash
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` (`lpath` alias)
- `policy-sudo` (`lsudo` alias)

Hide these built-ins if needed:

```ini
policy_commands : 0
```

## Hardened profile generator

`harden-init` ships secure-by-default templates to bootstrap restricted accounts quickly:

- `sftp-only`
- `rsync-backup`
- `deploy-minimal`
- `readonly-support`

Examples:

```bash
# 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 --stdout
```

## Configuration

Primary template: [`etc/lshell.conf`](etc/lshell.conf)

Key settings to review:

- `allowed` / `forbidden`
- `path`
- `sudo_commands`
- `overssh`, `scp`, `sftp`, `scp_upload`, `scp_download`
- `allowed_shell_escape`
- `allowed_file_extensions`
- `messages`
- `warning_counter`, `strict`
- `umask`
- runtime containment: `max_sessions_per_user`, `max_background_jobs`, `command_timeout`, `max_processes`

CLI overrides are supported, for example:

```bash
lshell --config /path/to/lshell.conf --log /var/log/lshell --umask 0077
```

### Runtime containment limits

Runtime limits are optional and disabled by default when set to `0`.

```ini
max_sessions_per_user : 2
max_background_jobs   : 4
command_timeout       : 30
max_processes         : 64
```

Operational notes:

- `max_sessions_per_user` is tracked with lock-protected session records; stale entries are cleaned automatically.
- `max_background_jobs` denies new `&` jobs once the configured active count is reached.
- `command_timeout` enforces a per-command wall-clock timeout (foreground and background commands).
- `max_processes` is applied via POSIX `RLIMIT_NPROC` on spawned command processes.
- Best practice: keep `command_timeout` enabled whenever `max_processes` is strict (especially `1`).

### Best practices

- Prefer an explicit `allowed` allow-list instead of `'all'`.
- Keep `allowed_shell_escape` short and audit every entry. Never add tools that execute arbitrary commands (for example `find`, `vim`, `xargs`).
- Use `allowed_file_extensions` when users are expected to work with a known set of file types.
- Keep `warning_counter` enabled (avoid `-1` unless you intentionally want warning-only behavior).
- Use `policy-show` during 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.

### 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:

1. User section
2. Group section
3. Default section
### Example configuration

For users `foo` and `bar` in UNIX group `users`:

```ini
# 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 END
```

For full option details, use:

- `man lshell`
- `man ./man/lshell.1`

## Testing

Run test services directly:

```bash
docker compose up ubuntu_tests debian_tests fedora_tests
```

Run full validation:

```bash
just test-all
```

Run only SSH end-to-end checks:

```bash
just test-ssh-e2e
```

### Justfile usage

List commands:

```bash
just --list
```

Run distro-specific tests:

```bash
just test-debian
just test-ubuntu
just test-fedora
```

Run sample configs interactively:

```bash
just sample-list
just sample-ubuntu 01_baseline_allowlist.conf
```

### Fuzzing parser/policy checks

Run Atheris fuzzing in Debian Docker (dependencies installed in-container):

```bash
just test-fuzz-security-parser 20000
```

Optional local run (if you want to fuzz outside Docker):

```bash
pip install -r requirements-fuzz.txt
python3 fuzz/fuzz_parser_policy.py -runs=20000
```

## Contributing

Open an issue or pull request: https://github.com/ghantoos/lshell/issues
