2025-12-10 23:27:05 -03:00
Doc
2025-12-10 23:27:05 -03:00

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.jsonlisten (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:

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:

{
  "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:

Authorization: Bearer <ADMIN_TOKEN>

If the header is missing or incorrect:

  • 401 Unauthorized if token doesnt 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:

[
  {
    "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):

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:

{
  "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:

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):

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:

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:

/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):

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:

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.

Description
No description provided
Readme 26 KiB