Skip to main content

Self-hosted web application for remote server management via SSH terminal and SFTP file browser

Project description

webgate

Python FastAPI License Docker Status Tests Docs

Self-hosted web application for remote server management via SSH terminal and SFTP file browser. A modern Python replacement combining the best of webssh and filebrowser into a single unified tool with a FileZilla-inspired interface.

Documentation: kalexnolasco.github.io/webgate


Why webgate?

Managing remote servers often means juggling SSH clients, SFTP tools, credentials, and VPN configs across your team. In many real-world scenarios, direct SSH access to every server isn't possible -- you only have HTTP/HTTPS access to a single entry point.

The problem

A typical client infrastructure looks like this: a network of internal servers (databases, application servers, workers, etc.) behind a firewall, with only one server exposed via HTTP. Your team needs to manage all of them, but each engineer would need VPN access, local SSH clients, and credential management.

flowchart TB
    subgraph internet ["Internet"]
        YOU["Your Team"]
    end

    subgraph firewall ["Client Firewall"]
        GW["Gateway Server
        (HTTP only)"]

        subgraph internal ["Internal Network"]
            DB1[(PostgreSQL
            10.0.1.10)]
            DB2[(MySQL
            10.0.1.11)]
            APP1["App Server
            10.0.1.20"]
            APP2["App Server
            10.0.1.21"]
            WORKER["Worker
            10.0.1.30"]
            REDIS["Redis
            10.0.1.40"]
        end
    end

    YOU -- "HTTPS :443" --> GW
    GW -. "SSH :22" .-> DB1
    GW -. "SSH :22" .-> DB2
    GW -. "SSH :22" .-> APP1
    GW -. "SSH :22" .-> APP2
    GW -. "SSH :22" .-> WORKER
    GW -. "SSH :22" .-> REDIS

    style internet fill:#e8f0fe,stroke:#4a90d9
    style firewall fill:#fff3e0,stroke:#ff9800
    style internal fill:#f0f9e8,stroke:#5cb85c
    style GW fill:#ffcc02,stroke:#e6a800,color:#333
    style YOU fill:#4a90d9,stroke:#2a6cb5,color:#fff

The solution

Deploy webgate on the gateway server. Your entire team gets browser-based SSH and SFTP access to every internal server -- no VPN, no local SSH clients, no credential files scattered across laptops.

flowchart TB
    subgraph internet ["Internet"]
        ENG1["Engineer 1
        (Browser)"]
        ENG2["Engineer 2
        (Browser)"]
        ENG3["Engineer 3
        (Browser)"]
    end

    subgraph firewall ["Client Firewall"]
        WG["webgate
        Gateway Server
        :443"]

        subgraph internal ["Internal Network"]
            DB1[(PostgreSQL
            10.0.1.10)]
            DB2[(MySQL
            10.0.1.11)]
            APP1["App Server
            10.0.1.20"]
            APP2["App Server
            10.0.1.21"]
            WORKER["Worker
            10.0.1.30"]
            REDIS["Redis
            10.0.1.40"]
        end
    end

    ENG1 -- "HTTPS" --> WG
    ENG2 -- "HTTPS" --> WG
    ENG3 -- "HTTPS" --> WG
    WG -- "SSH" --> DB1
    WG -- "SSH" --> DB2
    WG -- "SSH" --> APP1
    WG -- "SSH" --> APP2
    WG -- "SSH" --> WORKER
    WG -- "SSH" --> REDIS

    style internet fill:#e8f0fe,stroke:#4a90d9
    style firewall fill:#fff3e0,stroke:#ff9800
    style internal fill:#f0f9e8,stroke:#5cb85c
    style WG fill:#5cb85c,stroke:#449d44,color:#fff
    style ENG1 fill:#4a90d9,stroke:#2a6cb5,color:#fff
    style ENG2 fill:#4a90d9,stroke:#2a6cb5,color:#fff
    style ENG3 fill:#4a90d9,stroke:#2a6cb5,color:#fff

Use cases

Scenario How webgate helps
Client with restricted access Deploy on the only HTTP-accessible server, reach all internal machines via SSH/SFTP
On-call / incident response Open a browser from any device, connect instantly -- no laptop with SSH keys needed
Team onboarding Admin creates a user, assigns groups -- new engineer has access in seconds
Audit & compliance Centralized access point, all connections go through one managed gateway
Air-gapped environments Only needs outbound HTTP from the gateway, no inbound ports on internal servers
Multi-client management Run separate webgate instances per client, each with its own server registry

Features

Category Capabilities
SSH Web Terminal Browser-based terminal via xterm.js + asyncssh, resize handling, copy/paste, multi-tab sessions
SFTP File Browser Directory listing, upload/download, drag & drop, rename, delete, mkdir, chmod, breadcrumb navigation
File Editor In-browser text editor powered by CodeMirror 6 with syntax highlighting (oneDark theme)
File Preview PDF viewer, image preview (PNG, JPG, GIF, SVG, WebP) directly in the browser
Server Registry Add/edit/delete servers, groups, tags, password & key auth, test connectivity, import/export
Quick Connect One-off SSH/SFTP connections without saving server config (FileZilla-style toolbar)
User Management Admin creates users and assigns server groups -- users only see what they're allowed to
Credential Security SSH passwords and private keys encrypted at rest with Fernet (derived from app secret)
Connection Pool SFTP connections are reused per server (5 min TTL), not opened/closed on every request
Docker Ready Multi-stage Dockerfile, single-command deployment, health checks, persistent volumes
Zero Frontend Build Vanilla JS + Alpine.js, no npm/node required, all assets from CDN

Screenshots

Site Manager

Site Manager

SSH Terminal

SSH Terminal

SFTP File Browser

SFTP Browser

File Editor (CodeMirror 6)

Editor

Quick Start

Docker (recommended)

# Clone and run
git clone https://gitlab.wdna.com/wdna/webgate.git
cd webgate
docker compose up -d

# Open http://localhost:8443
# Login with admin / admin (you'll be asked to change the password)

Important: On first launch, webgate creates a default admin / admin account. You will be forced to change the password on first login. Set a strong secret key in production:

WEBGATE_SECRET_KEY=$(openssl rand -hex 32) docker compose up -d

Development with SSH demo server

A compose.dev.yml is included with a demo SSH container for testing:

docker compose -f compose.dev.yml up -d
# webgate at :8443 + SSH demo server (user: demo, password: demo)

Local Development

# Install dependencies
uv sync

# Run dev server with auto-reload
uv run uvicorn webgate.app:create_app --factory --reload --host 0.0.0.0 --port 8443

# Or simply
uv run python -m webgate

User Management

webgate uses a role-based access model with two roles: admin and user.

How it works

flowchart LR
    ADMIN["Admin"] -->|creates| SERVERS["Servers
    (grouped)"]
    ADMIN -->|creates| USERS["Users"]
    ADMIN -->|assigns groups to| USERS

    USERS -->|can access| ALLOWED["Servers in
    allowed groups"]

    style ADMIN fill:#5cb85c,stroke:#449d44,color:#fff
    style SERVERS fill:#e8f0fe,stroke:#4a90d9
    style USERS fill:#fff3e0,stroke:#ff9800
    style ALLOWED fill:#f0f9e8,stroke:#5cb85c
Admin User
Add/edit/delete servers Yes No
Create/delete users Yes No
Assign groups to users Yes No
See all servers Yes Only servers in assigned groups
SSH / SFTP / Test Yes Yes (allowed servers only)
Quick Connect Yes Yes

Example workflow

  1. Admin logs in (default admin/admin, forced to change password)
  2. Admin adds servers and organizes them into groups (production, staging, dev)
  3. Admin creates users and assigns groups:
    • alice -> production, staging (senior engineer)
    • bob -> staging, dev (junior, no prod access)
    • charlie -> production (read-only ops, only prod)
  4. Each user logs in and sees only the servers in their assigned groups

Architecture

webgate/
├── src/webgate/
│   ├── __main__.py          # Entry point: uvicorn launcher
│   ├── app.py               # FastAPI app factory, lifespan, middleware
│   ├── config.py            # Pydantic Settings (env vars, defaults)
│   ├── auth/                # JWT auth, user management, admin routes
│   │   ├── models.py        # User model + Pydantic schemas
│   │   ├── service.py       # Password hashing, JWT, seed_admin, CRUD
│   │   └── routes.py        # Login, change-password, admin user CRUD
│   ├── servers/             # Server registry CRUD, connectivity test
│   │   ├── models.py        # Server model + schemas
│   │   ├── service.py       # CRUD with group-based filtering
│   │   └── crypto.py        # Fernet encrypt/decrypt for credentials
│   ├── terminal/            # WebSocket SSH bridge (xterm.js <-> asyncssh)
│   ├── files/               # SFTP operations with connection pooling
│   │   ├── sftp_service.py  # SFTP client wrapper
│   │   ├── pool.py          # Connection pool (reuse per server, 5min TTL)
│   │   └── routes.py        # REST API: ls, read, write, upload, download, chmod
│   ├── db/                  # SQLAlchemy async engine + session factory
│   └── static/              # Frontend (single index.html, FileZilla-style UI)
├── tests/
├── Dockerfile               # Multi-stage build (python:3.13-slim)
├── Dockerfile.ssh-demo      # Demo SSH container for development
├── compose.yml              # Production deployment
├── compose.dev.yml          # Development with SSH demo server
└── pyproject.toml
flowchart LR
    subgraph Browser
        XT[xterm.js] --> WS[WebSocket]
        UI[Alpine.js UI] --> REST[REST API]
        CM[CodeMirror] --> REST
    end

    subgraph webgate ["webgate (FastAPI)"]
        WS --> SSH[asyncssh Session]
        REST --> POOL[SFTP Pool]
        POOL --> SFTP[asyncssh SFTP]
        REST --> DB[(SQLite)]
        REST --> AUTH[JWT Auth]
    end

    SSH --> RS[Remote Server]
    SFTP --> RS

    style Browser fill:#e8f0fe,stroke:#4a90d9
    style webgate fill:#f0f9e8,stroke:#5cb85c
    style RS fill:#fff3e0,stroke:#ff9800

API Reference

Auth

Method Endpoint Description
POST /api/auth/login Login, returns JWT token
GET /api/auth/me Current user info (includes must_change_password)
POST /api/auth/change-password Change own password
GET /api/auth/users List all users (admin only)
POST /api/auth/users Create user with allowed groups (admin only)
PUT /api/auth/users/{id}/groups Update user's allowed groups (admin only)
PUT /api/auth/users/{id}/password Reset user's password (admin only)
DELETE /api/auth/users/{id} Delete user (admin only)
GET /api/auth/audit Audit log with filters (admin only)

Server Registry

Method Endpoint Description
GET /api/servers List servers (filtered by user's allowed groups)
POST /api/servers Create server (admin only)
GET /api/servers/{id} Server details
PUT /api/servers/{id} Update server (admin only)
DELETE /api/servers/{id} Delete server (admin only)
POST /api/servers/{id}/test Test SSH connectivity
GET /api/servers/groups List groups (filtered by user's access)
POST /api/servers/import Bulk import (admin only)
GET /api/servers/export Export all servers (admin only)

Terminal (WebSocket)

Method Endpoint Description
WS /api/ws/terminal/{server_id} SSH terminal to saved server
WS /api/ws/terminal/quick Quick connect with inline credentials

SFTP File Manager

Method Endpoint Description
GET /api/files/{id}/ls?path= Directory listing
GET /api/files/{id}/read?path= Read text file
GET /api/files/{id}/download?path= Download file
POST /api/files/{id}/upload?path= Upload files (multipart)
PUT /api/files/{id}/write Save text file
POST /api/files/{id}/mkdir Create directory
POST /api/files/{id}/rename Rename/move
DELETE /api/files/{id}/delete?path= Delete file/directory
POST /api/files/{id}/chmod Change permissions
GET /api/files/{id}/stat?path= File metadata

Health

Method Endpoint Description
GET /api/health Health check ({"status": "ok"})

Configuration

All settings are configurable via environment variables with the WEBGATE_ prefix:

Variable Default Description
WEBGATE_HOST 0.0.0.0 Server bind address
WEBGATE_PORT 8443 Server port
WEBGATE_SECRET_KEY change-me-in-production JWT signing + Fernet encryption key
WEBGATE_DB_URL sqlite+aiosqlite:///./webgate.db Database URL
WEBGATE_ALLOWED_ORIGINS * CORS origins (comma-separated)
WEBGATE_LOG_LEVEL info Log level
WEBGATE_SESSION_TIMEOUT 3600 SSH session timeout (seconds)
WEBGATE_MAX_UPLOAD_SIZE 104857600 Max upload size (100 MB)
WEBGATE_JWT_ALGORITHM HS256 JWT algorithm
WEBGATE_JWT_EXPIRE_MINUTES 1440 Token expiry (24 hours)

Docker

Build and Run

docker compose up -d
# Login at http://localhost:8443 with admin / admin

Production with TLS (Caddy reverse proxy)

# compose.yml
services:
  webgate:
    build: .
    container_name: webgate
    restart: unless-stopped
    ports:
      - "8443:8443"
    volumes:
      - webgate-data:/data
    environment:
      WEBGATE_SECRET_KEY: "${WEBGATE_SECRET_KEY}"

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data

volumes:
  webgate-data:
  caddy-data:
# Caddyfile
webgate.example.com {
    reverse_proxy webgate:8443
}

Useful Commands

docker compose logs -f webgate    # View logs
docker compose down               # Stop (data preserved)
docker compose down -v            # Stop + delete data (fresh start)
docker compose build --no-cache   # Rebuild from scratch

Development

# Install with dev dependencies
uv sync

# Run with auto-reload
uv run uvicorn webgate.app:create_app --factory --reload --host 0.0.0.0 --port 8443

# Run with demo SSH server
docker compose -f compose.dev.yml up -d

# Lint & format
uv run ruff check src/ tests/
uv run ruff format src/ tests/

# Type check
uv run pyright src/

# Test
uv run pytest tests/ -v

# Test with coverage
uv run coverage run -m pytest tests/ -v && uv run coverage report

Tech Stack

Layer Technology
Backend Python 3.11+, FastAPI, uvicorn, asyncssh, SQLAlchemy, aiosqlite, Pydantic v2
Auth JWT (python-jose), bcrypt, Fernet (cryptography), slowapi (rate limiting)
Frontend Vanilla JS, Alpine.js, xterm.js, CodeMirror 6
Database SQLite (default), any SQLAlchemy-supported DB
Container Docker, multi-stage build, python:3.13-slim

Security

  • Default admin password forced change -- admin/admin is temporary, must be changed on first login
  • SSH credentials encrypted at rest with Fernet (key derived from WEBGATE_SECRET_KEY)
  • JWT authentication with configurable expiry
  • Group-based access control -- users only see servers in their assigned groups
  • SFTP path traversal protection (server-side normalization and validation)
  • CORS configurable per deployment
  • No plaintext password storage (user passwords hashed with bcrypt)
  • Admin-only server management -- regular users cannot add, edit, or delete servers

Roadmap

  • SSH web terminal (xterm.js + asyncssh)
  • SFTP file browser with full CRUD
  • Server registry with groups and tags
  • In-browser text editor (CodeMirror 6)
  • PDF and image preview
  • Quick connect toolbar
  • Docker deployment
  • JWT authentication with forced password change
  • Fernet credential encryption
  • Admin user management with group-based access
  • SFTP connection pool (reuse connections, 5 min TTL)
  • Multi-tab split pane (terminal + files side by side)
  • SSH key management (per-server key upload with visual indicator)
  • File search / filter within SFTP listings
  • Server import/export from UI (JSON)
  • Session persistence across page reloads
  • Rate limiting on auth endpoints (slowapi)
  • Audit log (admin-viewable action history)

Requirements

  • Python 3.11+
  • Linux, macOS, or WSL (asyncssh requires Unix-like OS)
  • Target servers must have SSH enabled

License

MIT


webgate -- Remote server management, simplified.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

webgate-0.1.0.tar.gz (407.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

webgate-0.1.0-py3-none-any.whl (46.2 kB view details)

Uploaded Python 3

File details

Details for the file webgate-0.1.0.tar.gz.

File metadata

  • Download URL: webgate-0.1.0.tar.gz
  • Upload date:
  • Size: 407.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for webgate-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6a90d9bdbf8eee227d5ff3dbf5cd63368bf6593ac8f5579bc334b619c014216c
MD5 79e8ee2a613341ff39198927af209843
BLAKE2b-256 936378a16ab19493dbf1bc783aadee8cd58834fe63e6cb843b23f983a4af9730

See more details on using hashes here.

File details

Details for the file webgate-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: webgate-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 46.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for webgate-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e32c2fff944c10fda0cedc3f0ac7d37ed472e13dff6cfdaa24301f76adb559a2
MD5 93ee693b8cdca51819dddd38789a9215
BLAKE2b-256 c3fbf46dea64daf02e24031ddf273b6952b4e4e634dafa6e42cc7aea945566fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page