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:
1 parent
9b2882d5f4
commit
5de0501cbc
6 files changed
+57
-33
No files matched your search
@@ -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):
|
||||||
|
|||||||
@@ -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 +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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in new issue
Block a user