Compose: require JWT secret, seed admin via env, stop publishing internal ports

- JWT_SECRET is mandatory (no more "supersecret" fallback).
- Pass OVPMON_INITIAL_ADMIN_USER/PASSWORD and OVPMON_CORS_ORIGINS to the
  APIs; add restart policy and drop the obsolete compose "version".
- Publish only 80/tcp and 1194/udp; expose 5001 and 8000 on ovp-net.
- CORS origins now come from OVPMON_CORS_ORIGINS instead of a hardcoded
  host (default: same-origin only).
- Update Docker/native deployment docs and README accordingly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
iclaoudezinandClaude Sonnet 5.5 committed 2026-09-30 12:14:22 +00:00
1 parent 9b2882d5f4
commit 5de0501cbc
6 files changed
+57 -33

No files matched your search

+2 -1
View File
@@ -25,7 +25,8 @@ logger = logging.getLogger(__name__)
app = Flask(__name__) app = Flask(__name__)
# Enable CORS for all routes with specific headers support # 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: class OpenVPNAPI:
def get_config_value(self, section, key, fallback=None): def get_config_value(self, section, key, fallback=None):
+1 -1
View File
@@ -26,7 +26,7 @@ app = FastAPI(
# Enable CORS # Enable CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, 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_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"], allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"], allow_headers=["Authorization", "Content-Type"],
+15 -7
View File
@@ -15,7 +15,7 @@ Volumes: `ovp_logs`, `ovp_config`, `ovp_pki`, `ovp_client_config`, `db_data`.
## Steps ## Steps
1. Set a random JWT secret and the initial admin (compose reads them from the environment / `.env`): 1. Create `.env` next to `docker-compose.yml` (`JWT_SECRET` is mandatory: compose refuses to start without it):
```bash ```bash
cat > .env <<EOT cat > .env <<EOT
JWT_SECRET=$(openssl rand -hex 32) JWT_SECRET=$(openssl rand -hex 32)
@@ -24,17 +24,25 @@ Volumes: `ovp_logs`, `ovp_config`, `ovp_pki`, `ovp_client_config`, `db_data`.
EOT EOT
chmod 600 .env chmod 600 .env
``` ```
Pass the two `OVPMON_INITIAL_ADMIN_*` variables to `app-api` (`environment:`) for the first start.
2. `docker-compose up -d --build` 2. `docker-compose up -d --build`
3. Open `http://<host>`, sign in, **PKI Configuration → Initialize PKI**. 3. Open `http://<host>`, sign in, **PKI Configuration → Initialize PKI**.
4. Remove `OVPMON_INITIAL_ADMIN_*` from `.env` and recreate `app-api`. 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 ## Hardening
- Publish only what is needed: in production drop the `5001:5001` and `8000:8000` mappings (Nginx already reaches them on `ovp-net`). - 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.
- Terminate TLS in front of `app-ui` (reverse proxy or mount a cert into the UI container); see [Nginx configuration](Nginx_Configuration.md). - `ovp-profiler` is privileged (`NET_ADMIN`, TUN): keep its API off the host network.
- `ovp-profiler` is privileged (`NET_ADMIN`, TUN): restrict access to its port to the UI network. - Back up the `db_data` and `ovp_pki` volumes; never commit `.env`.
- Back up the `db_data` and `ovp_pki` volumes.
## Operations ## Operations
+1
View File
@@ -27,6 +27,7 @@ OVPMON_OPENVPN_MONITOR_DB_PATH=/var/lib/ovpmon/openvpn_monitor.db
OVPMON_OPENVPN_MONITOR_LOG_PATH=/var/log/openvpn/openvpn-status.log OVPMON_OPENVPN_MONITOR_LOG_PATH=/var/log/openvpn/openvpn-status.log
OVPMON_PROFILER_DB_PATH=/var/lib/ovpmon/ovpn_profiler.db OVPMON_PROFILER_DB_PATH=/var/lib/ovpmon/ovpn_profiler.db
OVPMON_LOGGING_LEVEL=INFO 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. 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.
+1
View File
@@ -30,6 +30,7 @@ No default user is created. Seed the initial admin with `OVPMON_INITIAL_ADMIN_US
|---|---| |---|---|
| `OVPMON_API_SECRET_KEY` | JWT secret shared by both APIs (**must be random**) | | `OVPMON_API_SECRET_KEY` | JWT secret shared by both APIs (**must be random**) |
| `OVPMON_INITIAL_ADMIN_USER` / `_PASSWORD` | One-time admin seed | | `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_OPENVPN_MONITOR_DB_PATH` | Monitoring DB |
| `OVPMON_PROFILER_DB_PATH` | Profiler DB | | `OVPMON_PROFILER_DB_PATH` | Profiler DB |
| `OVPMON_OPENVPN_MONITOR_LOG_PATH` | `openvpn-status.log` path | | `OVPMON_OPENVPN_MONITOR_LOG_PATH` | `openvpn-status.log` path |
+37 -24
View File
@@ -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: services:
app-ui: app-ui:
build: ./APP_UI build: ./APP_UI
container_name: ovp-ui container_name: ovp-ui
restart: unless-stopped
ports: ports:
- "80:80" - "80:80"
depends_on: depends_on:
@@ -12,75 +19,81 @@ services:
networks: networks:
- ovp-net - ovp-net
environment: environment:
- OVP_API_HOST=ovp-api OVP_API_HOST: ovp-api
- OVP_API_PORT=5001 OVP_API_PORT: 5001
- OVP_PROFILER_HOST=ovp-profiler OVP_PROFILER_HOST: ovp-profiler
- OVP_PROFILER_PORT=8000 OVP_PROFILER_PORT: 8000
app-gatherer: app-gatherer:
build: build:
context: ./APP_CORE context: ./APP_CORE
dockerfile: Dockerfile.gatherer dockerfile: Dockerfile.gatherer
container_name: ovp-gatherer container_name: ovp-gatherer
restart: unless-stopped
volumes: volumes:
- ovp_logs:/var/log/openvpn - ovp_logs:/var/log/openvpn
- db_data:/app/db - db_data:/app/db
depends_on: depends_on:
- app-profiler - 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: networks:
- ovp-net - 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: app-api:
build: build:
context: ./APP_CORE context: ./APP_CORE
dockerfile: Dockerfile.api dockerfile: Dockerfile.api
container_name: ovp-api container_name: ovp-api
ports: restart: unless-stopped
- "5001:5001" # Not published: reached only through app-ui (Nginx) on ovp-net
expose:
- "5001"
volumes: volumes:
- db_data:/app/db - db_data:/app/db
networks: networks:
- ovp-net - 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: depends_on:
- app-gatherer - 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: app-profiler:
build: ./APP_PROFILER build: ./APP_PROFILER
container_name: ovp-profiler container_name: ovp-profiler
restart: unless-stopped
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
sysctls: sysctls:
- net.ipv4.ip_forward=1 - net.ipv4.ip_forward=1
devices: devices:
- "/dev/net/tun:/dev/net/tun" - "/dev/net/tun:/dev/net/tun"
ports: ports:
- "8000:8000" # VPN port only; the profiler API (8000) is reached through app-ui
- "1194:1194/udp" - "1194:1194/udp"
expose:
- "8000"
volumes: volumes:
- ovp_logs:/var/log/openvpn - ovp_logs:/var/log/openvpn
- ovp_config:/etc/openvpn - ovp_config:/etc/openvpn
- db_data:/app/db - db_data:/app/db
- ovp_client_config:/app/client-config - ovp_client_config:/app/client-config
- ovp_pki:/app/easy-rsa - ovp_pki:/app/easy-rsa
networks: networks:
- ovp-net - ovp-net
environment: environment:
- OVPMON_API_SECRET_KEY=${JWT_SECRET:-supersecret} <<: *jwt-secret
- OVPMON_PROFILER_DB_PATH=/app/db/ovpn_profiler.db OVPMON_PROFILER_DB_PATH: /app/db/ovpn_profiler.db
OVPMON_CORS_ORIGINS: ${OVPMON_CORS_ORIGINS:-}
networks: networks:
ovp-net: ovp-net: