Files

28 lines
3.0 KiB
Markdown
Raw Permalink Normal View History

# 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`.