Changelog¶
v0.5.3 (2026-04-16) — UI hotfix: server dashboard¶
Bug fixes¶
- Site Manager sidebar no longer hides servers behind the status bar. With enough registered servers, the left-hand list expanded past the viewport because the sidebar flex column was missing
min-height:0, so the inner.fz-serverlistnever clipped. The last cards scrolled off-screen underneath the blue status bar. Root cause was Alpine'sx-showstripping the inlinedisplay:flexfrom the dashboard row when it toggled visibility. Fixed by introducing a.fz-flex-rowCSS class (mirror of the existing.fz-flex-col) so the flex layout survives thex-showtoggle, plus a propermin-height:0/overflow:hiddenchain all the way down to the scrollable list. - Global status bar no longer leaks file-browser state onto the dashboard. When an SFTP tab was open in the background and the user switched to Site Manager, the bottom status bar kept showing
75 items (38 directories, 37 files)//home/wdna. The dashboard view now shows its own summary:N servers · X online · Y offline · Z uncheckedon the left and the active filter (group: RANorsearch: "foo") on the right. auth/models.pycommitted. TheUser.totp_secret/User.totp_enabledcolumns and theApiKeymodel had been in use byauth/routes.pyandauth/service.pysince the 2FA / API-keys features shipped, butauth/models.pyhad never been rolled into a commit. A freshgit cloneofmainfailed to start withImportError: cannot import name 'ApiKey' from 'webgate.auth.models'. Docker Hub and fly.io kept working because those builds happened from the dirty working tree. Committing the file resolves the drift.
Dashboard redesign (triggered by the fix above)¶
While fixing the overflow bug, the Site Manager's server list was redesigned to scale to fleets of dozens of servers without becoming a wall of buttons.
- Compact cards with hover-reveal secondary actions. Each server row now shows
SSHandSFTPby default;Split,Test,Edit, andDelonly appear when the row is hovered or selected. This drops the per-row visual weight from ~100 px with 6 visible buttons to ~55 px with 2 primary buttons, while keeping every action one hover away. - Sticky group headers in the sidebar. When the "All Groups" filter is active and the fleet spans multiple groups, the list gets per-group headers (
CORE 12,RAN 20,STAGING 4…) that stay pinned while you scroll. Per-card group pills are automatically hidden when the header is showing, since the grouping context is already visible. - Compact / comfortable density toggle (
▦/▤button in the sidebar header) persisted inlocalStorage. Compact mode hides host:port and all action buttons until hover — roughly doubles the number of servers visible per screen. - Server count next to the
Serversheading (Servers (43)) updates with the current filter. - Status bar filter context. When a group filter or search is active, the right-hand status cell shows
group: <name>orsearch: "<query>"so the current view state is always visible.
Tests¶
test_healthwas stale since v0.5.0 introduced HA (instance_id+monitor_rolein the payload). Updated to assert the current shape instead of the original two-key response.
v0.5.2 (2026-04-16) — Hardening¶
Defense-in-depth follow-ups to v0.5.1's security hotfix — no active exploit fixed here, but the attack surface is reduced.
Hardening¶
- Demo-mode write allowlist is now an exact (method, path) list instead of a
startswith("/api/terminal/share/")prefix. Previously anyPOST /api/terminal/share/<anything-here>slipped past the middleware; now only the two real share routes (POSTandDELETE /api/terminal/share/{session_id}) plus login and totp/verify are allowed. Any future endpoint accidentally added under that prefix stays blocked unless its route is explicitly added. user_login_failedwebhook payload is now sanitized. An unauthenticated attacker can hit/api/auth/loginwith anyusernamestring, which used to be dispatched verbatim to admin-configured webhook receivers (Slack, Discord, in-house). The dispatcher now strips non-printable characters (terminal escapes, NULLs) and truncates to 64 chars before emitting, so receivers that render the payload directly can't be targeted with control codes or HTML.
Docs¶
CLAUDE.md: corrected a stale line that described the access model as "each user sees only their own servers". The actual model is team-access-by-group: admins assign a server to agroupand any user with that group in theirallowed_groupscan reach it. README already documented this correctly in the "Access model" section.
v0.5.1 (2026-04-16) — Security hotfix¶
Security (please upgrade)¶
Three authentication gaps fixed after a focused security review.
must_change_passwordis now enforced server-side. Previously the flag was only surfaced by the UI; a valid JWT from aadmin/adminfirst-run login let attackers hit every admin endpoint (/api/auth/users,/api/servers, audit log, ...) without ever changing the password.get_current_usernow returns403 Password change requiredfor any path other than/api/auth/meand/api/auth/change-passwordwhile the flag isTrue. (Credit: user report on the public demo.)- Pre-2FA "temp token" is now truly short-lived and scoped. The
create_access_token({"pending_2fa": True, "exp_minutes": 2})call ignoredexp_minutes— the token got the full 24-hour session TTL, and no endpoint checked thepending_2faclaim, so the "temp" token bypassed 2FA completely. Fixed by: addingexpires_minutesparameter tocreate_access_token, setting it to 2, and rejecting any token withpending_2fa=Trueon every endpoint except/api/auth/loginitself. - API keys cannot bypass forced password change. An account with
must_change_password=Truecan no longer create or use an API key until the password has been rotated. Prevents workaround paths when an admin issues a temporary password. - Recording replay page no longer leaks the session token via Referer.
GET /api/recordings/{id}/playnow sendsReferrer-Policy: no-referrerandCache-Control: private, no-store, and all third-party assets on the page (asciinema-player CDN) are fetched withreferrerpolicy="no-referrer".
Docs¶
- README: screenshot sub-sections renamed to functional titles (jump host / snippets / shared terminal / session recording / demo mode) instead of release-pack labels, so the README always describes what the current version ships.
v0.5.0 (2026-04-15)¶
Features¶
- Multi-instance HA deployment -- run N webgate workers behind a load balancer, sharing a PostgreSQL database. Only one worker at a time performs server-connectivity probes (leader election via a singleton lease row); all workers serve REST + WS traffic normally.
compose.ha.ymlreference deployment: 2 webgate replicas + Postgres + nginx LB withip_hashsticky sessions. Verified end-to-end including automatic failover./api/healthnow reportsinstance_id(per-worker UUID) andmonitor_role(leader/follower) so LB health checks and observability can tell replicas apart.
Configuration¶
| Variable | Default | Description |
|---|---|---|
WEBGATE_INSTANCE_ID |
auto (UUID) | Stable identifier for this worker |
WEBGATE_DISABLE_MONITOR |
false |
Skip leader election entirely (pure follower worker, useful if a separate process owns the monitor) |
Details¶
- New
monitor_leasesingleton table (auto-created at startup) holds the current leader's instance id and expiry. 90 s TTL with 30 s heartbeat. - On leader loss / expiry, any other worker picks up the probe loop within ~1 check cycle (≤ 90 s).
- Dialect-agnostic (works on SQLite for single-instance dev, PostgreSQL for real HA).
Known limitation¶
- Shared terminal sessions (
/api/ws/terminal/join/{token}) still require owner and joiner to land on the same worker. Sticky-session routing handles same-browser joins; cross-worker cross-engineer sharing needs Redis pub/sub (planned in v0.5.x).
Verified¶
compose.ha.yml stack (2 replicas + Postgres + nginx):
- Both replicas show
{"instance_id":"…","monitor_role":"follower"|"leader"} - Exactly one replica holds the lease row in Postgres
- Servers created on replica A immediately visible from replica B (shared DB)
- Killed the leader → follower promoted automatically, LB kept serving
v0.4.2 (2026-04-15)¶
Features¶
- LDAP / Active Directory authentication -- enable with
WEBGATE_LDAP_ENABLED=trueand login flow falls back to LDAP after the local user table. On a successful LDAP bind the user is auto-provisioned (or refreshed) in the local DB, withallowed_groupsderived from LDAP group memberships and admin status from a configurable list of admin groups.
Configuration¶
| Env var | Description |
|---|---|
WEBGATE_LDAP_ENABLED |
true to enable LDAP login |
WEBGATE_LDAP_URL |
ldap://host:389 or ldaps://host:636 |
WEBGATE_LDAP_BIND_DN |
service account DN, e.g. cn=admin,dc=example,dc=com |
WEBGATE_LDAP_BIND_PASSWORD |
service account password |
WEBGATE_LDAP_USER_BASE |
e.g. ou=people,dc=example,dc=com |
WEBGATE_LDAP_USER_FILTER |
default (uid={username}) (AD: (sAMAccountName={username})) |
WEBGATE_LDAP_GROUP_BASE |
e.g. ou=groups,dc=example,dc=com (empty = no group lookup) |
WEBGATE_LDAP_GROUP_FILTER |
default (member={dn}) (AD nested: (member:1.2.840.113556.1.4.1941:={dn})) |
WEBGATE_LDAP_GROUP_MAP |
JSON {"ldap-cn":"webgate-group"} |
WEBGATE_LDAP_ADMIN_GROUPS |
JSON list of LDAP CNs that grant admin |
Details¶
- Search-then-bind flow: bind as service account, search by username, re-bind as the user with their password
- LDAP filter values are properly escaped (RFC 4515)
- All
ldap3calls run inasyncio.to_threadto avoid blocking the event loop - Local accounts (admin, API keys, 2FA) keep working as before -- LDAP is only consulted after a local-credential miss
- Re-login refreshes admin status and group mapping from LDAP every time
Verified¶
End-to-end against osixia/openldap with an alice user in groups devs and admins:
alice / alicepass→ 200, JWT issued,/api/auth/mereturnsis_admin=true,allowed_groups=["all","production"](mapped from LDAP CNs)alice / WRONG→ 401 Invalid credentialsadmin / admin(local fallback) → still works
v0.4.1 (2026-04-15)¶
Features¶
- SSH session recording -- when
WEBGATE_RECORD_SESSIONS=true, every SSH terminal session is captured to an asciinema cast v2 file underWEBGATE_RECORDINGS_DIR(default./recordings). Both owner-only and shared sessions are recorded. A newrecordingstable tracks file path, server, user, start/end, duration and size. - Built-in web replay --
📹 Recordingsbutton (top toolbar) opens a list with ▶ Play / DL / Del actions. Play opens an asciinema-player tab that streams the cast directly from the API. Non-admins see only their own recordings; admins see everyone's. - Compliance-grade audit trail -- the recording captures the full PTY output that every participant saw, including pasted commands and environment, in a portable, replayable, vendor-independent format (you can also
asciinema play file.castlocally).
Details¶
- New
WEBGATE_RECORD_SESSIONSandWEBGATE_RECORDINGS_DIRsettings (both off /./recordingsby default) - New
Recordingmodel + REST CRUD at/api/recordings - New
webgate.recordings.recorder.CastRecorderwrites asciinema cast v2 (JSON Lines) line-buffered, very low overhead - Hook in
SharedSession.broadcast-- recorder receives the same byte stream as every WS client - Player endpoint accepts a
?token=query param (Authorization header isn't available when opening a tab)
Verified¶
End-to-end against a real container with WEBGATE_RECORD_SESSIONS=true:
- Open SSH, run
echo HELLO_RECORDED+uname -n, disconnect - DB row populated:
started_at,ended_at,duration_s=3.0,size_bytes=1142 - File on disk: valid asciinema v2 cast, captures welcome banner + commands + responses
- Open
/api/recordings/{id}/play?token=...in browser → asciinema-player loads, hit Play → terminal replays the captured commands
v0.4.0 (2026-04-15)¶
Features¶
- Shared terminal sessions -- the killer feature. The owner of an active SSH terminal can click 🔗 Share to mint a one-time URL; anyone they send it to can join the same live session. Output is broadcast to every participant, and any RW participant can type. Useful for pair debugging in production, onboarding juniors, or remote support.
- Backend: new
SharedSessionregistry holds oneasyncsshprocess per session and broadcasts its PTY output to N WebSockets._client_input_loopmultiplexes input from any RW client into the samestdin. - Endpoints:
POST /api/terminal/share/{session_id}mints/returns the token,DELETErevokes,WS /api/ws/terminal/join/{token}?mode=rw|roattaches a joiner. - Frontend: 🔗 Share button copies the URL to clipboard; toolbar shows
👥 joined (rw|ro)for participants. Pasting?join=<token>into the URL after login auto-attaches. - Demo middleware whitelists
/api/terminal/share/*so the public demo can showcase the feature.
Verified¶
End-to-end with two browser sessions against the local container:
- Owner opens SSH to
bastion, clicks Share → token minted, URL copied - Joiner navigates to
?join=<token>→ "Joined session of demo on bastion (rw)" - Joiner types
echo HELLO_FROM_JOINER→ output appears in BOTH terminals - Owner types
uname -n→ output appears in BOTH terminals
v0.3.3 (2026-04-15)¶
Fixed¶
- Snippets toolbar was empty after a fresh login until the page was reloaded.
loadSnippets()was only called frominit()(which only runs when a token is already in localStorage). Now also called after a successful login and after a forced password change.
Docs¶
- Added end-to-end UI screenshots for v0.3.x features: jump host, snippets toolbar, snippet execution, SFTP browse via jump, webhooks management modal with delivery telemetry, server form with Jump Via dropdown.
- Browser-tested every UI flow with
playwright-cliagainst both the live Fly.io demo and a local container.
v0.3.2 (2026-04-15)¶
Features¶
- Webhook notifications -- admin can register HTTPS endpoints that receive a JSON POST when significant events fire. Supported events:
user_login,user_login_failed,ssh_connect,sftp_upload,sftp_delete,server_added,server_deleted. Each webhook can subscribe to all events (*) or a specific subset. - HMAC-SHA256 signing -- optional shared secret per webhook; the dispatcher signs the body and sends it as
X-Webgate-Signature: sha256=<hex>so the receiver can verify authenticity. - Test button + delivery telemetry -- one-click test fire from the admin UI; each webhook row shows the last HTTP status and timestamp.
Details¶
- New
Webhookmodel + REST CRUD at/api/webhooks(admin-only) - New
webgate.webhooks.dispatcher.fire(event, data)-- fire-and-forget, schedules HTTP delivery viahttpx, never blocks the caller - Frontend: "Webhooks" button in the top toolbar (admin only) with full management modal
- New base dependency:
httpx>=0.28.0(used by the dispatcher)
Verified end-to-end against a real HTTP echo receiver: user_login, server_added and a manual test all delivered with HTTP 200 and signature header.
v0.3.1 (2026-04-15)¶
Fixed¶
- PostgreSQL actually works in Docker now. v0.3.0 advertised PostgreSQL support but two bugs prevented it from working:
asyncpgwas an optional extra (webgate[postgres]), so the official Docker image (andpip install webgate) didn't have the driver. Now bundled by default.- Lightweight migrations ran inside the same transaction as
create_all. PostgreSQL aborts the entire transaction on any error (even when caught), so the first "column already exists" error rolled back table creation on subsequent runs. Each migration now uses its own transaction.
Verified end-to-end against a real postgres:16-alpine container: tables created, login works, server creation and persistence across restarts confirmed.
v0.3.0 (2026-04-15)¶
Features¶
- SSH jump host / bastion -- per-server
jump_via_idfield. Webgate opens a tunneled SSH connection through the bastion usingasyncssh'stunnel=parameter. Works for both the terminal WebSocket and the SFTP browser. Solves the common "internal servers reachable only through one public bastion" scenario. - SSH command snippets -- per-user library of named commands. Click a snippet button in the terminal toolbar to send the command (with Enter) to the active session. Right-click to delete,
+to create. - PostgreSQL support -- install with
pip install 'webgate[postgres]'and setWEBGATE_DB_URL=postgresql+asyncpg://.... SQLite remains the default. Lightweight migrations are now dialect-aware.
Details¶
- New
Server.jump_via_id(FK toservers.id, nullable) +resolve_jump_creds()helper - New
Snippetmodel + REST CRUD at/api/snippets - Frontend: dropdown to pick a jump host in the Add/Edit Server modal;
↺badge in the server card when a jump is configured - Frontend: snippet toolbar in the terminal tab (hidden when there are no snippets)
- Demo seed pre-populates
bastion+internal-app(jump-host pair) and 4 example snippets
v0.2.2 (2026-04-15)¶
Features¶
- Demo mode (
WEBGATE_DEMO_MODE=true) -- read-only public demo deployments. Blocks every write request on/api/*(except login), disables the WebSocket quick-connect endpoint, seeds ademo/demouser with a sample server, and shows a top banner in the UI. Dockerfile.demo-- single-container image bundling webgate + a sandboxedsshdtarget via supervisord. Ready for free hosting tiers.fly.toml-- Fly.io configuration for one-command demo deployments (flyctl deploy).
Details¶
- New public endpoint
GET /api/configexposes{"demo_mode": bool}for the frontend to render the banner before login WEBGATE_DEMO_MODE=trueadds an HTTP middleware that returns403for anyPOST/PUT/PATCH/DELETEon/api/*(allowlist:/api/auth/login,/api/auth/totp/verify)- Demo seed (
webgate.demo) is idempotent and only runs when the flag is on - Hourly state reset for the public demo can be done with a cron pinging the container restart, so DB returns to seed state
v0.2.1 (2026-04-15)¶
Features¶
- Reverse proxy sub-path support -- webgate can now be served behind a reverse proxy at a URL prefix (e.g.
https://example.com/webgate/). Previously the frontend used absolute/api/...paths that broke under any prefix.
Details¶
- New config setting:
WEBGATE_ROOT_PATH(default""), passed to FastAPI'sroot_pathfor correct OpenAPI URLs behind proxies - Frontend derives the path prefix at runtime from
window.location.pathnameand prepends it to all REST calls and the terminal WebSocket URL - README documents nginx, Apache, and Traefik reverse-proxy configurations for sub-path deployments
- The proxy must forward the prefix unchanged (do not strip it) -- webgate handles the prefix natively
v0.2.0 (2026-04-09)¶
Features¶
- SFTP read-only mode -- per-server flag to allow browse and download only, blocking all write operations (upload, write, mkdir, rename, delete, chmod)
- Server status monitoring -- background task checks SSH connectivity every 60 seconds; online/offline indicator (green/red dot) on server dashboard
- Dark/light theme toggle -- user preference saved in localStorage; CSS custom properties for full theme support; terminal and editor adapt to theme
- Keyboard shortcuts -- Escape closes modals, Ctrl+1 goes to Site Manager, Ctrl+N opens New Server
- Drag & drop upload progress -- visual progress bar with percentage during file uploads
- Folder download as ZIP -- right-click a directory in SFTP browser to download it as a ZIP archive
- Lightweight DB migrations -- automatic ALTER TABLE for new columns on existing databases
Details¶
- New server model field:
sftp_read_only(bool, default false) - New file:
servers/monitor.py— ServerMonitor class with asyncio background task - New API endpoints:
GET /api/servers/status,GET /api/servers/{id}/status - New API endpoint:
GET /api/files/{id}/download-zip?path= - Config settings:
monitor_interval,monitor_timeout,monitor_concurrency - CSS variables for theming:
--bg-primary,--text-primary,--accent, etc. - Terminal theme switches between dark (Tokyo Night) and light on toggle
v0.1.1 (2026-04-08)¶
Features¶
- Per-server SSH/SFTP toggles -- admin can enable or disable SSH terminal and SFTP file browser independently for each server
- SFTP path restrictions -- admin can configure allowed directory paths per server; users are restricted to only those directories and their subdirectories (empty list = unrestricted)
Details¶
- New server model fields:
ssh_enabled(bool),sftp_enabled(bool),sftp_allowed_paths(JSON list of paths) - SSH disabled servers return WebSocket close code 4003
- SFTP disabled servers return HTTP 403
- Path restriction enforced on all SFTP operations (ls, read, write, upload, download, mkdir, rename, delete, chmod)
- Rename operations validate both source and destination paths against allowed paths
- All existing tests continue to pass (34 tests)
v0.1.0 (2026-04-08)¶
First public release.
Features¶
- SSH web terminal (xterm.js + asyncssh WebSocket bridge)
- SFTP file browser (directory listing, upload, download, rename, delete, mkdir, chmod)
- In-browser text editor (CodeMirror 6 with oneDark theme)
- PDF and image preview (PDF, PNG, JPG, GIF, SVG, WebP)
- Server registry with groups, tags, and encrypted credentials (Fernet)
- Quick Connect toolbar for one-off SSH connections
- Admin/user role system with group-based access control
- Default admin account with forced password change on first login
- User management panel (create, delete, assign groups)
- Multi-tab split pane (terminal + file browser side by side)
- File search/filter within SFTP listings
- Server import/export (JSON) from the UI
- Session persistence across page reloads
- Rate limiting on auth endpoints (slowapi)
- Audit log (admin-viewable action history)
- SFTP connection pool (reuse per server, 5 min TTL)
- Modern dark UI (GitHub-inspired theme)
- Docker multi-stage build with demo SSH container
- 34 automated tests