# 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 ` -c ""` 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 (` -c ""`). | | `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.