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.
- {webgate-0.4.1 → webgate-0.4.2}/CHANGELOG.md +39 -0
- webgate-0.4.2/PKG-INFO +649 -0
- webgate-0.4.2/README.md +600 -0
- webgate-0.4.2/ROADMAP.md +79 -0
- {webgate-0.4.1 → webgate-0.4.2}/compose.dev.yml +9 -0
- webgate-0.4.2/compose.yml +29 -0
- {webgate-0.4.1 → webgate-0.4.2}/pyproject.toml +2 -1
- webgate-0.4.2/src/webgate/auth/ldap.py +163 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/routes.py +29 -8
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/config.py +12 -0
- {webgate-0.4.1 → webgate-0.4.2}/uv.lock +15 -1
- webgate-0.4.1/PKG-INFO +0 -834
- webgate-0.4.1/README.md +0 -786
- webgate-0.4.1/ROADMAP.md +0 -93
- webgate-0.4.1/compose.yml +0 -15
- {webgate-0.4.1 → webgate-0.4.2}/.dockerignore +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/.github/workflows/docs.yml +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/.gitignore +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/Dockerfile +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/Dockerfile.demo +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/Dockerfile.ssh-demo +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/LICENSE +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/VERSION +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/after-reload.yaml +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/api/auth.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/api/files.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/api/servers.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/api/terminal.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/changelog.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/getting-started/installation.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/getting-started/quickstart.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/guide/files.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/guide/servers.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/guide/split.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/guide/terminal.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/guide/users.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/index.md +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/access-control.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/audit.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/edit-access-control.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/editor.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/light-theme.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/login.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/new-server-form.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/sftp-restricted.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/sftp.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/site-manager.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/split-view.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/ssh-disabled.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/terminal.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/users.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/01-login-demo-banner.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/02-dashboard-jump-host.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/03-terminal-snippets-jump.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/04-snippet-executed.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/05-sftp-via-jump.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/06-webhooks-modal.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/07-webhook-test-fired.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.3/08-add-server-jump-via.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/01-shared-terminal-owner.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/02-shared-terminal-joiner.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/03-recording-replay.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/docs/screenshots/v0.4/04-recordings-modal.png +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/fly.toml +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/mkdocs.yml +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/__main__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/app.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/audit/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/audit/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/audit/service.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/auth/service.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/db/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/db/engine.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/demo.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/pool.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/routes.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/files/sftp_service.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/recorder.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/recordings/routes.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/crypto.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/monitor.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/routes.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/servers/service.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/snippets/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/snippets/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/snippets/routes.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/static/index.html +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/routes.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/shared.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/ssh_session.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/terminal/ws_handler.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/dispatcher.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/models.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/src/webgate/webhooks/routes.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/tests/__init__.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/tests/conftest.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/tests/test_auth.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/tests/test_files.py +0 -0
- {webgate-0.4.1 → webgate-0.4.2}/tests/test_servers.py +0 -0
- {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
|
+
[](https://pypi.org/project/webgate/)
|
|
53
|
+
[](https://pypi.org/project/webgate/)
|
|
54
|
+
[](https://github.com/kalexnolasco/webgate/blob/main/LICENSE)
|
|
55
|
+
[](https://fastapi.tiangolo.com)
|
|
56
|
+
[](https://hub.docker.com/r/kalexnolasco/webgate)
|
|
57
|
+
[](https://pypi.org/project/webgate/)
|
|
58
|
+
[](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
|
+
|  |  |
|
|
189
|
+
|  |  |
|
|
190
|
+
|  |  |
|
|
191
|
+
|
|
192
|
+
### Access control & admin
|
|
193
|
+
|
|
194
|
+
| | |
|
|
195
|
+
|---|---|
|
|
196
|
+
|  |  |
|
|
197
|
+
|  |  |
|
|
198
|
+
|
|
199
|
+
### Operations Pack — jump host, snippets, webhooks
|
|
200
|
+
|
|
201
|
+
| | |
|
|
202
|
+
|---|---|
|
|
203
|
+
|  |  |
|
|
204
|
+
|  |  |
|
|
205
|
+
|  |  |
|
|
206
|
+
|  | |
|
|
207
|
+
|
|
208
|
+
### Shared terminal & session recording
|
|
209
|
+
|
|
210
|
+
| Owner sees | Joiner sees |
|
|
211
|
+
|---|---|
|
|
212
|
+
|  |  |
|
|
213
|
+
|
|
214
|
+
| Recordings list | Browser replay |
|
|
215
|
+
|---|---|
|
|
216
|
+
|  |  |
|
|
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/> "devs": "production",<br/> "sre": "all"<br/>}"]
|
|
348
|
+
end
|
|
349
|
+
subgraph User
|
|
350
|
+
U["alice.allowed_groups<br/>= ["production"]"]
|
|
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).
|