BBSFirewall sits in front of your BBS on the open internet, and its admin tools can rewrite its own configuration and run commands on the host. This guide covers how to set it up so the firewall protects your board without becoming the weak point itself.
It is written for v1.5.0 and later. A few protections described here are new in 1.5.0 and are marked as such.
- Reporting a vulnerability
- Quick checklist
- 1. The web config editor
- 2. Admin accounts, passwords and MFA
- 3. The Management API
- 4. The status endpoint
- 5. SSH: choosing a mode
- 6. PROXY Protocol and your backend
- 7. Filtering callers
- 8. The host firewall (ufw)
- 9. The server itself
- 10. Updates
- 11. Logs and monitoring
- 12. If you think something is wrong
Please do not open a public issue for a security problem. Use GitHub's private reporting instead: on the BBSFirewall repository, open the Security tab and choose Report a vulnerability. Only the maintainers can see the report.
Include what you found, how to reproduce it, and which version you ran
("version" in package.json, or the header of the web editor). You will get a
reply, and a fix will go out in a release with a note in CHANGELOG.md.
Supported versions: only the latest release gets security fixes. If you are on an older one, update first (see Updates) and check whether the problem is still there.
If you only do ten things:
- Keep
trustedhosts.txtdown to the addresses you actually administer from. Never put0.0.0.0/0or::/0in it. - Turn on MFA for every admin account, and store the backup codes offline.
- Use long passwords β 12 characters is the minimum; a 4β5 word passphrase is better.
- Give helpers a Firewall Admin account, not a Global Admin one.
- Leave the Management API off unless something uses it. If it is on, use a
random key and fill in
api-trustedhosts.txt. - Make sure nobody can reach your BBS directly β only through the firewall.
- Only enable PROXY Protocol when your backend understands it, and only accept it from the firewall.
- Keep the Whitelist tiny. Whitelisted addresses skip every check.
- Turn on ufw, and let BBSFirewall manage its rules from the Tools tab.
- Keep BBSFirewall and the OS updated, and read the release notes.
The editor (default port 8443) can change every setting, edit every list, restart the firewall and run the Tools actions. Treat it like root access to the box.
The editor checks the caller's address against trustedhosts.txt before it shows
the login page. Anyone not listed gets 403 Forbidden. An empty or missing file
locks everyone out β this is deliberate (fail-closed).
- List only the addresses you administer from: your home IP, your office, a VPN exit.
- Prefer single addresses (
/32,/128) or small ranges. A mobile carrier's whole range lets in everyone else on that carrier too β they still need your password and MFA, but they can now try. - Never add
0.0.0.0/0or::/0. That turns the gate off. - Locked yourself out? Edit
trustedhosts.txtover SSH. The editor re-reads it when it is saved from the UI; after a hand edit, restart (pm2 restart bbsfirewall).
The trusted-host check uses the real TCP connection's address and ignores
X-Forwarded-For and similar headers on purpose β those are trivial to fake. Behind
a reverse proxy, every request would appear to come from the proxy, and the gate
would let everyone in. The editor logs a warning if it sees a forwarding header.
If you would rather not expose the port at all, bind it to localhost and use an SSH tunnel:
CONFIG_EDITOR_BIND=127.0.0.1ssh -L 8443:127.0.0.1:8443 you@your-firewall # then browse to https://localhost:8443(Put 127.0.0.1 in trustedhosts.txt for this.)
Use a real Let's Encrypt certificate if the box has a hostname (Tools tab, or
bash setup-config-cert.sh admin.example.com --email you@example.com). A self-signed
certificate (--self-signed) still encrypts the connection, but your browser can't
tell you're talking to the right server. Check the fingerprint the first time you
accept it.
You do not need to configure these, but it helps to know they are there:
- Session cookies are
HttpOnly,Secure,SameSite=Strict, and tied to the IP you logged in from. - Sessions end after 30 minutes idle (
CONFIG_EDITOR_SESSION_TIMEOUT_MS) and after 12 hours regardless. - Every change needs a per-session CSRF token. Pages are served with a strict Content-Security-Policy.
- 5 wrong passwords or 5 wrong MFA codes lock that IP out for 15 minutes.
Log out when you are done on a shared or public computer (your username, top right β Log out).
The first admin account is created on the server, never over the web:
node setup-admin.jsIt stores a scrypt hash of the password (never the password itself) in
.admin-security.json, readable by root only. Further accounts are added from
Security Settings β Admin Accounts in the editor.
| Global Admin | Firewall Admin | |
|---|---|---|
| Change ordinary settings (ports, limits, country blocking, triggers, β¦) | β | β |
| Edit Whitelist, Blocklist and Triggers | β | β |
| Restart the firewall, GeoIP and certificate tools | β | β |
| View and download logs | β | β |
| Change own password and MFA, "Whitelist my IP" | β | β |
| Config Editor, Management API and Host Firewall (UFW) settings | β | β |
| Trusted Hosts, API Trusted Hosts and Status allowlists; list-file paths | β | β |
| Install updates or roll back (1.5.0) | β | β |
| Delete log files (1.5.0) | β | β |
| Replace the SSH host key (1.5.0) | β | β |
| Manage admin accounts, UFW rules, reboot the server | β | β |
The rule of thumb: a Firewall Admin can run the firewall day to day, but cannot change who can reach the admin tools, cannot change what code runs, and cannot erase the record of what they did. Give co-sysops and helpers a Firewall Admin account.
- One account per person. Shared logins make the logs useless.
- Delete an account the moment someone no longer needs it. Their open sessions end immediately.
- Keep more than one Global Admin, or keep SSH access to the box, so losing one account doesn't lock you out.
The minimum is 12 characters. There are no "must contain a symbol" rules β length matters far more. Four or five random words make a good passphrase. Use a password manager, and don't reuse a password from anywhere else.
Turn it on under Security Settings β Enable MFA and scan the QR code with any authenticator app (Google Authenticator, Authy, 1Password, Bitwarden, β¦).
- You get one-time backup codes when you enable MFA. Print them or store them in your password manager β not on the same phone as the authenticator.
- A Global Admin can mark an account "Require MFA". That account must set up MFA at its next login before it can do anything else.
- Turning MFA on asks for your current password (1.5.0), so a stolen session can't swap in its own authenticator.
Lost your phone and your backup codes? Anyone with root on the server can clear MFA for an account:
node disable-mfa.js --yes --user <username>That is intentional: shell access to the box is the real trust boundary, so protect it accordingly (see The server itself).
The API (API_ENABLED=true) gives a program the same power as a Global Admin:
it can rewrite .env, edit every list, restart, update and roll back. It exists for
dashboards and hosting tools.
-
Leave it off unless something actually uses it.
-
Use a long random key, not a password you made up. The minimum is 24 characters:
openssl rand -hex 32
-
Fill in
api-trustedhosts.txtwith the addresses of the systems that call the API. Unliketrustedhosts.txt, an empty file here allows any address (fail-open), so the key is the only protection until you add entries. -
The key goes in a header (
Authorization: Bearer β¦orX-API-Key: β¦), never in a URL, so it doesn't end up in logs or browser history. -
5 wrong keys lock that IP out of the API for 15 minutes.
-
Rotating the key needs a restart to take effect. Change it if it may have leaked β for example if it was pasted into a chat or committed to a repository.
GET /status lets an uptime monitor (Uptime Kuma, etc.) check the firewall without
a login or key. It reports whether each listener is up, the uptime, and the
BBSFirewall version.
Because there is no key, it is guarded only by status-trustedhosts.txt β and that
file is fail-closed: empty or missing blocks everyone. List only your monitoring
server(s).
SSH_MODE decides what listens on the BBS's SSH port (SSH_LISTEN_PORT). This is
the port callers use. Your own admin SSH login to the server is a different
port and a different program (sshd).
| Mode | What it does | Who checks the login |
|---|---|---|
off |
No SSH port. | β |
terminate |
BBSFirewall is the SSH server. It accepts any username and password and connects the caller to the BBS over telnet. | Your BBS's own login screen |
passthrough |
BBSFirewall only filters by IP and passes the encrypted SSH stream to a backend that runs its own SSH server. | The backend SSH server |
- Use
offif your board has no SSH callers. terminateis not an authentication layer. It exists so callers can use an SSH client; the BBS's own login is what protects accounts. All the firewall's checks (blocklist, rate limit, caps, country, triggers) still apply. As of 1.5.0 they apply the moment a caller connects β a caller that connects and stays silent is checked and dropped too β and every caller must finish logging in within 2 minutes.terminateneeds a host key (Tools tab, orssh-keygen). Keep it: replacing it makes every caller's SSH client warn that the server's identity changed, which trains people to click through real warnings.- Use
passthroughwhen your backend has its own SSH server, so public-key logins and SFTP keep working end to end.
With PROXY_PROTOCOL_ENABLED=true (and SSH_PROXY_PROTOCOL=true for passthrough),
the firewall tells the backend the caller's real IP in a header at the start of each
connection. Two things matter:
- Only enable it if the backend understands it. Otherwise the BBS sees the header
as garbage and every connection breaks. Stock OpenSSH does not understand it,
and neither does the Major BBS SSH daemon without an add-on module that reads the
header β leave
SSH_PROXY_PROTOCOL=falseunless yours does. - The backend must only accept PROXY headers from the firewall. The header is just text. If anyone else can reach the backend directly, they can send their own header and claim to be any IP β including an address your BBS trusts.
That second point applies even without PROXY Protocol: if callers can reach your
BBS directly, the firewall is optional for them. Make sure the BBS's telnet and SSH
ports only accept connections from the firewall β run them on the same host bound to
127.0.0.1, put them on a private network, or restrict them with the backend host's
own firewall.
These settings protect your board from scanners, floods and bots.
- Whitelist β addresses here skip every check: blocklist, rate limit, caps,
country blocking and auto-block triggers. Keep it to a handful of addresses you
truly trust, and avoid large ranges. Admin access to the editor is controlled by
trustedhosts.txt, not the whitelist β you don't need to whitelist yourself to administer the box. - Blocklist β permanent blocks, single IPs or CIDR ranges, IPv4 and IPv6.
- Rate limiting (on by default) β more than
MAX_CONNECTIONS_PER_WINDOWconnections inRATE_LIMIT_WINDOW_MSblocks that IP forRATE_LIMIT_BLOCK_DURATION_MS. MAX_CONNECTIONS_PER_IPβ the default0means unlimited. A small number (say 3β10) stops one address from taking every line.CONNECTION_TIMEOUTβ drops idle sessions.0turns it off, which lets idle or abandoned connections hold a slot indefinitely; only do that if your callers really need it.- Country blocking β needs the free MaxMind database (Tools tab).
BLOCK_UNKNOWN_COUNTRIES=trueis stricter but also blocks some legitimate callers. - Auto-block triggers (
TRIGGER_BLOCK_ENABLED) β if a caller's first bytes match a pattern intriggers.txt(known exploit strings, HTTP requests on the telnet port, β¦), its IP is blocked. Start fromtriggers.txt.example.TRIGGER_BLOCK_MODE=blocklistadds the IP toblocklist.txtfor good;tempblocks it forTRIGGER_BLOCK_DURATION_MS.- Prefer plain-text patterns. Regex patterns that could take exponential time
(
(a+)+,(a|aa)+, β¦) are refused, but simple patterns are still faster and easier to get right. - Test a new pattern on a quiet day and watch the logs: a pattern that is too broad will block real callers.
BBSFirewall filters callers in the application. A host firewall adds a second layer in the kernel that keeps working even while BBSFirewall restarts.
-
Install ufw and turn it on yourself. BBSFirewall never turns ufw on or off. Allow your admin SSH port before enabling it:
ufw allow 22/tcp # or whatever port YOU log in on ufw enable
-
Set
UFW_ENABLED=trueandHOST_ADMIN_SSH_PORT(your admin SSH port, not the BBS's), then use Tools β Host firewall (UFW) β Preview / Apply. BBSFirewall opens the BBS ports to everyone and limits the editor and your admin SSH port to Trusted Hosts. Apply refuses to remove access to your admin SSH port.UFW_AUTO_APPLY=truemakes the same changes automatically at startup and after each Save β removing someone from Trusted Hosts then closes their ufw access straight away. When a change looks risky, it is left for you to Apply. -
Optionally set
UFW_PUSH_BLOCKS=trueto copy blocklist entries and trigger auto-blocks into ufw, so blocked addresses are dropped by the kernel. It never pushes anything that overlaps Trusted Hosts, the Whitelist or localhost, and it refuses to run with an empty Trusted Hosts list.
Before changing firewall rules on a remote box, make sure you know how to reach your hosting provider's web console. It is the way back in if a rule ever locks you out.
BBSFirewall is only as safe as the machine it runs on.
- Keep the OS patched. On Ubuntu/Debian, turn on automatic security updates:
apt install unattended-upgrades. - Lock down your own SSH login: SSH keys instead of passwords
(
PasswordAuthentication no), and limit it to Trusted Hosts with ufw (above). - It usually runs as root, because it binds ports below 1024 and manages ufw and
certificates. That makes the admin editor root-equivalent β another reason to keep
trustedhosts.txttight and MFA on. - Secrets stay on the box. These files hold credentials and are created
readable by root only. They are already in
.gitignoreβ never copy them into a repository, a support ticket or a chat:.env(API key, MaxMind license key).admin-security.json(password hashes, MFA secrets, backup-code hashes)ssh_host_keycerts/(TLS private keys)ENVBACKUPS/,ADMINBACKUPS/,.config-editor-sessions.json
- Back them up somewhere safe and private. Restoring
.env, the list files and.admin-security.jsonbrings a rebuilt server back exactly as it was. REBOOT_ENABLEDadds a full server reboot button for Global Admins. Leave it off unless you need it.
- Update from the Tools tab (Global Admin only) or on the server with
node update.js. Each update backs up the current version first and restores it automatically if anything fails. Your.envand list files are never touched. - Updates come from tagged releases of the official repository, SysopNetwork/BBSFirewall. Be wary of copies from anywhere else.
- Read the release notes (CHANGELOG.md) before updating. Security fixes are listed under π Security.
- Roll back from Tools β Updates β Backups, or
node update.js --rollback, if a release misbehaves. Rolling back to a version older than a security fix brings the problem back, so go forward again once the issue is fixed. - If you installed with
git clone,git pull && npm installalso works; checkgit logto see what changed.
- File logs are on by default, one file per day per service, kept for
LOG_RETENTION_DAYS(30). At the default level (connections) theconfig-editorlog records every admin login (and failed attempt), saved settings, restarts, updates, and password, MFA and account changes, with the account name and IP. Don't lowerCONFIG_EDITOR_LOG_LEVELbelowconnections, or you lose that record. - Only a Global Admin can delete log files (1.5.0), so a Firewall Admin can't remove the record of their own actions.
- Look at the logs now and then: repeated failed logins to the editor, a blocklist that suddenly grows fast, or a lot of rejected connections from one range are all worth a closer look. The Logs tab has a search.
- Watch uptime with the status endpoint, so you hear about an outage before your callers do.
If you suspect someone else got into the admin editor or the server:
- Restrict access first. Over SSH, cut
trustedhosts.txtdown to your current address only and restart. - Check the admin accounts (Security Settings β Admin Accounts). Delete any you don't recognise. Their sessions end immediately.
- Change every admin password, re-enroll MFA, and regenerate backup codes.
- Rotate the
API_KEYand restart, if the API is on. - Review
.envand the lists for changes you didn't make β look especially at Trusted Hosts, the Whitelist, the API settings andBACKEND_HOST.ENVBACKUPS/holds earlier copies of.envto compare against. - Read the
config-editorlogs around the time things changed. - Check the server itself: unknown SSH keys in
~/.ssh/authorized_keys, new users, cron jobs. If root itself may be compromised, rebuild the server and restore your backed-up configuration rather than trying to clean it. - If you believe BBSFirewall has a flaw that let this happen, please report it.