SSH App Hub is a small local control panel for web apps that already run on your SSH hosts.
It finds tools such as JupyterLab, TensorBoard, MLflow, Streamlit, Grafana, Prometheus and code-server on a remote
machine, then opens them through ordinary ssh -N -L tunnels bound to your loopback interface. The goal is simple:
stop remembering remote ports, stop copy-pasting half-redacted URLs, and keep SSH in charge of authentication.
- Scans an SSH alias with read-only remote commands:
ps,ss,lsof, and Jupyter's server list. - Shows discovered apps in a local web UI and a CLI.
- Starts and reuses OpenSSH local-forward tunnels for selected apps.
- Supports manifest files for apps that should be named, pinned, or treated as defaults.
- Stores hosts, discovered apps, and tunnel events in a local SQLite database.
- Keeps API access local and token-protected by default.
This is not a hosted gateway, a reverse proxy, or a new SSH client. It is a local helper around the OpenSSH you already use.
- Python 3.12+
uv- OpenSSH client available as
ssh - SSH aliases configured in
~/.ssh/config
Remote discovery works best when the target host has the usual process and socket tools available. Missing tools do not abort the whole scan; SSH App Hub returns warnings and keeps whatever it could confidently identify.
uv sync --extra dev
uv run ssh-apphub init
uv run ssh-apphub check-ssh gpu-box
uv run ssh-apphub serve --no-browserOpen:
http://127.0.0.1:8765/
If the UI asks for an API token:
uv run ssh-apphub token --showScan a host and open an app:
uv run ssh-apphub scan gpu-box
uv run ssh-apphub open gpu-box :8888scan, open, hosts, and tunnels talk to the running local server. If the server is not running, the CLI exits with
SERVER_NOT_RUNNING.
- Add an SSH alias to
~/.ssh/config, for examplegpu-box. - Start your remote tool on that host: JupyterLab on
127.0.0.1:8888, TensorBoard on6006, and so on. - Run
ssh-apphub serve. - Use the web UI or
ssh-apphub scan gpu-boxto discover apps. - Click
Open, or runssh-apphub open gpu-box :8888.
SSH App Hub allocates a local port, starts an OpenSSH tunnel, and opens the resulting local URL.
ssh-apphub init
ssh-apphub serve --host 127.0.0.1 --port 8765 --no-browser
ssh-apphub check-ssh gpu-box
ssh-apphub scan gpu-box
ssh-apphub open gpu-box :8888
ssh-apphub hosts list
ssh-apphub hosts add gpu-box --name "GPU box"
ssh-apphub hosts remove <host_id>
ssh-apphub hosts import --dry-run
ssh-apphub tunnels list
ssh-apphub tunnels stop <tunnel_id>Install it as a tool from a checkout:
uv tool install .Automatic discovery is useful, but explicit manifests are better for tools you use every day. Put YAML files in:
~/.config/ssh-apphub/manifests/*.yaml
Example:
version: 1
apps:
- id: "jupyter-lab-main"
host: "gpu-box"
type: "jupyter"
name: "JupyterLab main"
is_default: true
connect:
scheme: "http"
remote_host: "127.0.0.1"
remote_port: 8888
path: "/lab"
health:
path: "/api/status"
expected_statuses: [200, 302, 401, 403]
timeout_seconds: 5
tags: ["ml", "gpu"]See docs/manifest-format.md for the full schema and validation rules.
ssh-apphub init creates private config and data directories using XDG paths:
~/.config/ssh-apphub/config.yaml
~/.config/ssh-apphub/manifests/
~/.local/share/ssh-apphub/token
~/.local/share/ssh-apphub/apphub.sqlite3
Useful environment overrides:
SSH_APPHUB_SERVER__HOST=127.0.0.1
SSH_APPHUB_SERVER__PORT=8765
SSH_APPHUB_SERVER__OPEN_BROWSER=false
SSH_APPHUB_RUNTIME__MAX_PARALLEL_SCANS=3
SSH_APPHUB_NO_BROWSER=trueFor isolated development or tests:
SSH_APPHUB_CONFIG_DIR=/tmp/ssh-apphub-config \
SSH_APPHUB_DATA_DIR=/tmp/ssh-apphub-data \
uv run ssh-apphub serve --no-browserThe short version:
- The HTTP server binds to
127.0.0.1by default. - Every
/api/v1/*endpoint except/api/v1/healthzrequiresX-AppHub-Token. - The token is stored separately from
config.yaml. - Private keys are never read by the app; OpenSSH handles keys, agents,
ProxyJump, and host verification. - API responses and logs avoid returning discovered secrets.
- Host-header checks reduce DNS rebinding risk for local API calls.
The default host key mode is accept-new, which is convenient for first-time use. If you want stricter behavior, edit
config.yaml:
security:
host_key_checking: strictRead docs/threat-model.md before exposing anything beyond loopback.
SERVER_NOT_RUNNING
: Start ssh-apphub serve first. Most CLI commands are local API clients.
SSH_CONFIG_INVALID
: Run ssh -G -- <alias> and fix the SSH config entry.
SSH_AUTH_FAILED
: Confirm key auth and ssh-agent outside SSH App Hub.
REMOTE_COMMAND_TIMEOUT
: Increase the scan timeout or check whether the remote shell is slow.
LOCAL_PORT_UNAVAILABLE
: Adjust runtime.local_port_min and runtime.local_port_max.
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run mypy srcThe test suite covers config loading, parsing, discovery, API contracts, tunnel lifecycle behavior, redaction, database repositories, and the static web UI.
This project was developed with AI assistance and is maintained by the author.
