- README: short overview, quick start, config table and links. - DOCS/General: Deployment_Docker.md and Deployment_Native.md (system services, HTTPS, host hardening); refresh Index.md. - DOCS/Changes: security hardening, admin username change and egress via Hysteria2 with results and verification. - Drop mentions of the built-in admin/password account. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
3.0 KiB
3.0 KiB
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.