Version 0.01
This commit is contained in:
252
README.md
Normal file
252
README.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Cascade
|
||||
|
||||
Cascade is a lightweight, terminal-based interactive menu that lets administrators expose a curated set of shell commands to users without giving them a full shell. It is written in Go with no external dependencies.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [How It Works](#how-it-works)
|
||||
- [Building and Installing](#building-and-installing)
|
||||
- [Configuration Files](#configuration-files)
|
||||
- [Application Config (`config.ini`)](#application-config-configini)
|
||||
- [Menu Config (`menu.ini`)](#menu-config-menuini)
|
||||
- [Configuration Resolution Order](#configuration-resolution-order)
|
||||
- [Menu File Format](#menu-file-format)
|
||||
- [Comments and Section Headers](#comments-and-section-headers)
|
||||
- [Dot-Separated Keys](#dot-separated-keys)
|
||||
- [Mixed Nodes (Branch + Leaf)](#mixed-nodes-branch--leaf)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Use Case: SSH User Isolation](#use-case-ssh-user-isolation)
|
||||
- [Security Notes](#security-notes)
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
When Cascade starts it:
|
||||
|
||||
1. Reads the **application config** to know which shell to use and where the menu file lives.
|
||||
2. Reads the **menu file** and builds an in-memory tree from dot-separated keys.
|
||||
3. Switches stdin to **raw mode** and presents the current menu level.
|
||||
4. The user navigates and selects an item using **arrow keys** or by **typing a number** — both methods work simultaneously.
|
||||
5. `0` / selecting the Back/Exit item goes one level up (or exits at the root).
|
||||
6. The selected command is executed by spawning `<shell> -c "<command>"` with stdin/stdout/stderr inherited from the current terminal, so fully interactive programs (shells, `less`, `psql`, etc.) work correctly.
|
||||
7. After the command exits, the menu is redrawn. The menu file is **reloaded on every redraw**, so changes take effect without restarting Cascade.
|
||||
|
||||
---
|
||||
|
||||
## Building and Installing
|
||||
|
||||
```bash
|
||||
# Build binary into ./build/cascade
|
||||
make build
|
||||
|
||||
# Build and run locally
|
||||
make run
|
||||
|
||||
# Install to /opt/cascade/ (copies binary + config files)
|
||||
sudo make install
|
||||
|
||||
# Remove /opt/cascade/
|
||||
sudo make delete
|
||||
|
||||
# Remove ./build/
|
||||
make clean
|
||||
```
|
||||
|
||||
`make install` backs up existing `/opt/cascade/config.ini` and `/opt/cascade/menu.ini` before overwriting them (timestamped `.old.*` copies).
|
||||
|
||||
---
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Application Config (`config.ini`)
|
||||
|
||||
A simple `key=value` file. Blank lines and lines starting with `#` are ignored.
|
||||
|
||||
| Key | Default | Description |
|
||||
|---------|-------------|-------------|
|
||||
| `shell` | `/bin/sh` | The shell used to execute menu commands (`<shell> -c "<cmd>"`). |
|
||||
| `menu` | *(none)* | Explicit path to the menu file. Overridden by `CASCADE_MENU`. |
|
||||
|
||||
**Example (`config.ini.example`):**
|
||||
```ini
|
||||
shell=/bin/bash
|
||||
menu=/etc/cascade/menu.ini
|
||||
```
|
||||
|
||||
### Menu Config (`menu.ini`)
|
||||
|
||||
Defines the tree of commands shown to the user. See [Menu File Format](#menu-file-format) below.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Resolution Order
|
||||
|
||||
### Application config
|
||||
|
||||
Cascade tries each candidate in order and stops at the first one it can read:
|
||||
|
||||
1. Path in `$CASCADE_CONF` environment variable.
|
||||
2. `$HOME/.cascade/config.ini`
|
||||
3. `/opt/cascade/config.ini`
|
||||
4. Built-in defaults (`shell=/bin/sh`, no menu path).
|
||||
|
||||
### Menu file
|
||||
|
||||
1. Path in `$CASCADE_MENU` environment variable.
|
||||
2. `menu=` value from the application config.
|
||||
3. `$HOME/.cascade/menu.ini` (only if the file exists).
|
||||
4. `/opt/cascade/menu.ini` (only if the file exists).
|
||||
5. Fatal error — Cascade exits with a descriptive message.
|
||||
|
||||
---
|
||||
|
||||
## Menu File Format
|
||||
|
||||
### Comments and Section Headers
|
||||
|
||||
Lines starting with `#` are comments and are ignored entirely.
|
||||
|
||||
```ini
|
||||
# This is a top-level comment
|
||||
## This is a sub-section comment
|
||||
Key=command
|
||||
```
|
||||
|
||||
### Dot-Separated Keys
|
||||
|
||||
Each non-comment, non-blank line must be `Key=command`.
|
||||
The **key** uses dots (`.`) as level separators to express a hierarchy:
|
||||
|
||||
```
|
||||
Level1.Level2.Level3=shell command here
|
||||
```
|
||||
|
||||
This creates a three-level menu: entering `Level1` shows `Level2`; entering `Level2` shows `Level3`; selecting `Level3` runs the command.
|
||||
|
||||
**Example:**
|
||||
```ini
|
||||
Nginx.Restart=docker restart portal-nginx
|
||||
Nginx.Logs.Access=docker exec -it portal-nginx less /var/log/nginx/access.log
|
||||
Nginx.Logs.Error=docker exec -it portal-nginx less /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
Results in this navigation tree:
|
||||
|
||||
```
|
||||
[ Cascade v0.01 ]
|
||||
|
||||
Select Option:
|
||||
|
||||
1) Nginx <-
|
||||
|
||||
0) Exit
|
||||
|
||||
Enter number:
|
||||
```
|
||||
|
||||
After pressing Enter (or ↓ then Enter):
|
||||
|
||||
```
|
||||
[ Nginx ]
|
||||
|
||||
Select Option:
|
||||
|
||||
1) Logs
|
||||
2) Restart <-
|
||||
|
||||
0) Back
|
||||
|
||||
Enter number:
|
||||
```
|
||||
|
||||
```
|
||||
[ Nginx > Logs ]
|
||||
|
||||
Select Option:
|
||||
|
||||
1) Access
|
||||
2) Error
|
||||
|
||||
0) Back <-
|
||||
|
||||
Enter number:
|
||||
```
|
||||
|
||||
Keys at the same level are displayed **sorted alphabetically**.
|
||||
|
||||
### Navigation Controls
|
||||
|
||||
| Input | Action |
|
||||
|-------|--------|
|
||||
| ↑ / ↓ | Move the cursor one item up or down. Wraps around at both ends. Moving clears any typed number. |
|
||||
| Digit keys (`0`–`9`) | Type a number directly. The cursor jumps to the matching item as you type. |
|
||||
| Backspace | Delete the last typed digit. |
|
||||
| Enter | Confirm the selection — uses the typed number if one is being entered, otherwise uses the cursor position. |
|
||||
|
||||
Both input methods (arrows and number typing) can be mixed freely at any time.
|
||||
|
||||
### Mixed Nodes (Branch + Leaf)
|
||||
|
||||
A node can have both a direct command **and** child items. When this happens, an extra option `(run directly)` appears at the top of the submenu, allowing the user to execute the node's own command without navigating further.
|
||||
|
||||
```ini
|
||||
Hrm.Shell=docker exec -it portal-hrm-php sh
|
||||
Hrm.Service.PhpFpm.Restart=docker restart portal-hrm-php
|
||||
```
|
||||
|
||||
`Hrm` has its own command (`Shell`) and also has children (`Service`). Selecting `Hrm` opens a submenu that includes `(run directly)` as option 1.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|-----------------|-------------|
|
||||
| `CASCADE_CONF` | Override the path to the application config file. |
|
||||
| `CASCADE_MENU` | Override the path to the menu config file. |
|
||||
|
||||
These variables take highest priority in their respective resolution chains.
|
||||
|
||||
---
|
||||
|
||||
## Use Case: SSH User Isolation
|
||||
|
||||
Cascade is designed to be used as a restricted `ForceCommand` in OpenSSH, so that certain SSH users see only the allowed menu instead of a real shell.
|
||||
|
||||
**`/etc/ssh/sshd_config` (or a drop-in file):**
|
||||
|
||||
```
|
||||
Match User dev
|
||||
SetEnv CASCADE_MENU=/opt/cascade/dev.ini
|
||||
SetEnv CASCADE_CONF=/opt/cascade/config.ini
|
||||
ForceCommand /usr/bin/cascade
|
||||
```
|
||||
|
||||
What this does:
|
||||
|
||||
- `ForceCommand /usr/bin/cascade` — every time the user `dev` connects, Cascade is launched instead of their login shell, regardless of what command they pass to `ssh`.
|
||||
- `SetEnv CASCADE_CONF` — points to the application config (defines the shell used to run commands).
|
||||
- `SetEnv CASCADE_MENU` — points to a menu file specific to this user, so different users can have different sets of allowed commands.
|
||||
|
||||
Different users (or `Match` blocks) can each have their own `CASCADE_MENU` pointing to different `.ini` files, giving fine-grained control over which commands each user can run.
|
||||
|
||||
**Deployment checklist:**
|
||||
|
||||
1. Copy the `cascade` binary to `/usr/bin/cascade` (or `/opt/cascade/cascade`).
|
||||
2. Create `/opt/cascade/config.ini` with at minimum `shell=/bin/bash`.
|
||||
3. Create per-user (or shared) menu files, e.g. `/opt/cascade/dev.ini`.
|
||||
4. Add the `Match` block(s) to `sshd_config` and reload sshd (`systemctl reload sshd`).
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Raw terminal mode** — stdin is switched to raw mode only while waiting for a key press and is restored before running any command, so executed programs see a normal terminal.
|
||||
- **Input validation** — typed number input is limited to 16 characters and must be all digits. Only arrow keys, digits, Backspace, and Enter are acted upon; all other key sequences are ignored.
|
||||
- **Line length** — config lines longer than 4096 bytes are rejected by the scanner.
|
||||
- **No shell access** — commands are defined entirely by the administrator in the menu file. The user can only pick a number; they cannot inject arbitrary commands.
|
||||
- **ForceCommand** — when used with SSH `ForceCommand`, the user cannot bypass Cascade by passing a command to `ssh` directly.
|
||||
- **Menu hot-reload** — the menu file is re-read on every screen redraw. Ensure the file is writable only by root/admin to prevent privilege escalation.
|
||||
Reference in New Issue
Block a user