Files
iclaoudezinandClaude Sonnet 5.5 9ffdbfa259 Publish CRL for the unprivileged OpenVPN user so crl_verify works
- ovpmon-helper: new publish-crl command (validated copy of pki/crl.pem to
  /etc/openvpn/crl.pem, root:root 644); crl-verify allowlisted only for
  that path; CRL is refreshed on service start/restart.
- Profiler: publish after gen-crl (init/revoke) and on server/configure;
  generator renders the published path when running unprivileged.
- doas rule for publish-crl; docs and helper copy updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:38:30 +00:00

4.8 KiB

Deployment: system services (no containers)

Tested on Alpine 3.23 (OpenRC); Debian/Ubuntu (systemd) differs only in service manager. Unit/init templates: systemd/, openrc/. Generic notes: Deployment, Service management, Nginx.

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

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.

Create the ovpmon user, give it the data directories, add command_user="ovpmon:ovpmon" to the init scripts (or User=ovpmon in systemd units), install the root helper and the doas rules. The API then renders server.conf to /var/lib/ovpmon/staging/; the helper validates and installs it, publishes the CRL for OpenVPN and controls the openvpn service. Full procedure, helper source and results: Privilege separation; files in privilege-separation/.

5. UI and Nginx (HTTPS on 8088)

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):

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. 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.
  • 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

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