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
-
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)
- per-user
- Listens on
-
Admin HTTP server
- Listens on
ADMIN_HTTP_ADDR(default127.0.0.1:8080) - Serves:
- static panel (from
./admin/index.html) - JSON API under
/api/*
- static panel (from
- Requires
Authorization: Bearer <ADMIN_TOKEN>on all/api/*endpoints.
- Listens on
-
PostgreSQL
- Connection string from
PG_DSN - Users are stored in table
ssh_users - On startup:
- If
PG_DSNis set, users are loaded from DB and overrideconfig.jsonusers.
- If
- 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.
- Connection string from
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 Unauthorizedif token doesn’t match403 ForbiddenifADMIN_TOKENis 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 OKwith JSON array of users401/403on 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
usernamedoes not exist in DB:passwordmust be non-empty.- Creates a new user row in
ssh_users.
- If
usernamealready exists:- If
passwordis non-empty, it replaces the old password. - If
passwordis missing or empty, the existing password is kept. - Other fields are updated.
- If
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 Createdon success (empty body)400 Bad Requeston invalid body or missing required fields (username, orpasswordwhen creating a new user)401/403on auth failure500 Internal Server Erroron 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 Contenton success400 Bad Requestifusernameis missing401/403on auth failure500 Internal Server Erroron 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
localStorageand used asAuthorization: Bearer ...for all/api/*calls.
- The panel shows a login screen asking for
- 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_ADDRpublicly only if you are confident theADMIN_TOKENhas sufficient entropy (long, random) and you trust the network. - For extra safety:
- Bind
ADMIN_HTTP_ADDRto127.0.0.1:8080and access through an SSH tunnel or reverse proxy with TLS. - Rotate
ADMIN_TOKENregularly; restart the binary after changing it.
- Bind
- Any change (create, update, delete) to a user via the API immediately disconnects their current SSH sessions, so clients must reconnect.