Security defaults & production hardening

This is the alpha. The defaults are designed so a fresh install isn't
trivially compromised, but production deployment requires a few extra
steps. Read this whole file before exposing the BBS to the internet.

What's enabled by default

  • Random initial admin password. First start generates a 16-byte
    URL-safe token, hashes it for the admin user, writes it plaintext to
    data/admin_password.txt (mode 0600), and prints it once at startup.
    No more admin/admin123. Change it on first login.
  • CSRF on all WTForms POSTs. Every FlaskForm-backed route gets a
    CSRF token automatically. AJAX endpoints (/api/vote, /imsg/*)
    read the token from a <meta name="csrf-token"> tag injected into
    every page.
  • Open-redirect guard on /auth/login. The ?next= parameter is
    rejected unless it's a same-origin path (urlparse().netloc == '',
    not protocol-relative, not Windows-path-tricks).
  • Rate limits (sliding window, in-memory):
  • /auth/login — 10 attempts / 5 min / IP
  • /imsg/send — 30 messages / hour / user
  • /api/vote — 60 votes / min / user
  • Path traversal mitigated — uploads stored under UUID filenames; the
    download route uses send_from_directory against the configured
    uploads dir.
  • Per-area file upload permission — each FileArea carries a
    upload_permission of users / sysop / none. The upload route
    enforces it before writing.
  • SECRET_KEY guard. If SECRET_KEY is the dev default and the app
    is started in production mode (FLASK_ENV=production or
    config_name='production'), it raises RuntimeError and refuses to
    boot. In development it just logs a loud warning.
  • Session cookie HttpOnly + SameSite=Lax by default. Production
    config also sets Secure (cookie only sent over HTTPS).
  • Optional virus scan on uploads if clamav-daemon is installed —
    infected files are deleted before the DB row is created.

What you MUST do for production

  1. Set SECRET_KEY. Generate once, set in your systemd unit's
    Environment= or EnvironmentFile=:
    python -c 'import secrets; print(secrets.token_urlsafe(48))'
  2. Change the admin password from the random one — and
    rm data/admin_password.txt once you've memorized / vaulted it.
  3. Front the web app with TLS. nginx + Let's Encrypt is the easy
    path. The deploy/anetbbs-nginx.conf.template is a starting point.
    Without a TLS terminator, every login posts plaintext over HTTP.
  4. Set FLASK_ENV=production so the prod config kicks in
    (Secure cookies, stricter SECRET_KEY check).
  5. Pick one privileged-port option for MSP/SYSTAT (see
    docs/INSTALL.md §6). Leaving the default ports unbound silently
    degrades inter-BBS messaging — peers will get connection refused.
  6. Run as a non-root user (the systemd templates already do —
    User=anetbbs). Use setcap or AmbientCapabilities for the
    privileged ports rather than running the whole process as root.
  7. Back up data/ — that's where the SQLite DB, uploads,
    configuration, and admin-password-on-first-install all live.

Known limitations

  • Rate limiter is in-memory and per-process. Fine for a single
    gunicorn worker. With multiple workers or a multi-host deployment,
    the limiter is effectively per-worker — the user gets N × workers
    attempts. For a real multi-worker setup, replace with a Redis-backed
    store (the rate_limit decorator is the only call site to change).
  • No 2FA. Single password for both web and terminal logins.
  • No account lockout (just rate limit). Brute-force is throttled
    but never permanently locks an account.
  • Uploads are not sandboxed beyond ClamAV if you have it installed.
    Don't run the BBS on a host where executable uploads matter.
  • Synchronet .js doors without real jsexec get a Node.js
    shim that is best-effort — a malicious door script can do anything
    Node.js can. Same trust model as any door game: only install ones
    you trust.

Reporting issues

Security reports: please email the sysop privately rather than open a
public issue.