# SSH Tunnel Server & Admin API This project is a single Go binary that: - Runs an SSH tunnel server (password or public-key auth) - Tracks per-user limits, expiry and active connections - Persists users in PostgreSQL - Exposes a secure HTTP admin API - Serves a modern web panel (SPA) that calls the admin API The admin API is protected by a static bearer token (`ADMIN_TOKEN`) and is intended to be used only by the bundled web panel or trusted automation. --- ## Process overview 1. **SSH server** - Listens on `config.json` → `listen` (default `:2222`) - Supports: - password auth (per-user) - optional per-user public key auth - Enforces: - per-user `max_connections` - per-connection bandwidth limits (up / down) - optional expiry (`expires_at`) 2. **Admin HTTP server** - Listens on `ADMIN_HTTP_ADDR` (default `127.0.0.1:8080`) - Serves: - static panel (from `./admin/index.html`) - JSON API under `/api/*` - Requires `Authorization: Bearer ` on all `/api/*` endpoints. 3. **PostgreSQL** - Connection string from `PG_DSN` - Users are stored in table `ssh_users` - On startup: - If `PG_DSN` is set, users are loaded from DB and override `config.json` users. - On any create/update/delete via API: - DB is updated - In-memory users are reloaded - All active connections for that user are disconnected so new config applies immediately. --- ## Environment variables | Variable | Required | Default | Description | |-------------------|----------|------------------|-----------------------------------------------------------------------------| | `PG_DSN` | Optional | – | PostgreSQL DSN. If empty, runs with config-only users (no DB persistence). | | `ADMIN_TOKEN` | Required | – | Static bearer token for admin API & panel login. | | `ADMIN_HTTP_ADDR` | Optional | `127.0.0.1:8080` | Address for the admin HTTP server (panel + API). | Example: ```bash export PG_DSN='postgres://sshpanel:password@localhost:5432/sshpanel?sslmode=disable' export ADMIN_TOKEN='super-long-random-token' export ADMIN_HTTP_ADDR='0.0.0.0:8080' ./sshpanel -config config.json ``` --- ## Data model (simplified) The admin API works on a user model with these key fields: ```jsonc { "username": "test", "password": "secret", // write-only; never returned by API "max_connections": 1, // 0 = unlimited "expires_at": "2025-12-12T22:37:00Z", // RFC3339 or empty string for "never" "limit_mbps_up": 10, // per-connection upstream limit (Mbps) "limit_mbps_down": 10 // per-connection downstream limit (Mbps) } ``` Additional runtime field: - `active_conns` – current number of live SSH connections for that user (read-only, calculated in memory). --- ## Authentication All admin API endpoints require: ```http Authorization: Bearer ``` If the header is missing or incorrect: - `401 Unauthorized` if token doesn’t match - `403 Forbidden` if `ADMIN_TOKEN` is not configured on the server The static panel (`/`) does **not** expose data until a token is provided in the login screen; it then stores it in `localStorage` and sends it as `Authorization: Bearer ...` on each request. --- ## API routes Base URL: `http://` ### 1. `GET /api/users` List all users with current status. **Auth:** required (Bearer token) **Request:** no body **Response:** - `200 OK` with JSON array of users - `401/403` on auth failure Schema: ```jsonc [ { "username": "test", "active_conns": 1, "max_connections": 1, "expires_at": "2025-12-12T22:37:00Z", // or null if no expiry "limit_mbps_up": 10, "limit_mbps_down": 10 } ] ``` **Example (curl):** ```bash curl -H "Authorization: Bearer $ADMIN_TOKEN" \ http://127.0.0.1:8080/api/users ``` --- ### 2. `POST /api/users/create` Create a new user or update an existing user (upsert). **Auth:** required **Request body:** JSON, fields: ```jsonc { "username": "test", // required "password": "secret", // required for new user; optional on update "max_connections": 1, // required; 0 = unlimited "expires_at": "2025-12-12T22:37:00Z", // optional; "" = no expiry "limit_mbps_up": 10, // required; 0 = use default or unlimited "limit_mbps_down": 10 // required; 0 = use default or unlimited } ``` Behavior: - If `username` **does not exist** in DB: - `password` **must** be non-empty. - Creates a new user row in `ssh_users`. - If `username` **already exists**: - If `password` is **non-empty**, it replaces the old password. - If `password` is **missing or empty**, the existing password is kept. - Other fields are updated. Side effect: - After a successful upsert, **all active SSH connections for that user are closed**. New connections must use the updated credentials/limits. **Responses:** - `201 Created` on success (empty body) - `400 Bad Request` on invalid body or missing required fields (`username`, or `password` when creating a new user) - `401/403` on auth failure - `500 Internal Server Error` on DB issues **Example – create a new user:** ```bash curl -X POST http://127.0.0.1:8080/api/users/create \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "username": "alice", "password": "alicepass", "max_connections": 2, "expires_at": "2025-12-31T23:59:00Z", "limit_mbps_up": 10, "limit_mbps_down": 10 }' ``` **Example – update only limits (keep password):** ```bash curl -X POST http://127.0.0.1:8080/api/users/create \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "username": "alice", "max_connections": 3, "expires_at": "2025-12-31T23:59:00Z", "limit_mbps_up": 20, "limit_mbps_down": 20 }' # password omitted => password is kept as-is # any active sessions for alice are disconnected ``` **Example – change password and limits:** ```bash curl -X POST http://127.0.0.1:8080/api/users/create \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "username": "alice", "password": "newpass", "max_connections": 1, "expires_at": "", "limit_mbps_up": 5, "limit_mbps_down": 5 }' ``` --- ### 3. `DELETE /api/users/delete` Delete a user and disconnect all their active sessions. **Auth:** required **Request:** - Method: `DELETE` - Query parameter: `username` (required) Example URL: ```text /api/users/delete?username=alice ``` Behavior: - User row is deleted from `ssh_users`. - In-memory state is reloaded. - All active SSH connections for that user are closed. **Responses:** - `204 No Content` on success - `400 Bad Request` if `username` is missing - `401/403` on auth failure - `500 Internal Server Error` on DB issues **Example (curl):** ```bash curl -X DELETE "http://127.0.0.1:8080/api/users/delete?username=alice" \ -H "Authorization: Bearer $ADMIN_TOKEN" ``` --- ### 4. `GET /` (static panel) Serves the SPA admin panel (`admin/index.html`). - No auth at HTTP level, but: - The panel shows a **login screen** asking for `ADMIN_TOKEN`. - The token is stored in `localStorage` and used as `Authorization: Bearer ...` for all `/api/*` calls. - If the token is wrong, the UI shows an error and stays on the login view. You typically access it in a browser at: ```text http://127.0.0.1:8080/ ``` (or whatever you put in `ADMIN_HTTP_ADDR`). --- ## Notes and recommendations - Expose `ADMIN_HTTP_ADDR` publicly **only** if you are confident the `ADMIN_TOKEN` has sufficient entropy (long, random) and you trust the network. - For extra safety: - Bind `ADMIN_HTTP_ADDR` to `127.0.0.1:8080` and access through an SSH tunnel or reverse proxy with TLS. - Rotate `ADMIN_TOKEN` regularly; restart the binary after changing it. - Any change (create, update, delete) to a user via the API immediately disconnects their current SSH sessions, so clients must reconnect. ---