webgate 0.4.1__tar.gz → 0.4.2__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 (111) hide show
  1. {webgate-0.4.1 → webgate-0.4.2}/CHANGELOG.md +39 -0
  2. webgate-0.4.2/PKG-INFO +649 -0
  3. webgate-0.4.2/README.md +600 -0
  4. webgate-0.4.2/ROADMAP.md +79 -0
  5. {webgate-0.4.1 → webgate-0.4.2}/compose.dev.yml +9 -0
  6. webgate-0.4.2/compose.yml +29 -0
  7. {webgate-0.4.1 → webgate-0.4.2}/pyproject.toml +2 -1
  8. webgate-0.4.2/src/webgate/auth/ldap.py +163 -0
  9. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/routes.py +29 -8
  10. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/config.py +12 -0
  11. {webgate-0.4.1 → webgate-0.4.2}/uv.lock +15 -1
  12. webgate-0.4.1/PKG-INFO +0 -834
  13. webgate-0.4.1/README.md +0 -786
  14. webgate-0.4.1/ROADMAP.md +0 -93
  15. webgate-0.4.1/compose.yml +0 -15
  16. {webgate-0.4.1 → webgate-0.4.2}/.dockerignore +0 -0
  17. {webgate-0.4.1 → webgate-0.4.2}/.github/workflows/docs.yml +0 -0
  18. {webgate-0.4.1 → webgate-0.4.2}/.gitignore +0 -0
  19. {webgate-0.4.1 → webgate-0.4.2}/Dockerfile +0 -0
  20. {webgate-0.4.1 → webgate-0.4.2}/Dockerfile.demo +0 -0
  21. {webgate-0.4.1 → webgate-0.4.2}/Dockerfile.ssh-demo +0 -0
  22. {webgate-0.4.1 → webgate-0.4.2}/LICENSE +0 -0
  23. {webgate-0.4.1 → webgate-0.4.2}/VERSION +0 -0
  24. {webgate-0.4.1 → webgate-0.4.2}/after-reload.yaml +0 -0
  25. {webgate-0.4.1 → webgate-0.4.2}/docs/api/auth.md +0 -0
  26. {webgate-0.4.1 → webgate-0.4.2}/docs/api/files.md +0 -0
  27. {webgate-0.4.1 → webgate-0.4.2}/docs/api/servers.md +0 -0
  28. {webgate-0.4.1 → webgate-0.4.2}/docs/api/terminal.md +0 -0
  29. {webgate-0.4.1 → webgate-0.4.2}/docs/changelog.md +0 -0
  30. {webgate-0.4.1 → webgate-0.4.2}/docs/getting-started/installation.md +0 -0
  31. {webgate-0.4.1 → webgate-0.4.2}/docs/getting-started/quickstart.md +0 -0
  32. {webgate-0.4.1 → webgate-0.4.2}/docs/guide/files.md +0 -0
  33. {webgate-0.4.1 → webgate-0.4.2}/docs/guide/servers.md +0 -0
  34. {webgate-0.4.1 → webgate-0.4.2}/docs/guide/split.md +0 -0
  35. {webgate-0.4.1 → webgate-0.4.2}/docs/guide/terminal.md +0 -0
  36. {webgate-0.4.1 → webgate-0.4.2}/docs/guide/users.md +0 -0
  37. {webgate-0.4.1 → webgate-0.4.2}/docs/index.md +0 -0
  38. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/access-control.png +0 -0
  39. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/audit.png +0 -0
  40. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/edit-access-control.png +0 -0
  41. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/editor.png +0 -0
  42. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/light-theme.png +0 -0
  43. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/login.png +0 -0
  44. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/new-server-form.png +0 -0
  45. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/sftp-restricted.png +0 -0
  46. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/sftp.png +0 -0
  47. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/site-manager.png +0 -0
  48. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/split-view.png +0 -0
  49. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/ssh-disabled.png +0 -0
  50. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/terminal.png +0 -0
  51. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/users.png +0 -0
  52. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/01-login-demo-banner.png +0 -0
  53. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/02-dashboard-jump-host.png +0 -0
  54. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/03-terminal-snippets-jump.png +0 -0
  55. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/04-snippet-executed.png +0 -0
  56. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/05-sftp-via-jump.png +0 -0
  57. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/06-webhooks-modal.png +0 -0
  58. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/07-webhook-test-fired.png +0 -0
  59. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/08-add-server-jump-via.png +0 -0
  60. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/01-shared-terminal-owner.png +0 -0
  61. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/02-shared-terminal-joiner.png +0 -0
  62. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/03-recording-replay.png +0 -0
  63. {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/04-recordings-modal.png +0 -0
  64. {webgate-0.4.1 → webgate-0.4.2}/fly.toml +0 -0
  65. {webgate-0.4.1 → webgate-0.4.2}/mkdocs.yml +0 -0
  66. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/__init__.py +0 -0
  67. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/__main__.py +0 -0
  68. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/app.py +0 -0
  69. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/audit/__init__.py +0 -0
  70. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/audit/models.py +0 -0
  71. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/audit/service.py +0 -0
  72. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/__init__.py +0 -0
  73. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/models.py +0 -0
  74. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/service.py +0 -0
  75. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/db/__init__.py +0 -0
  76. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/db/engine.py +0 -0
  77. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/demo.py +0 -0
  78. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/__init__.py +0 -0
  79. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/models.py +0 -0
  80. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/pool.py +0 -0
  81. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/routes.py +0 -0
  82. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/sftp_service.py +0 -0
  83. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/__init__.py +0 -0
  84. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/models.py +0 -0
  85. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/recorder.py +0 -0
  86. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/routes.py +0 -0
  87. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/__init__.py +0 -0
  88. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/crypto.py +0 -0
  89. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/models.py +0 -0
  90. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/monitor.py +0 -0
  91. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/routes.py +0 -0
  92. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/service.py +0 -0
  93. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/snippets/__init__.py +0 -0
  94. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/snippets/models.py +0 -0
  95. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/snippets/routes.py +0 -0
  96. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/static/index.html +0 -0
  97. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/__init__.py +0 -0
  98. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/routes.py +0 -0
  99. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/shared.py +0 -0
  100. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/ssh_session.py +0 -0
  101. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/ws_handler.py +0 -0
  102. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/__init__.py +0 -0
  103. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/dispatcher.py +0 -0
  104. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/models.py +0 -0
  105. {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/routes.py +0 -0
  106. {webgate-0.4.1 → webgate-0.4.2}/tests/__init__.py +0 -0
  107. {webgate-0.4.1 → webgate-0.4.2}/tests/conftest.py +0 -0
  108. {webgate-0.4.1 → webgate-0.4.2}/tests/test_auth.py +0 -0
  109. {webgate-0.4.1 → webgate-0.4.2}/tests/test_files.py +0 -0
  110. {webgate-0.4.1 → webgate-0.4.2}/tests/test_servers.py +0 -0
  111. {webgate-0.4.1 → webgate-0.4.2}/tests/test_terminal.py +0 -0
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.2 (2026-04-15)
4
+
5
+ ### Features
6
+
7
+ - **LDAP / Active Directory authentication** -- enable with `WEBGATE_LDAP_ENABLED=true` and login flow falls back to LDAP after the local user table. On a successful LDAP bind the user is auto-provisioned (or refreshed) in the local DB, with `allowed_groups` derived from LDAP group memberships and admin status from a configurable list of admin groups.
8
+
9
+ ### Configuration
10
+
11
+ | Env var | Description |
12
+ |---|---|
13
+ | `WEBGATE_LDAP_ENABLED` | `true` to enable LDAP login |
14
+ | `WEBGATE_LDAP_URL` | `ldap://host:389` or `ldaps://host:636` |
15
+ | `WEBGATE_LDAP_BIND_DN` | service account DN, e.g. `cn=admin,dc=example,dc=com` |
16
+ | `WEBGATE_LDAP_BIND_PASSWORD` | service account password |
17
+ | `WEBGATE_LDAP_USER_BASE` | e.g. `ou=people,dc=example,dc=com` |
18
+ | `WEBGATE_LDAP_USER_FILTER` | default `(uid={username})` (AD: `(sAMAccountName={username})`) |
19
+ | `WEBGATE_LDAP_GROUP_BASE` | e.g. `ou=groups,dc=example,dc=com` (empty = no group lookup) |
20
+ | `WEBGATE_LDAP_GROUP_FILTER` | default `(member={dn})` (AD nested: `(member:1.2.840.113556.1.4.1941:={dn})`) |
21
+ | `WEBGATE_LDAP_GROUP_MAP` | JSON `{"ldap-cn":"webgate-group"}` |
22
+ | `WEBGATE_LDAP_ADMIN_GROUPS` | JSON list of LDAP CNs that grant admin |
23
+
24
+ ### Details
25
+
26
+ - Search-then-bind flow: bind as service account, search by username, re-bind as the user with their password
27
+ - LDAP filter values are properly escaped (RFC 4515)
28
+ - All `ldap3` calls run in `asyncio.to_thread` to avoid blocking the event loop
29
+ - Local accounts (admin, API keys, 2FA) keep working as before -- LDAP is only consulted after a local-credential miss
30
+ - Re-login refreshes admin status and group mapping from LDAP every time
31
+
32
+ ### Verified
33
+
34
+ End-to-end against `osixia/openldap` with an `alice` user in groups `devs` and `admins`:
35
+
36
+ - `alice / alicepass` → 200, JWT issued, `/api/auth/me` returns `is_admin=true`, `allowed_groups=["all","production"]` (mapped from LDAP CNs)
37
+ - `alice / WRONG` → 401 Invalid credentials
38
+ - `admin / admin` (local fallback) → still works
39
+
40
+ ---
41
+
3
42
  ## v0.4.1 (2026-04-15)
4
43
 
5
44
  ### Features
webgate-0.4.2/PKG-INFO ADDED
@@ -0,0 +1,649 @@
1
+ Metadata-Version: 2.4
2
+ Name: webgate
3
+ Version: 0.4.2
4
+ Summary: Self-hosted web application for remote server management via SSH terminal and SFTP file browser
5
+ Project-URL: Homepage, https://github.com/kalexnolasco/webgate
6
+ Project-URL: Documentation, https://kalexnolasco.github.io/webgate/
7
+ Project-URL: Repository, https://github.com/kalexnolasco/webgate
8
+ Project-URL: Changelog, https://github.com/kalexnolasco/webgate/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/kalexnolasco/webgate/issues
10
+ Author-email: Kevin Nolasco <kalex.nolasco@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: asyncssh,fastapi,server-management,sftp,ssh,terminal,web
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: System Administrators
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: MacOS
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Requires-Dist: aiofiles>=24.1.0
29
+ Requires-Dist: aiosqlite>=0.21.0
30
+ Requires-Dist: asyncpg>=0.30.0
31
+ Requires-Dist: asyncssh>=2.18.0
32
+ Requires-Dist: bcrypt>=4.2.0
33
+ Requires-Dist: cryptography>=44.0.0
34
+ Requires-Dist: fastapi>=0.115.0
35
+ Requires-Dist: httpx>=0.28.0
36
+ Requires-Dist: ldap3>=2.9.1
37
+ Requires-Dist: passlib[bcrypt]>=1.7.4
38
+ Requires-Dist: pydantic-settings>=2.7.0
39
+ Requires-Dist: pydantic>=2.10.0
40
+ Requires-Dist: pyotp>=2.9.0
41
+ Requires-Dist: python-jose[cryptography]>=3.3.0
42
+ Requires-Dist: python-multipart>=0.0.18
43
+ Requires-Dist: qrcode[pil]>=8.2
44
+ Requires-Dist: slowapi>=0.1.9
45
+ Requires-Dist: sqlalchemy>=2.0.36
46
+ Requires-Dist: uvicorn[standard]>=0.34.0
47
+ Requires-Dist: websockets>=14.0
48
+ Description-Content-Type: text/markdown
49
+
50
+ # webgate
51
+
52
+ [![PyPI](https://img.shields.io/pypi/v/webgate?style=flat-square)](https://pypi.org/project/webgate/)
53
+ [![Python](https://img.shields.io/pypi/pyversions/webgate?style=flat-square)](https://pypi.org/project/webgate/)
54
+ [![License](https://img.shields.io/pypi/l/webgate?style=flat-square)](https://github.com/kalexnolasco/webgate/blob/main/LICENSE)
55
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-009688?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com)
56
+ [![Docker](https://img.shields.io/badge/docker-ready-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/r/kalexnolasco/webgate)
57
+ [![Status](https://img.shields.io/badge/status-beta-orange?style=flat-square)](https://pypi.org/project/webgate/)
58
+ [![Docs](https://img.shields.io/badge/docs-kalexnolasco.github.io-blue?style=flat-square)](https://kalexnolasco.github.io/webgate/)
59
+
60
+ Self-hosted web app for remote server management — **SSH terminal**, **SFTP file browser**, **server registry**, all in your browser. A modern Python replacement that combines the best of [webssh](https://github.com/huashengdun/webssh) and [filebrowser](https://github.com/filebrowser/filebrowser) into a single tool with a FileZilla-inspired interface.
61
+
62
+ > 🎮 **Try it live: [webgate-demo.fly.dev](https://webgate-demo.fly.dev/)** — login `demo` / `demo` (read-only sandbox, resets hourly)
63
+ >
64
+ > 📖 **Docs: [kalexnolasco.github.io/webgate](https://kalexnolasco.github.io/webgate/)**
65
+
66
+ ---
67
+
68
+ ## Quick start
69
+
70
+ ```bash
71
+ export WEBGATE_SECRET_KEY=$(openssl rand -hex 32)
72
+ docker compose up -d
73
+ # open http://localhost:8443/ — login: admin / admin
74
+ ```
75
+
76
+ That's it. The first login forces a password change. Add servers from the **Site Manager**, click **SSH** or **SFTP** to connect.
77
+
78
+ For a richer dev environment with a sandboxed SSH target pre-baked: `docker compose -f compose.dev.yml up --build`.
79
+
80
+ ---
81
+
82
+ ## Why webgate?
83
+
84
+ Managing remote servers means juggling SSH clients, SFTP tools, credentials and VPN configs across your team. In many real-world setups **direct SSH access to every server isn't possible** — only HTTP(S) reaches the gateway.
85
+
86
+ ### The problem
87
+
88
+ ```mermaid
89
+ flowchart TB
90
+ subgraph internet ["Internet"]
91
+ YOU["Your Team"]
92
+ end
93
+ subgraph firewall ["Client Firewall"]
94
+ GW["Gateway Server<br/>(HTTP only)"]
95
+ subgraph internal ["Internal Network"]
96
+ DB1[(PostgreSQL<br/>10.0.1.10)]
97
+ DB2[(MySQL<br/>10.0.1.11)]
98
+ APP1["App Server<br/>10.0.1.20"]
99
+ APP2["App Server<br/>10.0.1.21"]
100
+ WORKER["Worker<br/>10.0.1.30"]
101
+ REDIS["Redis<br/>10.0.1.40"]
102
+ end
103
+ end
104
+ YOU -- "HTTPS :443" --> GW
105
+ GW -. "SSH :22" .-> DB1
106
+ GW -. "SSH :22" .-> DB2
107
+ GW -. "SSH :22" .-> APP1
108
+ GW -. "SSH :22" .-> APP2
109
+ GW -. "SSH :22" .-> WORKER
110
+ GW -. "SSH :22" .-> REDIS
111
+ style internet fill:#e8f0fe,stroke:#4a90d9
112
+ style firewall fill:#fff3e0,stroke:#ff9800
113
+ style internal fill:#f0f9e8,stroke:#5cb85c
114
+ style GW fill:#ffcc02,stroke:#e6a800,color:#333
115
+ style YOU fill:#4a90d9,stroke:#2a6cb5,color:#fff
116
+ ```
117
+
118
+ ### The solution
119
+
120
+ Deploy webgate on the gateway. Everyone gets browser-based SSH and SFTP to every internal server — no VPN, no scattered SSH keys, full audit trail.
121
+
122
+ ```mermaid
123
+ flowchart TB
124
+ subgraph internet ["Internet"]
125
+ ENG1["Engineer 1<br/>(Browser)"]
126
+ ENG2["Engineer 2<br/>(Browser)"]
127
+ ENG3["Engineer 3<br/>(Browser)"]
128
+ end
129
+ subgraph firewall ["Client Firewall"]
130
+ WG["webgate<br/>Gateway Server :443"]
131
+ subgraph internal ["Internal Network"]
132
+ DB1[(PostgreSQL)]
133
+ APP1["App Server"]
134
+ WORKER["Worker"]
135
+ REDIS["Redis"]
136
+ end
137
+ end
138
+ ENG1 -- "HTTPS" --> WG
139
+ ENG2 -- "HTTPS" --> WG
140
+ ENG3 -- "HTTPS" --> WG
141
+ WG -- "SSH/SFTP" --> DB1
142
+ WG -- "SSH/SFTP" --> APP1
143
+ WG -- "SSH/SFTP" --> WORKER
144
+ WG -- "SSH/SFTP" --> REDIS
145
+ style internet fill:#e8f0fe,stroke:#4a90d9
146
+ style firewall fill:#fff3e0,stroke:#ff9800
147
+ style internal fill:#f0f9e8,stroke:#5cb85c
148
+ style WG fill:#5cb85c,stroke:#449d44,color:#fff
149
+ style ENG1 fill:#4a90d9,stroke:#2a6cb5,color:#fff
150
+ style ENG2 fill:#4a90d9,stroke:#2a6cb5,color:#fff
151
+ style ENG3 fill:#4a90d9,stroke:#2a6cb5,color:#fff
152
+ ```
153
+
154
+ ### Use cases
155
+
156
+ | Scenario | How webgate helps |
157
+ |---|---|
158
+ | **Restricted client networks** | Only the gateway is HTTP-reachable; webgate proxies SSH/SFTP from there |
159
+ | **On-call / incident response** | Open a browser anywhere, no laptop with keys needed; share the live session for pair-debugging |
160
+ | **Team onboarding** | Admin creates a user, assigns groups; new engineer has access in seconds |
161
+ | **Audit & compliance** | Centralized access point, structured audit log, optional asciinema session recording |
162
+ | **Multi-client / agency** | One webgate per client, isolated server registries; run lots of them cheaply |
163
+
164
+ ---
165
+
166
+ ## Features
167
+
168
+ | Category | Capabilities |
169
+ |---|---|
170
+ | **Terminal** | xterm.js + asyncssh, multi-tab, resize, copy/paste, **shared sessions** with one-click URL, **command snippets** library |
171
+ | **SFTP** | Full file ops + drag & drop upload, ZIP folder download, in-browser editor (CodeMirror 6), PDF/image preview |
172
+ | **Server Registry** | Groups, tags, password/key auth, encrypted at rest (Fernet), import/export JSON, **jump host / bastion** chaining |
173
+ | **Access Control** | Admin/user roles, per-server SSH/SFTP toggles, SFTP path restrictions, read-only SFTP mode, group-based visibility |
174
+ | **Auth** | JWT + bcrypt locally, **2FA TOTP**, **API keys** for automation, **LDAP / Active Directory** with group→role mapping |
175
+ | **Compliance** | **Session recording** to asciinema cast files with browser replay, structured **audit log**, **webhooks** (HMAC-signed) on key events |
176
+ | **Monitoring** | Background SSH connectivity probes, online/offline indicator |
177
+ | **Deployment** | Multi-stage Docker image, SQLite default or PostgreSQL, runs behind any reverse proxy at any sub-path, **demo mode** for public read-only deployments |
178
+ | **UX** | Dark/light theme, responsive, keyboard shortcuts, vanilla JS + Alpine.js (no npm needed), session persistence across reloads |
179
+
180
+ ---
181
+
182
+ ## Screenshots
183
+
184
+ ### Core (Site Manager, terminal, SFTP, editor)
185
+
186
+ | | |
187
+ |---|---|
188
+ | ![Login](docs/screenshots/login.png) | ![Site Manager](docs/screenshots/site-manager.png) |
189
+ | ![SSH Terminal](docs/screenshots/terminal.png) | ![SFTP Browser](docs/screenshots/sftp.png) |
190
+ | ![Editor](docs/screenshots/editor.png) | ![Split View](docs/screenshots/split-view.png) |
191
+
192
+ ### Access control & admin
193
+
194
+ | | |
195
+ |---|---|
196
+ | ![Access Control](docs/screenshots/access-control.png) | ![Users](docs/screenshots/users.png) |
197
+ | ![Audit](docs/screenshots/audit.png) | ![Light Theme](docs/screenshots/light-theme.png) |
198
+
199
+ ### Operations Pack — jump host, snippets, webhooks
200
+
201
+ | | |
202
+ |---|---|
203
+ | ![Demo banner](docs/screenshots/v0.3/01-login-demo-banner.png) | ![Dashboard with jump host](docs/screenshots/v0.3/02-dashboard-jump-host.png) |
204
+ | ![Terminal snippets](docs/screenshots/v0.3/03-terminal-snippets-jump.png) | ![Snippet executed](docs/screenshots/v0.3/04-snippet-executed.png) |
205
+ | ![SFTP via jump](docs/screenshots/v0.3/05-sftp-via-jump.png) | ![Add Server with Jump Via](docs/screenshots/v0.3/08-add-server-jump-via.png) |
206
+ | ![Webhooks modal](docs/screenshots/v0.3/06-webhooks-modal.png) | |
207
+
208
+ ### Shared terminal & session recording
209
+
210
+ | Owner sees | Joiner sees |
211
+ |---|---|
212
+ | ![Shared owner](docs/screenshots/v0.4/01-shared-terminal-owner.png) | ![Shared joiner](docs/screenshots/v0.4/02-shared-terminal-joiner.png) |
213
+
214
+ | Recordings list | Browser replay |
215
+ |---|---|
216
+ | ![Recordings](docs/screenshots/v0.4/04-recordings-modal.png) | ![Replay](docs/screenshots/v0.4/03-recording-replay.png) |
217
+
218
+ ---
219
+
220
+ ## Architecture
221
+
222
+ ### Project layout
223
+
224
+ ```
225
+ src/webgate/
226
+ ├── __main__.py uvicorn launcher
227
+ ├── app.py FastAPI factory, lifespan, middleware
228
+ ├── config.py Pydantic Settings
229
+ ├── auth/ JWT + bcrypt, 2FA TOTP, API keys, LDAP, user mgmt
230
+ ├── audit/ Immutable action log
231
+ ├── servers/ Registry CRUD, jump-host resolution, Fernet crypto
232
+ ├── terminal/
233
+ │ ├── ssh_session.py asyncssh wrapper (with optional jump tunnel)
234
+ │ ├── shared.py SharedSession registry: 1 PTY ↔ N WebSockets
235
+ │ ├── ws_handler.py WS bridge: input multiplex / output broadcast
236
+ │ └── routes.py WS endpoints + share-token mint/revoke
237
+ ├── files/ SFTP service + connection pool (5 min TTL)
238
+ ├── snippets/ Per-user command library
239
+ ├── webhooks/ HMAC-signed event dispatcher
240
+ ├── recordings/ asciinema cast v2 writer + browser replay
241
+ ├── db/ SQLAlchemy async engine + dialect-aware migrations
242
+ └── static/index.html Single-file frontend (Alpine.js + xterm.js + CodeMirror)
243
+ ```
244
+
245
+ ### Request lifecycle
246
+
247
+ ```mermaid
248
+ flowchart LR
249
+ Browser["Browser<br/>(Alpine + xterm.js + CodeMirror)"]
250
+ subgraph webgate ["webgate (FastAPI)"]
251
+ AUTH["JWT / API key / LDAP"]
252
+ REST["REST routes"]
253
+ WS["WebSocket handler"]
254
+ POOL["SFTP pool<br/>(5 min TTL)"]
255
+ SHARED["SharedSession<br/>registry"]
256
+ REC["CastRecorder"]
257
+ DB[("DB<br/>SQLite / PostgreSQL")]
258
+ end
259
+ SSH(["asyncssh"])
260
+ REMOTE["Remote server"]
261
+
262
+ Browser <-- "HTTPS / WSS" --> AUTH
263
+ AUTH --> REST
264
+ AUTH --> WS
265
+ REST --> POOL
266
+ REST --> DB
267
+ WS --> SHARED
268
+ SHARED -. write .-> REC
269
+ SHARED --> SSH
270
+ POOL --> SSH
271
+ SSH --> REMOTE
272
+
273
+ style Browser fill:#e8f0fe,stroke:#4a90d9
274
+ style webgate fill:#f0f9e8,stroke:#5cb85c
275
+ style REMOTE fill:#fff3e0,stroke:#ff9800
276
+ ```
277
+
278
+ ### Jump host (bastion) chaining
279
+
280
+ When a server has `jump_via_id` set, webgate opens the SSH connection to the bastion first and tunnels the target connection through it. Same chain is used for the SFTP browser. No VPN required, only outbound SSH from the gateway to the bastion.
281
+
282
+ ```mermaid
283
+ flowchart LR
284
+ B["Browser"]
285
+ WG["webgate"]
286
+ BAST["bastion<br/>10.0.0.1"]
287
+ INT["internal-app<br/>10.0.1.50"]
288
+ B -- "HTTPS / WSS" --> WG
289
+ WG -- "SSH" --> BAST
290
+ BAST -- "SSH (tunneled)" --> INT
291
+ style B fill:#4a90d9,stroke:#2a6cb5,color:#fff
292
+ style WG fill:#5cb85c,stroke:#449d44,color:#fff
293
+ style BAST fill:#ffcc02,stroke:#e6a800,color:#333
294
+ style INT fill:#fff3e0,stroke:#ff9800
295
+ ```
296
+
297
+ ### Shared terminal session
298
+
299
+ The owner's terminal is registered with a `SharedSession`. When the owner clicks **🔗 Share**, a token is minted and any joiner with the URL attaches a second WebSocket. There's still **one** SSH PTY — output is broadcast to all clients, input from any RW client is multiplexed into the same `stdin`.
300
+
301
+ ```mermaid
302
+ flowchart LR
303
+ O["Owner WS"]
304
+ J1["Joiner WS (rw)"]
305
+ J2["Joiner WS (ro)"]
306
+ SS["SharedSession"]
307
+ PTY["asyncssh PTY"]
308
+ REMOTE["Remote SSH server"]
309
+ REC["CastRecorder<br/>(if recording on)"]
310
+
311
+ O -- "input" --> SS
312
+ J1 -- "input" --> SS
313
+ J2 -. "no input" .-> SS
314
+ SS -- "write stdin" --> PTY
315
+ PTY -- "stdout" --> SS
316
+ SS -- "broadcast" --> O
317
+ SS -- "broadcast" --> J1
318
+ SS -- "broadcast" --> J2
319
+ SS -. "tee" .-> REC
320
+ PTY <--> REMOTE
321
+
322
+ style SS fill:#5cb85c,stroke:#449d44,color:#fff
323
+ style PTY fill:#ffcc02,stroke:#e6a800,color:#333
324
+ style REC fill:#a78bfa,stroke:#7c3aed,color:#fff
325
+ ```
326
+
327
+ ### Access model — groups, tags, LDAP
328
+
329
+ Three concepts, easy to mix up. Here's how they fit together:
330
+
331
+ | Concept | Type | Defined by | What it does |
332
+ |---|---|---|---|
333
+ | `Server.group` | single string per server (e.g. `production`) | admin, in the Add Server form | gates **visibility**: a non-admin user only sees servers whose `group` is in their `allowed_groups` |
334
+ | `Server.tags` | list of strings (e.g. `["nginx","eu-west-1"]`) | admin, in the Add Server form | **cosmetic / search only** — does **not** affect access |
335
+ | `User.allowed_groups` | list of strings | admin (Users panel) **or** LDAP mapping | the set of `Server.group` values a non-admin user is allowed to see |
336
+ | `User.is_admin` | bool | admin (Users panel) **or** LDAP `WEBGATE_LDAP_ADMIN_GROUPS` | admins see everything regardless of `allowed_groups` |
337
+
338
+ **With LDAP**, the admin still controls **which group names exist** by typing them when registering each server. LDAP only populates the user side of the equation:
339
+
340
+ ```mermaid
341
+ flowchart LR
342
+ subgraph LDAP
343
+ L1["alice ∈ cn=devs"]
344
+ L2["alice ∈ cn=admins"]
345
+ end
346
+ subgraph "WEBGATE_LDAP_GROUP_MAP<br/>(env var)"
347
+ M["{<br/> &quot;devs&quot;: &quot;production&quot;,<br/> &quot;sre&quot;: &quot;all&quot;<br/>}"]
348
+ end
349
+ subgraph User
350
+ U["alice.allowed_groups<br/>= [&quot;production&quot;]"]
351
+ end
352
+ subgraph Servers
353
+ S1["app-1<br/>group=production ✅"]
354
+ S2["app-2<br/>group=staging ❌"]
355
+ S3["db-1<br/>group=production ✅"]
356
+ end
357
+ L1 -- mapped --> M
358
+ L2 -. ignored<br/>(not in map) .-> M
359
+ M --> U
360
+ U --> S1
361
+ U --> S3
362
+ ```
363
+
364
+ Key rules:
365
+
366
+ - LDAP **does not create** groups on the webgate side. The right-hand value of `WEBGATE_LDAP_GROUP_MAP` must match exactly what you typed in `Server.group`.
367
+ - An LDAP group that isn't in the map is silently ignored.
368
+ - `WEBGATE_LDAP_ADMIN_GROUPS` is independent of the map: any membership in those groups grants admin (and admins see all servers).
369
+ - **Tags** are never used for access control, only for filtering / search in the UI.
370
+
371
+ ### LDAP authentication
372
+
373
+ Search-then-bind: webgate binds as the service account, finds the user DN, re-binds as the user with their password to verify credentials, then enumerates LDAP groups and maps them to webgate groups (and admin status).
374
+
375
+ ```mermaid
376
+ sequenceDiagram
377
+ participant Browser
378
+ participant webgate
379
+ participant LDAP
380
+
381
+ Browser->>webgate: POST /api/auth/login (alice, ****)
382
+ webgate->>webgate: try local password (miss)
383
+ webgate->>LDAP: bind(svc-DN, svc-password)
384
+ LDAP-->>webgate: ok
385
+ webgate->>LDAP: search(uid=alice) under user_base
386
+ LDAP-->>webgate: dn=uid=alice,ou=people,...
387
+ webgate->>LDAP: re-bind(user-DN, user-password)
388
+ LDAP-->>webgate: ok ✅
389
+ webgate->>LDAP: search(member=user-DN) under group_base
390
+ LDAP-->>webgate: [devs, admins]
391
+ webgate->>webgate: map → allowed_groups, is_admin
392
+ webgate->>webgate: upsert local User row
393
+ webgate-->>Browser: JWT
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Configuration
399
+
400
+ All settings are environment variables prefixed with `WEBGATE_`.
401
+
402
+ ### Core
403
+
404
+ | Variable | Default | Description |
405
+ |---|---|---|
406
+ | `WEBGATE_SECRET_KEY` | `change-me-in-production` | JWT signing + Fernet credential encryption (set this!) |
407
+ | `WEBGATE_HOST` | `0.0.0.0` | Bind address |
408
+ | `WEBGATE_PORT` | `8443` | Bind port |
409
+ | `WEBGATE_LOG_LEVEL` | `info` | uvicorn log level |
410
+ | `WEBGATE_FIRST_RUN` | `true` | Allow first-user auto-creation as admin |
411
+
412
+ ### Database
413
+
414
+ | Variable | Default | Description |
415
+ |---|---|---|
416
+ | `WEBGATE_DB_URL` | `sqlite+aiosqlite:///./webgate.db` | SQLAlchemy async URL. Use `postgresql+asyncpg://user:pass@host:5432/webgate` for Postgres |
417
+
418
+ ### Sessions, JWT, monitoring
419
+
420
+ | Variable | Default | Description |
421
+ |---|---|---|
422
+ | `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session idle timeout (seconds) |
423
+ | `WEBGATE_MAX_UPLOAD_SIZE` | `104857600` | Max upload size (100 MB) |
424
+ | `WEBGATE_JWT_ALGORITHM` | `HS256` | JWT algorithm |
425
+ | `WEBGATE_JWT_EXPIRE_MINUTES` | `1440` | Token expiry (24 h) |
426
+ | `WEBGATE_MONITOR_INTERVAL` | `60` | Server status check interval (s) |
427
+ | `WEBGATE_MONITOR_TIMEOUT` | `5` | SSH connect timeout for status checks (s) |
428
+ | `WEBGATE_MONITOR_CONCURRENCY` | `10` | Max parallel status checks |
429
+ | `WEBGATE_ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated) |
430
+
431
+ ### Reverse proxy & demo mode
432
+
433
+ | Variable | Default | Description |
434
+ |---|---|---|
435
+ | `WEBGATE_ROOT_PATH` | `` (empty) | URL prefix when served behind a sub-path (e.g. `/webgate`). The proxy must forward the prefix unchanged |
436
+ | `WEBGATE_DEMO_MODE` | `false` | Read-only public demo: blocks writes, hides admin UI, seeds `demo`/`demo` user, shows top banner |
437
+
438
+ ### Session recording (asciinema)
439
+
440
+ | Variable | Default | Description |
441
+ |---|---|---|
442
+ | `WEBGATE_RECORD_SESSIONS` | `false` | Capture every terminal session to a cast v2 file |
443
+ | `WEBGATE_RECORDINGS_DIR` | `./recordings` | Storage directory for `.cast` files |
444
+
445
+ ### LDAP / Active Directory
446
+
447
+ | Variable | Default | Description |
448
+ |---|---|---|
449
+ | `WEBGATE_LDAP_ENABLED` | `false` | Enable LDAP fallback after local credential check |
450
+ | `WEBGATE_LDAP_URL` | `` | `ldap://host:389` or `ldaps://host:636` |
451
+ | `WEBGATE_LDAP_BIND_DN` | `` | Service account DN, e.g. `cn=admin,dc=example,dc=com` |
452
+ | `WEBGATE_LDAP_BIND_PASSWORD` | `` | Service account password |
453
+ | `WEBGATE_LDAP_USER_BASE` | `` | e.g. `ou=people,dc=example,dc=com` |
454
+ | `WEBGATE_LDAP_USER_FILTER` | `(uid={username})` | AD: `(sAMAccountName={username})` |
455
+ | `WEBGATE_LDAP_GROUP_BASE` | `` | e.g. `ou=groups,dc=example,dc=com` (empty = no group lookup) |
456
+ | `WEBGATE_LDAP_GROUP_FILTER` | `(member={dn})` | AD nested: `(member:1.2.840.113556.1.4.1941:={dn})` |
457
+ | `WEBGATE_LDAP_GROUP_MAP` | `{}` | JSON `{"ldap-cn":"webgate-group"}` |
458
+ | `WEBGATE_LDAP_ADMIN_GROUPS` | `[]` | JSON list of LDAP CNs that grant admin |
459
+
460
+ ---
461
+
462
+ ## Deployment
463
+
464
+ ### Production with Docker
465
+
466
+ ```bash
467
+ export WEBGATE_SECRET_KEY=$(openssl rand -hex 32)
468
+ docker compose up -d
469
+ ```
470
+
471
+ The default [`compose.yml`](compose.yml) pulls `kalexnolasco/webgate:latest`, persists state in a named volume, and lists the optional features as commented env vars you can opt into.
472
+
473
+ ### Behind a reverse proxy with TLS
474
+
475
+ #### Caddy (simplest)
476
+
477
+ ```yaml
478
+ # add to compose.yml
479
+ caddy:
480
+ image: caddy:2-alpine
481
+ restart: unless-stopped
482
+ ports: ["443:443", "80:80"]
483
+ volumes:
484
+ - ./Caddyfile:/etc/caddy/Caddyfile
485
+ - caddy-data:/data
486
+ ```
487
+ ```caddy
488
+ # Caddyfile
489
+ webgate.example.com {
490
+ reverse_proxy webgate:8443
491
+ }
492
+ ```
493
+
494
+ #### nginx (sub-path `/webgate/`)
495
+
496
+ Set `WEBGATE_ROOT_PATH=/webgate` on the container, then:
497
+
498
+ ```nginx
499
+ server {
500
+ listen 443 ssl http2;
501
+ server_name example.com;
502
+ ssl_certificate /etc/ssl/certs/example.com.crt;
503
+ ssl_certificate_key /etc/ssl/private/example.com.key;
504
+
505
+ # WebSocket — must come before the generic location
506
+ location /webgate/api/ws/ {
507
+ proxy_pass http://127.0.0.1:8443;
508
+ proxy_http_version 1.1;
509
+ proxy_set_header Upgrade $http_upgrade;
510
+ proxy_set_header Connection "upgrade";
511
+ proxy_set_header Host $host;
512
+ proxy_read_timeout 86400s;
513
+ proxy_send_timeout 86400s;
514
+ }
515
+ location /webgate/ {
516
+ proxy_pass http://127.0.0.1:8443;
517
+ proxy_set_header Host $host;
518
+ proxy_set_header X-Forwarded-Proto https;
519
+ proxy_set_header X-Forwarded-Prefix /webgate;
520
+ client_max_body_size 100m;
521
+ }
522
+ }
523
+ ```
524
+
525
+ #### Apache 2.4 (sub-path `/webgate/`)
526
+
527
+ ```apache
528
+ # Required modules: proxy proxy_http proxy_wstunnel headers rewrite ssl
529
+ RewriteEngine On
530
+ RewriteRule ^/webgate$ /webgate/ [R=301,L]
531
+
532
+ ProxyPreserveHost On
533
+ RequestHeader set X-Forwarded-Proto "https"
534
+ RequestHeader set X-Forwarded-Prefix "/webgate"
535
+
536
+ ProxyPass /webgate/api/ws/ ws://127.0.0.1:8443/webgate/api/ws/
537
+ ProxyPassReverse /webgate/api/ws/ ws://127.0.0.1:8443/webgate/api/ws/
538
+ ProxyPass /webgate/ http://127.0.0.1:8443/webgate/
539
+ ProxyPassReverse /webgate/ http://127.0.0.1:8443/webgate/
540
+ ```
541
+
542
+ > ⚠️ The proxy must **forward the prefix unchanged** — webgate handles `/webgate/api/...` natively, do not strip it.
543
+
544
+ #### Traefik (Docker labels)
545
+
546
+ ```yaml
547
+ labels:
548
+ - "traefik.enable=true"
549
+ - "traefik.http.routers.webgate.rule=Host(`example.com`) && PathPrefix(`/webgate`)"
550
+ - "traefik.http.routers.webgate.entrypoints=websecure"
551
+ - "traefik.http.routers.webgate.tls=true"
552
+ - "traefik.http.services.webgate.loadbalancer.server.port=8443"
553
+ ```
554
+
555
+ ### Public read-only demo (Fly.io)
556
+
557
+ The repo includes [`Dockerfile.demo`](Dockerfile.demo) (webgate + sandboxed sshd via supervisord) and [`fly.toml`](fly.toml). Deploy:
558
+
559
+ ```bash
560
+ flyctl launch --no-deploy --copy-config
561
+ flyctl secrets set WEBGATE_SECRET_KEY=$(openssl rand -hex 32)
562
+ flyctl volumes create webgate_demo_data --size 1 --region cdg
563
+ flyctl deploy
564
+ ```
565
+
566
+ The demo middleware blocks all writes on `/api/*` (login, terminal share and totp/verify whitelisted), so anyone hitting the URL can browse the seeded `bastion` + `internal-app` pair without poking holes in your infra. The official live demo at https://webgate-demo.fly.dev runs exactly this.
567
+
568
+ ---
569
+
570
+ ## API reference
571
+
572
+ | Group | Methods (summary) |
573
+ |---|---|
574
+ | **Auth** | `POST /api/auth/login`, `GET /api/auth/me`, `POST/PUT /api/auth/users/...`, `POST /api/auth/totp/setup`, `GET/POST/DELETE /api/auth/api-keys`, `GET /api/auth/audit` |
575
+ | **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` |
576
+ | **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}` |
577
+ | **Files (SFTP)** | `GET /ls`, `GET /read`, `GET /download`, `GET /download-zip`, `POST /upload`, `PUT /write`, `POST /mkdir`, `POST /rename`, `DELETE /delete`, `POST /chmod`, `GET /stat` (all under `/api/files/{server_id}/`) |
578
+ | **Snippets** | `GET/POST /api/snippets`, `DELETE /api/snippets/{id}` |
579
+ | **Webhooks** | `GET/POST /api/webhooks`, `PUT/DELETE /api/webhooks/{id}`, `POST /api/webhooks/{id}/test`, `GET /api/webhooks/events` |
580
+ | **Recordings** | `GET /api/recordings`, `GET /api/recordings/{id}/download`, `GET /api/recordings/{id}/play`, `GET /api/recordings/{id}/cast`, `DELETE /api/recordings/{id}` |
581
+ | **Health / Config** | `GET /api/health`, `GET /api/config` (public — exposes `demo_mode` to the frontend) |
582
+
583
+ Full OpenAPI is auto-generated at `/docs` (Swagger UI) and `/redoc`.
584
+
585
+ ---
586
+
587
+ ## Development
588
+
589
+ ```bash
590
+ uv sync --all-extras --dev # install
591
+ uv run python -m webgate # run
592
+ uv run uvicorn webgate.app:create_app --factory --reload --host 0.0.0.0 --port 8443
593
+
594
+ # tests
595
+ uv run pytest tests/ -v
596
+ uv run pytest tests/ -v --cov=webgate
597
+
598
+ # lint + types
599
+ uv run ruff check src/ tests/
600
+ uv run ruff format src/ tests/
601
+ uv run pyright src/
602
+
603
+ # build wheel
604
+ uv build
605
+ ```
606
+
607
+ Or use the dev compose with a sandboxed SSH target ready to register:
608
+
609
+ ```bash
610
+ docker compose -f compose.dev.yml up --build
611
+ # Inside the UI register: hostname=ssh-demo user=demo password=demo
612
+ ```
613
+
614
+ ---
615
+
616
+ ## Tech stack
617
+
618
+ - **Backend:** Python 3.11+, FastAPI, uvicorn, asyncssh, SQLAlchemy 2 async, aiosqlite/asyncpg, Pydantic v2, slowapi, ldap3, pyotp, httpx
619
+ - **Frontend:** Alpine.js, xterm.js, CodeMirror 6, vanilla CSS (no build step)
620
+ - **Storage:** SQLite by default, PostgreSQL via `WEBGATE_DB_URL`. Credentials encrypted at rest with Fernet
621
+ - **Recording:** asciinema cast v2 (JSON Lines), replay via embedded asciinema-player from CDN
622
+ - **Build/Dev:** uv, ruff, pyright, pytest, Docker (multi-stage)
623
+
624
+ ## Security
625
+
626
+ - All SSH passwords and private keys are encrypted at rest with **Fernet** (key derived from `WEBGATE_SECRET_KEY`)
627
+ - Passwords use **bcrypt**; sessions use **JWT** (HS256)
628
+ - **2FA TOTP** available per user
629
+ - **API keys** for non-interactive auth (`Authorization: Bearer wg_…`)
630
+ - **Rate limiting** on auth endpoints (slowapi)
631
+ - **Path traversal** validation on every SFTP operation
632
+ - **Per-server access control** — admins can disable SSH or SFTP independently, restrict SFTP to allow-listed paths, mark SFTP read-only
633
+ - **Group-based visibility** — non-admin users only see servers in their assigned groups
634
+ - **HMAC-signed webhooks** so receivers can verify the payload came from your webgate
635
+ - **Recommended:** put webgate behind a TLS-terminating reverse proxy (Caddy/nginx/Traefik) in production
636
+
637
+ ## Requirements
638
+
639
+ - Python 3.11+ (or just Docker)
640
+ - 256 MB RAM minimum (512 MB recommended)
641
+ - ~100 MB disk for the image plus your data (DB + uploaded SSH keys + recordings)
642
+
643
+ ## Roadmap
644
+
645
+ See [ROADMAP.md](ROADMAP.md) for the full plan and what's shipped per release.
646
+
647
+ ## License
648
+
649
+ MIT — see [LICENSE](LICENSE).