Compare commits
2
Commits
11c1b6379b
...
5de0501cbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5de0501cbc | ||
|
|
9b2882d5f4 |
No files matched your search
@@ -25,7 +25,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
# Enable CORS for all routes with specific headers support
|
||||
CORS(app, resources={r"/api/*": {"origins": ["https://213.226.125.13:8088"]}}, supports_credentials=True)
|
||||
# Cross-origin access is off by default (the UI is same-origin behind Nginx); allow extra origins via OVPMON_CORS_ORIGINS (comma-separated)
|
||||
CORS(app, resources={r"/api/*": {"origins": [o.strip() for o in os.getenv("OVPMON_CORS_ORIGINS", "").split(",") if o.strip()]}}, supports_credentials=True)
|
||||
|
||||
class OpenVPNAPI:
|
||||
def get_config_value(self, section, key, fallback=None):
|
||||
|
||||
@@ -26,7 +26,7 @@ app = FastAPI(
|
||||
# Enable CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://213.226.125.13:8088"],
|
||||
allow_origins=[o.strip() for o in os.getenv("OVPMON_CORS_ORIGINS", "").split(",") if o.strip()],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
||||
allow_headers=["Authorization", "Content-Type"],
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Admin username change (2026-09-30)
|
||||
|
||||
Goal: get rid of the well-known `admin` login and of the built-in `admin/password` account.
|
||||
|
||||
## Design
|
||||
|
||||
- The JWT carries `user_id`, not the name, and no other table references `users.username`, so a rename does not invalidate sessions.
|
||||
- Changing the username requires the current password and, when 2FA is enabled, a valid OTP. Wrong password/OTP counts toward the login rate limit.
|
||||
|
||||
## Changes
|
||||
|
||||
| Area | Change |
|
||||
|---|---|
|
||||
| API (`APP_CORE/openvpn_api_v3.py`) | `POST /api/auth/change-username`: body `new_username`, `current_password`, optional `otp`. Rules: `^[A-Za-z][A-Za-z0-9_.-]{2,31}$`, reserved names rejected (`admin`, `administrator`, `root`, `user`, `test`, `guest`), case-insensitive uniqueness (409). Helper `_current_user_id()` |
|
||||
| 2FA | `setup_2fa` puts the real username into the authenticator URI (was hardcoded `admin`) |
|
||||
| Bootstrap | `ensure_default_admin` no longer creates `admin/password`. With an empty `users` table it creates a user only from `OVPMON_INITIAL_ADMIN_USER` / `OVPMON_INITIAL_ADMIN_PASSWORD`; otherwise it logs an error |
|
||||
| UI (`Account.vue`) | "Change Username" button and modal (OTP field shown only if 2FA is on) |
|
||||
| UI (`App.vue`) | Header name synced from `/user/me` on start and on route change, and on the `ovpmon-user-changed` event; fixed the stale/hardcoded `Admin` |
|
||||
|
||||
## Existing installations
|
||||
|
||||
Rename directly in the DB (stop nothing; sessions stay valid):
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
c = sqlite3.connect("/var/lib/ovpmon/openvpn_monitor.db")
|
||||
c.execute("UPDATE users SET username=? WHERE username='admin'", ("<new-login>",)); c.commit()
|
||||
```
|
||||
|
||||
Take a DB backup first. Recovery when `users` is empty: temporarily set `OVPMON_INITIAL_ADMIN_USER/PASSWORD`, restart `ovpmon-api`, then remove the variables.
|
||||
|
||||
## Verification
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Login with new name / with `admin` | 200 / 401 |
|
||||
| No token | 401 |
|
||||
| `admin`, `root`, `ab`, `a/b`, `1abc` | 400 |
|
||||
| Wrong current password | 401 |
|
||||
| Rename to another valid name and back | 200, login with the new name works |
|
||||
| Empty `users` without / with seed variables (DB copy) | no user / user created |
|
||||
| Manual UI test (password change, 2FA enable) | passed; header-name bug found and fixed |
|
||||
@@ -0,0 +1,48 @@
|
||||
# Egress of OpenVPN clients via a Hysteria2 tunnel (2026-09-30)
|
||||
|
||||
Goal: all traffic of VPN clients leaves the internet through an exit node reached over an existing Hysteria2 client tunnel (`hytun`) on the VPN host.
|
||||
|
||||
```
|
||||
OpenVPN client --udp/1194--> tun0 (172.20.1.1/24)
|
||||
-> ip rule 102: from 172.20.1.0/24 -> table 100
|
||||
-> table 100: default dev hytun (metric 10), blackhole default (metric 1000)
|
||||
-> iptables nat POSTROUTING -s 172.20.1.0/24 -o hytun -j MASQUERADE
|
||||
-> hytun (TUN of the Hysteria2 client) --QUIC/UDP 443--> exit node --> Internet
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Hysteria2 client with a TUN device (`hytun`), a routing table (100) with `default dev hytun` and a `blackhole` fallback, and `rp_filter=2` (loose) on all/default/hytun (strict mode drops TUN replies).
|
||||
- Hysteria server-side `socks5`/`http` outbounds are TCP-only, so UDP needs TUN + policy routing, not a SOCKS chain.
|
||||
- The exit node needs no changes.
|
||||
|
||||
## Implementation (OpenRC service `ovpn-hytun`)
|
||||
|
||||
`depend: need hysteria-route; before openvpn`. On start:
|
||||
|
||||
```sh
|
||||
sysctl -qw net.ipv4.ip_forward=1
|
||||
ip rule add priority 102 from 172.20.1.0/24 lookup 100
|
||||
iptables -t nat -A POSTROUTING -s 172.20.1.0/24 -o hytun -j MASQUERADE
|
||||
iptables -t mangle -A FORWARD -o hytun -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu # and -i hytun
|
||||
iptables -A FORWARD -s 172.20.1.0/24 ! -o hytun -j DROP # anti-leak
|
||||
```
|
||||
|
||||
Rules are idempotent (`-C` before `-A`); `stop` removes them. Adjust `172.20.1.0/24` to the `server` network in `server.conf`. Persist `ip_forward` in `/etc/sysctl.d/`. Use POSIX `sh` in OpenRC scripts (no bash arrays).
|
||||
|
||||
## Properties
|
||||
|
||||
- Only the VPN subnet enters the tunnel; SSH, OpenVPN's own port (1194) and management traffic use the main table.
|
||||
- If the Hysteria client dies, table 100 falls to `blackhole`: clients lose internet, they do not leak through `eth0`.
|
||||
- Clients get public DNS (e.g. 1.1.1.1) that is resolved through the same tunnel.
|
||||
|
||||
## Verification
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Node: TCP and UDP (DNS) through `hytun` | works |
|
||||
| Reference (uid routed to `hytun`): external IP | exit node address |
|
||||
| Real VPN client: `https://ifconfig.me` | exit node address (end-to-end test passed) |
|
||||
| `ip rule`, `iptables -t nat -S POSTROUTING`, `rc-status` | rules present, services started |
|
||||
|
||||
Testing tip: a network namespace test that reuses the VPN subnet conflicts with a live `tun0`; test with a real client or a different, temporary subnet added to the rules.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Security hardening (2026-09-30)
|
||||
|
||||
Scope: a native (OpenRC) deployment of this suite on an internet-facing host. Findings from a review of exposed services, the fixes and their verification.
|
||||
|
||||
## Findings and results
|
||||
|
||||
| # | Severity | Finding | Fix | Result |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Critical | SSH accepted passwords for `root` (`PasswordAuthentication yes`, cloud-init drop-in overrode the main config); the host was already being brute-forced | `/etc/ssh/sshd_config.d/00-hardening.conf`: `PasswordAuthentication no`, `KbdInteractiveAuthentication no`, `PermitRootLogin prohibit-password`, `MaxAuthTries 3`, `LoginGraceTime 30` | key login works; password login → `Permission denied (publickey)` |
|
||||
| 2 | High | Path traversal in Profiler: `username` was an unvalidated string used in `client-config/<username>.ovpn` and as an `easyrsa` argument, service runs as root | pattern `^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$` in `schemas.py`; `validate_username()` in `services/pki.py`; realpath containment checks in `routers/profiles.py` and `services/generator.py` | `../../tmp/pwn`, `a/b`, `..`, `-x` → 422, no file created; valid profile create/revoke works |
|
||||
| 3 | High | 2FA bypass: the temporary token issued after the password step was accepted by every protected route | `token_required` (Flask) and `verify_token` (Profiler) reject tokens with `is_2fa_pending`; only `/api/auth/verify-2fa` uses them | temp token → 401 on `/api/v1/user/me`, `/profiles-api/config`, `change-username`; full token → OK |
|
||||
| 4 | Medium | `enable_2fa` logged the OTP and TOTP secret | log line reduced to "Attempting 2FA activation" | no secrets in logs |
|
||||
| 5 | Medium | CORS `*` with credentials on both APIs | allowed origin restricted to the panel origin; Profiler methods/headers narrowed | foreign `Origin` gets no `Access-Control-Allow-Origin` |
|
||||
| 6 | Medium | No brute-force throttling outside the app rate limit | fail2ban jails `sshd`, `sshd-ddos`, `ovpmon-login` (401/429/503 on `POST /api/auth/login`); admin IP in `ignoreip` | jails active, bans observed for SSH scanners |
|
||||
| 7 | Medium | Panel served over plain HTTP | Nginx TLS on the panel port (TLS 1.2/1.3, HSTS, `nosniff`, `X-Frame-Options`, `no-store`, login `limit_req` 5 r/min, HTTP→HTTPS redirect via `error_page 497`) | HTTPS 200, TLS 1.1 rejected, rate limit returns 503 |
|
||||
|
||||
Checked, no issue: JWT algorithm pinned to HS256, random 32-byte secret; SQL f-strings only use internal table/column names; backends bound to `127.0.0.1`; Nginx workers unprivileged.
|
||||
|
||||
## Residual risks
|
||||
|
||||
- Self-signed certificate: verify the fingerprint on first visit; use a CA-issued certificate when a domain exists.
|
||||
- The APIs run as root; split privileged OpenVPN/PKI operations into a separate helper.
|
||||
- Enable 2FA for the admin account.
|
||||
- Changes were applied in place on the host; keep them in the repository (see the commit "Harden auth and API").
|
||||
|
||||
## Rollback
|
||||
|
||||
Backups were taken on the host before each change: `sshd_config`, `ovpmon.conf` (Nginx), application files in `/root/app-bak/`, previous UI build `/var/www/ovpmon.bak`.
|
||||
@@ -105,6 +105,6 @@ server {
|
||||
|
||||
## 5. First Run & Initialization
|
||||
1. Access the UI via browser.
|
||||
2. Login with default credentials: `admin` / `password`.
|
||||
2. Sign in with the admin seeded via OVPMON_INITIAL_ADMIN_USER / OVPMON_INITIAL_ADMIN_PASSWORD (no built-in default user exists).
|
||||
3. **Immediately** change the password and set up 2FA in the Settings/Profile section.
|
||||
4. If using the Profiler, ensure the `easy-rsa` directory is present and initialized via the UI.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Deployment: Docker
|
||||
|
||||
Uses `docker-compose.yml` from the repository root.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Container | Ports | Notes |
|
||||
|---|---|---|---|
|
||||
| `app-ui` | `ovp-ui` | 80 | Nginx + built UI; proxies `/api/` and `/profiles-api/` |
|
||||
| `app-api` | `ovp-api` | 5001 | Flask monitoring API |
|
||||
| `app-gatherer` | `ovp-gatherer` | - | Parses `openvpn-status.log` |
|
||||
| `app-profiler` | `ovp-profiler` | 8000, 1194/udp | FastAPI + OpenVPN; needs `NET_ADMIN` and `/dev/net/tun` |
|
||||
|
||||
Volumes: `ovp_logs`, `ovp_config`, `ovp_pki`, `ovp_client_config`, `db_data`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `.env` next to `docker-compose.yml` (`JWT_SECRET` is mandatory: compose refuses to start without it):
|
||||
```bash
|
||||
cat > .env <<EOT
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
OVPMON_INITIAL_ADMIN_USER=<login>
|
||||
OVPMON_INITIAL_ADMIN_PASSWORD=<strong password>
|
||||
EOT
|
||||
chmod 600 .env
|
||||
```
|
||||
2. `docker-compose up -d --build`
|
||||
3. Open `http://<host>`, sign in, **PKI Configuration → Initialize PKI**.
|
||||
4. Remove `OVPMON_INITIAL_ADMIN_*` from `.env` and run `docker-compose up -d app-api` (the seed is used only while the users table is empty).
|
||||
|
||||
## Compose settings
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Published ports | `80/tcp` (UI) and `1194/udp` (VPN) only; `5001` and `8000` are `expose`d on `ovp-net` and reached through Nginx in `app-ui` |
|
||||
| Secrets | `JWT_SECRET` (required) → `OVPMON_API_SECRET_KEY` for both APIs |
|
||||
| Initial admin | `OVPMON_INITIAL_ADMIN_USER` / `OVPMON_INITIAL_ADMIN_PASSWORD` (optional, first start) |
|
||||
| CORS | `OVPMON_CORS_ORIGINS` (optional, comma-separated; off by default because the UI is same-origin) |
|
||||
| Restart policy | `unless-stopped` |
|
||||
|
||||
## Hardening
|
||||
|
||||
- Terminate TLS in front of `app-ui` (reverse proxy or a certificate mounted into the UI container); see [Nginx configuration](Nginx_Configuration.md). The container serves plain HTTP on 80.
|
||||
- `ovp-profiler` is privileged (`NET_ADMIN`, TUN): keep its API off the host network.
|
||||
- Back up the `db_data` and `ovp_pki` volumes; never commit `.env`.
|
||||
|
||||
## Operations
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
docker-compose logs -f app-api app-profiler
|
||||
docker-compose up -d --build app-ui # after UI changes
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
# Deployment: system services (no containers)
|
||||
|
||||
Tested on **Alpine 3.23 (OpenRC)**; Debian/Ubuntu (systemd) differs only in service manager. Unit/init templates: [`systemd/`](systemd), [`openrc/`](openrc/INSTALL.md). Generic notes: [Deployment](Deployment.md), [Service management](Service_Management.md), [Nginx](Nginx_Configuration.md).
|
||||
|
||||
## 1. Packages
|
||||
|
||||
- Alpine: `apk add python3 py3-pip nginx nodejs npm openvpn easy-rsa iptables bash`
|
||||
- Debian/Ubuntu: `apt install python3-venv nginx nodejs npm openvpn easy-rsa iptables`
|
||||
|
||||
## 2. Backend
|
||||
|
||||
```bash
|
||||
cd APP_CORE && python3 -m venv venv && venv/bin/pip install -r requirements.txt gunicorn
|
||||
cd APP_PROFILER && python3 -m venv venv && venv/bin/pip install -r requirements.txt
|
||||
mkdir -p APP_PROFILER/easy-rsa && cp -r /usr/share/easy-rsa/* APP_PROFILER/easy-rsa/ # Docker entrypoint does this automatically
|
||||
```
|
||||
|
||||
## 3. Environment file
|
||||
|
||||
`/etc/ovpmon/env` (chmod 600), loaded by the services:
|
||||
|
||||
```
|
||||
OVPMON_API_SECRET_KEY=<openssl rand -hex 32>
|
||||
OVPMON_API_HOST=127.0.0.1
|
||||
OVPMON_API_PORT=5001
|
||||
OVPMON_OPENVPN_MONITOR_DB_PATH=/var/lib/ovpmon/openvpn_monitor.db
|
||||
OVPMON_OPENVPN_MONITOR_LOG_PATH=/var/log/openvpn/openvpn-status.log
|
||||
OVPMON_PROFILER_DB_PATH=/var/lib/ovpmon/ovpn_profiler.db
|
||||
OVPMON_LOGGING_LEVEL=INFO
|
||||
OVPMON_CORS_ORIGINS=https://<HOST>:8088
|
||||
```
|
||||
|
||||
Create `/var/lib/ovpmon`, `/var/log/ovpmon`, `/var/log/openvpn`. For the first start also set `OVPMON_INITIAL_ADMIN_USER` / `OVPMON_INITIAL_ADMIN_PASSWORD`, then remove them.
|
||||
|
||||
## 4. Services
|
||||
|
||||
| Service | Command | Listens |
|
||||
|---|---|---|
|
||||
| `ovpmon-api` | `APP_CORE/venv/bin/gunicorn -w 2 -b 127.0.0.1:5001 openvpn_api_v3:app` (cwd `APP_CORE`) | 127.0.0.1:5001 |
|
||||
| `ovpmon-gatherer` | `APP_CORE/venv/bin/python openvpn_gatherer_v3.py` (cwd `APP_CORE`) | - |
|
||||
| `ovpmon-profiler` | `APP_PROFILER/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000` (cwd `APP_PROFILER`) | 127.0.0.1:8000 |
|
||||
|
||||
OpenRC (Alpine): `supervisor=supervise-daemon`, `respawn_delay=3`, source `/etc/ovpmon/env` in `start_pre`, then `rc-update add <svc> default && rc-service <svc> start`. systemd: use the units from `systemd/` with `EnvironmentFile=/etc/ovpmon/env`.
|
||||
|
||||
The Profiler restarts OpenVPN through `rc-service openvpn` (Alpine) or `systemctl openvpn`; on Alpine link the config: `ln -s server.conf /etc/openvpn/openvpn.conf` and enable the `openvpn` service.
|
||||
|
||||
## 5. UI and Nginx (HTTPS on 8088)
|
||||
|
||||
```bash
|
||||
cd APP_UI && npm install && npm run build
|
||||
mkdir -p /var/www/ovpmon && cp -r dist/. /var/www/ovpmon/
|
||||
```
|
||||
|
||||
Certificate (self-signed, replace with a real one when a domain is available):
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/ovpmon/tls && cd /etc/ovpmon/tls
|
||||
openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 825 \
|
||||
-subj "/CN=ovpmon" -addext "subjectAltName=IP:<HOST-IP>,DNS:ovpmon" -keyout ovpmon.key -out ovpmon.crt
|
||||
chmod 600 ovpmon.key
|
||||
```
|
||||
|
||||
Nginx server (`/etc/nginx/http.d/ovpmon.conf` on Alpine): `listen 8088 ssl`, TLS 1.2/1.3, `error_page 497 =301 https://$host:8088$request_uri`, security headers, `limit_req` (5 r/min) on `/api/auth/login`, `/` → static UI, `/api/` → `127.0.0.1:5001`, `/profiles-api/` → `127.0.0.1:8000/api/`. Full listing: [Nginx configuration](Nginx_Configuration.md). Do not declare a second `ssl_session_cache shared:SSL` zone with a different size than the main `nginx.conf`.
|
||||
|
||||
## 6. First run
|
||||
|
||||
1. `https://<host>:8088/` → sign in with the seeded admin → **Account**: change username/password, enable 2FA.
|
||||
2. **PKI Configuration → Initialize PKI** → generate server config → start OpenVPN → create profiles.
|
||||
|
||||
## 7. Host hardening (recommended)
|
||||
|
||||
- SSH: key-only (`PasswordAuthentication no`, `KbdInteractiveAuthentication no`, `PermitRootLogin prohibit-password`); note that cloud-init drop-ins in `/etc/ssh/sshd_config.d/` can override the main file, so put settings in `00-*.conf`.
|
||||
- fail2ban: jails `sshd` and one for `POST /api/auth/login` (401/429/503) on the Nginx access log.
|
||||
- Backends bound to `127.0.0.1`; only 8088 (UI/API) and the VPN port are public.
|
||||
|
||||
## 8. Verify
|
||||
|
||||
```bash
|
||||
rc-status | grep -E "ovpmon|nginx|openvpn" # or: systemctl status ovpmon-*
|
||||
ss -tlnp | grep -E ":(8088|5001|8000) "
|
||||
curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1:8088/ # 200
|
||||
curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1:8088/profiles-api/config # 401 without token
|
||||
```
|
||||
@@ -3,10 +3,18 @@
|
||||
Welcome to the documentation for the OpenVPN Monitor suite.
|
||||
|
||||
## 📚 General
|
||||
- [Deployment Guide](Deployment.md): How to install and configure the application on a Linux server.
|
||||
- [Deployment: Docker](Deployment_Docker.md): containers with docker-compose.
|
||||
- [Deployment: system services](Deployment_Native.md): systemd/OpenRC, HTTPS, host hardening.
|
||||
- [Deployment Guide](Deployment.md): generic native install notes.
|
||||
- [Service Management](Service_Management.md): Setting up systemd/OpenRC services.
|
||||
- [Nginx Configuration](Nginx_Configuration.md)
|
||||
- [Security Architecture](Security_Architecture.md): Details on Authentication, 2FA, and Security features.
|
||||
|
||||
## 🛠 Changes and results
|
||||
- [Security hardening (2026-09-30)](../Changes/2026-09-30_Security_Hardening.md)
|
||||
- [Admin username change (2026-09-30)](../Changes/2026-09-30_Admin_Username_Change.md)
|
||||
- [Egress via Hysteria2 (2026-09-30)](../Changes/2026-09-30_Egress_via_Hysteria2.md)
|
||||
|
||||
## 🔍 Core Monitoring (`APP_CORE`)
|
||||
The core module responsible for log parsing, real-time statistics, and the primary API.
|
||||
- [API Reference](../Core_Monitoring/API_Reference.md): Endpoints for monitoring data.
|
||||
|
||||
@@ -10,7 +10,7 @@ This includes:
|
||||
|
||||
## User Review Required
|
||||
> [!IMPORTANT]
|
||||
> **Default Credentials**: We will create a default admin user (e.g., `admin` / `password`) on first run if no users exist. The user MUST change this immediately.
|
||||
> **Initial admin**: no default user is created. On first run with an empty users table the admin is seeded only from OVPMON_INITIAL_ADMIN_USER / OVPMON_INITIAL_ADMIN_PASSWORD; the username can be changed in Account (see DOCS/Changes/2026-09-30_Admin_Username_Change.md).
|
||||
|
||||
> [!WARNING]
|
||||
> **Breaking Change**: Access to the current dashboard will be blocked until the user logs in.
|
||||
|
||||
@@ -1,61 +1,57 @@
|
||||
# OpenVPN Monitor & Profiler
|
||||
|
||||
A modern, full-stack management solution for OpenVPN servers. It combines real-time traffic monitoring, historical analytics, and comprehensive user profile/PKI management into a unified web interface. Perfect for both containerized (Docker) and native (Alpine/Debian/Ubuntu) deployments.
|
||||
Web suite for OpenVPN servers: real-time traffic monitoring, history/analytics, PKI and client-profile management, one UI.
|
||||
|
||||
## 🏗️ Project Architecture
|
||||
| Component | Dir | Stack | Default port |
|
||||
|---|---|---|---|
|
||||
| UI | `APP_UI/` | Vue 3 + Vite, served by Nginx | 80 (Docker) / 8088 (native, TLS) |
|
||||
| Monitoring API | `APP_CORE/` | Flask (gunicorn) | 5001 (internal) |
|
||||
| Data gatherer | `APP_CORE/` | Python daemon | - |
|
||||
| Profiler API | `APP_PROFILER/` | FastAPI (uvicorn) | 8000 (internal) |
|
||||
|
||||
The project is modularized into four core microservices, split between **Monitoring (Core)** and **Management (Profiler)**:
|
||||
Nginx is the only public entry point: `/` UI, `/api/` Monitoring API, `/profiles-api/` Profiler API.
|
||||
|
||||
| Component | Directory | Service Name | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **User Interface** | `APP_UI/` | `ovp-ui` | Vue 3 + Vite SPA + Nginx. Communicates with both APIs. |
|
||||
| **Monitoring API** | `APP_CORE/` | `ovp-api` | Flask API for real-time stats, sessions, and bandwidth data. |
|
||||
| **Data Gatherer** | `APP_CORE/` | `ovp-gatherer` | Background service for traffic log aggregation & TSDB logic. |
|
||||
| **Profiler API** | `APP_PROFILER/` | `ovp-profiler` | FastAPI module for PKI management, User Profiles, and VPN control. |
|
||||
## Quick start
|
||||
|
||||
## 📦 Quick Start (Docker)
|
||||
- **Containers:** `docker-compose up -d --build`, open `http://<host>`. Details: [Deployment: Docker](DOCS/General/Deployment_Docker.md).
|
||||
- **System services** (systemd / OpenRC, no containers): [Deployment: native](DOCS/General/Deployment_Native.md).
|
||||
|
||||
The recommended way to deploy is using Docker Compose:
|
||||
After the first start: sign in, open **PKI Configuration** → **Initialize PKI**, generate the server config, start OpenVPN, create profiles.
|
||||
|
||||
1. **Clone the repository**
|
||||
2. **Start all services**:
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
3. **Access the Dashboard**: Open `http://localhost` (or your server IP) in your browser.
|
||||
4. **Initialize PKI**: On the first run, navigate to the **PKI Configuration** page in the UI and click **Initialize PKI**. This sets up the CA and Easy-RSA workspace.
|
||||
## First login and credentials
|
||||
|
||||
## ⚙️ Configuration
|
||||
No default user is created. Seed the initial admin with `OVPMON_INITIAL_ADMIN_USER` / `OVPMON_INITIAL_ADMIN_PASSWORD` on first start (empty `users` table only), then remove them. Change the username and password and enable 2FA in **Account**.
|
||||
|
||||
The system uses a unified configuration approach. Settings can be defined in `config.ini` files or overridden by environment variables following the `OVPMON_{SECTION}_{KEY}` format.
|
||||
## Configuration
|
||||
|
||||
### Key Environment Variables
|
||||
`config.ini` per component; overridden by `OVPMON_{SECTION}_{KEY}` environment variables.
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| :--- | :--- | :--- |
|
||||
| `OVPMON_API_SECRET_KEY` | Unified JWT Secret Key (used by both APIs) | `supersecret` |
|
||||
| `OVPMON_PROFILER_DB_PATH` | Path to Profiler (users/pki) SQLite DB | `/app/db/ovpn_profiler.db` |
|
||||
| `OVPMON_OPENVPN_MONITOR_DB_PATH` | Path to Monitoring (traffic) SQLite DB | `/app/db/openvpn_monitor.db` |
|
||||
| `OVPMON_OPENVPN_MONITOR_LOG_PATH`| Path to OpenVPN status log | `/var/log/openvpn/openvpn-status.log` |
|
||||
| `OVPMON_LOGGING_LEVEL` | Logging level (INFO/DEBUG) | `INFO` |
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `OVPMON_API_SECRET_KEY` | JWT secret shared by both APIs (**must be random**) |
|
||||
| `OVPMON_INITIAL_ADMIN_USER` / `_PASSWORD` | One-time admin seed |
|
||||
| `OVPMON_CORS_ORIGINS` | Extra allowed CORS origins, comma-separated (empty = same-origin only) |
|
||||
| `OVPMON_OPENVPN_MONITOR_DB_PATH` | Monitoring DB |
|
||||
| `OVPMON_PROFILER_DB_PATH` | Profiler DB |
|
||||
| `OVPMON_OPENVPN_MONITOR_LOG_PATH` | `openvpn-status.log` path |
|
||||
| `OVPMON_LOGGING_LEVEL` | `INFO` / `DEBUG` |
|
||||
|
||||
## 🛠️ Performance & Environment Awareness
|
||||
## Documentation
|
||||
|
||||
- **Container Transparency**: When running in Docker, the Profiler manages OpenVPN directly to bypass cgroups restrictions.
|
||||
- **Host Integration**: When running natively on Alpine or Debian/Ubuntu, it automatically switches to `rc-service` or `systemctl`.
|
||||
- **Persistent Data**: Logs, Certificates (PKI), and Databases are stored in Docker volumes (`ovp_logs`, `ovp_pki`, `db_data`).
|
||||
- Index: [DOCS/General/Index.md](DOCS/General/Index.md)
|
||||
- Deployment: [Docker](DOCS/General/Deployment_Docker.md) · [System services](DOCS/General/Deployment_Native.md) · [Nginx](DOCS/General/Nginx_Configuration.md) · [Service management](DOCS/General/Service_Management.md)
|
||||
- Security model: [Security Architecture](DOCS/General/Security_Architecture.md)
|
||||
- APIs: [Monitoring](DOCS/Core_Monitoring/API_Reference.md) · [Profiler](DOCS/Profiler_Management/API_Reference.md)
|
||||
|
||||
## 📚 Development
|
||||
## Changes and results
|
||||
|
||||
### Component Development
|
||||
- **UI**: Uses `composables/useApi.js` to route requests to the appropriate backend service based on URL.
|
||||
- **Profiler**: Clean Python/FastAPI code with SQLAlchemy models. Supports "staging" local mode for development without root access.
|
||||
- **Core**: Lightweight Flask services focused on high-performance log parsing.
|
||||
| Date | Change | Document |
|
||||
|---|---|---|
|
||||
| 2026-09-30 | Security hardening: path traversal, 2FA token bypass, CORS, log leak, HTTPS, SSH, fail2ban | [Security hardening](DOCS/Changes/2026-09-30_Security_Hardening.md) |
|
||||
| 2026-09-30 | Admin username change (API + UI), no built-in default admin | [Admin username change](DOCS/Changes/2026-09-30_Admin_Username_Change.md) |
|
||||
| 2026-09-30 | Route OpenVPN clients through a Hysteria2 tunnel to an exit node | [Egress via Hysteria2](DOCS/Changes/2026-09-30_Egress_via_Hysteria2.md) |
|
||||
|
||||
---
|
||||
## Notes
|
||||
|
||||
### ⚠️ Important Notes
|
||||
|
||||
1. **Privileged Mode**: The `ovp-profiler` container requires `NET_ADMIN` capabilities for iptables and TUN management.
|
||||
2. **Network Setup**: Ensure `net.ipv4.ip_forward=1` is enabled (handled automatically in the docker-compose `sysctls` section).
|
||||
3. **JWT Safety**: Always change the `OVPMON_API_SECRET_KEY` in production.
|
||||
- `ovpmon-api` and `ovpmon-profiler` currently run as root (they manage OpenVPN and PKI).
|
||||
- Keep `easy-rsa/`, `client-config/`, databases and `*.env` out of git: they contain private keys and secrets.
|
||||
+37
-24
@@ -1,9 +1,16 @@
|
||||
version: '3.8'
|
||||
# OpenVPN Monitor & Profiler (containers)
|
||||
# Required: JWT_SECRET in .env (openssl rand -hex 32)
|
||||
# First start only: OVPMON_INITIAL_ADMIN_USER / OVPMON_INITIAL_ADMIN_PASSWORD in .env
|
||||
# Optional: OVPMON_CORS_ORIGINS (comma-separated origins; off by default, UI is same-origin via Nginx)
|
||||
|
||||
x-secret: &jwt-secret
|
||||
OVPMON_API_SECRET_KEY: ${JWT_SECRET:?JWT_SECRET must be set in .env (openssl rand -hex 32)}
|
||||
|
||||
services:
|
||||
app-ui:
|
||||
build: ./APP_UI
|
||||
container_name: ovp-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
@@ -12,75 +19,81 @@ services:
|
||||
networks:
|
||||
- ovp-net
|
||||
environment:
|
||||
- OVP_API_HOST=ovp-api
|
||||
- OVP_API_PORT=5001
|
||||
- OVP_PROFILER_HOST=ovp-profiler
|
||||
- OVP_PROFILER_PORT=8000
|
||||
|
||||
OVP_API_HOST: ovp-api
|
||||
OVP_API_PORT: 5001
|
||||
OVP_PROFILER_HOST: ovp-profiler
|
||||
OVP_PROFILER_PORT: 8000
|
||||
|
||||
app-gatherer:
|
||||
build:
|
||||
context: ./APP_CORE
|
||||
dockerfile: Dockerfile.gatherer
|
||||
container_name: ovp-gatherer
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ovp_logs:/var/log/openvpn
|
||||
- db_data:/app/db
|
||||
depends_on:
|
||||
- app-profiler
|
||||
environment:
|
||||
- OVPMON_OPENVPN_MONITOR_DB_PATH=/app/db/openvpn_monitor.db
|
||||
- OVPMON_OPENVPN_MONITOR_LOG_PATH=/var/log/openvpn/openvpn-status.log
|
||||
- OVPMON_LOGGING_LEVEL=INFO
|
||||
networks:
|
||||
- ovp-net
|
||||
environment:
|
||||
OVPMON_OPENVPN_MONITOR_DB_PATH: /app/db/openvpn_monitor.db
|
||||
OVPMON_OPENVPN_MONITOR_LOG_PATH: /var/log/openvpn/openvpn-status.log
|
||||
OVPMON_LOGGING_LEVEL: INFO
|
||||
|
||||
app-api:
|
||||
build:
|
||||
context: ./APP_CORE
|
||||
dockerfile: Dockerfile.api
|
||||
container_name: ovp-api
|
||||
ports:
|
||||
- "5001:5001"
|
||||
restart: unless-stopped
|
||||
# Not published: reached only through app-ui (Nginx) on ovp-net
|
||||
expose:
|
||||
- "5001"
|
||||
volumes:
|
||||
- db_data:/app/db
|
||||
networks:
|
||||
- ovp-net
|
||||
environment:
|
||||
- OVPMON_API_SECRET_KEY=${JWT_SECRET:-supersecret}
|
||||
- OVPMON_API_PORT=5001
|
||||
- OVPMON_OPENVPN_MONITOR_DB_PATH=/app/db/openvpn_monitor.db
|
||||
- OVPMON_LOGGING_LEVEL=INFO
|
||||
depends_on:
|
||||
- app-gatherer
|
||||
environment:
|
||||
<<: *jwt-secret
|
||||
OVPMON_API_PORT: 5001
|
||||
OVPMON_OPENVPN_MONITOR_DB_PATH: /app/db/openvpn_monitor.db
|
||||
OVPMON_LOGGING_LEVEL: INFO
|
||||
# Initial admin: used only while the users table is empty (remove after first start)
|
||||
OVPMON_INITIAL_ADMIN_USER: ${OVPMON_INITIAL_ADMIN_USER:-}
|
||||
OVPMON_INITIAL_ADMIN_PASSWORD: ${OVPMON_INITIAL_ADMIN_PASSWORD:-}
|
||||
OVPMON_CORS_ORIGINS: ${OVPMON_CORS_ORIGINS:-}
|
||||
|
||||
app-profiler:
|
||||
build: ./APP_PROFILER
|
||||
container_name: ovp-profiler
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
devices:
|
||||
|
||||
- "/dev/net/tun:/dev/net/tun"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
# VPN port only; the profiler API (8000) is reached through app-ui
|
||||
- "1194:1194/udp"
|
||||
expose:
|
||||
- "8000"
|
||||
volumes:
|
||||
- ovp_logs:/var/log/openvpn
|
||||
- ovp_config:/etc/openvpn
|
||||
- db_data:/app/db
|
||||
- ovp_client_config:/app/client-config
|
||||
- ovp_pki:/app/easy-rsa
|
||||
|
||||
|
||||
networks:
|
||||
- ovp-net
|
||||
environment:
|
||||
- OVPMON_API_SECRET_KEY=${JWT_SECRET:-supersecret}
|
||||
- OVPMON_PROFILER_DB_PATH=/app/db/ovpn_profiler.db
|
||||
|
||||
<<: *jwt-secret
|
||||
OVPMON_PROFILER_DB_PATH: /app/db/ovpn_profiler.db
|
||||
OVPMON_CORS_ORIGINS: ${OVPMON_CORS_ORIGINS:-}
|
||||
|
||||
networks:
|
||||
ovp-net:
|
||||
|
||||
Reference in new issue
Block a user