307 lines
8.2 KiB
Markdown
307 lines
8.2 KiB
Markdown
# 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 <ADMIN_TOKEN>` 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 <ADMIN_TOKEN>
|
||
```
|
||
|
||
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://<ADMIN_HTTP_ADDR>`
|
||
|
||
### 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.
|
||
|
||
---
|