Administrator reviewing a printed security checklist beside a lock icon on a muted monitor

A public forum in 2026 is a login form, a file store, a mail pipe, and a staff list. Attackers do not care whether you picked XenForo or Discourse. They care whether HTTPS is real, whether staff have a second factor, whether last night’s backup restores, whether debug is printing SQL, and whether a CDN is rewriting the JavaScript the application already shipped. This is the cross-platform checklist for those jobs.

This is not the XenForo 2.3 security baseline. That article is the XF-only deep dive: password hashes you cannot read back, the two security-lock modes, nginx internal for src/ and internal_data/, staff primary-group hygiene. Read it if you run XenForo. Stay here if you need the same ideas on more than one product, or you are comparing stacks. It is also not a WAF shopping guide. This page will not rank Cloudflare versus a paid box versus a host panel. If you already run an edge filter, it is your edge.

Official and notebook facts this page is allowed to use: XenForo login limit after 4 failures (CAPTCHA or 15-minute lock), $config['enableTfa'], cookie prefix xf_, debug / fullJs, Symfony Mailer in 2.3 (transport is a sibling article). Discourse: skip SMTP at install = Discourse ID (not a security product), Cloudflare disable JS minify / JS modifications or Discourse breaks, backups (Meta 14855), fail2ban on the Docker host. Combined sources win when one notebook is silent. If a control is not in those sources, this article says so instead of inventing a threshold.

What you are protecting

Four surfaces, every product:

Surface What “done” looks like
Transport HTTPS everywhere. Board / site URL is the https:// form. No mixed content.
Accounts Unique passwords, 2FA on staff, a lock / reset path that does not require you to “look up” a password.
Application Debug off. Installer locked. Cookies scoped. Login brute-force limited.
Recovery A backup you have restored once. Off the same box. Same version.

Spam and toxic people are adjacent, not this page. Registration approval, the review queue, and trust levels are moderation and Discourse TL. Credentials and host hygiene stay here.

1. HTTPS is a control, not a badge

Several features simply refuse to work on http://:

  • XenForo PWA / push — first absolute requirement on Setup → PWA setup. No certificate, no installable app, no push.
  • OAuth / connected accounts — redirect URLs are HTTPS. The Discord article already tripped over a Board URL mismatch.
  • Modern browsers marking cookies Secure or refusing them.
  • Discourse official Docker assumes the app owns 80 and 443 and gets a certificate as part of standing up.

A board still on http:// in 2026 is not “fine until we add push.” Password managers warn. Embeds break. Search treats you as a relic.

Do this, in this order:

  1. Certificate at the vhost or at the proxy you actually control.
  2. Force HTTPS at that layer (redirect 80 → 443).
  3. Set the application’s public URL to the https:// form (XenForo Board URL; Discourse hostname in app.yml / hosted settings).
  4. Walk cookies (section 6). A URL change without a cookie-path check is how everyone looks like a guest.

