Security
How Zefira defends itself — and how to keep it that way.
Built-in defenses
| Layer | Implementation |
|---|---|
| Passwords | scrypt hashing (N=2^14), 10+ chars with letters + digits enforced on change |
| Sessions | JWT in HttpOnly + SameSite=Strict cookies, Secure behind HTTPS, versioned (logout / password change kills all sessions) |
| Brute force | 8 tries / 15 min per IP+user, plus a loose global per-user bucket (lockout-DoS resistant); identical 401s + dummy-hash timing for unknown users |
| CSRF | Mutating /api calls require X-Requested-With: XMLHttpRequest (Bearer tokens exempt: they cannot be sent cross-origin without a preflight the panel never passes) |
| Headers | CSP, frame-ancestors 'none', X-Frame-Options DENY, nosniff, no-referrer, CORP same-origin, HSTS on HTTPS, no Server banner |
| Input | Strict Pydantic schemas everywhere, 1 MiB body cap (64 MiB only for restore) enforced on actual streamed bytes — chunked bodies without Content-Length cannot bypass it |
| SSRF | AI base_url rejects userinfo, metadata hosts, link-local/multicast targets + DNS-rebinding (any resolved IP blocked = refused) and never follows redirects; IPv4-mapped IPv6, 6to4 and Teredo forms are unwrapped first, so a wrapped metadata address cannot receive your AI key; node probes filter link-local/metadata dials; create/patch reject metadata addresses |
| Quota | Subscriptions with used_gb >= volume_gb 404 like expired/disabled (raw, Clash and dashboard). device_limit is advisory only — shown in panel + Clash comment, never blocks |
| Tokens | API tokens are full (default) or bot least-privilege (GET /api/me, GET /api/stats, GET /api/users, username lookup, POST /api/users, GET /api/templates, QR, developer resets). Bot tokens can create and renew users including start_on_first_use (server-computed expiry, bot-safe) but can never delete/patch, backup/restore, or manage settings/tokens |
| Restore upload | The body is buffered before the route authenticates, so anonymous uploads are cut off first: Content-Length is mandatory (chunked → 411), a per-source budget and one global slot bound it to a single in-flight restore, and a 60 s deadline stops a slow trickle from holding memory. |
| Restore | Admin hashes must be scrypt with N>=2^14 (weak/forged hashes skipped); zero-volume users skipped; trusted_proxies and AI URL re-validated strictly on import; token scopes preserved |
| Secrets | Bot tokens, tunnel tokens and REALITY private keys encrypted with a host-local master key. ZEFIRA_ADMIN_PASSWORD is one-time: scrubbed from .env on first boot |
| Not encrypted | Customer VPN credentials in the database (secret_data) are plain JSON — the panel rebuilds links from them without a decrypt round-trip, so they are also in plain form in an unencrypted backup. Treat the DB and unencrypted backups as customer credentials: keep instance/ at 700, prefer encrypted backups ({"encrypt": true}) or full-disk encryption. |
| Sensitive exports | Backup download is POST-only with password confirm (no GET, so no CSRF-able download); {"encrypt": true} returns a scrypt + Fernet blob restorable via POST /api/restore-encrypted; access logs omit subscription tokens |
| Service | Non-root systemd (User=zefira, ProtectSystem=strict, UMask=0077); updater fetches only mrlurix/zefira-panel@main, installs exactly the advertised commit SHA (a force-push mid-update is refused), reports that commit's GitHub signature state and can refuse unsigned commits with ZEFIRA_REQUIRE_SIGNED_UPDATE=1 , fixed service name, ZEFIRA_ALLOW_UPDATE=0 kill-switch |
| Proxies | trusted_proxies refuses 0.0.0.0/0, multicast and overly-broad ranges at input and ignores them at runtime (XFF spoofing = rate-limit bypass) |
| Visibility | Audit log (last 2000 events), Telegram brute-force alerts (spam-throttled) |
Penetration-test suite
security_test.py attacks a live panel: auth boundary, JWT forgery (alg=none, tampering, bad signature), rate limits, CSRF, mass assignment, backup/restore atomicity, fuzzing, XSS sources, header checks — currently 117/117 passing:
.venv\Scripts\python security_test.py http://127.0.0.1:8000 admin YOUR_PASSWORD
Restore/atomicity tests wipe users. The suite refuses to run against a panel with users unless you pass
--allow-live — back up first.Operator checklist
- Change the password right after first login (the installer password is auto-scrubbed from
.envon first boot, but rotate it anyway). - Serve the panel over HTTPS (installer step 7 or Settings → SSL).
- Keep
/opt/zefira/instance/secret.keybacked up offline — without it, encrypted data is unrecoverable. - Behind a proxy, set
trusted_proxiesto specific IPs (e.g.127.0.0.1) — never0.0.0.0/0(refused) — so rate limits and audit IPs see real clients. - Never share backup JSON files — they hold password hashes and secrets. Use
{"encrypt": true}for off-server storage. - Give reseller bots
bot-scoped tokens, neverfull. - Service runs as user
zefira(non-root). Verify withsystemctl show zefira -p User.
ZEF