webgate 0.2.0__tar.gz → 0.2.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.2.0 → webgate-0.2.2}/CHANGELOG.md +32 -0
- webgate-0.2.2/Dockerfile.demo +63 -0
- {webgate-0.2.0 → webgate-0.2.2}/PKG-INFO +109 -1
- {webgate-0.2.0 → webgate-0.2.2}/README.md +106 -0
- webgate-0.2.2/docs/screenshots/access-control.png +0 -0
- webgate-0.2.2/docs/screenshots/audit.png +0 -0
- webgate-0.2.2/docs/screenshots/editor.png +0 -0
- webgate-0.2.2/docs/screenshots/light-theme.png +0 -0
- webgate-0.2.2/docs/screenshots/sftp.png +0 -0
- webgate-0.2.2/docs/screenshots/site-manager.png +0 -0
- webgate-0.2.2/docs/screenshots/split-view.png +0 -0
- webgate-0.2.2/docs/screenshots/terminal.png +0 -0
- webgate-0.2.2/docs/screenshots/users.png +0 -0
- webgate-0.2.2/fly.toml +48 -0
- {webgate-0.2.0 → webgate-0.2.2}/pyproject.toml +3 -1
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/app.py +37 -2
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/auth/models.py +59 -1
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/auth/routes.py +153 -6
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/auth/service.py +80 -1
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/config.py +2 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/db/engine.py +2 -0
- webgate-0.2.2/src/webgate/demo.py +55 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/static/index.html +218 -12
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/terminal/ws_handler.py +11 -1
- {webgate-0.2.0 → webgate-0.2.2}/uv.lock +117 -0
- webgate-0.2.0/docs/screenshots/access-control.png +0 -0
- webgate-0.2.0/docs/screenshots/audit.png +0 -0
- webgate-0.2.0/docs/screenshots/editor.png +0 -0
- webgate-0.2.0/docs/screenshots/sftp.png +0 -0
- webgate-0.2.0/docs/screenshots/site-manager.png +0 -0
- webgate-0.2.0/docs/screenshots/split-view.png +0 -0
- webgate-0.2.0/docs/screenshots/terminal.png +0 -0
- webgate-0.2.0/docs/screenshots/users.png +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/.dockerignore +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/.github/workflows/docs.yml +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/.gitignore +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/Dockerfile +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/Dockerfile.ssh-demo +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/LICENSE +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/ROADMAP.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/VERSION +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/compose.dev.yml +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/compose.yml +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/api/auth.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/api/files.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/api/servers.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/api/terminal.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/changelog.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/getting-started/installation.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/getting-started/quickstart.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/guide/files.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/guide/servers.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/guide/split.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/guide/terminal.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/guide/users.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/index.md +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/screenshots/edit-access-control.png +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/screenshots/login.png +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/screenshots/new-server-form.png +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/screenshots/sftp-restricted.png +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/docs/screenshots/ssh-disabled.png +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/mkdocs.yml +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/__main__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/audit/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/audit/models.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/audit/service.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/auth/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/db/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/files/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/files/models.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/files/pool.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/files/routes.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/files/sftp_service.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/servers/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/servers/crypto.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/servers/models.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/servers/monitor.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/servers/routes.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/servers/service.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/terminal/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/terminal/routes.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/src/webgate/terminal/ssh_session.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/tests/__init__.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/tests/conftest.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/tests/test_auth.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/tests/test_files.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/tests/test_servers.py +0 -0
- {webgate-0.2.0 → webgate-0.2.2}/tests/test_terminal.py +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.2 (2026-04-15)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Demo mode** (`WEBGATE_DEMO_MODE=true`) -- read-only public demo deployments. Blocks every write request on `/api/*` (except login), disables the WebSocket quick-connect endpoint, seeds a `demo`/`demo` user with a sample server, and shows a top banner in the UI.
|
|
8
|
+
- **`Dockerfile.demo`** -- single-container image bundling webgate + a sandboxed `sshd` target via supervisord. Ready for free hosting tiers.
|
|
9
|
+
- **`fly.toml`** -- Fly.io configuration for one-command demo deployments (`flyctl deploy`).
|
|
10
|
+
|
|
11
|
+
### Details
|
|
12
|
+
|
|
13
|
+
- New public endpoint `GET /api/config` exposes `{"demo_mode": bool}` for the frontend to render the banner before login
|
|
14
|
+
- `WEBGATE_DEMO_MODE=true` adds an HTTP middleware that returns `403` for any `POST/PUT/PATCH/DELETE` on `/api/*` (allowlist: `/api/auth/login`, `/api/auth/totp/verify`)
|
|
15
|
+
- Demo seed (`webgate.demo`) is idempotent and only runs when the flag is on
|
|
16
|
+
- Hourly state reset for the public demo can be done with a cron pinging the container restart, so DB returns to seed state
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## v0.2.1 (2026-04-15)
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
- **Reverse proxy sub-path support** -- webgate can now be served behind a reverse proxy at a URL prefix (e.g. `https://example.com/webgate/`). Previously the frontend used absolute `/api/...` paths that broke under any prefix.
|
|
25
|
+
|
|
26
|
+
### Details
|
|
27
|
+
|
|
28
|
+
- New config setting: `WEBGATE_ROOT_PATH` (default `""`), passed to FastAPI's `root_path` for correct OpenAPI URLs behind proxies
|
|
29
|
+
- Frontend derives the path prefix at runtime from `window.location.pathname` and prepends it to all REST calls and the terminal WebSocket URL
|
|
30
|
+
- README documents nginx, Apache, and Traefik reverse-proxy configurations for sub-path deployments
|
|
31
|
+
- The proxy must forward the prefix unchanged (do not strip it) -- webgate handles the prefix natively
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
3
35
|
## v0.2.0 (2026-04-09)
|
|
4
36
|
|
|
5
37
|
### Features
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Single-container demo image: webgate + a sandboxed SSH/SFTP target
|
|
2
|
+
# managed by supervisord. Suitable for free tiers (Fly.io, Render) where
|
|
3
|
+
# spinning a sidecar VM isn't ideal.
|
|
4
|
+
#
|
|
5
|
+
# Build: docker build -f Dockerfile.demo -t webgate-demo .
|
|
6
|
+
# Run: docker run -p 8443:8443 -e WEBGATE_SECRET_KEY=demo webgate-demo
|
|
7
|
+
|
|
8
|
+
# --- Stage 1: build webgate ---
|
|
9
|
+
FROM python:3.13-slim AS builder
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
12
|
+
COPY pyproject.toml uv.lock README.md ./
|
|
13
|
+
RUN uv sync --frozen --no-dev --no-editable
|
|
14
|
+
COPY src/ src/
|
|
15
|
+
|
|
16
|
+
# --- Stage 2: runtime with webgate + sshd + supervisord ---
|
|
17
|
+
FROM python:3.13-slim
|
|
18
|
+
|
|
19
|
+
RUN apt-get update && \
|
|
20
|
+
apt-get install -y --no-install-recommends \
|
|
21
|
+
openssh-server supervisor procps curl nano htop tree && \
|
|
22
|
+
rm -rf /var/lib/apt/lists/*
|
|
23
|
+
|
|
24
|
+
# sshd config: only the demo user, password auth allowed for the demo
|
|
25
|
+
RUN mkdir -p /run/sshd && \
|
|
26
|
+
sed -i 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config && \
|
|
27
|
+
sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \
|
|
28
|
+
useradd -m -s /bin/bash demo && \
|
|
29
|
+
echo 'demo:demo' | chpasswd
|
|
30
|
+
|
|
31
|
+
# Sample tree so SFTP browsing has something to show
|
|
32
|
+
RUN mkdir -p /home/demo/projects/webgate /home/demo/projects/api-server \
|
|
33
|
+
/home/demo/documents /home/demo/logs /home/demo/scripts && \
|
|
34
|
+
printf '# webgate\nSelf-hosted SSH + SFTP manager\n' > /home/demo/projects/webgate/README.md && \
|
|
35
|
+
printf '{"name":"api-server","version":"2.1.0"}\n' > /home/demo/projects/api-server/package.json && \
|
|
36
|
+
printf 'Hello from the webgate demo!\n' > /home/demo/documents/welcome.txt && \
|
|
37
|
+
printf '2026-04-08 10:00:00 INFO Server started on port 8080\n' > /home/demo/logs/app.log && \
|
|
38
|
+
printf '#!/bin/bash\necho "Deploy complete"\n' > /home/demo/scripts/deploy.sh && \
|
|
39
|
+
chmod +x /home/demo/scripts/deploy.sh && \
|
|
40
|
+
chown -R demo:demo /home/demo
|
|
41
|
+
|
|
42
|
+
# webgate
|
|
43
|
+
WORKDIR /app
|
|
44
|
+
COPY --from=builder /app/.venv /app/.venv
|
|
45
|
+
COPY --from=builder /app/src /app/src
|
|
46
|
+
RUN mkdir -p /data
|
|
47
|
+
ENV PATH="/app/.venv/bin:$PATH" \
|
|
48
|
+
PYTHONPATH="/app/src" \
|
|
49
|
+
WEBGATE_HOST=0.0.0.0 \
|
|
50
|
+
WEBGATE_PORT=8443 \
|
|
51
|
+
WEBGATE_DB_URL=sqlite+aiosqlite:////data/webgate.db \
|
|
52
|
+
WEBGATE_LOG_LEVEL=info \
|
|
53
|
+
WEBGATE_DEMO_MODE=true
|
|
54
|
+
|
|
55
|
+
# supervisord config
|
|
56
|
+
RUN printf '[supervisord]\nnodaemon=true\nuser=root\nlogfile=/dev/stdout\nlogfile_maxbytes=0\n\n[program:sshd]\ncommand=/usr/sbin/sshd -D -e\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:webgate]\ncommand=python -m webgate\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/webgate.conf
|
|
57
|
+
|
|
58
|
+
EXPOSE 8443
|
|
59
|
+
|
|
60
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
61
|
+
CMD curl -fsS http://localhost:8443/api/health || exit 1
|
|
62
|
+
|
|
63
|
+
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/webgate.conf"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webgate
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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/
|
|
@@ -34,8 +34,10 @@ Requires-Dist: fastapi>=0.115.0
|
|
|
34
34
|
Requires-Dist: passlib[bcrypt]>=1.7.4
|
|
35
35
|
Requires-Dist: pydantic-settings>=2.7.0
|
|
36
36
|
Requires-Dist: pydantic>=2.10.0
|
|
37
|
+
Requires-Dist: pyotp>=2.9.0
|
|
37
38
|
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
38
39
|
Requires-Dist: python-multipart>=0.0.18
|
|
40
|
+
Requires-Dist: qrcode[pil]>=8.2
|
|
39
41
|
Requires-Dist: slowapi>=0.1.9
|
|
40
42
|
Requires-Dist: sqlalchemy>=2.0.36
|
|
41
43
|
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
@@ -56,6 +58,8 @@ Description-Content-Type: text/markdown
|
|
|
56
58
|
Self-hosted web application for remote server management via **SSH terminal** and **SFTP file browser**. A modern Python replacement combining the best of [webssh](https://github.com/huashengdun/webssh) and [filebrowser](https://github.com/filebrowser/filebrowser) into a single unified tool with a FileZilla-inspired interface.
|
|
57
59
|
|
|
58
60
|
> **Documentation: [kalexnolasco.github.io/webgate](https://kalexnolasco.github.io/webgate/)**
|
|
61
|
+
>
|
|
62
|
+
> **🎮 Live demo: [webgate-demo.fly.dev](https://webgate-demo.fly.dev/)** — login `demo` / `demo` (read-only, resets hourly)
|
|
59
63
|
|
|
60
64
|
---
|
|
61
65
|
|
|
@@ -233,6 +237,9 @@ flowchart TB
|
|
|
233
237
|
### Audit Log
|
|
234
238
|

|
|
235
239
|
|
|
240
|
+
### Light Theme
|
|
241
|
+

|
|
242
|
+
|
|
236
243
|
## Quick Start
|
|
237
244
|
|
|
238
245
|
### Docker (recommended)
|
|
@@ -443,6 +450,8 @@ All settings are configurable via environment variables with the `WEBGATE_` pref
|
|
|
443
450
|
| `WEBGATE_SECRET_KEY` | `change-me-in-production` | JWT signing + Fernet encryption key |
|
|
444
451
|
| `WEBGATE_DB_URL` | `sqlite+aiosqlite:///./webgate.db` | Database URL |
|
|
445
452
|
| `WEBGATE_ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated) |
|
|
453
|
+
| `WEBGATE_ROOT_PATH` | `` (empty) | URL prefix when served behind a reverse proxy at a sub-path (e.g. `/webgate`) |
|
|
454
|
+
| `WEBGATE_DEMO_MODE` | `false` | Read-only public demo: blocks all writes, seeds a `demo`/`demo` user and shows a banner |
|
|
446
455
|
| `WEBGATE_LOG_LEVEL` | `info` | Log level |
|
|
447
456
|
| `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session timeout (seconds) |
|
|
448
457
|
| `WEBGATE_MAX_UPLOAD_SIZE` | `104857600` | Max upload size (100 MB) |
|
|
@@ -499,6 +508,105 @@ webgate.example.com {
|
|
|
499
508
|
}
|
|
500
509
|
```
|
|
501
510
|
|
|
511
|
+
### Reverse proxy at a sub-path (e.g. `https://example.com/webgate/`)
|
|
512
|
+
|
|
513
|
+
When webgate is exposed behind an existing site under a URL prefix, set `WEBGATE_ROOT_PATH` so FastAPI and the frontend both build URLs with the prefix. The frontend derives the prefix automatically from `window.location.pathname`, but `WEBGATE_ROOT_PATH` is still needed on the backend for OpenAPI URLs.
|
|
514
|
+
|
|
515
|
+
**Docker Compose:**
|
|
516
|
+
|
|
517
|
+
```yaml
|
|
518
|
+
services:
|
|
519
|
+
webgate:
|
|
520
|
+
image: kalexnolasco/webgate:latest
|
|
521
|
+
environment:
|
|
522
|
+
WEBGATE_SECRET_KEY: "${WEBGATE_SECRET_KEY}"
|
|
523
|
+
WEBGATE_ROOT_PATH: "/webgate"
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
> **Important:** the reverse proxy must **forward the prefix unchanged** (not strip it). Webgate receives requests as `/webgate/api/...` and handles them natively — do not rewrite them to `/api/...`.
|
|
527
|
+
|
|
528
|
+
#### nginx
|
|
529
|
+
|
|
530
|
+
```nginx
|
|
531
|
+
server {
|
|
532
|
+
listen 443 ssl http2;
|
|
533
|
+
server_name example.com;
|
|
534
|
+
|
|
535
|
+
ssl_certificate /etc/ssl/certs/example.com.crt;
|
|
536
|
+
ssl_certificate_key /etc/ssl/private/example.com.key;
|
|
537
|
+
|
|
538
|
+
# WebSocket (xterm.js terminal) -- must come before the generic location
|
|
539
|
+
location /webgate/api/ws/ {
|
|
540
|
+
proxy_pass http://127.0.0.1:8443;
|
|
541
|
+
proxy_http_version 1.1;
|
|
542
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
543
|
+
proxy_set_header Connection "upgrade";
|
|
544
|
+
proxy_set_header Host $host;
|
|
545
|
+
proxy_read_timeout 86400s; # keep long-lived SSH sessions alive
|
|
546
|
+
proxy_send_timeout 86400s;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
location /webgate/ {
|
|
550
|
+
proxy_pass http://127.0.0.1:8443;
|
|
551
|
+
proxy_set_header Host $host;
|
|
552
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
553
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
554
|
+
proxy_set_header X-Forwarded-Proto https;
|
|
555
|
+
proxy_set_header X-Forwarded-Prefix /webgate;
|
|
556
|
+
client_max_body_size 100m; # match WEBGATE_MAX_UPLOAD_SIZE
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### Apache 2.4
|
|
562
|
+
|
|
563
|
+
```apache
|
|
564
|
+
# Required modules: proxy proxy_http proxy_wstunnel headers rewrite ssl
|
|
565
|
+
# sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite
|
|
566
|
+
|
|
567
|
+
<VirtualHost *:443>
|
|
568
|
+
ServerName example.com
|
|
569
|
+
|
|
570
|
+
SSLEngine on
|
|
571
|
+
SSLCertificateFile /etc/ssl/certs/example.com.crt
|
|
572
|
+
SSLCertificateKeyFile /etc/ssl/private/example.com.key
|
|
573
|
+
|
|
574
|
+
ProxyPreserveHost On
|
|
575
|
+
RequestHeader set X-Forwarded-Proto "https"
|
|
576
|
+
RequestHeader set X-Forwarded-Prefix "/webgate"
|
|
577
|
+
|
|
578
|
+
# Redirect /webgate -> /webgate/ (trailing slash)
|
|
579
|
+
RewriteEngine On
|
|
580
|
+
RewriteRule ^/webgate$ /webgate/ [R=301,L]
|
|
581
|
+
|
|
582
|
+
# WebSocket (xterm.js terminal) -- MUST come before the HTTP ProxyPass
|
|
583
|
+
ProxyPass /webgate/api/ws/ ws://127.0.0.1:8443/webgate/api/ws/
|
|
584
|
+
ProxyPassReverse /webgate/api/ws/ ws://127.0.0.1:8443/webgate/api/ws/
|
|
585
|
+
|
|
586
|
+
# HTTP
|
|
587
|
+
ProxyPass /webgate/ http://127.0.0.1:8443/webgate/
|
|
588
|
+
ProxyPassReverse /webgate/ http://127.0.0.1:8443/webgate/
|
|
589
|
+
</VirtualHost>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
#### Traefik (labels)
|
|
593
|
+
|
|
594
|
+
```yaml
|
|
595
|
+
services:
|
|
596
|
+
webgate:
|
|
597
|
+
image: kalexnolasco/webgate:latest
|
|
598
|
+
environment:
|
|
599
|
+
WEBGATE_ROOT_PATH: "/webgate"
|
|
600
|
+
labels:
|
|
601
|
+
- "traefik.enable=true"
|
|
602
|
+
- "traefik.http.routers.webgate.rule=Host(`example.com`) && PathPrefix(`/webgate`)"
|
|
603
|
+
- "traefik.http.routers.webgate.entrypoints=websecure"
|
|
604
|
+
- "traefik.http.routers.webgate.tls=true"
|
|
605
|
+
- "traefik.http.services.webgate.loadbalancer.server.port=8443"
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Traefik forwards the full path by default, so no extra middleware is needed. Long WebSocket timeouts can be tuned via `forwardingTimeouts` in the Traefik static config.
|
|
609
|
+
|
|
502
610
|
### Useful Commands
|
|
503
611
|
|
|
504
612
|
```bash
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
Self-hosted web application for remote server management via **SSH terminal** and **SFTP file browser**. A modern Python replacement combining the best of [webssh](https://github.com/huashengdun/webssh) and [filebrowser](https://github.com/filebrowser/filebrowser) into a single unified tool with a FileZilla-inspired interface.
|
|
13
13
|
|
|
14
14
|
> **Documentation: [kalexnolasco.github.io/webgate](https://kalexnolasco.github.io/webgate/)**
|
|
15
|
+
>
|
|
16
|
+
> **🎮 Live demo: [webgate-demo.fly.dev](https://webgate-demo.fly.dev/)** — login `demo` / `demo` (read-only, resets hourly)
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
@@ -189,6 +191,9 @@ flowchart TB
|
|
|
189
191
|
### Audit Log
|
|
190
192
|

|
|
191
193
|
|
|
194
|
+
### Light Theme
|
|
195
|
+

|
|
196
|
+
|
|
192
197
|
## Quick Start
|
|
193
198
|
|
|
194
199
|
### Docker (recommended)
|
|
@@ -399,6 +404,8 @@ All settings are configurable via environment variables with the `WEBGATE_` pref
|
|
|
399
404
|
| `WEBGATE_SECRET_KEY` | `change-me-in-production` | JWT signing + Fernet encryption key |
|
|
400
405
|
| `WEBGATE_DB_URL` | `sqlite+aiosqlite:///./webgate.db` | Database URL |
|
|
401
406
|
| `WEBGATE_ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated) |
|
|
407
|
+
| `WEBGATE_ROOT_PATH` | `` (empty) | URL prefix when served behind a reverse proxy at a sub-path (e.g. `/webgate`) |
|
|
408
|
+
| `WEBGATE_DEMO_MODE` | `false` | Read-only public demo: blocks all writes, seeds a `demo`/`demo` user and shows a banner |
|
|
402
409
|
| `WEBGATE_LOG_LEVEL` | `info` | Log level |
|
|
403
410
|
| `WEBGATE_SESSION_TIMEOUT` | `3600` | SSH session timeout (seconds) |
|
|
404
411
|
| `WEBGATE_MAX_UPLOAD_SIZE` | `104857600` | Max upload size (100 MB) |
|
|
@@ -455,6 +462,105 @@ webgate.example.com {
|
|
|
455
462
|
}
|
|
456
463
|
```
|
|
457
464
|
|
|
465
|
+
### Reverse proxy at a sub-path (e.g. `https://example.com/webgate/`)
|
|
466
|
+
|
|
467
|
+
When webgate is exposed behind an existing site under a URL prefix, set `WEBGATE_ROOT_PATH` so FastAPI and the frontend both build URLs with the prefix. The frontend derives the prefix automatically from `window.location.pathname`, but `WEBGATE_ROOT_PATH` is still needed on the backend for OpenAPI URLs.
|
|
468
|
+
|
|
469
|
+
**Docker Compose:**
|
|
470
|
+
|
|
471
|
+
```yaml
|
|
472
|
+
services:
|
|
473
|
+
webgate:
|
|
474
|
+
image: kalexnolasco/webgate:latest
|
|
475
|
+
environment:
|
|
476
|
+
WEBGATE_SECRET_KEY: "${WEBGATE_SECRET_KEY}"
|
|
477
|
+
WEBGATE_ROOT_PATH: "/webgate"
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
> **Important:** the reverse proxy must **forward the prefix unchanged** (not strip it). Webgate receives requests as `/webgate/api/...` and handles them natively — do not rewrite them to `/api/...`.
|
|
481
|
+
|
|
482
|
+
#### nginx
|
|
483
|
+
|
|
484
|
+
```nginx
|
|
485
|
+
server {
|
|
486
|
+
listen 443 ssl http2;
|
|
487
|
+
server_name example.com;
|
|
488
|
+
|
|
489
|
+
ssl_certificate /etc/ssl/certs/example.com.crt;
|
|
490
|
+
ssl_certificate_key /etc/ssl/private/example.com.key;
|
|
491
|
+
|
|
492
|
+
# WebSocket (xterm.js terminal) -- must come before the generic location
|
|
493
|
+
location /webgate/api/ws/ {
|
|
494
|
+
proxy_pass http://127.0.0.1:8443;
|
|
495
|
+
proxy_http_version 1.1;
|
|
496
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
497
|
+
proxy_set_header Connection "upgrade";
|
|
498
|
+
proxy_set_header Host $host;
|
|
499
|
+
proxy_read_timeout 86400s; # keep long-lived SSH sessions alive
|
|
500
|
+
proxy_send_timeout 86400s;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
location /webgate/ {
|
|
504
|
+
proxy_pass http://127.0.0.1:8443;
|
|
505
|
+
proxy_set_header Host $host;
|
|
506
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
507
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
508
|
+
proxy_set_header X-Forwarded-Proto https;
|
|
509
|
+
proxy_set_header X-Forwarded-Prefix /webgate;
|
|
510
|
+
client_max_body_size 100m; # match WEBGATE_MAX_UPLOAD_SIZE
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### Apache 2.4
|
|
516
|
+
|
|
517
|
+
```apache
|
|
518
|
+
# Required modules: proxy proxy_http proxy_wstunnel headers rewrite ssl
|
|
519
|
+
# sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite
|
|
520
|
+
|
|
521
|
+
<VirtualHost *:443>
|
|
522
|
+
ServerName example.com
|
|
523
|
+
|
|
524
|
+
SSLEngine on
|
|
525
|
+
SSLCertificateFile /etc/ssl/certs/example.com.crt
|
|
526
|
+
SSLCertificateKeyFile /etc/ssl/private/example.com.key
|
|
527
|
+
|
|
528
|
+
ProxyPreserveHost On
|
|
529
|
+
RequestHeader set X-Forwarded-Proto "https"
|
|
530
|
+
RequestHeader set X-Forwarded-Prefix "/webgate"
|
|
531
|
+
|
|
532
|
+
# Redirect /webgate -> /webgate/ (trailing slash)
|
|
533
|
+
RewriteEngine On
|
|
534
|
+
RewriteRule ^/webgate$ /webgate/ [R=301,L]
|
|
535
|
+
|
|
536
|
+
# WebSocket (xterm.js terminal) -- MUST come before the HTTP ProxyPass
|
|
537
|
+
ProxyPass /webgate/api/ws/ ws://127.0.0.1:8443/webgate/api/ws/
|
|
538
|
+
ProxyPassReverse /webgate/api/ws/ ws://127.0.0.1:8443/webgate/api/ws/
|
|
539
|
+
|
|
540
|
+
# HTTP
|
|
541
|
+
ProxyPass /webgate/ http://127.0.0.1:8443/webgate/
|
|
542
|
+
ProxyPassReverse /webgate/ http://127.0.0.1:8443/webgate/
|
|
543
|
+
</VirtualHost>
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### Traefik (labels)
|
|
547
|
+
|
|
548
|
+
```yaml
|
|
549
|
+
services:
|
|
550
|
+
webgate:
|
|
551
|
+
image: kalexnolasco/webgate:latest
|
|
552
|
+
environment:
|
|
553
|
+
WEBGATE_ROOT_PATH: "/webgate"
|
|
554
|
+
labels:
|
|
555
|
+
- "traefik.enable=true"
|
|
556
|
+
- "traefik.http.routers.webgate.rule=Host(`example.com`) && PathPrefix(`/webgate`)"
|
|
557
|
+
- "traefik.http.routers.webgate.entrypoints=websecure"
|
|
558
|
+
- "traefik.http.routers.webgate.tls=true"
|
|
559
|
+
- "traefik.http.services.webgate.loadbalancer.server.port=8443"
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Traefik forwards the full path by default, so no extra middleware is needed. Long WebSocket timeouts can be tuned via `forwardingTimeouts` in the Traefik static config.
|
|
563
|
+
|
|
458
564
|
### Useful Commands
|
|
459
565
|
|
|
460
566
|
```bash
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
webgate-0.2.2/fly.toml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Fly.io deployment for the public webgate demo.
|
|
2
|
+
# Edit `app` to your fly app name, then:
|
|
3
|
+
# flyctl launch --no-deploy --copy-config
|
|
4
|
+
# flyctl secrets set WEBGATE_SECRET_KEY=$(openssl rand -hex 32)
|
|
5
|
+
# flyctl deploy
|
|
6
|
+
#
|
|
7
|
+
# Free tier (1x shared-cpu, 256-512 MB) is enough for light demo traffic.
|
|
8
|
+
|
|
9
|
+
app = "webgate-demo"
|
|
10
|
+
primary_region = "cdg" # Paris; closest EU region to Spain
|
|
11
|
+
|
|
12
|
+
[build]
|
|
13
|
+
dockerfile = "Dockerfile.demo"
|
|
14
|
+
|
|
15
|
+
[env]
|
|
16
|
+
WEBGATE_DEMO_MODE = "true"
|
|
17
|
+
WEBGATE_LOG_LEVEL = "info"
|
|
18
|
+
|
|
19
|
+
[http_service]
|
|
20
|
+
internal_port = 8443
|
|
21
|
+
force_https = true
|
|
22
|
+
auto_stop_machines = "stop" # save free-tier hours when idle
|
|
23
|
+
auto_start_machines = true
|
|
24
|
+
min_machines_running = 0
|
|
25
|
+
|
|
26
|
+
[http_service.concurrency]
|
|
27
|
+
type = "requests"
|
|
28
|
+
soft_limit = 50
|
|
29
|
+
hard_limit = 100
|
|
30
|
+
|
|
31
|
+
[[http_service.checks]]
|
|
32
|
+
interval = "30s"
|
|
33
|
+
timeout = "5s"
|
|
34
|
+
grace_period = "15s"
|
|
35
|
+
method = "get"
|
|
36
|
+
path = "/api/health"
|
|
37
|
+
|
|
38
|
+
[[vm]]
|
|
39
|
+
cpu_kind = "shared"
|
|
40
|
+
cpus = 1
|
|
41
|
+
memory_mb = 512
|
|
42
|
+
|
|
43
|
+
# Persistent volume so the demo seed survives restarts. The hourly reset
|
|
44
|
+
# (see scripts/demo-reset.sh) wipes the DB inside the container, not the
|
|
45
|
+
# whole volume, so this stays cheap.
|
|
46
|
+
[mounts]
|
|
47
|
+
source = "webgate_demo_data"
|
|
48
|
+
destination = "/data"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "webgate"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
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"
|
|
@@ -38,6 +38,8 @@ dependencies = [
|
|
|
38
38
|
"cryptography>=44.0.0",
|
|
39
39
|
"websockets>=14.0",
|
|
40
40
|
"slowapi>=0.1.9",
|
|
41
|
+
"pyotp>=2.9.0",
|
|
42
|
+
"qrcode[pil]>=8.2",
|
|
41
43
|
]
|
|
42
44
|
|
|
43
45
|
[project.urls]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator
|
|
2
2
|
from contextlib import asynccontextmanager
|
|
3
3
|
|
|
4
|
-
from fastapi import FastAPI
|
|
4
|
+
from fastapi import FastAPI, Request
|
|
5
5
|
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
6
7
|
from fastapi.staticfiles import StaticFiles
|
|
7
8
|
from slowapi import _rate_limit_exceeded_handler
|
|
8
9
|
from slowapi.errors import RateLimitExceeded
|
|
@@ -12,6 +13,7 @@ from webgate.auth.routes import router as auth_router
|
|
|
12
13
|
from webgate.auth.service import seed_admin
|
|
13
14
|
from webgate.config import settings
|
|
14
15
|
from webgate.db.engine import async_session_factory, close_db, init_db
|
|
16
|
+
from webgate.demo import seed_demo
|
|
15
17
|
from webgate.files.pool import sftp_pool
|
|
16
18
|
from webgate.files.routes import router as files_router
|
|
17
19
|
from webgate.servers.monitor import server_monitor
|
|
@@ -24,6 +26,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
|
24
26
|
await init_db()
|
|
25
27
|
async with async_session_factory() as session:
|
|
26
28
|
await seed_admin(session)
|
|
29
|
+
if settings.demo_mode:
|
|
30
|
+
await seed_demo(session)
|
|
27
31
|
await sftp_pool.start()
|
|
28
32
|
await server_monitor.start()
|
|
29
33
|
yield
|
|
@@ -33,7 +37,12 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
def create_app() -> FastAPI:
|
|
36
|
-
app = FastAPI(
|
|
40
|
+
app = FastAPI(
|
|
41
|
+
title="webgate",
|
|
42
|
+
version="0.1.0",
|
|
43
|
+
lifespan=lifespan,
|
|
44
|
+
root_path=settings.root_path,
|
|
45
|
+
)
|
|
37
46
|
app.state.limiter = limiter
|
|
38
47
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
39
48
|
|
|
@@ -50,6 +59,32 @@ def create_app() -> FastAPI:
|
|
|
50
59
|
async def health() -> dict[str, str]: # pyright: ignore[reportUnusedFunction]
|
|
51
60
|
return {"status": "ok"}
|
|
52
61
|
|
|
62
|
+
@app.get("/api/config")
|
|
63
|
+
async def public_config() -> dict[str, object]: # pyright: ignore[reportUnusedFunction]
|
|
64
|
+
# Public flags consumed by the frontend before login.
|
|
65
|
+
return {"demo_mode": settings.demo_mode}
|
|
66
|
+
|
|
67
|
+
if settings.demo_mode:
|
|
68
|
+
# In demo mode block any state-changing request on /api/* except login
|
|
69
|
+
# and the WS quick-connect endpoint (no arbitrary SSH targets allowed).
|
|
70
|
+
WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
|
71
|
+
WRITE_ALLOWLIST = {"/api/auth/login", "/api/auth/totp/verify"}
|
|
72
|
+
|
|
73
|
+
@app.middleware("http")
|
|
74
|
+
async def _demo_readonly(request: Request, call_next): # pyright: ignore[reportUnusedFunction]
|
|
75
|
+
path = request.url.path
|
|
76
|
+
if request.method in WRITE_METHODS and path.startswith("/api/") and path not in WRITE_ALLOWLIST:
|
|
77
|
+
return JSONResponse(
|
|
78
|
+
status_code=403,
|
|
79
|
+
content={"detail": "Demo mode: write operations are disabled"},
|
|
80
|
+
)
|
|
81
|
+
if path.endswith("/api/ws/terminal/quick"):
|
|
82
|
+
return JSONResponse(
|
|
83
|
+
status_code=403,
|
|
84
|
+
content={"detail": "Demo mode: quick-connect is disabled"},
|
|
85
|
+
)
|
|
86
|
+
return await call_next(request)
|
|
87
|
+
|
|
53
88
|
app.include_router(auth_router)
|
|
54
89
|
app.include_router(servers_router)
|
|
55
90
|
app.include_router(terminal_router)
|
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, field_validator
|
|
5
|
-
from sqlalchemy import Boolean, DateTime, String, Text, func
|
|
5
|
+
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
|
|
6
6
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
7
|
|
|
8
8
|
from webgate.db.engine import Base
|
|
@@ -17,6 +17,20 @@ class User(Base):
|
|
|
17
17
|
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
18
18
|
must_change_password: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
19
19
|
allowed_groups: Mapped[str] = mapped_column(Text, default="[]") # JSON array of group names
|
|
20
|
+
totp_secret: Mapped[str] = mapped_column(String(255), default="")
|
|
21
|
+
totp_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
22
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApiKey(Base):
|
|
26
|
+
__tablename__ = "api_keys"
|
|
27
|
+
|
|
28
|
+
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
29
|
+
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
|
|
30
|
+
name: Mapped[str] = mapped_column(String(255))
|
|
31
|
+
key_hash: Mapped[str] = mapped_column(String(255))
|
|
32
|
+
key_prefix: Mapped[str] = mapped_column(String(10))
|
|
33
|
+
last_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
20
34
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
21
35
|
|
|
22
36
|
|
|
@@ -28,6 +42,7 @@ class UserCreate(BaseModel):
|
|
|
28
42
|
class UserLogin(BaseModel):
|
|
29
43
|
username: str
|
|
30
44
|
password: str
|
|
45
|
+
totp_code: str = ""
|
|
31
46
|
|
|
32
47
|
|
|
33
48
|
class UserOut(BaseModel):
|
|
@@ -35,6 +50,7 @@ class UserOut(BaseModel):
|
|
|
35
50
|
username: str
|
|
36
51
|
is_admin: bool
|
|
37
52
|
must_change_password: bool = False
|
|
53
|
+
totp_enabled: bool = False
|
|
38
54
|
allowed_groups: list[str] = []
|
|
39
55
|
|
|
40
56
|
model_config = {"from_attributes": True}
|
|
@@ -65,6 +81,48 @@ class ChangePassword(BaseModel):
|
|
|
65
81
|
new_password: str
|
|
66
82
|
|
|
67
83
|
|
|
84
|
+
class TotpSetupOut(BaseModel):
|
|
85
|
+
secret: str
|
|
86
|
+
qr_uri: str
|
|
87
|
+
qr_base64: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TotpVerifyIn(BaseModel):
|
|
91
|
+
code: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TotpStatusOut(BaseModel):
|
|
95
|
+
enabled: bool
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LoginOut(BaseModel):
|
|
99
|
+
access_token: str = ""
|
|
100
|
+
token_type: str = "bearer"
|
|
101
|
+
requires_2fa: bool = False
|
|
102
|
+
temp_token: str = ""
|
|
103
|
+
|
|
104
|
+
|
|
68
105
|
class TokenOut(BaseModel):
|
|
69
106
|
access_token: str
|
|
70
107
|
token_type: str = "bearer"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ApiKeyCreate(BaseModel):
|
|
111
|
+
name: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ApiKeyOut(BaseModel):
|
|
115
|
+
id: int
|
|
116
|
+
name: str
|
|
117
|
+
key_prefix: str
|
|
118
|
+
last_used_at: datetime | None
|
|
119
|
+
created_at: datetime
|
|
120
|
+
|
|
121
|
+
model_config = {"from_attributes": True}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ApiKeyCreated(BaseModel):
|
|
125
|
+
id: int
|
|
126
|
+
name: str
|
|
127
|
+
key: str
|
|
128
|
+
key_prefix: str
|