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 theadminuser, writes it plaintext to
data/admin_password.txt(mode0600), and prints it once at startup.
No moreadmin/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
downloadroute usessend_from_directoryagainst the configured
uploads dir. - Per-area file upload permission — each
FileAreacarries a
upload_permissionofusers/sysop/none. The upload route
enforces it before writing. - SECRET_KEY guard. If
SECRET_KEYis the dev default and the app
is started in production mode (FLASK_ENV=productionor
config_name='production'), it raisesRuntimeErrorand refuses to
boot. In development it just logs a loud warning. - Session cookie
HttpOnly+SameSite=Laxby default. Production
config also setsSecure(cookie only sent over HTTPS). - Optional virus scan on uploads if
clamav-daemonis installed —
infected files are deleted before the DB row is created.
What you MUST do for production
- Set
SECRET_KEY. Generate once, set in your systemd unit's
Environment=orEnvironmentFile=:
python -c 'import secrets; print(secrets.token_urlsafe(48))' - Change the admin password from the random one — and
rm data/admin_password.txtonce you've memorized / vaulted it. - Front the web app with TLS. nginx + Let's Encrypt is the easy
path. Thedeploy/anetbbs-nginx.conf.templateis a starting point.
Without a TLS terminator, every login posts plaintext over HTTP. - Set
FLASK_ENV=productionso the prod config kicks in
(Securecookies, stricter SECRET_KEY check). - 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 getconnection refused. - Run as a non-root user (the systemd templates already do —
User=anetbbs). UsesetcaporAmbientCapabilitiesfor the
privileged ports rather than running the whole process as root. - 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 getsN × workers
attempts. For a real multi-worker setup, replace with a Redis-backed
store (therate_limitdecorator 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
.jsdoors without realjsexecget 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.