webgate 2.5.0__tar.gz → 2.7.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.
Files changed (205) hide show
  1. {webgate-2.5.0 → webgate-2.7.0}/.github/workflows/demo.yml +4 -2
  2. {webgate-2.5.0 → webgate-2.7.0}/.github/workflows/release.yml +5 -0
  3. {webgate-2.5.0 → webgate-2.7.0}/CHANGELOG.md +88 -0
  4. {webgate-2.5.0 → webgate-2.7.0}/PKG-INFO +15 -13
  5. {webgate-2.5.0 → webgate-2.7.0}/README.md +14 -12
  6. {webgate-2.5.0 → webgate-2.7.0}/ROADMAP.md +11 -2
  7. webgate-2.7.0/VERSION +1 -0
  8. {webgate-2.5.0 → webgate-2.7.0}/docs/LOCAL_TESTING.md +1 -1
  9. {webgate-2.5.0 → webgate-2.7.0}/docs/changelog.md +88 -0
  10. webgate-2.7.0/docs/guide/sso.md +83 -0
  11. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/users.md +2 -2
  12. webgate-2.7.0/docs/screenshots/v2/audit.png +0 -0
  13. webgate-2.7.0/docs/screenshots/v2/palette.png +0 -0
  14. webgate-2.7.0/docs/screenshots/v2/settings.png +0 -0
  15. webgate-2.7.0/docs/screenshots/v2/sftp.png +0 -0
  16. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/site-manager.png +0 -0
  17. {webgate-2.5.0 → webgate-2.7.0}/mkdocs.yml +1 -0
  18. {webgate-2.5.0 → webgate-2.7.0}/pyproject.toml +1 -1
  19. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/app.py +8 -1
  20. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/auth/models.py +4 -0
  21. webgate-2.7.0/src/webgate/auth/oidc.py +278 -0
  22. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/auth/routes.py +112 -0
  23. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/config.py +14 -0
  24. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/db/engine.py +1 -0
  25. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/files/routes.py +5 -1
  26. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/runtime_config/registry.py +144 -0
  27. webgate-2.7.0/src/webgate/servers/crypto.py +47 -0
  28. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/servers/monitor.py +17 -6
  29. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/servers/service.py +21 -7
  30. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/static/index.html +99 -7
  31. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/terminal/routes.py +9 -1
  32. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/terminal/shared.py +10 -0
  33. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/terminal/ws_handler.py +21 -0
  34. {webgate-2.5.0 → webgate-2.7.0}/tests/e2e/test_ui.py +4 -2
  35. webgate-2.7.0/tests/test_credential_rotation.py +124 -0
  36. webgate-2.7.0/tests/test_session_limit.py +168 -0
  37. webgate-2.7.0/tests/test_sso.py +365 -0
  38. {webgate-2.5.0 → webgate-2.7.0}/uv.lock +1 -1
  39. webgate-2.5.0/VERSION +0 -1
  40. webgate-2.5.0/docs/screenshots/v2/audit.png +0 -0
  41. webgate-2.5.0/docs/screenshots/v2/palette.png +0 -0
  42. webgate-2.5.0/docs/screenshots/v2/settings.png +0 -0
  43. webgate-2.5.0/docs/screenshots/v2/sftp.png +0 -0
  44. webgate-2.5.0/src/webgate/servers/crypto.py +0 -27
  45. {webgate-2.5.0 → webgate-2.7.0}/.dockerignore +0 -0
  46. {webgate-2.5.0 → webgate-2.7.0}/.github/workflows/ci.yml +0 -0
  47. {webgate-2.5.0 → webgate-2.7.0}/.github/workflows/docs.yml +0 -0
  48. {webgate-2.5.0 → webgate-2.7.0}/.gitignore +0 -0
  49. {webgate-2.5.0 → webgate-2.7.0}/DESIGN.md +0 -0
  50. {webgate-2.5.0 → webgate-2.7.0}/Dockerfile +0 -0
  51. {webgate-2.5.0 → webgate-2.7.0}/Dockerfile.demo +0 -0
  52. {webgate-2.5.0 → webgate-2.7.0}/Dockerfile.ssh-demo +0 -0
  53. {webgate-2.5.0 → webgate-2.7.0}/LICENSE +0 -0
  54. {webgate-2.5.0 → webgate-2.7.0}/PRODUCT.md +0 -0
  55. {webgate-2.5.0 → webgate-2.7.0}/RELEASING.md +0 -0
  56. {webgate-2.5.0 → webgate-2.7.0}/compose.dev.yml +0 -0
  57. {webgate-2.5.0 → webgate-2.7.0}/compose.ha.yml +0 -0
  58. {webgate-2.5.0 → webgate-2.7.0}/compose.playground.yml +0 -0
  59. {webgate-2.5.0 → webgate-2.7.0}/compose.yml +0 -0
  60. {webgate-2.5.0 → webgate-2.7.0}/deploy/nginx-ha.conf +0 -0
  61. {webgate-2.5.0 → webgate-2.7.0}/docs/api/auth.md +0 -0
  62. {webgate-2.5.0 → webgate-2.7.0}/docs/api/files.md +0 -0
  63. {webgate-2.5.0 → webgate-2.7.0}/docs/api/servers.md +0 -0
  64. {webgate-2.5.0 → webgate-2.7.0}/docs/api/terminal.md +0 -0
  65. {webgate-2.5.0 → webgate-2.7.0}/docs/architecture.md +0 -0
  66. {webgate-2.5.0 → webgate-2.7.0}/docs/getting-started/installation.md +0 -0
  67. {webgate-2.5.0 → webgate-2.7.0}/docs/getting-started/quickstart.md +0 -0
  68. {webgate-2.5.0 → webgate-2.7.0}/docs/getting-started/upgrade.md +0 -0
  69. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/access-control.md +0 -0
  70. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/advanced.md +0 -0
  71. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/files.md +0 -0
  72. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/ha.md +0 -0
  73. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/integrations.md +0 -0
  74. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/servers.md +0 -0
  75. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/settings.md +0 -0
  76. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/split.md +0 -0
  77. {webgate-2.5.0 → webgate-2.7.0}/docs/guide/terminal.md +0 -0
  78. {webgate-2.5.0 → webgate-2.7.0}/docs/index.md +0 -0
  79. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/access-control.png +0 -0
  80. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/audit.png +0 -0
  81. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/edit-access-control.png +0 -0
  82. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/editor.png +0 -0
  83. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/light-theme.png +0 -0
  84. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/login.png +0 -0
  85. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/new-server-form.png +0 -0
  86. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/01-login.png +0 -0
  87. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/02-alice-ldap-empty.png +0 -0
  88. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/03-add-server-jump-via.png +0 -0
  89. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/04-dashboard-3-servers.png +0 -0
  90. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/05-terminal-via-jump-host.png +0 -0
  91. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/06-snippet-whoami.png +0 -0
  92. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/07-shared-joiner.png +0 -0
  93. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/07-shared-owner.png +0 -0
  94. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/08-webhook-fired.png +0 -0
  95. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/09-recordings-list.png +0 -0
  96. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/10-recording-replay.png +0 -0
  97. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/playground/11-bob-limited.png +0 -0
  98. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/sftp-restricted.png +0 -0
  99. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/sftp.png +0 -0
  100. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/site-manager.png +0 -0
  101. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/split-view.png +0 -0
  102. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/ssh-disabled.png +0 -0
  103. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/terminal.png +0 -0
  104. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/users.png +0 -0
  105. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/01-login-demo-banner.png +0 -0
  106. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/02-dashboard-jump-host.png +0 -0
  107. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/03-terminal-snippets-jump.png +0 -0
  108. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/04-snippet-executed.png +0 -0
  109. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/05-sftp-via-jump.png +0 -0
  110. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/06-webhooks-modal.png +0 -0
  111. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/07-webhook-test-fired.png +0 -0
  112. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.3/08-add-server-jump-via.png +0 -0
  113. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.4/01-shared-terminal-owner.png +0 -0
  114. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.4/02-shared-terminal-joiner.png +0 -0
  115. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.4/03-recording-replay.png +0 -0
  116. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v0.4/04-recordings-modal.png +0 -0
  117. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/agent.png +0 -0
  118. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/branding.png +0 -0
  119. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/light.png +0 -0
  120. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/login.png +0 -0
  121. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/terminal.png +0 -0
  122. {webgate-2.5.0 → webgate-2.7.0}/docs/screenshots/v2/users.png +0 -0
  123. {webgate-2.5.0 → webgate-2.7.0}/fly.toml +0 -0
  124. {webgate-2.5.0 → webgate-2.7.0}/scripts/release_notes.py +0 -0
  125. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/__init__.py +0 -0
  126. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/__main__.py +0 -0
  127. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/__init__.py +0 -0
  128. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/cache.py +0 -0
  129. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/conversation.py +0 -0
  130. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/memory.py +0 -0
  131. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/models.py +0 -0
  132. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/provider.py +0 -0
  133. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/routes.py +0 -0
  134. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/service.py +0 -0
  135. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/sftp_tools.py +0 -0
  136. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/store.py +0 -0
  137. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/agent/tools.py +0 -0
  138. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/audit/__init__.py +0 -0
  139. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/audit/models.py +0 -0
  140. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/audit/service.py +0 -0
  141. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/auth/__init__.py +0 -0
  142. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/auth/ldap.py +0 -0
  143. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/auth/service.py +0 -0
  144. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/backup/__init__.py +0 -0
  145. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/backup/models.py +0 -0
  146. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/backup/routes.py +0 -0
  147. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/backup/service.py +0 -0
  148. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/branding/__init__.py +0 -0
  149. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/branding/routes.py +0 -0
  150. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/branding/store.py +0 -0
  151. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/db/__init__.py +0 -0
  152. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/demo.py +0 -0
  153. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/files/__init__.py +0 -0
  154. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/files/limits.py +0 -0
  155. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/files/models.py +0 -0
  156. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/files/pool.py +0 -0
  157. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/files/sftp_service.py +0 -0
  158. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/recordings/__init__.py +0 -0
  159. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/recordings/models.py +0 -0
  160. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/recordings/recorder.py +0 -0
  161. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/recordings/routes.py +0 -0
  162. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/runtime_config/__init__.py +0 -0
  163. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/runtime_config/routes.py +0 -0
  164. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/runtime_config/store.py +0 -0
  165. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/servers/__init__.py +0 -0
  166. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/servers/hostkeys.py +0 -0
  167. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/servers/models.py +0 -0
  168. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/servers/routes.py +0 -0
  169. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/snippets/__init__.py +0 -0
  170. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/snippets/models.py +0 -0
  171. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/snippets/routes.py +0 -0
  172. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/terminal/__init__.py +0 -0
  173. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/terminal/ssh_session.py +0 -0
  174. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/webhooks/__init__.py +0 -0
  175. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/webhooks/dispatcher.py +0 -0
  176. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/webhooks/models.py +0 -0
  177. {webgate-2.5.0 → webgate-2.7.0}/src/webgate/webhooks/routes.py +0 -0
  178. {webgate-2.5.0 → webgate-2.7.0}/tests/__init__.py +0 -0
  179. {webgate-2.5.0 → webgate-2.7.0}/tests/conftest.py +0 -0
  180. {webgate-2.5.0 → webgate-2.7.0}/tests/e2e/conftest.py +0 -0
  181. {webgate-2.5.0 → webgate-2.7.0}/tests/e2e/sshlab.py +0 -0
  182. {webgate-2.5.0 → webgate-2.7.0}/tests/test_agent_conversation.py +0 -0
  183. {webgate-2.5.0 → webgate-2.7.0}/tests/test_agent_memory.py +0 -0
  184. {webgate-2.5.0 → webgate-2.7.0}/tests/test_agent_settings.py +0 -0
  185. {webgate-2.5.0 → webgate-2.7.0}/tests/test_agent_sftp_tools.py +0 -0
  186. {webgate-2.5.0 → webgate-2.7.0}/tests/test_agent_tools.py +0 -0
  187. {webgate-2.5.0 → webgate-2.7.0}/tests/test_audit_coverage.py +0 -0
  188. {webgate-2.5.0 → webgate-2.7.0}/tests/test_auth.py +0 -0
  189. {webgate-2.5.0 → webgate-2.7.0}/tests/test_backup.py +0 -0
  190. {webgate-2.5.0 → webgate-2.7.0}/tests/test_branding.py +0 -0
  191. {webgate-2.5.0 → webgate-2.7.0}/tests/test_files.py +0 -0
  192. {webgate-2.5.0 → webgate-2.7.0}/tests/test_files_listing.py +0 -0
  193. {webgate-2.5.0 → webgate-2.7.0}/tests/test_host_keys.py +0 -0
  194. {webgate-2.5.0 → webgate-2.7.0}/tests/test_idle_sessions.py +0 -0
  195. {webgate-2.5.0 → webgate-2.7.0}/tests/test_limits.py +0 -0
  196. {webgate-2.5.0 → webgate-2.7.0}/tests/test_migrations.py +0 -0
  197. {webgate-2.5.0 → webgate-2.7.0}/tests/test_monitor.py +0 -0
  198. {webgate-2.5.0 → webgate-2.7.0}/tests/test_recordings.py +0 -0
  199. {webgate-2.5.0 → webgate-2.7.0}/tests/test_release_notes.py +0 -0
  200. {webgate-2.5.0 → webgate-2.7.0}/tests/test_secret_key_gate.py +0 -0
  201. {webgate-2.5.0 → webgate-2.7.0}/tests/test_servers.py +0 -0
  202. {webgate-2.5.0 → webgate-2.7.0}/tests/test_settings.py +0 -0
  203. {webgate-2.5.0 → webgate-2.7.0}/tests/test_snippets.py +0 -0
  204. {webgate-2.5.0 → webgate-2.7.0}/tests/test_terminal.py +0 -0
  205. {webgate-2.5.0 → webgate-2.7.0}/tests/test_webhook_dispatch.py +0 -0