If the proxy terminates TLS and talks HTTP to PHP or to the Discourse container, that is a deployment choice. The application must still generate HTTPS URLs. Mixed content (http:// images on an https:// page) is how you get a broken lock icon and a blocked embed. The image proxy exists partly for that reason on XenForo.

This article will not recommend a certificate vendor or a specific CDN WAF.

2. Two-factor authentication on every staff account

Staff accounts are the prize. A leaked founder password without a second factor is the whole site.

XenForo. Official name: two-step verification. Methods the user-security page names: an app code (TOTP), a link in email. The list of methods enabled on your board is Setup → Service providers → Two-step verification. Mandate it:

Groups & permissions → User groups → (Moderating or Administrative) → General permissions → Require two-step verification = Yes

Do both groups. Administrative-only leaves every moderator as a password-only account with a moderator bar. Emergency brake in src/config.php:

$config['enableTfa'] = false;

Official use: you lost the ACP because 2FA is on and the device is gone. It is not a lifestyle setting. Put it back (or remove the line) the same hour. The XF security article owns the rest of this paragraph.

Discourse. Staff (admins, moderators) should enroll 2FA in their user preferences. Official hosted and current Discourse expose TOTP / security-key style options in the user security UI — believe the UI you have. This page will not invent a site-setting name that was not in the notebook. Operational rule: no shared mods@ login, because 2FA attaches to a person. Invite-only staff is not a substitute for a second factor; it is a different control.

Both. Email-link 2FA is only as strong as mail delivery. A 2FA email in spam is a lockout. App-based TOTP plus printed backup codes is the staff default. Store those codes next to the registrar login, not in a public staff forum.

3. Login limits (the number we actually have)

XenForo, official. After four unsuccessful password attempts, the board acts. Setup → Options → User options → Login limit method:

Method After 4 failures
CAPTCHA Next attempt must complete a CAPTCHA
Lockout Account locked fifteen minutes

Pick one. CAPTCHA needs a working provider under Setup → Options → Basic board information. A login limit set to CAPTCHA with no provider is a hole that looks like a feature. Related official knobs, same group: minimum time between password requests, CAPTCHA on lost password.

This is not a per-IP ban table. Official does not give you one on that page. Edge rate-limits are your web server or your host.

Discourse. Trust-level sandboxes and the review queue catch content. Host-level fail2ban on SSH is what the official cloud install guide actually names. Application-level “4 failures / 15 minutes” is a XenForo number; do not write it on a Discourse SOP as if it were core. Rate limits exist in Discourse (including chat: TL0 20/30s, TL1+ 40/30s, mods unlimited — notebook). They are not a substitute for staff 2FA.

4. Locks, resets, and not reading passwords back

You will never “look up” a password on a modern forum. XenForo stores a complex one-way hash unique to each user. Official: it is impossible to extract the original password. The answer to “what’s my password?” is the lost-password form.

When you think an account is not the member any more:

XenForo security lock (edit the user):

Lock What they must do When
Must change password Enter the existing password, set a new one You want a rotation; you still believe they know it
Must reset password Reset via the registered email Official: use this if the original password may already be compromised

Incident mode is must reset. “Must change” is a form the attacker can complete.

Discourse. Use the admin user page: log the user out, reset / send password reset, remove unexpected 2FA devices, suspend if they are posting. Exact button labels move; the jobs do not. Confirm you are not emailing the attacker’s inbox if the inbox is the prize — that is an identity problem, same as on XenForo.

Do not confuse a lock with a ban. A ban stops them posting. A lock forces a credential ceremony. You often want both.

5. Backups you have restored once

A backup you have never restored is a rumour.

XenForo. Backup = database + data/ + internal_data/. Merge those folders on restore; never replace them blindly. No downgrade: restore is the only rollback. internal_data/install-lock.php is the empty file that stops the installer thinking you are a fresh site. The XF backup article is the runbook.

Discourse. Meta 14855: backup_frequency default 7 days (0 = off, max 30); backup_time_of_day default 3:30 UTC; backup_with_uploads default on; maximum_backups default 5; remove_older_backups. Local path /var/discourse/shared/standalone/backups/default. S3: backup_location = S3, separate s3_backup_bucket — not the same folder as uploads. The hosting cookbook owns the rest.

Both.

  • At least one copy off the same VPS.
  • Test restore on a staging hostname, not by overwriting production “to see.”
  • Provider snapshots are a disk picture. Application backups are an application picture. Keep both until you have restored both once.
  • After restore: Board URL / hostname, cookie path, HTTPS, mail test, one staff login with 2FA.

A later engine-agnostic backup inventory article can go wider. This checklist only requires that you have one and that it fits the product.

6. Cookies: prefix, path, domain

Cookies are how “you are logged in” survives a request. Get the scope wrong and everyone is a guest — then you enable a guest cache and “logged-in users see the guest homepage.” They were never logged in. The configuration mistakes article already has this ticket.

XenForo src/config.php: cookies support prefix (default xf_), path, and domain. The prefix is how you avoid colliding with another app on the same host. The path must cover the XenForo root. The domain must match how members actually type the site (www versus apex is the classic split).

Discourse. Session cookies are the app’s problem inside the container. Your problem is the hostname you put in app.yml and whether you accidentally serve the same site on a second name without a canonical redirect. Two hostnames are two cookie jars.

After any URL or path change: private window, log in, refresh, still logged in. Then a second browser. Then the PWA if you offer one.

7. Debug off, installer locked, JS left alone

XenForo production src/config.php:

$config['debug'] = false;
$config['development']['fullJs'] = false;

Official: debug on a live site slows page generation and prints SQL. That is an information leak. fullJs ships unminified modules. $config['enableTfa'] is not left false after a recovery.

install-lock.php must exist after install. A cloned tree without it is an installer waiting for a stranger.

Discourse. Do not run a production site off a development template. Official production is the Docker stack or official hosted. Unofficial “Discourse on a panel” images are how you inherit someone else’s debug defaults. /admin/upgrade (or ./launcher rebuild app) is the upgrade path; there is no “leave debug on to see why rebuild is slow.”

Cloudflare / CDN JS minify.

  • XenForo 2.3 already ships deferred, modular JS. Community: disable Auto Minify if anything else already minifies (XFOptimize is the named example). Double-minify breaks script compilation. Rocket Loader is not in the notebooks; treat any “rewrite scripts” toggle as the same class of mistake.
  • Discourse, notebook / Meta: disable JS minify / JS modifications or Discourse breaks. This is not optional tuning. It is a known incompatibility. If a host dashboard offers “optimize JavaScript” in front of a Discourse origin, turn it off, then retest composer, login, and admin.

Caching static assets at the edge is fine. Rewriting application JavaScript is not a security feature and it is not a free performance win. It is a second compiler you do not control.

8. Files that must not be public

XenForo. src/ (includes config.php), internal_data/, install/data, install/templates are not public routes. Official nginx snippet uses internal; on those locations (docs.xenforo.com/manual/configuration/friendly-urls). Apache / LiteSpeed: the shipped .htaccess is the deny/rewrite set. After every host move, request https://yourboard.example/src/config.php as a guest. You want 403/404, not a source dump, not a blank 200.

data/ is public (avatars, thumbs). internal_data/ can sit outside the web root (raw attachments). PHP must write both. PHP must not need world-writable 0777 on the whole tree.

Discourse. Uploads live inside the Docker volume (or on S3 if you set that up). You do not serve the PostgreSQL data directory. You do not publish app.yml (it has SMTP and S3 secrets). You do not leave /var/discourse world-readable on a box with extra Unix users.

Both. Secrets belong in files that are not in git, not in a public debug gist, not in a staff thread guests can see the title of.

9. Staff hygiene (product-agnostic)

  • No shared staff logins. 2FA and locks attach to a person. “mods@” is how you cannot lock one leaver.
  • Fewer founders. Super-admin access is a count, not a compliment.
  • Revoke the same day: groups, admin records, API keys, OAuth apps the leaver created, object-storage keys they had on a laptop.
  • XenForo-specific: every human keeps Registered as primary; Administrative / Moderating are secondary. Changing primary to Administrative is how add-ons and Analyze permissions go weird. Details in the XF security piece.
  • Discourse-specific: do not add redundant groups (admins + mods + TL1–4) on a category — hierarchical; causes “not permitted to view.” That is a permissions foot-gun with a security symptom. The first-month catalogue already printed it.
  • Do not post casually as Admin on XenForo. Use a personal account without the staff badge. Community practice (SF-2). The badge is a target and a tone problem.

10. Mail, Discourse ID, and “we turned email off”

Mail is a security control. Lost password, security-lock reset, email-link 2FA, and “the site emailed me, therefore the site is alive” all ride it.

XenForo 2.3 uses Symfony Mailer (not SwiftMailer). Transports live in Setup → Options → Email options. $config['enableMail'] and $config['enableMailQueue'] are the emergency / queue switches. Turning mail off to “stop spam” also turns off the reset path. The push / email article is the transport chapter; the newsletter article is what to do when you wanted a digest.

Discourse. SMTP is optional at install. Skip it and you get Discourse ID (id.discourse.com) plus social logins. That is a documented 2026 option. What you do not get: digests, mailing-list mode, reply-by-email, traditional confirm mail. Security implication: you cannot email a password reset through your domain until SMTP exists. Discourse ID / social is then the recovery path. That is acceptable for a pilot. It is a poor fit for a staff-only board that promised inbox-driven recovery. Say so in the staff SOP.

SPF and DKIM still matter when you do send. The application will not invent them.

11. What you should not invent

A ranked WAF shopping list. Core XenForo has login limits, CAPTCHA, 2FA, security lock, spam cleaner (elsewhere), reports. Discourse has trust levels, watched words, the review queue, hosted edge on official plans. Cloudflare / nginx rate-limit / fail2ban are your stack if you want them. This article will not name a paid WAF SKU or claim one is “best in 2026.”

Akismet-as-stock-XenForo. It is not. CleanTalk and Spaminator are named community add-ons in our notes; they are optional, not a baseline this checklist requires.

A custom user field for “security level.” 2FA state is not a profile field. Do not store backup codes in staff_notes.

Disabling lost password because you got flooded. Use the official throttle and CAPTCHA. Then fix why bots like the form.

HTTP “just for the LAN.” Staff laptops leave the LAN.

A worked hour (any stack)

  1. Certificate live. Public URL is https://…. Mixed content gone.
  2. Guest fetch of secret paths fails (XF: /src/config.php, /internal_data/). Discourse: app.yml not web-reachable.
  3. Debug / development JS off. CDN JS minify / JS rewrite off — mandatory if the origin is Discourse; strongly recommended in front of XenForo 2.3.
  4. Staff: 2FA enrolled. XenForo: mandate on Moderating and Administrative. $config['enableTfa'] not false.
  5. Login limit exists (XF: 4 failures → CAPTCHA or 15-minute lock, provider configured if CAPTCHA). Lost-password throttle intentional.
  6. Cookie prefix / path / domain (XF) or single canonical hostname (Discourse) checked: login survives refresh.
  7. Last backup date is this week. Off-box copy exists. You can name the last time you restored onto staging.
  8. Mail: a test message arrives, or you have documented that Discourse ID is the recovery path and staff accepted that.
  9. Leavers from the last quarter: no admin flag, no API key, no group.
  10. One throwaway user: fail login, reset or lock, delete. You know what the member sees.

Incident card (print this)

When you think an account is not the member:

  1. Lock / force reset (must reset, not “change,” on XenForo).
  2. Clear unexpected 2FA methods; leave the staff mandate on.
  3. Pull admin / moderator until they are back.
  4. Ban or suspend if they are posting. Lock and ban are different tools.
  5. Search for the same email / last IP with the tools you already have. Do not invent a correlation product.
  6. Tell the real human on a channel that is not the compromised inbox if you can.
  7. After recovery: unique password, TOTP re-enrolled, backup codes rotated, sessions empty.

Checklist

  • HTTPS everywhere; application URL matches
  • 2FA on every staff account; XF mandate on both staff groups
  • $config['enableTfa'] not left false (XF)
  • Login limit configured (XF: 4 / CAPTCHA or 15 min)
  • Lost-password throttle + CAPTCHA intentional
  • Staff know lock versus ban
  • Backup = the product’s real inventory; off-box; restored once
  • Cookies: prefix xf_ / path / domain (XF); one hostname (Discourse)
  • Debug off; installer locked; fullJs false
  • CDN JS minify / modifications off (hard requirement in front of Discourse)
  • Secret paths not public
  • No shared staff logins; leavers revoked same day
  • Mail works, or Discourse ID is an explicit recovery choice
  • No invented WAF SKU in the “we are secure” paragraph

What this is not

It is not a penetration test. Neither official manual claims the software is unhackable.

It is not the XF-only baseline. Go there for hashes, nginx internals, and the two lock modes with screenshots-in-prose.

It is not backups as a migration and not hosting Discourse. Those are the runbooks this checklist points at.

It is not a privacy-policy writer. Put legal text in the product’s Terms / Privacy pages, not in a notice that expires.

Takeaways

  • 2026 security on a forum is boring: HTTPS, staff 2FA, a login limit, debug off, cookies scoped, a backup you have restored, and a CDN that does not rewrite JavaScript.
  • XenForo gives you numbers worth writing down: 4 failures, 15-minute lock, prefix xf_, $config['enableTfa']. Discourse gives you Docker backups (Meta 14855), optional SMTP / Discourse ID, and a hard no JS minify rule at Cloudflare.
  • For the XF-deep controls — hashes, security lock modes, nginx internal, Registered-as-primary — use the dedicated article.
  • Do not buy a WAF to avoid the hour above. Do the hour.

Print the incident card. Mandate 2FA. Fetch config.php as a guest and be glad it fails.