webgate 2.5.0__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webgate-2.5.0 → webgate-2.6.0}/CHANGELOG.md +45 -0
- {webgate-2.5.0 → webgate-2.6.0}/PKG-INFO +12 -12
- {webgate-2.5.0 → webgate-2.6.0}/README.md +11 -11
- {webgate-2.5.0 → webgate-2.6.0}/ROADMAP.md +6 -2
- webgate-2.6.0/VERSION +1 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/LOCAL_TESTING.md +1 -1
- {webgate-2.5.0 → webgate-2.6.0}/docs/changelog.md +45 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/users.md +2 -2
- webgate-2.6.0/docs/screenshots/v2/audit.png +0 -0
- webgate-2.6.0/docs/screenshots/v2/palette.png +0 -0
- webgate-2.6.0/docs/screenshots/v2/settings.png +0 -0
- webgate-2.6.0/docs/screenshots/v2/sftp.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/site-manager.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/pyproject.toml +1 -1
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/config.py +1 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/files/routes.py +5 -1
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/runtime_config/registry.py +17 -0
- webgate-2.6.0/src/webgate/servers/crypto.py +47 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/servers/monitor.py +17 -6
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/servers/service.py +21 -7
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/static/index.html +62 -6
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/terminal/routes.py +9 -1
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/terminal/shared.py +10 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/terminal/ws_handler.py +21 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/e2e/test_ui.py +4 -2
- webgate-2.6.0/tests/test_credential_rotation.py +124 -0
- webgate-2.6.0/tests/test_session_limit.py +168 -0
- {webgate-2.5.0 → webgate-2.6.0}/uv.lock +1 -1
- webgate-2.5.0/VERSION +0 -1
- webgate-2.5.0/docs/screenshots/v2/audit.png +0 -0
- webgate-2.5.0/docs/screenshots/v2/palette.png +0 -0
- webgate-2.5.0/docs/screenshots/v2/settings.png +0 -0
- webgate-2.5.0/docs/screenshots/v2/sftp.png +0 -0
- webgate-2.5.0/src/webgate/servers/crypto.py +0 -27
- {webgate-2.5.0 → webgate-2.6.0}/.dockerignore +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/.github/workflows/ci.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/.github/workflows/demo.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/.github/workflows/docs.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/.github/workflows/release.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/.gitignore +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/DESIGN.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/Dockerfile +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/Dockerfile.demo +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/Dockerfile.ssh-demo +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/LICENSE +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/PRODUCT.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/RELEASING.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/compose.dev.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/compose.ha.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/compose.playground.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/compose.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/deploy/nginx-ha.conf +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/api/auth.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/api/files.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/api/servers.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/api/terminal.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/architecture.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/getting-started/installation.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/getting-started/quickstart.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/getting-started/upgrade.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/access-control.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/advanced.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/files.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/ha.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/integrations.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/servers.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/settings.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/split.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/guide/terminal.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/index.md +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/access-control.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/audit.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/edit-access-control.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/editor.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/light-theme.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/login.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/new-server-form.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/01-login.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/02-alice-ldap-empty.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/03-add-server-jump-via.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/04-dashboard-3-servers.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/05-terminal-via-jump-host.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/06-snippet-whoami.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/07-shared-joiner.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/07-shared-owner.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/08-webhook-fired.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/09-recordings-list.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/10-recording-replay.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/playground/11-bob-limited.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/sftp-restricted.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/sftp.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/site-manager.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/split-view.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/ssh-disabled.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/terminal.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/users.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/01-login-demo-banner.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/02-dashboard-jump-host.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/03-terminal-snippets-jump.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/04-snippet-executed.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/05-sftp-via-jump.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/06-webhooks-modal.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/07-webhook-test-fired.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.3/08-add-server-jump-via.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.4/01-shared-terminal-owner.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.4/02-shared-terminal-joiner.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.4/03-recording-replay.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v0.4/04-recordings-modal.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/agent.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/branding.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/light.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/login.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/terminal.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/docs/screenshots/v2/users.png +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/fly.toml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/mkdocs.yml +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/scripts/release_notes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/__main__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/cache.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/conversation.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/memory.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/provider.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/service.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/sftp_tools.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/store.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/agent/tools.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/app.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/audit/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/audit/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/audit/service.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/auth/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/auth/ldap.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/auth/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/auth/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/auth/service.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/backup/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/backup/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/backup/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/backup/service.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/branding/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/branding/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/branding/store.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/db/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/db/engine.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/demo.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/files/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/files/limits.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/files/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/files/pool.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/files/sftp_service.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/recordings/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/recordings/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/recordings/recorder.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/recordings/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/runtime_config/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/runtime_config/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/runtime_config/store.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/servers/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/servers/hostkeys.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/servers/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/servers/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/snippets/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/snippets/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/snippets/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/terminal/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/terminal/ssh_session.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/webhooks/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/webhooks/dispatcher.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/webhooks/models.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/src/webgate/webhooks/routes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/__init__.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/conftest.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/e2e/conftest.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/e2e/sshlab.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_agent_conversation.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_agent_memory.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_agent_settings.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_agent_sftp_tools.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_agent_tools.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_audit_coverage.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_auth.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_backup.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_branding.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_files.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_files_listing.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_host_keys.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_idle_sessions.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_limits.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_migrations.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_monitor.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_recordings.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_release_notes.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_secret_key_gate.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_servers.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_settings.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_snippets.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_terminal.py +0 -0
- {webgate-2.5.0 → webgate-2.6.0}/tests/test_webhook_dispatch.py +0 -0
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.0 (2026-09-16) — rotating the secret key no longer breaks everything
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A changed `WEBGATE_SECRET_KEY` made webgate fall over instead of explaining
|
|
8
|
+
itself.** v2.2.0 refuses the shipped default, which asks every existing install to
|
|
9
|
+
set a real one; doing that leaves rows encrypted under the old key. `decrypt_value`
|
|
10
|
+
raised a bare Fernet error from twelve call sites and none of them caught it. The
|
|
11
|
+
status monitor died on its first sweep and froze every server's dot; opening a
|
|
12
|
+
terminal failed the WebSocket **upgrade**, so the browser got a 500 with nothing in
|
|
13
|
+
it. Found on the public demo, by walking into it.
|
|
14
|
+
|
|
15
|
+
An unreadable credential is now its own error, naming the server, why it happened
|
|
16
|
+
and what to do. The monitor marks that one server offline and carries on, the
|
|
17
|
+
terminal answers with an error frame, SFTP returns 502, and a connection test
|
|
18
|
+
reports it.
|
|
19
|
+
|
|
20
|
+
- **The demo banner covered the top of the interface.** It was `position: fixed` with
|
|
21
|
+
nothing making room for it, so the app bar sat underneath. It is in flow now, which
|
|
22
|
+
also holds when it wraps to two lines on a narrow screen. Only visible with
|
|
23
|
+
`WEBGATE_DEMO_MODE=true`, which is why no test caught it.
|
|
24
|
+
|
|
25
|
+
- **Diagrams that did not show.** Nine style directives set a pale fill and left the
|
|
26
|
+
label to the theme, so on GitHub's dark background they were light text on a light
|
|
27
|
+
box. All 21 diagrams in the repository now render, and every label is readable on
|
|
28
|
+
both grounds — checked with the Mermaid version GitHub actually uses, in a browser,
|
|
29
|
+
one diagram per page.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **The pinned host key is visible.** The fingerprint has been in the API since
|
|
34
|
+
v2.0.0 and nothing showed it. Servers carry a shield when a key is pinned, the edit
|
|
35
|
+
form shows the fingerprint, and an admin can clear it there — with the consequence
|
|
36
|
+
spelled out, since accepting a new key is the one moment the protection is stood
|
|
37
|
+
down.
|
|
38
|
+
- **A cap on concurrent SSH sessions per account** (**Admin → Settings → Security**,
|
|
39
|
+
0 for none). There was nothing stopping a looping tab from opening sessions until
|
|
40
|
+
the worker ran out. Checked before the SSH connection is made, and counted per
|
|
41
|
+
worker — which the setting says, because a gateway behind a load balancer allows
|
|
42
|
+
that many on each.
|
|
43
|
+
|
|
44
|
+
333 tests.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
3
48
|
## v2.5.0 (2026-09-16) — browser tests
|
|
4
49
|
|
|
5
50
|
The audit gap in v2.4.0 was reported by a person using the interface, and no test in
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webgate
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Self-hosted web application for remote server management via SSH terminal and SFTP file browser
|
|
5
5
|
Project-URL: Homepage, https://github.com/kalexnolasco/webgate
|
|
6
6
|
Project-URL: Documentation, https://kalexnolasco.github.io/webgate/
|
|
@@ -126,9 +126,9 @@ flowchart TB
|
|
|
126
126
|
GW -. "SSH :22" .-> APP2
|
|
127
127
|
GW -. "SSH :22" .-> WORKER
|
|
128
128
|
GW -. "SSH :22" .-> REDIS
|
|
129
|
-
style internet fill
|
|
130
|
-
style firewall fill
|
|
131
|
-
style internal fill
|
|
129
|
+
style internet fill:none,stroke:#4a90d9,stroke-width:2px
|
|
130
|
+
style firewall fill:none,stroke:#ff9800,stroke-width:2px
|
|
131
|
+
style internal fill:none,stroke:#5cb85c,stroke-width:2px
|
|
132
132
|
style GW fill:#ffcc02,stroke:#e6a800,color:#333
|
|
133
133
|
style YOU fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
134
134
|
```
|
|
@@ -160,9 +160,9 @@ flowchart TB
|
|
|
160
160
|
WG -- "SSH/SFTP" --> APP1
|
|
161
161
|
WG -- "SSH/SFTP" --> WORKER
|
|
162
162
|
WG -- "SSH/SFTP" --> REDIS
|
|
163
|
-
style internet fill
|
|
164
|
-
style firewall fill
|
|
165
|
-
style internal fill
|
|
163
|
+
style internet fill:none,stroke:#4a90d9,stroke-width:2px
|
|
164
|
+
style firewall fill:none,stroke:#ff9800,stroke-width:2px
|
|
165
|
+
style internal fill:none,stroke:#5cb85c,stroke-width:2px
|
|
166
166
|
style WG fill:#5cb85c,stroke:#449d44,color:#fff
|
|
167
167
|
style ENG1 fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
168
168
|
style ENG2 fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
@@ -355,7 +355,7 @@ flowchart LR
|
|
|
355
355
|
style B fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
356
356
|
style WG fill:#5cb85c,stroke:#449d44,color:#fff
|
|
357
357
|
style BAST fill:#ffcc02,stroke:#e6a800,color:#333
|
|
358
|
-
style INT fill:#
|
|
358
|
+
style INT fill:#b25e00,stroke:#ff9800,color:#fff
|
|
359
359
|
```
|
|
360
360
|
|
|
361
361
|
### Shared terminal session
|
|
@@ -407,7 +407,7 @@ flowchart LR
|
|
|
407
407
|
L1["alice ∈ cn=devs"]
|
|
408
408
|
L2["alice ∈ cn=admins"]
|
|
409
409
|
end
|
|
410
|
-
subgraph "Group mapping<br/>
|
|
410
|
+
subgraph MAP["Group mapping<br/>Admin → Settings → LDAP"]
|
|
411
411
|
M["{<br/> "devs": "production",<br/> "sre": "all"<br/>}"]
|
|
412
412
|
end
|
|
413
413
|
subgraph User
|
|
@@ -650,9 +650,9 @@ flowchart LR
|
|
|
650
650
|
W3 -. watches .-> LEASE
|
|
651
651
|
style LB fill:#ffcc02,stroke:#e6a800,color:#333
|
|
652
652
|
style W1 fill:#5cb85c,stroke:#449d44,color:#fff
|
|
653
|
-
style W2 fill:#
|
|
654
|
-
style W3 fill:#
|
|
655
|
-
style PG fill:#
|
|
653
|
+
style W2 fill:#2a6cb5,stroke:#4a90d9,color:#fff
|
|
654
|
+
style W3 fill:#2a6cb5,stroke:#4a90d9,color:#fff
|
|
655
|
+
style PG fill:#b25e00,stroke:#ff9800,color:#fff
|
|
656
656
|
```
|
|
657
657
|
|
|
658
658
|
Reference stack:
|
|
@@ -77,9 +77,9 @@ flowchart TB
|
|
|
77
77
|
GW -. "SSH :22" .-> APP2
|
|
78
78
|
GW -. "SSH :22" .-> WORKER
|
|
79
79
|
GW -. "SSH :22" .-> REDIS
|
|
80
|
-
style internet fill
|
|
81
|
-
style firewall fill
|
|
82
|
-
style internal fill
|
|
80
|
+
style internet fill:none,stroke:#4a90d9,stroke-width:2px
|
|
81
|
+
style firewall fill:none,stroke:#ff9800,stroke-width:2px
|
|
82
|
+
style internal fill:none,stroke:#5cb85c,stroke-width:2px
|
|
83
83
|
style GW fill:#ffcc02,stroke:#e6a800,color:#333
|
|
84
84
|
style YOU fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
85
85
|
```
|
|
@@ -111,9 +111,9 @@ flowchart TB
|
|
|
111
111
|
WG -- "SSH/SFTP" --> APP1
|
|
112
112
|
WG -- "SSH/SFTP" --> WORKER
|
|
113
113
|
WG -- "SSH/SFTP" --> REDIS
|
|
114
|
-
style internet fill
|
|
115
|
-
style firewall fill
|
|
116
|
-
style internal fill
|
|
114
|
+
style internet fill:none,stroke:#4a90d9,stroke-width:2px
|
|
115
|
+
style firewall fill:none,stroke:#ff9800,stroke-width:2px
|
|
116
|
+
style internal fill:none,stroke:#5cb85c,stroke-width:2px
|
|
117
117
|
style WG fill:#5cb85c,stroke:#449d44,color:#fff
|
|
118
118
|
style ENG1 fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
119
119
|
style ENG2 fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
@@ -306,7 +306,7 @@ flowchart LR
|
|
|
306
306
|
style B fill:#4a90d9,stroke:#2a6cb5,color:#fff
|
|
307
307
|
style WG fill:#5cb85c,stroke:#449d44,color:#fff
|
|
308
308
|
style BAST fill:#ffcc02,stroke:#e6a800,color:#333
|
|
309
|
-
style INT fill:#
|
|
309
|
+
style INT fill:#b25e00,stroke:#ff9800,color:#fff
|
|
310
310
|
```
|
|
311
311
|
|
|
312
312
|
### Shared terminal session
|
|
@@ -358,7 +358,7 @@ flowchart LR
|
|
|
358
358
|
L1["alice ∈ cn=devs"]
|
|
359
359
|
L2["alice ∈ cn=admins"]
|
|
360
360
|
end
|
|
361
|
-
subgraph "Group mapping<br/>
|
|
361
|
+
subgraph MAP["Group mapping<br/>Admin → Settings → LDAP"]
|
|
362
362
|
M["{<br/> "devs": "production",<br/> "sre": "all"<br/>}"]
|
|
363
363
|
end
|
|
364
364
|
subgraph User
|
|
@@ -601,9 +601,9 @@ flowchart LR
|
|
|
601
601
|
W3 -. watches .-> LEASE
|
|
602
602
|
style LB fill:#ffcc02,stroke:#e6a800,color:#333
|
|
603
603
|
style W1 fill:#5cb85c,stroke:#449d44,color:#fff
|
|
604
|
-
style W2 fill:#
|
|
605
|
-
style W3 fill:#
|
|
606
|
-
style PG fill:#
|
|
604
|
+
style W2 fill:#2a6cb5,stroke:#4a90d9,color:#fff
|
|
605
|
+
style W3 fill:#2a6cb5,stroke:#4a90d9,color:#fff
|
|
606
|
+
style PG fill:#b25e00,stroke:#ff9800,color:#fff
|
|
607
607
|
```
|
|
608
608
|
|
|
609
609
|
Reference stack:
|
|
@@ -90,6 +90,12 @@ Tracks the development plan for **webgate**. Items are organized by release.
|
|
|
90
90
|
- [x] **The documentation screenshots are taken by those tests**, so they cannot drift from the shipped interface
|
|
91
91
|
- [x] **CI runs them on every push**, and uploads what they captured
|
|
92
92
|
|
|
93
|
+
### v2.6.0 — Key rotation, host key UI, session cap (2026-09-16)
|
|
94
|
+
- [x] **A changed secret key is handled** — it used to kill the monitor and fail the terminal's WebSocket upgrade with a bare 500
|
|
95
|
+
- [x] **The pinned host key fingerprint is visible and clearable** from the Site Manager
|
|
96
|
+
- [x] **A per-account cap on concurrent SSH sessions**
|
|
97
|
+
- [x] **The demo banner no longer covers the interface**, and every diagram in the repository renders legibly on both GitHub grounds
|
|
98
|
+
|
|
93
99
|
---
|
|
94
100
|
|
|
95
101
|
## Planned
|
|
@@ -97,8 +103,6 @@ Tracks the development plan for **webgate**. Items are organized by release.
|
|
|
97
103
|
### v2.1.x — Next
|
|
98
104
|
| Feature | Priority | Description |
|
|
99
105
|
|---------|----------|-------------|
|
|
100
|
-
| Per-user concurrent-session limit | Medium | Nothing caps how many SSH sessions one account may hold open |
|
|
101
|
-
| Host key fingerprint in the UI | Medium | The API exposes it and can clear it; the Site Manager does not show it yet |
|
|
102
106
|
| Internationalization (i18n) | Low | UI translations starting with English / Spanish |
|
|
103
107
|
|
|
104
108
|
### Later
|
webgate-2.6.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.6.0
|
|
@@ -55,7 +55,7 @@ flowchart LR
|
|
|
55
55
|
B -- SSH tunnel --> I
|
|
56
56
|
style WG fill:#5cb85c,stroke:#449d44,color:#fff
|
|
57
57
|
style B fill:#ffcc02,stroke:#e6a800,color:#333
|
|
58
|
-
style I fill:#
|
|
58
|
+
style I fill:#b25e00,stroke:#ff9800,color:#fff
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
`webgate` and `ssh-internal` share **no network**, so reaching `ssh-internal` is only possible via the bastion.
|
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.0 (2026-09-16) — rotating the secret key no longer breaks everything
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A changed `WEBGATE_SECRET_KEY` made webgate fall over instead of explaining
|
|
8
|
+
itself.** v2.2.0 refuses the shipped default, which asks every existing install to
|
|
9
|
+
set a real one; doing that leaves rows encrypted under the old key. `decrypt_value`
|
|
10
|
+
raised a bare Fernet error from twelve call sites and none of them caught it. The
|
|
11
|
+
status monitor died on its first sweep and froze every server's dot; opening a
|
|
12
|
+
terminal failed the WebSocket **upgrade**, so the browser got a 500 with nothing in
|
|
13
|
+
it. Found on the public demo, by walking into it.
|
|
14
|
+
|
|
15
|
+
An unreadable credential is now its own error, naming the server, why it happened
|
|
16
|
+
and what to do. The monitor marks that one server offline and carries on, the
|
|
17
|
+
terminal answers with an error frame, SFTP returns 502, and a connection test
|
|
18
|
+
reports it.
|
|
19
|
+
|
|
20
|
+
- **The demo banner covered the top of the interface.** It was `position: fixed` with
|
|
21
|
+
nothing making room for it, so the app bar sat underneath. It is in flow now, which
|
|
22
|
+
also holds when it wraps to two lines on a narrow screen. Only visible with
|
|
23
|
+
`WEBGATE_DEMO_MODE=true`, which is why no test caught it.
|
|
24
|
+
|
|
25
|
+
- **Diagrams that did not show.** Nine style directives set a pale fill and left the
|
|
26
|
+
label to the theme, so on GitHub's dark background they were light text on a light
|
|
27
|
+
box. All 21 diagrams in the repository now render, and every label is readable on
|
|
28
|
+
both grounds — checked with the Mermaid version GitHub actually uses, in a browser,
|
|
29
|
+
one diagram per page.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **The pinned host key is visible.** The fingerprint has been in the API since
|
|
34
|
+
v2.0.0 and nothing showed it. Servers carry a shield when a key is pinned, the edit
|
|
35
|
+
form shows the fingerprint, and an admin can clear it there — with the consequence
|
|
36
|
+
spelled out, since accepting a new key is the one moment the protection is stood
|
|
37
|
+
down.
|
|
38
|
+
- **A cap on concurrent SSH sessions per account** (**Admin → Settings → Security**,
|
|
39
|
+
0 for none). There was nothing stopping a looping tab from opening sessions until
|
|
40
|
+
the worker ran out. Checked before the SSH connection is made, and counted per
|
|
41
|
+
worker — which the setting says, because a gateway behind a load balancer allows
|
|
42
|
+
that many on each.
|
|
43
|
+
|
|
44
|
+
333 tests.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
3
48
|
## v2.5.0 (2026-09-16) — browser tests
|
|
4
49
|
|
|
5
50
|
The audit gap in v2.4.0 was reported by a person using the interface, and no test in
|
|
@@ -42,8 +42,8 @@ flowchart LR
|
|
|
42
42
|
BOB -->|sees| S3
|
|
43
43
|
|
|
44
44
|
style ADMIN fill:#5cb85c,stroke:#449d44,color:#fff
|
|
45
|
-
style ALICE fill:#
|
|
46
|
-
style BOB fill:#
|
|
45
|
+
style ALICE fill:#2a6cb5,stroke:#4a90d9,color:#fff
|
|
46
|
+
style BOB fill:#b25e00,stroke:#ff9800,color:#fff
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
## Editing Groups
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -61,6 +61,7 @@ class Settings(BaseSettings):
|
|
|
61
61
|
disable_monitor: bool = False # Skip leader election; never run server monitor
|
|
62
62
|
log_level: str = "info"
|
|
63
63
|
session_timeout: int = 3600
|
|
64
|
+
max_sessions_per_user: int = 0 # 0 = no cap
|
|
64
65
|
max_upload_size: int = 104857600 # 100MB
|
|
65
66
|
first_run: bool = True
|
|
66
67
|
|
|
@@ -29,6 +29,7 @@ from webgate.files.models import (
|
|
|
29
29
|
)
|
|
30
30
|
from webgate.files.pool import sftp_pool
|
|
31
31
|
from webgate.files.sftp_service import SFTPClient, validate_path
|
|
32
|
+
from webgate.servers.crypto import CredentialUnreadable
|
|
32
33
|
from webgate.servers.hostkeys import remember, translate
|
|
33
34
|
from webgate.servers.models import Server
|
|
34
35
|
from webgate.servers.service import get_server, get_server_credentials, resolve_jump_creds
|
|
@@ -111,7 +112,10 @@ async def _sftp(
|
|
|
111
112
|
)
|
|
112
113
|
allowed_paths = _get_allowed_paths(server)
|
|
113
114
|
read_only = server.sftp_read_only
|
|
114
|
-
|
|
115
|
+
try:
|
|
116
|
+
password, private_key = get_server_credentials(server)
|
|
117
|
+
except CredentialUnreadable as exc:
|
|
118
|
+
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
|
115
119
|
jump_kwargs = await resolve_jump_creds(session, server)
|
|
116
120
|
try:
|
|
117
121
|
client = await sftp_pool.acquire(
|
|
@@ -154,6 +154,23 @@ SPECS: tuple[Spec, ...] = (
|
|
|
154
154
|
minimum=0,
|
|
155
155
|
maximum=86400,
|
|
156
156
|
),
|
|
157
|
+
Spec(
|
|
158
|
+
key="max_sessions_per_user",
|
|
159
|
+
section="Security",
|
|
160
|
+
label="Concurrent SSH sessions per user",
|
|
161
|
+
help=(
|
|
162
|
+
"How many terminals one account may hold open on a single worker. 0 means "
|
|
163
|
+
"no limit. Counted per worker, so a gateway behind a load balancer allows "
|
|
164
|
+
"this many on each."
|
|
165
|
+
),
|
|
166
|
+
kind="int",
|
|
167
|
+
minimum=0,
|
|
168
|
+
maximum=200,
|
|
169
|
+
warning=(
|
|
170
|
+
"0 means nothing stops a looping browser tab from opening SSH sessions "
|
|
171
|
+
"until the worker runs out."
|
|
172
|
+
),
|
|
173
|
+
),
|
|
157
174
|
Spec(
|
|
158
175
|
key="max_upload_size",
|
|
159
176
|
section="Security",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
5
|
+
|
|
6
|
+
from webgate.config import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _derive_key(secret: str) -> bytes:
|
|
10
|
+
digest = hashlib.sha256(secret.encode()).digest()
|
|
11
|
+
return base64.urlsafe_b64encode(digest)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_fernet() -> Fernet:
|
|
15
|
+
return Fernet(_derive_key(settings.secret_key))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def encrypt_value(value: str) -> str:
|
|
19
|
+
if not value:
|
|
20
|
+
return ""
|
|
21
|
+
return get_fernet().encrypt(value.encode()).decode()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CredentialUnreadable(Exception):
|
|
25
|
+
"""Stored credentials will not decrypt with the key this process is using.
|
|
26
|
+
|
|
27
|
+
Almost always means WEBGATE_SECRET_KEY changed after the row was written --
|
|
28
|
+
which is exactly what upgrading from the shipped default asks an operator to do.
|
|
29
|
+
Raised as itself rather than as a Fernet error so every caller can say something
|
|
30
|
+
useful instead of returning a 500.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, what: str = "These credentials") -> None:
|
|
34
|
+
super().__init__(
|
|
35
|
+
f"{what} cannot be decrypted with the current WEBGATE_SECRET_KEY. The key "
|
|
36
|
+
f"changed after they were saved. Restore a backup taken before the change, "
|
|
37
|
+
f"or re-enter the credentials."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decrypt_value(token: str, what: str = "These credentials") -> str:
|
|
42
|
+
if not token:
|
|
43
|
+
return ""
|
|
44
|
+
try:
|
|
45
|
+
return get_fernet().decrypt(token.encode()).decode()
|
|
46
|
+
except InvalidToken as exc:
|
|
47
|
+
raise CredentialUnreadable(what) from exc
|
|
@@ -26,7 +26,7 @@ from sqlalchemy import select, text
|
|
|
26
26
|
from webgate.config import settings
|
|
27
27
|
from webgate.db.engine import async_session_factory, engine
|
|
28
28
|
from webgate.runtime_config import store as runtime
|
|
29
|
-
from webgate.servers.crypto import decrypt_value
|
|
29
|
+
from webgate.servers.crypto import CredentialUnreadable, decrypt_value
|
|
30
30
|
from webgate.servers.hostkeys import known_hosts_for
|
|
31
31
|
from webgate.servers.models import Server
|
|
32
32
|
|
|
@@ -232,17 +232,28 @@ class ServerMonitor:
|
|
|
232
232
|
await asyncio.gather(*[_check_one(s) for s in servers])
|
|
233
233
|
|
|
234
234
|
async def _check_server(self, server: Server) -> ServerStatus:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
now = datetime.now(UTC)
|
|
236
|
+
try:
|
|
237
|
+
password = (
|
|
238
|
+
decrypt_value(server.encrypted_password, server.name)
|
|
239
|
+
if server.encrypted_password
|
|
240
|
+
else None
|
|
241
|
+
)
|
|
242
|
+
private_key_str = (
|
|
243
|
+
decrypt_value(server.encrypted_private_key, server.name)
|
|
244
|
+
if server.encrypted_private_key
|
|
245
|
+
else None
|
|
246
|
+
)
|
|
247
|
+
except CredentialUnreadable as exc:
|
|
248
|
+
# Reported against this server, not raised: the sweep has other hosts to
|
|
249
|
+
# check, and the operator needs to know which one is unreadable.
|
|
250
|
+
return ServerStatus(online=False, last_checked=now, error=str(exc))
|
|
239
251
|
kwargs: dict[str, object] = {
|
|
240
252
|
"host": server.hostname,
|
|
241
253
|
"port": server.port,
|
|
242
254
|
"username": server.username,
|
|
243
255
|
"known_hosts": known_hosts_for(getattr(server, "host_key", "") or ""),
|
|
244
256
|
}
|
|
245
|
-
now = datetime.now(UTC)
|
|
246
257
|
start = time.monotonic()
|
|
247
258
|
try:
|
|
248
259
|
# Parsing the key belongs inside the try. An unreadable key row raised out
|
|
@@ -8,7 +8,7 @@ import asyncssh
|
|
|
8
8
|
from sqlalchemy import select
|
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
10
|
|
|
11
|
-
from webgate.servers.crypto import decrypt_value, encrypt_value
|
|
11
|
+
from webgate.servers.crypto import CredentialUnreadable, decrypt_value, encrypt_value
|
|
12
12
|
from webgate.servers.hostkeys import describe, known_hosts_for, remember, translate
|
|
13
13
|
from webgate.servers.models import Server, ServerCreate, ServerOut, ServerUpdate
|
|
14
14
|
|
|
@@ -189,10 +189,19 @@ async def delete_server(session: AsyncSession, server: Server) -> None:
|
|
|
189
189
|
async def test_server_connectivity(
|
|
190
190
|
server: Server, session: AsyncSession | None = None
|
|
191
191
|
) -> tuple[bool, str]:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
try:
|
|
193
|
+
password = (
|
|
194
|
+
decrypt_value(server.encrypted_password, server.name)
|
|
195
|
+
if server.encrypted_password
|
|
196
|
+
else None
|
|
197
|
+
)
|
|
198
|
+
private_key_str = (
|
|
199
|
+
decrypt_value(server.encrypted_private_key, server.name)
|
|
200
|
+
if server.encrypted_private_key
|
|
201
|
+
else None
|
|
202
|
+
)
|
|
203
|
+
except CredentialUnreadable as exc:
|
|
204
|
+
return False, str(exc)
|
|
196
205
|
|
|
197
206
|
kwargs: dict[str, object] = {
|
|
198
207
|
"host": server.hostname,
|
|
@@ -289,8 +298,13 @@ async def resolve_jump_creds(session: AsyncSession, server: Server) -> dict[str,
|
|
|
289
298
|
|
|
290
299
|
|
|
291
300
|
def get_server_credentials(server: Server) -> tuple[str | None, str | None]:
|
|
292
|
-
|
|
301
|
+
"""Raises CredentialUnreadable when the secret key no longer matches the rows."""
|
|
302
|
+
password = (
|
|
303
|
+
decrypt_value(server.encrypted_password, server.name) if server.encrypted_password else None
|
|
304
|
+
)
|
|
293
305
|
private_key = (
|
|
294
|
-
decrypt_value(server.encrypted_private_key
|
|
306
|
+
decrypt_value(server.encrypted_private_key, server.name)
|
|
307
|
+
if server.encrypted_private_key
|
|
308
|
+
else None
|
|
295
309
|
)
|
|
296
310
|
return password, private_key
|
|
@@ -309,7 +309,9 @@
|
|
|
309
309
|
.cm-editor .cm-scroller { overflow: auto; }
|
|
310
310
|
|
|
311
311
|
/* Demo banner */
|
|
312
|
-
.fz-demo-banner {
|
|
312
|
+
.fz-demo-banner { flex: none; z-index: 200; background: var(--bg-secondary); border-bottom: 1px solid var(--border); color: var(--text-secondary); text-align: center; font-size: 12px; padding: 6px 12px; display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }
|
|
313
|
+
.fz-root { height: 100vh; display: flex; flex-direction: column; }
|
|
314
|
+
.fz-app-shell { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
313
315
|
.fz-demo-banner strong { color: var(--text-primary); font-weight: 600; }
|
|
314
316
|
.fz-demo-banner code { background: var(--btn-bg); border: 1px solid var(--border); padding: 1px 5px; border-radius: var(--radius-sm); font-family: var(--mono); font-size: 11px; color: var(--text-primary); }
|
|
315
317
|
.fz-demo-banner a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
@@ -360,6 +362,12 @@
|
|
|
360
362
|
.fz-audit-filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); }
|
|
361
363
|
.fz-audit-filters input, .fz-audit-filters select { margin: 0; }
|
|
362
364
|
.fz-audit-empty { padding: 28px 16px; text-align: center; color: var(--text-muted); font-size: 13px; }
|
|
365
|
+
.fz-srv-badge.pinned { color: var(--accent); }
|
|
366
|
+
.fz-hostkey { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
|
|
367
|
+
.fz-hostkey-head { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; margin-bottom: 8px; }
|
|
368
|
+
.fz-hostkey-head svg { color: var(--accent); }
|
|
369
|
+
.fz-hostkey-head button { margin-left: auto; }
|
|
370
|
+
.fz-hostkey-fp { display: block; font-family: var(--mono); font-size: 11px; word-break: break-all; padding: 7px 9px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 4px; }
|
|
363
371
|
.fz-modal-footer { border-top: 1px solid var(--border); padding: 12px 16px; display: flex; justify-content: flex-end; gap: 8px; position: sticky; bottom: 0; background: var(--modal-bg); }
|
|
364
372
|
.fz-row-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
365
373
|
.fz-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
@@ -488,7 +496,7 @@ window._showToastImpl = function(message, type) {
|
|
|
488
496
|
<g id="i-play"><path d="M7 4.5 19 12 7 19.5V4.5Z"/></g>
|
|
489
497
|
<g id="i-save"><path d="M5 3h11l4 4v14H5V3Z"/><path d="M8.5 3v6h7V3M8.5 21v-7h7v7"/></g>
|
|
490
498
|
</defs></svg>
|
|
491
|
-
<div x-data="app()" x-cloak @keydown.window="handleKeyboard($event)">
|
|
499
|
+
<div x-data="app()" x-cloak class="fz-root" @keydown.window="handleKeyboard($event)">
|
|
492
500
|
|
|
493
501
|
<!-- Demo banner (shown when WEBGATE_DEMO_MODE=true) -->
|
|
494
502
|
<div x-show="demoMode" class="fz-demo-banner">
|
|
@@ -539,7 +547,7 @@ window._showToastImpl = function(message, type) {
|
|
|
539
547
|
</div>
|
|
540
548
|
|
|
541
549
|
<!-- ======================== MAIN APP ======================== -->
|
|
542
|
-
<div :style="token ? '
|
|
550
|
+
<div class="fz-app-shell" :style="token ? '' : 'display:none'">
|
|
543
551
|
|
|
544
552
|
<!-- ---- App bar ---- -->
|
|
545
553
|
<header class="fz-appbar">
|
|
@@ -744,6 +752,10 @@ window._showToastImpl = function(message, type) {
|
|
|
744
752
|
<svg class="fz-icon fz-icon-sm" viewBox="0 0 24 24"><use href="#i-jump"/></svg>
|
|
745
753
|
</span>
|
|
746
754
|
<span x-show="srv.sftp_enabled && srv.sftp_read_only" class="fz-srv-badge ro" title="SFTP is read-only">RO</span>
|
|
755
|
+
<span x-show="srv.host_key_fingerprint" class="fz-srv-badge pinned"
|
|
756
|
+
:title="'Host key pinned: ' + srv.host_key_fingerprint">
|
|
757
|
+
<svg class="fz-icon fz-icon-sm" viewBox="0 0 24 24"><use href="#i-shield"/></svg>
|
|
758
|
+
</span>
|
|
747
759
|
</div>
|
|
748
760
|
<div class="fz-srv-actions">
|
|
749
761
|
<button @click.stop="openServerTerminal(srv)" class="fz-btn fz-btn-primary" :disabled="srv.ssh_enabled===false" :title="srv.ssh_enabled===false ? 'SSH is disabled for this server' : 'Open SSH terminal'">SSH</button>
|
|
@@ -1713,10 +1725,31 @@ window._showToastImpl = function(message, type) {
|
|
|
1713
1725
|
<input type="checkbox" x-model="serverForm.sftp_read_only" style="width:auto;accent-color:var(--warning)"> SFTP Read-Only (browse & download only)
|
|
1714
1726
|
</label>
|
|
1715
1727
|
<label x-show="agentAvailable" style="display:flex;align-items:center;gap:8px;margin-top:12px">
|
|
1716
|
-
<input type="checkbox" x-model="serverForm.agent_enabled" style="width:auto;accent-color:var(--info)"> Allow the diagnostic agent
|
|
1717
|
-
|
|
1718
|
-
|
|
1728
|
+
<input type="checkbox" x-model="serverForm.agent_enabled" style="width:auto;accent-color:var(--info)"> Allow the diagnostic agent
|
|
1729
|
+
</label>
|
|
1730
|
+
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:13px">
|
|
1731
|
+
<input type="checkbox" x-model="serverForm.record_sessions" style="width:auto;accent-color:var(--info)"> Record SSH sessions
|
|
1719
1732
|
</label>
|
|
1733
|
+
|
|
1734
|
+
<div x-show="editingServerId" class="fz-hostkey">
|
|
1735
|
+
<div class="fz-hostkey-head">
|
|
1736
|
+
<svg class="fz-icon fz-icon-sm" viewBox="0 0 24 24"><use href="#i-shield"/></svg>
|
|
1737
|
+
<span>Host key</span>
|
|
1738
|
+
<button x-show="hostKeyOf(editingServerId) && currentUser?.is_admin" type="button"
|
|
1739
|
+
@click="clearHostKey(editingServerId)"
|
|
1740
|
+
class="fz-btn fz-btn-ghost fz-btn-sm">Clear pinned key</button>
|
|
1741
|
+
</div>
|
|
1742
|
+
<code x-show="hostKeyOf(editingServerId)" x-text="hostKeyOf(editingServerId)" class="fz-hostkey-fp"></code>
|
|
1743
|
+
<div class="hint" x-show="hostKeyOf(editingServerId)">
|
|
1744
|
+
Recorded the first time this server was reached. A connection is refused if the
|
|
1745
|
+
key changes, so clear it only when you know why it changed — a rebuild, a
|
|
1746
|
+
rotation — and confirm the new one out of band.
|
|
1747
|
+
</div>
|
|
1748
|
+
<div class="hint" x-show="!hostKeyOf(editingServerId)">
|
|
1749
|
+
Nothing pinned yet. The next connection records the key this server presents,
|
|
1750
|
+
and every connection after that is checked against it.
|
|
1751
|
+
</div>
|
|
1752
|
+
</div>
|
|
1720
1753
|
<div x-show="!agentAvailable" class="hint" style="margin-top:12px">
|
|
1721
1754
|
The AI agent is not set up on this gateway.
|
|
1722
1755
|
<a href="#" @click.prevent="showServerModal=false;openAgentSettings()" style="color:var(--accent)">Configure it in Admin → AI agent</a>
|
|
@@ -3079,6 +3112,29 @@ function app() {
|
|
|
3079
3112
|
},
|
|
3080
3113
|
async loadStatuses() { try { this.serverStatuses = await this.apiCall('/api/servers/status'); } catch {} },
|
|
3081
3114
|
async loadGroups() { try { this.groups = await this.apiCall('/api/servers/groups'); } catch {} },
|
|
3115
|
+
hostKeyOf(serverId) {
|
|
3116
|
+
return (this.servers.find(s => s.id === serverId) || {}).host_key_fingerprint || '';
|
|
3117
|
+
},
|
|
3118
|
+
async clearHostKey(serverId) {
|
|
3119
|
+
const srv = this.servers.find(s => s.id === serverId);
|
|
3120
|
+
if (!srv) return;
|
|
3121
|
+
// Spelled out, because accepting a new key is the one moment the protection
|
|
3122
|
+
// is deliberately stood down.
|
|
3123
|
+
const ok = confirm(
|
|
3124
|
+
`Forget the pinned host key for ${srv.name}?\n\n` +
|
|
3125
|
+
`${srv.host_key_fingerprint}\n\n` +
|
|
3126
|
+
`The next connection will record whatever key the server presents and trust it. ` +
|
|
3127
|
+
`Do this only if you know why the key changed and have confirmed the new one ` +
|
|
3128
|
+
`some other way.`
|
|
3129
|
+
);
|
|
3130
|
+
if (!ok) return;
|
|
3131
|
+
try {
|
|
3132
|
+
const res = await this.apiCall(`/api/servers/${serverId}/host-key`, { method: 'DELETE' });
|
|
3133
|
+
await this.loadServers();
|
|
3134
|
+
this.showToast(`Forgot ${res.cleared || 'the pinned key'}`, 'success');
|
|
3135
|
+
this.log('status', `Cleared the host key for ${srv.name}`);
|
|
3136
|
+
} catch (e) { this.showToast(e.message || 'Could not clear the key', 'error'); }
|
|
3137
|
+
},
|
|
3082
3138
|
resetServerForm() { this.editingServerId = null; this.serverForm = { name: '', hostname: '', port: 22, username: '', auth_method: 'password', password: '', private_key: '', keyFileName: '', group: '', tagsStr: '', description: '', ssh_enabled: true, sftp_enabled: true, sftp_read_only: false, agent_enabled: false, record_sessions: false, sftp_allowed_paths_str: '', jump_via_id: null }; },
|
|
3083
3139
|
loadKeyFile(event) {
|
|
3084
3140
|
const file = event.target.files?.[0]; if (!file) return;
|
|
@@ -16,6 +16,7 @@ from webgate.db.engine import async_session_factory as _session_factory
|
|
|
16
16
|
from webgate.recordings.models import Recording
|
|
17
17
|
from webgate.recordings.recorder import CastRecorder
|
|
18
18
|
from webgate.runtime_config import store as runtime
|
|
19
|
+
from webgate.servers.crypto import CredentialUnreadable
|
|
19
20
|
from webgate.servers.service import (
|
|
20
21
|
get_server,
|
|
21
22
|
get_server_credentials,
|
|
@@ -140,7 +141,14 @@ async def ws_terminal_server(ws: WebSocket, server_id: int) -> None:
|
|
|
140
141
|
await ws.close(code=4003, reason="SSH is disabled for this server")
|
|
141
142
|
return
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
try:
|
|
145
|
+
password, private_key = get_server_credentials(server)
|
|
146
|
+
except CredentialUnreadable as exc:
|
|
147
|
+
# Raising here fails the WebSocket upgrade itself, so the browser sees a
|
|
148
|
+
# bare 500 with nothing to act on.
|
|
149
|
+
await ws.send_json({"type": "error", "message": str(exc)})
|
|
150
|
+
await ws.close(code=1011)
|
|
151
|
+
return
|
|
144
152
|
jump_kwargs = await resolve_jump_creds(session, server)
|
|
145
153
|
|
|
146
154
|
await ws.accept()
|
|
@@ -114,6 +114,16 @@ class SharedSessionManager:
|
|
|
114
114
|
self._by_token.pop(sess.share_token, None)
|
|
115
115
|
return sess
|
|
116
116
|
|
|
117
|
+
def count_for(self, username: str) -> int:
|
|
118
|
+
"""Live sessions this account owns on this worker.
|
|
119
|
+
|
|
120
|
+
Per worker, not per fleet: sessions are held in memory beside the PTY they
|
|
121
|
+
belong to, and the failure this guards against -- one account exhausting a
|
|
122
|
+
worker -- is per worker too. With N workers a cap of M allows up to N*M in
|
|
123
|
+
total, which the setting's help text says out loud.
|
|
124
|
+
"""
|
|
125
|
+
return sum(1 for s in self._by_id.values() if s.owner_username == username and not s.closed)
|
|
126
|
+
|
|
117
127
|
def get_by_id(self, session_id: str) -> SharedSession | None:
|
|
118
128
|
return self._by_id.get(session_id)
|
|
119
129
|
|