@@ -4,8 +4,10 @@ name: Deploy demo
4
4
  # it spent months serving a version several releases behind the repository.
5
5
 
6
6
  on:
7
- release:
8
- types: [published]
7
+ # Called by release.yml rather than triggered by the release it creates: GitHub
8
+ # does not start workflows from events raised with GITHUB_TOKEN, so a
9
+ # `release: published` trigger here never fires and the demo silently stays behind.
10
+ workflow_call:
9
11
  workflow_dispatch:
10
12
 
11
13
  permissions:
@@ -121,3 +121,8 @@ jobs:
121
121
  tag_name: ${{ inputs.tag || github.ref_name }}
122
122
  name: v${{ needs.verify.outputs.version }}
123
123
  body_path: notes.md
124
+
125
+ demo:
126
+ needs: github_release
127
+ uses: ./.github/workflows/demo.yml
128
+ secrets: inherit
@@ -1,5 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.7.0 (2026-09-17) — single sign-on
4
+
5
+ LDAP already existed, but no company running Entra ID, Okta or Google Workspace is
6
+ going to keep a second directory for one tool. It was the first thing anyone asked
7
+ about, and the thing most likely to decide whether webgate gets deployed at all.
8
+
9
+ ### Single sign-on (OpenID Connect)
10
+
11
+ **Admin → Settings → Single sign-on**, alongside local accounts and LDAP rather than
12
+ instead of them. Register one redirect URI with your provider, fill in the issuer,
13
+ client ID, secret and scopes, and everything else — the endpoints, the signing keys —
14
+ comes from the provider's discovery document.
15
+
16
+ - **The ID token is actually verified**: signature against the provider's published
17
+ keys, plus issuer, audience, expiry, and a nonce tied to that one sign-in, so a token
18
+ captured from another flow cannot be replayed into this one. A token that merely
19
+ decodes proves nothing.
20
+ - **Authorization code with PKCE**, so it is safe with or without a client secret.
21
+ - **The in-progress sign-in lives in the database**, not in worker memory. The browser
22
+ can come back to a different worker than the one it left, which is the whole point of
23
+ the stateless design.
24
+ - **The session token never travels in a URL.** The callback hands the page a one-time
25
+ code, good for about a minute, which it trades for a session — so nothing sensitive
26
+ reaches browser history or a proxy log.
27
+ - **A provider group grants nothing until an admin maps it**, exactly as LDAP works. A
28
+ directory group created next month cannot quietly open a webgate group that happens
29
+ to share its name. A separate admin-groups list grants admin.
30
+ - Sign-ins are audited as `sso_login`, with the groups granted.
31
+ - Accounts created this way have **no local password** — nothing to reset, nothing to
32
+ leak.
33
+
34
+ Twenty-one tests, run against a real identity provider started for the test: it
35
+ publishes a discovery document and a JWKS and signs its own tokens, so the
36
+ verification is exercised rather than mocked away. A token signed with the wrong key,
37
+ issued for another audience, expired, or carrying another sign-in's nonce is refused,
38
+ and each of those is a test.
39
+
40
+ [Setup, and what to do when nobody gets any groups](https://kalexnolasco.github.io/webgate/guide/sso/).
41
+
42
+ 354 tests: 345 unit, 9 browser.
43
+
44
+ ---
45
+
46
+ ## v2.6.0 (2026-09-16) — rotating the secret key no longer breaks everything
47
+
48
+ ### Fixed
49
+
50
+ - **A changed `WEBGATE_SECRET_KEY` made webgate fall over instead of explaining
51
+ itself.** v2.2.0 refuses the shipped default, which asks every existing install to
52
+ set a real one; doing that leaves rows encrypted under the old key. `decrypt_value`
53
+ raised a bare Fernet error from twelve call sites and none of them caught it. The
54
+ status monitor died on its first sweep and froze every server's dot; opening a
55
+ terminal failed the WebSocket **upgrade**, so the browser got a 500 with nothing in
56
+ it. Found on the public demo, by walking into it.
57
+
58
+ An unreadable credential is now its own error, naming the server, why it happened
59
+ and what to do. The monitor marks that one server offline and carries on, the
60
+ terminal answers with an error frame, SFTP returns 502, and a connection test
61
+ reports it.
62
+
63
+ - **The demo banner covered the top of the interface.** It was `position: fixed` with
64
+ nothing making room for it, so the app bar sat underneath. It is in flow now, which
65
+ also holds when it wraps to two lines on a narrow screen. Only visible with
66
+ `WEBGATE_DEMO_MODE=true`, which is why no test caught it.
67
+
68
+ - **Diagrams that did not show.** Nine style directives set a pale fill and left the
69
+ label to the theme, so on GitHub's dark background they were light text on a light
70
+ box. All 21 diagrams in the repository now render, and every label is readable on
71
+ both grounds — checked with the Mermaid version GitHub actually uses, in a browser,
72
+ one diagram per page.
73
+
74
+ ### Added
75
+
76
+ - **The pinned host key is visible.** The fingerprint has been in the API since
77
+ v2.0.0 and nothing showed it. Servers carry a shield when a key is pinned, the edit
78
+ form shows the fingerprint, and an admin can clear it there — with the consequence
79
+ spelled out, since accepting a new key is the one moment the protection is stood
80
+ down.
81
+ - **A cap on concurrent SSH sessions per account** (**Admin → Settings → Security**,
82
+ 0 for none). There was nothing stopping a looping tab from opening sessions until
83
+ the worker ran out. Checked before the SSH connection is made, and counted per
84
+ worker — which the setting says, because a gateway behind a load balancer allows
85
+ that many on each.
86
+
87
+ 333 tests.
88
+
89
+ ---
90
+
3
91
  ## v2.5.0 (2026-09-16) — browser tests
4
92
 
5
93
  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.5.0
3
+ Version: 2.7.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:#e8f0fe,stroke:#4a90d9
130
- style firewall fill:#fff3e0,stroke:#ff9800
131
- style internal fill:#f0f9e8,stroke:#5cb85c
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:#e8f0fe,stroke:#4a90d9
164
- style firewall fill:#fff3e0,stroke:#ff9800
165
- style internal fill:#f0f9e8,stroke:#5cb85c
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
@@ -189,7 +189,7 @@ flowchart TB
189
189
  | **SFTP** | Full file ops + drag & drop upload, **sortable columns**, **multi-select** with batch ZIP download and delete, hidden-file toggle, in-browser editor (CodeMirror 6), PDF/image preview |
190
190
  | **Server Registry** | Groups, tags, password/key auth, encrypted at rest (Fernet), **verified host keys** (TOFU), **favourites and recents**, import/export JSON, **jump host / bastion** chaining |
191
191
  | **Access Control** | Admin/user roles, per-server SSH/SFTP toggles, SFTP path restrictions, read-only SFTP mode, group-based visibility |
192
- | **Auth** | JWT + bcrypt locally, **2FA TOTP**, **API keys** for automation, **LDAP / Active Directory** with group→role mapping |
192
+ | **Auth** | **Single sign-on (OpenID Connect)** — Entra ID, Okta, Google Workspace, Keycloak — with group→role mapping, plus JWT + bcrypt locally, **2FA TOTP**, **API keys** for automation, **LDAP / Active Directory** |
193
193
  | **Compliance** | **Session recording** to asciinema cast files with browser replay, off by default and opted into per server, **audit log** covering every SFTP operation, registry and account change, searchable by filename, **webhooks** (HMAC-signed) on key events |
194
194
  | **Migration** | **Full-state backup and restore** — servers with credentials, users, groups, webhooks and API keys in one passphrase-encrypted file, portable between instances |
195
195
  | **AI agent** | Per-server iterative chat over **Ollama** or **OpenRouter**, read-only inspection tools, **works on SFTP-only hosts**, cached results and searchable findings — configured in the admin panel, off until then |
@@ -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:#fff3e0,stroke:#ff9800
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/>(Admin → Settings → LDAP)"
410
+ subgraph MAP["Group mapping<br/>Admin &rarr; Settings &rarr; LDAP"]
411
411
  M["{<br/> &quot;devs&quot;: &quot;production&quot;,<br/> &quot;sre&quot;: &quot;all&quot;<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:#e8f0fe,stroke:#4a90d9
654
- style W3 fill:#e8f0fe,stroke:#4a90d9
655
- style PG fill:#fff3e0,stroke:#ff9800
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:
@@ -727,6 +727,7 @@ The demo middleware blocks all writes on `/api/*` (login, terminal share and tot
727
727
  | **Servers** | `GET/POST/PUT/DELETE /api/servers`, `POST /api/servers/{id}/test`, `GET /api/servers/groups`, `POST /api/servers/import`, `GET /api/servers/export`, `GET /api/servers/status` |
728
728
  | **Terminal** | `WS /api/ws/terminal/{server_id}` (owner), `WS /api/ws/terminal/quick` (one-off), `WS /api/ws/terminal/join/{token}?mode=rw\|ro` (joiner), `POST/DELETE /api/terminal/share/{session_id}` |
729
729
  | **Files (SFTP)** | `GET /ls`, `GET /read`, `GET /download`, `GET /download-zip` (one directory), `POST /download-zip` (a chosen selection), `POST /upload`, `PUT /write`, `POST /mkdir`, `POST /rename`, `DELETE /delete`, `POST /chmod`, `GET /stat` (all under `/api/files/{server_id}/`) |
730
+ | **Single sign-on** | `GET /api/auth/sso/start`, `GET /api/auth/sso/callback`, `POST /api/auth/sso/exchange` |
730
731
  | **Settings** | `GET /api/settings`, `PUT /api/settings`, `POST /api/settings/reset` (admin only) |
731
732
  | **Agent** | `GET/PUT /api/agent/settings`, `POST /api/agent/models`, `POST /api/agent/chat/{server_id}`, `GET/DELETE /api/agent/conversations/{server_id}`, `GET /api/agent/findings` |
732
733
  | **Branding** | `GET /api/branding` (public), `PUT/DELETE /api/branding` (admin only) |
@@ -790,6 +791,7 @@ docker compose -f compose.dev.yml up --build
790
791
 
791
792
  ## Security
792
793
 
794
+ - **Single sign-on** verifies the ID token properly: signature against the provider's published keys, plus issuer, audience, expiry and a per-sign-in nonce. The flow uses PKCE, its state lives in the database so it works across workers, and the session token is handed over as a one-time code rather than in a URL
793
795
  - **Host keys are verified** — trust on first use. The first connection to a server records the key it presents; every connection after that is checked against it *before authentication runs*, so a mismatch sends nothing. Accepting a changed key is a deliberate, audited admin action. This covers the terminal, SFTP, the connection pool, the status monitor, the agent and jump hosts
794
796
  - All SSH passwords and private keys are encrypted at rest with **Fernet** (key derived from `WEBGATE_SECRET_KEY`)
795
797
  - **The default secret key is refused.** That key signs every session token and derives the credential encryption key, and its default is published in this repository — a deployment that never changed it will hand a valid admin token to anyone who asks. webgate now stops at startup rather than serving under it, unless it is bound to loopback only
@@ -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:#e8f0fe,stroke:#4a90d9
81
- style firewall fill:#fff3e0,stroke:#ff9800
82
- style internal fill:#f0f9e8,stroke:#5cb85c
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:#e8f0fe,stroke:#4a90d9
115
- style firewall fill:#fff3e0,stroke:#ff9800
116
- style internal fill:#f0f9e8,stroke:#5cb85c
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
@@ -140,7 +140,7 @@ flowchart TB
140
140
  | **SFTP** | Full file ops + drag & drop upload, **sortable columns**, **multi-select** with batch ZIP download and delete, hidden-file toggle, in-browser editor (CodeMirror 6), PDF/image preview |
141
141
  | **Server Registry** | Groups, tags, password/key auth, encrypted at rest (Fernet), **verified host keys** (TOFU), **favourites and recents**, import/export JSON, **jump host / bastion** chaining |
142
142
  | **Access Control** | Admin/user roles, per-server SSH/SFTP toggles, SFTP path restrictions, read-only SFTP mode, group-based visibility |
143
- | **Auth** | JWT + bcrypt locally, **2FA TOTP**, **API keys** for automation, **LDAP / Active Directory** with group→role mapping |
143
+ | **Auth** | **Single sign-on (OpenID Connect)** — Entra ID, Okta, Google Workspace, Keycloak — with group→role mapping, plus JWT + bcrypt locally, **2FA TOTP**, **API keys** for automation, **LDAP / Active Directory** |
144
144
  | **Compliance** | **Session recording** to asciinema cast files with browser replay, off by default and opted into per server, **audit log** covering every SFTP operation, registry and account change, searchable by filename, **webhooks** (HMAC-signed) on key events |
145
145
  | **Migration** | **Full-state backup and restore** — servers with credentials, users, groups, webhooks and API keys in one passphrase-encrypted file, portable between instances |
146
146
  | **AI agent** | Per-server iterative chat over **Ollama** or **OpenRouter**, read-only inspection tools, **works on SFTP-only hosts**, cached results and searchable findings — configured in the admin panel, off until then |
@@ -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:#fff3e0,stroke:#ff9800
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/>(Admin → Settings → LDAP)"
361
+ subgraph MAP["Group mapping<br/>Admin &rarr; Settings &rarr; LDAP"]
362
362
  M["{<br/> &quot;devs&quot;: &quot;production&quot;,<br/> &quot;sre&quot;: &quot;all&quot;<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:#e8f0fe,stroke:#4a90d9
605
- style W3 fill:#e8f0fe,stroke:#4a90d9
606
- style PG fill:#fff3e0,stroke:#ff9800
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:
@@ -678,6 +678,7 @@ The demo middleware blocks all writes on `/api/*` (login, terminal share and tot
678
678
  | **Servers** | `GET/POST/PUT/DELETE /api/servers`, `POST /api/servers/{id}/test`, `GET /api/servers/groups`, `POST /api/servers/import`, `GET /api/servers/export`, `GET /api/servers/status` |
679
679
  | **Terminal** | `WS /api/ws/terminal/{server_id}` (owner), `WS /api/ws/terminal/quick` (one-off), `WS /api/ws/terminal/join/{token}?mode=rw\|ro` (joiner), `POST/DELETE /api/terminal/share/{session_id}` |
680
680
  | **Files (SFTP)** | `GET /ls`, `GET /read`, `GET /download`, `GET /download-zip` (one directory), `POST /download-zip` (a chosen selection), `POST /upload`, `PUT /write`, `POST /mkdir`, `POST /rename`, `DELETE /delete`, `POST /chmod`, `GET /stat` (all under `/api/files/{server_id}/`) |
681
+ | **Single sign-on** | `GET /api/auth/sso/start`, `GET /api/auth/sso/callback`, `POST /api/auth/sso/exchange` |
681
682
  | **Settings** | `GET /api/settings`, `PUT /api/settings`, `POST /api/settings/reset` (admin only) |
682
683
  | **Agent** | `GET/PUT /api/agent/settings`, `POST /api/agent/models`, `POST /api/agent/chat/{server_id}`, `GET/DELETE /api/agent/conversations/{server_id}`, `GET /api/agent/findings` |
683
684
  | **Branding** | `GET /api/branding` (public), `PUT/DELETE /api/branding` (admin only) |
@@ -741,6 +742,7 @@ docker compose -f compose.dev.yml up --build
741
742
 
742
743
  ## Security
743
744
 
745
+ - **Single sign-on** verifies the ID token properly: signature against the provider's published keys, plus issuer, audience, expiry and a per-sign-in nonce. The flow uses PKCE, its state lives in the database so it works across workers, and the session token is handed over as a one-time code rather than in a URL
744
746
  - **Host keys are verified** — trust on first use. The first connection to a server records the key it presents; every connection after that is checked against it *before authentication runs*, so a mismatch sends nothing. Accepting a changed key is a deliberate, audited admin action. This covers the terminal, SFTP, the connection pool, the status monitor, the agent and jump hosts
745
747
  - All SSH passwords and private keys are encrypted at rest with **Fernet** (key derived from `WEBGATE_SECRET_KEY`)
746
748
  - **The default secret key is refused.** That key signs every session token and derives the credential encryption key, and its default is published in this repository — a deployment that never changed it will hand a valid admin token to anyone who asks. webgate now stops at startup rather than serving under it, unless it is bound to loopback only
@@ -90,6 +90,17 @@ 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
+
99
+ ### v2.7.0 — Single sign-on (2026-09-17)
100
+ - [x] **OpenID Connect sign-on** for Entra ID, Okta, Google Workspace, Keycloak and Authentik, with the ID token verified against the provider's keys and a per-sign-in nonce
101
+ - [x] **Provider groups map to webgate groups**, and an unmapped group grants nothing
102
+ - [x] **The session token is handed over as a one-time code**, never in a URL
103
+
93
104
  ---
94
105
 
95
106
  ## Planned
@@ -97,8 +108,6 @@ Tracks the development plan for **webgate**. Items are organized by release.
97
108
  ### v2.1.x — Next
98
109
  | Feature | Priority | Description |
99
110
  |---------|----------|-------------|
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
111
  | Internationalization (i18n) | Low | UI translations starting with English / Spanish |
103
112
 
104
113
  ### Later
webgate-2.7.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.7.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:#fff3e0,stroke:#ff9800
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,93 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.7.0 (2026-09-17) — single sign-on
4
+
5
+ LDAP already existed, but no company running Entra ID, Okta or Google Workspace is
6
+ going to keep a second directory for one tool. It was the first thing anyone asked
7
+ about, and the thing most likely to decide whether webgate gets deployed at all.
8
+
9
+ ### Single sign-on (OpenID Connect)
10
+
11
+ **Admin → Settings → Single sign-on**, alongside local accounts and LDAP rather than
12
+ instead of them. Register one redirect URI with your provider, fill in the issuer,
13
+ client ID, secret and scopes, and everything else — the endpoints, the signing keys —
14
+ comes from the provider's discovery document.
15
+
16
+ - **The ID token is actually verified**: signature against the provider's published
17
+ keys, plus issuer, audience, expiry, and a nonce tied to that one sign-in, so a token
18
+ captured from another flow cannot be replayed into this one. A token that merely
19
+ decodes proves nothing.
20
+ - **Authorization code with PKCE**, so it is safe with or without a client secret.
21
+ - **The in-progress sign-in lives in the database**, not in worker memory. The browser
22
+ can come back to a different worker than the one it left, which is the whole point of
23
+ the stateless design.
24
+ - **The session token never travels in a URL.** The callback hands the page a one-time
25
+ code, good for about a minute, which it trades for a session — so nothing sensitive
26
+ reaches browser history or a proxy log.
27
+ - **A provider group grants nothing until an admin maps it**, exactly as LDAP works. A
28
+ directory group created next month cannot quietly open a webgate group that happens
29
+ to share its name. A separate admin-groups list grants admin.
30
+ - Sign-ins are audited as `sso_login`, with the groups granted.
31
+ - Accounts created this way have **no local password** — nothing to reset, nothing to
32
+ leak.
33
+
34
+ Twenty-one tests, run against a real identity provider started for the test: it
35
+ publishes a discovery document and a JWKS and signs its own tokens, so the
36
+ verification is exercised rather than mocked away. A token signed with the wrong key,
37
+ issued for another audience, expired, or carrying another sign-in's nonce is refused,
38
+ and each of those is a test.
39
+
40
+ [Setup, and what to do when nobody gets any groups](https://kalexnolasco.github.io/webgate/guide/sso/).
41
+
42
+ 354 tests: 345 unit, 9 browser.
43
+
44
+ ---
45
+
46
+ ## v2.6.0 (2026-09-16) — rotating the secret key no longer breaks everything
47
+
48
+ ### Fixed
49
+
50
+ - **A changed `WEBGATE_SECRET_KEY` made webgate fall over instead of explaining
51
+ itself.** v2.2.0 refuses the shipped default, which asks every existing install to
52
+ set a real one; doing that leaves rows encrypted under the old key. `decrypt_value`
53
+ raised a bare Fernet error from twelve call sites and none of them caught it. The
54
+ status monitor died on its first sweep and froze every server's dot; opening a
55
+ terminal failed the WebSocket **upgrade**, so the browser got a 500 with nothing in
56
+ it. Found on the public demo, by walking into it.
57
+
58
+ An unreadable credential is now its own error, naming the server, why it happened
59
+ and what to do. The monitor marks that one server offline and carries on, the
60
+ terminal answers with an error frame, SFTP returns 502, and a connection test
61
+ reports it.
62
+
63
+ - **The demo banner covered the top of the interface.** It was `position: fixed` with
64
+ nothing making room for it, so the app bar sat underneath. It is in flow now, which
65
+ also holds when it wraps to two lines on a narrow screen. Only visible with
66
+ `WEBGATE_DEMO_MODE=true`, which is why no test caught it.
67
+
68
+ - **Diagrams that did not show.** Nine style directives set a pale fill and left the
69
+ label to the theme, so on GitHub's dark background they were light text on a light
70
+ box. All 21 diagrams in the repository now render, and every label is readable on
71
+ both grounds — checked with the Mermaid version GitHub actually uses, in a browser,
72
+ one diagram per page.
73
+
74
+ ### Added
75
+
76
+ - **The pinned host key is visible.** The fingerprint has been in the API since
77
+ v2.0.0 and nothing showed it. Servers carry a shield when a key is pinned, the edit
78
+ form shows the fingerprint, and an admin can clear it there — with the consequence
79
+ spelled out, since accepting a new key is the one moment the protection is stood
80
+ down.
81
+ - **A cap on concurrent SSH sessions per account** (**Admin → Settings → Security**,
82
+ 0 for none). There was nothing stopping a looping tab from opening sessions until
83
+ the worker ran out. Checked before the SSH connection is made, and counted per
84
+ worker — which the setting says, because a gateway behind a load balancer allows
85
+ that many on each.
86
+
87
+ 333 tests.
88
+
89
+ ---
90
+
3
91
  ## v2.5.0 (2026-09-16) — browser tests
4
92
 
5
93
  The audit gap in v2.4.0 was reported by a person using the interface, and no test in
@@ -0,0 +1,83 @@
1
+ # Single sign-on
2
+
3
+ **Admin → Settings → Single sign-on.** OpenID Connect, which is what Entra ID, Okta,
4
+ Google Workspace, Keycloak and Authentik all speak.
5
+
6
+ LDAP still works, and so do local accounts. Turning this on adds a button to the
7
+ sign-in screen; it does not take anything away.
8
+
9
+ ## What to register with your provider
10
+
11
+ One redirect URI:
12
+
13
+ ```
14
+ https://your-webgate/api/auth/sso/callback
15
+ ```
16
+
17
+ Then fill in four things here: the **issuer URL**, the **client ID**, the **client
18
+ secret** if your provider issued one, and the **scopes**. Everything else — the
19
+ authorize and token endpoints, the signing keys — is read from
20
+ `{issuer}/.well-known/openid-configuration`, so there is nothing else to copy across.
21
+
22
+ | Provider | Issuer looks like |
23
+ |---|---|
24
+ | Entra ID | `https://login.microsoftonline.com/<tenant-id>/v2.0` |
25
+ | Okta | `https://<org>.okta.com/oauth2/default` |
26
+ | Google Workspace | `https://accounts.google.com` |
27
+ | Keycloak | `https://<host>/realms/<realm>` |
28
+ | Authentik | `https://<host>/application/o/<slug>/` |
29
+
30
+ !!! tip "Behind a proxy that rewrites the host"
31
+ webgate works out its own address from the request. If a proxy changes it, set
32
+ **Public URL** and the redirect will be that plus `/api/auth/sso/callback`.
33
+
34
+ ## Groups decide what people can reach
35
+
36
+ A group from your directory means nothing here until an admin maps it:
37
+
38
+ ```json
39
+ {"infra-oncall": "prod", "platform": "core"}
40
+ ```
41
+
42
+ An unmapped group grants nothing. That is deliberate: a directory group created next
43
+ month must not quietly open a webgate group that happens to share its name.
44
+
45
+ **Admin groups** is a separate list — membership in any of them makes the person a
46
+ webgate admin, and admins see every server.
47
+
48
+ Both are re-read on every sign-in, so removing somebody from a group in your directory
49
+ takes effect the next time they sign in. It does not end a session already open; the
50
+ session-token lifetime under **Security** decides that.
51
+
52
+ ### If nobody gets any groups
53
+
54
+ Your provider is probably not sending the claim. Two things to check: the **scopes**
55
+ usually need an extra one (`groups` on Okta and Keycloak, `GroupMember.Read.All`
56
+ consented on Entra), and the **groups claim** has to match the name your provider
57
+ uses. Entra sends group *object ids* by default rather than names, unless the app
58
+ registration is set to emit names.
59
+
60
+ ## What happens on sign-in
61
+
62
+ 1. The button sends the browser to your provider, with PKCE and a one-time `state` and
63
+ `nonce`.
64
+ 2. Your provider sends it back to the callback with a code.
65
+ 3. webgate exchanges the code, then **verifies the ID token** — signature against your
66
+ provider's published keys, issuer, audience, expiry, and that the nonce matches the
67
+ one it issued for this sign-in.
68
+ 4. The account is created or refreshed, with the groups from the mapping.
69
+ 5. The browser is handed a **one-time code**, and trades it for a session. The session
70
+ token is never put in a URL, so it cannot end up in browser history or a proxy log.
71
+
72
+ The in-progress sign-in lives in the database, not in memory, so the browser can come
73
+ back to a different worker than the one it left. Abandoned attempts are cleared after
74
+ ten minutes.
75
+
76
+ ## Notes
77
+
78
+ - A person who signs in this way has **no local password**. There is nothing to reset
79
+ and nothing to leak; your provider is the credential.
80
+ - 2FA is your provider's business here. webgate's own TOTP applies to local accounts.
81
+ - Every sign-in is recorded as `sso_login` in the audit log, with the groups granted.
82
+ - Turning SSO off leaves the accounts it created. They simply have no working password
83
+ until an admin sets one.
@@ -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:#e8f0fe,stroke:#4a90d9
46
- style BOB fill:#fff3e0,stroke:#ff9800
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
@@ -46,6 +46,7 @@ nav:
46
46
  - Split View: guide/split.md
47
47
  - User Management: guide/users.md
48
48
  - Access model (groups, LDAP, tags): guide/access-control.md
49
+ - Single sign-on (OIDC): guide/sso.md
49
50
  - Advanced (jump host, snippets, sharing, recording): guide/advanced.md
50
51
  - Integrations (webhooks, LDAP, API keys, reverse proxy): guide/integrations.md
51
52
  - Settings (admin panel): guide/settings.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webgate"
3
- version = "2.5.0"
3
+ version = "2.7.0"
4
4
  description = "Self-hosted web application for remote server management via SSH terminal and SFTP file browser"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
13
13
  from webgate import __version__
14
14
  from webgate.agent.routes import router as agent_router
15
15
  from webgate.agent.store import load_config
16
+ from webgate.auth import oidc
16
17
  from webgate.auth.routes import limiter
17
18
  from webgate.auth.routes import router as auth_router
18
19
  from webgate.auth.service import seed_admin
@@ -95,7 +96,13 @@ def create_app() -> FastAPI:
95
96
  agent_available = False
96
97
  if not settings.demo_mode:
97
98
  agent_available = (await load_config(session)).enabled
98
- return {"demo_mode": settings.demo_mode, "agent_available": agent_available}
99
+ return {
100
+ "demo_mode": settings.demo_mode,
101
+ "agent_available": agent_available,
102
+ # The sign-in screen needs this before anyone has authenticated.
103
+ "sso_enabled": oidc.enabled(),
104
+ "sso_name": oidc.provider_name() if oidc.enabled() else "",
105
+ }
99
106
 
100
107
  if settings.demo_mode:
101
108
  # In demo mode block any state-changing request on /api/* except an
@@ -77,6 +77,10 @@ class UserUpdateGroups(BaseModel):
77
77
  allowed_groups: list[str]
78
78
 
79
79
 
80
+ class SsoExchange(BaseModel):
81
+ code: str
82
+
83
+
80
84
  class ChangePassword(BaseModel):
81
85
  new_password: str
82
86