Joomla Security Hardening, Multi-factor Authentication and Passkeys

This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production.

This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages.

This page collects everything an administrator needs to make a Joomla site hard to take over and quick to recover. It was written against Joomla 6.2.x on PHP 8.3+ and checked against the official documentation on 2026-10-30. The running example is the Tallpine Hiking Club site: a handful of editors, a Trail Warden group that sees member-only trail conditions, and a headless "trail map" front end that reads the Web Services API with a token.

Joomla’s security model has layers, and each one is covered below in the order an attacker meets it:

  1. Identity: strong passwords, multi-factor authentication (MFA), passkeys and the rule of least privilege.

  2. Transport and browser: HTTPS, HSTS and the HTTP Headers plugin (Content-Security-Policy, Referrer-Policy and friends).

  3. Abuse resistance: the Proof-of-Work captcha and the MFA and password-reset throttles.

  4. Server: file permissions, configuration.php, .htaccess and restricting the administrator area.

  5. API: the API token and Basic Auth plugins.

  6. Supply chain and patching: the Security Centre, the Vulnerable Extensions List, Automated Core Updates and a monthly routine.

  7. Recovery: backups and a written incident-response procedure.

flowchart TD U[Visitor] --> H{HTTPS and security headers} H --> C[Captcha on public forms] C --> L[Login form] L --> P{Password correct?} P -- no --> T[Throttle and log the attempt] P -- yes --> M{MFA required for the user's groups?} M -- no --> S[Session with full access] M -- yes --> K[Captive MFA page] K -- valid code or key --> S K -- too many failures --> T L --> W[Passkey button, no password] W --> S

Fresh-install defaults worth knowing

Joomla 6.2 installs with sensible defaults, but they differ from what many older tutorials and books describe. The default plugin states below come from the installation SQL of the 6.2 branch and the plugin manifests.

Area Default on a fresh 6.2 install What to do

Multi-factor authentication

The Email, Verification Code (TOTP), Web Authentication and YubiKey method plugins are enabled. The Fixed Code method is disabled.

Keep Fixed Code disabled. Require MFA for every group that can reach the administrator area.

Passkeys

The System - WebAuthn Passwordless Login plugin is enabled and has no parameters.

Leave it on, and encourage administrators to register a passkey in addition to MFA.

API authentication

API Authentication - Token is enabled and API Authentication - Basic Auth is disabled.

Keep Basic Auth off unless a legacy client forces it. Use tokens.

HTTP Headers plugin

Enabled. X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin and Cross-Origin-Opener-Policy: same-origin are active. HSTS and Content-Security-Policy are off.

Turn on HSTS once HTTPS works everywhere. Introduce a CSP in report-only mode first.

Captcha

The Captcha - Proof of Work plugin ships enabled (6.1+), but no captcha is selected in Global Configuration.

Select it as the default captcha if you want spam protection on forms.

Automated Core Updates

On for new installs (opt-out in the installer), off for upgraded sites.

Decide explicitly. See Updating Joomla and Automated Core Updates.

Password rules

Minimum length 12. Minimum digits, symbols, upper-case and lower-case letters are all 0.

Raise the character-class minimums to 1 and, for administrators, rely on a password manager.

Older books on Joomla describe an earlier generation of these settings. Joomla! 4 Masterclass (2023) targets Joomla 4, which reached end of life in October 2025, and Developing Extensions for Joomla! 5 (2023) targets 5.0. Both predate the Proof-of-Work captcha (6.1), the security-severity flag on extension updates (6.2) and Automated Core Updates (5.4 and later), and both pre-date the move of the end-user documentation to the Joomla! User Guide at guide.joomla.org. The old wiki at docs.joomla.org marks many of its security pages as obsolete. Pages that mention the Google reCAPTCHA plugin are out of date as well: the core reCAPTCHA plugin was removed in Joomla 5.0, and the 6.x captcha is the Proof-of-Work plugin (or a third-party captcha extension).

Multi-factor authentication

Multi-factor authentication has been part of Joomla core since 4.2, where it replaced the earlier two-factor authentication (TFA) plugins. The feature is derived from Akeeba LoginGuard, and the official User Guide says its text was derived from Akeeba’s documentation with permission. An account that has MFA enabled must prove a second factor after the password was accepted.

How the login flow works

After the username and password are checked, a user who has at least one MFA method (or who must set one up) is sent to a captive page. Until a correct second step is provided the user cannot reach any other page of the site. Modules are disabled while the captive page renders, so no module positions appear, but plugins still run. When the second step succeeds the session is flagged as fully authorised and the user is redirected to the URL they originally requested.

There are several practical consequences:

  • The captive page applies to the front end and to the administrator. The template used on the site is chosen by the captive_template option, which defaults to the site’s default template style.

  • Any extension that logs a user in outside the normal form must either honour the captive flow or be treated as an MFA bypass. See "Silent logins" below.

  • Throttling is applied to the second step: the options mfatrycount (default 10 attempts) and mfatrytime (default 1 hour) in the Users component configuration limit repeated failures. The #__user_mfa table stores tries and last_try per method for this purpose.

The method plugins

Each MFA method is a plugin in the multifactorauth group, so a method can be disabled site-wide by disabling its plugin in System > Plugins.

Method (user-facing name) Plugin Notes

Verification Code

plg_multifactorauth_totp

Time-based one-time passwords (TOTP): six-digit codes from an authenticator app, a password manager or, in some cases, the browser. The user scans a QR code or types the shared secret once. This is the default recommendation for most staff.

Authentication Code by Email

plg_multifactorauth_email

Sends a six-digit, time-limited code to the account’s email address. The plugin options are timestep (the validity of a code, default 120 seconds, with choices of 30, 60, 120, 180 and 300) and force_enable (default No). It needs a working mail setup, see mail templates and email settings. It is only as secure as the mailbox, so use it as a convenience, not for Super Users.

Web Authentication

plg_multifactorauth_webauthn

A hardware security key, a platform authenticator (Windows Hello, Touch ID, Face ID, Android biometrics) or a phone, using the W3C WebAuthn standard. A user may register several authenticators.

YubiKey

plg_multifactorauth_yubikey

Uses a YubiKey one-time-password token. Each user needs their own key. The Yubico validation service is contacted from the server. For new deployments the WebAuthn method covers the same keys with a phishing-resistant protocol.

Fixed Code

plg_multifactorauth_fixed

For testing and illustration only. It is disabled by default and must not be enabled on a production site, because the "second factor" is a constant string.

Additionally, backup codes are generated by com_users itself, not by a method plugin. When the user creates their first method, Joomla offers to create a set of backup codes. Each code can be used once in place of a second factor and the user can regenerate the set with the Regenerate Backup Codes button. Backup codes should be printed or stored in a password manager, never in the same place as the password.

The data lives in the #__user_mfa table (the #__ prefix is replaced by your table prefix). Its columns are id, user_id, title, method, default, options, created_on, last_used, tries and last_try. The method-specific secret is in options. Treat a database dump as sensitive for this reason.

Setting MFA up as a user

  1. Log in, open User Menu > Edit Account in the administrator (or Edit Your Profile on the site).

  2. Open the Multi-factor Authentication tab. The tab is visible only to the account owner. Even a Super User cannot see or edit another person’s methods.

  3. Press the + Add …​ button for the method, scan the QR code or follow the prompts, enter the first code to prove that it works and save. Creating a method turns the feature on for the account.

  4. Create backup codes when offered, and store them offline.

  5. Add a second method or a second authenticator, so that losing a phone does not lock the account.

An account can hold several methods at once. The official documentation orders the options from most to least secure: WebAuthn as the primary login with MFA as the secondary step, WebAuthn as the only login, username and password with MFA, and finally username and password alone.

Forcing MFA per user group

Under Users > Options > Multi-factor Authentication the administrator controls who must or must not use MFA. The option names below are the keys stored in the Users component parameters.

Option Default Meaning

forceMFAUserGroups

none

Users in these groups are required to set up MFA. When they log in without a method they are redirected to the setup page and cannot use the rest of the site until they finish.

neverMFAUserGroups

none

Users in these groups never see MFA, for example a service account that logs in non-interactively. Use it sparingly, because a group that is both forced and excluded is a configuration mistake that is easy to make with nested groups.

allowed_positions_frontend, allowed_positions_backend

empty

Module positions that may be shown on the captive and setup pages for each client.

frontend_show_title

Yes

Shows the page title on the front-end captive page.

captive_template

site default

Template style used to render the captive page on the site.

mfaonsilent, silentresponses

No, cookie, passwordless

Silent logins and MFA, explained next.

mfaredirectonlogin, mfaredirecturl

No, empty

Redirect users who have no MFA method to a given URL after login (for example an internal help page).

mfatrycount, mfatrytime

10 and 1 (hour)

Throttle on failed second-step attempts.

For the Tallpine site the policy is: Super Users, Administrators, Editors and the custom Trail Warden group are all forced. Registered members are not forced, but the profile page invites them to add a method. The safest order is to force MFA for Super Users first, confirm that two people can still get in, and only then widen the forced groups.

Silent logins and MFA

Some login flows are "silent": the user is authenticated without typing a password, for example the remember-me cookie (Authentication - Cookie) or a passkey login. The mfaonsilent switch controls whether those logins still trigger the MFA captive page, and silentresponses lists the authentication responses that are exempt when mfaonsilent is No. The default list is cookie, passwordless. This is deliberate: a passkey is already a strong second factor in itself, and a remember-me cookie is only issued after a previous full login. If your policy says "MFA on every login", set mfaonsilent to Yes, understanding that remember-me then asks for the second factor on each return visit.

Recovery

Losing every method is the most common MFA support request. In order of preference:

  1. Use a backup code.

  2. Use another registered method or authenticator.

  3. A Super User opens Users > Manage. The list has a Multi-factor Authentication filter and an MFA column that shows which accounts have methods, and the component can turn off all methods of an account (the "Turn Off" action). Ask the user to register a new method straight away.

  4. As a last resort, delete the user’s rows from the #__user_mfa table in the database, with a backup taken first.

Every verification, creation and removal is written to the action log, see privacy and action logs. Because the Joomla documentation notes that the site does not lock accounts, rely on the throttle options and on a firewall or fail2ban for the web server.

Common MFA errors

  • Clock drift: TOTP codes are rejected when the server clock is wrong by more than a few seconds. Enable NTP on the server and in the Docker host.

  • Email codes never arrive: check the mail configuration and the 120 second validity. A slow mailbox makes a code expire.

  • Locked out after changing domain: WebAuthn credentials are bound to the exact domain. Moving staging to production or switching www. on and off invalidates every registered authenticator.

  • An extension or custom login module that bypasses the captive page. Test every login entry point after installing a login-related extension.

  • Fixed Code left enabled after a demo.

Passwordless login with passkeys (WebAuthn)

WebAuthn lets a user sign in with an authenticator, called a passkey, instead of a password. The private key never leaves the authenticator and the site stores only a public key. The username is still typed, because WebAuthn has no identity management and the browser needs the name to find the matching credentials.

Requirements

  • HTTPS with a valid, trusted certificate. WebAuthn only works over HTTPS. A free Let’s Encrypt certificate is enough. If $live_site in configuration.php begins with http://, the login button is not shown. See web server, reverse proxy and HTTPS for a proxy that forwards the scheme correctly.

  • PHP: the OpenSSL extension, GMP or BCMath (either), and ideally the Sodium library, which enables elliptic-curve cryptography for FIDO2 authenticators.

  • A stable domain. The credential is bound to the domain on which it was registered. Changing the domain (or serving the site on several) requires registering every authenticator again.

  • Browsers: Chromium-based browsers and Firefox since 2019, Safari from iOS and iPadOS 13 and macOS Catalina. Windows Hello needs a TPM enabled in firmware.

The plugins involved

Two different plugins carry the word WebAuthn, and they do different jobs.

Plugin Role

System - WebAuthn Passwordless Login (plg_system_webauthn)

Adds the Web Authentication button to the login form and handles passwordless login. Enabled by default, no parameters.

Multi-factor Authentication - Web Authentication (plg_multifactorauth_webauthn)

Uses the same kind of authenticator as a second step after a password login.

An authenticator registered for passwordless login is stored in the #__webauthn_credentials table. The credentials are encrypted with a key derived from the site secret, as defence in depth against an attacker who can write to that table but cannot read the configuration file. An attacker who can read configuration.php already controls the site.

Registering and using a passkey

  1. Log in once with the password. In the administrator choose User Menu > Edit Account > W3C Web Authentication (WebAuthn) Login and press Add New Authenticator.

  2. Follow the browser prompt (touch the key, use the fingerprint, face or device PIN) within about a minute.

  3. Register at least two authenticators: a hardware key kept in a drawer and a platform or phone passkey.

  4. To log in, type the username, leave the password empty and press the Web Authentication button on the login form.

Users can only manage their own authenticators, not even a Super User can add or remove those of somebody else. Username and password login keeps working, so keep strong MFA enabled on the account. If the passkey succeeds, the MFA step is skipped by design (see silentresponses above), because a passkey is already a possession plus a local biometric or PIN.

Pitfalls

  • No button on the login form: the site is not on valid HTTPS, $live_site uses http://, or the reverse proxy hides the scheme from PHP.

  • "Authenticator not registered": PHP notices or warnings corrupt the JSON response. Set Error Reporting to None in Global Configuration (or look at the response in the browser developer tools).

  • Third-party login modules only show the button when they call the extra login buttons API (Joomla\CMS\Helper\AuthenticationHelper::getLoginButtons, fed by the onUserLoginButtons event).

  • Lockout after losing all authenticators or changing domain. The password remains the fallback, which is exactly why that fallback must be protected by MFA.

Older books show the pre-4.2 "two-factor authentication" tab and a separate WebAuthn plugin under Users > Options. In 4.2 and later these are the multifactorauth plugin group and the passwordless login plugin described here.

Passwords and brute-force controls

Under Users > Options > Password Options the following keys exist (defaults in brackets): minimum_length (12), minimum_integers, minimum_symbols, minimum_uppercase and minimum_lowercase (all 0), reset_count (10) and reset_time (1 hour), which limit how many password-reset requests are accepted in the period. The User Guide recommends setting each character-class minimum to 1. Before Joomla 5.2 some symbols were accepted in a password but were not counted as symbols during validation. The guide’s older advice of "8 to 14 characters" and monthly password changes is not what current guidance recommends: prefer long passphrases from a password manager, change them when a compromise is suspected, and protect the account with MFA.

The HTTP Headers plugin

System - HTTP Headers (plg_system_httpheaders) is a core plugin, enabled by default, that sets response headers from the backend, so you do not have to edit web server configuration. It has the tabs Basic Settings, Strict-Transport-Security and Content Security Policy. The official User Guide article was written for Joomla 4 and states that it also applies to Joomla 5. The defaults below are taken from the plugin manifest on the 6.2 branch.

Basic settings

Setting (parameter) Default Effect

X-Frame Options (xframeoptions)

Yes

Sends X-Frame-Options: SAMEORIGIN, so only your own site can frame your pages. This prevents click-jacking.

Referrer-Policy (referrerpolicy)

strict-origin-when-cross-origin

Eight choices: no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url. unsafe-url can leak private URLs from HTTPS pages to insecure origins and should be avoided.

Cross-Origin-Opener-Policy (coop)

same-origin

Choices unsafe-none, same-origin-allow-popups and same-origin. The User Guide article lists unsafe-none first in one place and calls same-origin the default in another; the manifest default is same-origin.

Additional headers (additional_httpheader)

none

A repeatable list of header name, value and client (site, administrator or both). The header names offered are Content-Security-Policy, Content-Security-Policy-Report-Only, Cross-Origin-Opener-Policy, Expect-CT, Feature-Policy, Permissions-Policy, Referrer-Policy, Report-To, Strict-Transport-Security and X-Frame-Options. Expect-CT is discontinued and Feature-Policy is deprecated in favour of Permissions-Policy.

A typical addition for the Tallpine site is Permissions-Policy with the value geolocation=(self), camera=(), microphone=(), because the trail map uses geolocation and nothing needs the camera or microphone.

Strict-Transport-Security (HSTS)

HSTS tells browsers to use HTTPS only. It is off by default (hsts = 0). When enabled it exposes hsts_maxage (default 31536000 seconds, one year; the minimum accepted is 300), hsts_subdomains and hsts_preload, both off by default.

A safe roll-out avoids locking visitors out of a subdomain that lacks a certificate:

  1. Make HTTPS work on the whole site and redirect HTTP to HTTPS at the web server or proxy.

  2. Enable HSTS with a short max-age (for example 300), and test.

  3. Raise it step by step to one week, one month and finally a year (some guides suggest two years).

  4. Add includeSubDomains only when every subdomain, now and later, has a valid certificate.

  5. Enable preload and submit the domain to the browser preload list only when you intend to keep HTTPS permanently: removal from the list is slow.

HSTS is preferable to a bare 301 redirect because, once the browser knows the policy (or the domain is preloaded), the first request is never sent in clear text.

Content-Security-Policy (CSP)

CSP is off by default (contentsecuritypolicy = 0). When turned on, the plugin defaults to report-only (contentsecuritypolicy_report_only = Yes), which logs violations in the browser console without blocking anything. The other options are:

  • contentsecuritypolicy_client: apply the policy to the site (default), the administrator or both.

  • nonce_enabled: Joomla generates a random nonce for the script and style tags it adds through its API. Use the placeholder {nonce} in a directive, for example script-src 'self' {nonce}. The official article notes that core scripts and styles do not carry nonces everywhere.

  • script_hashes_enabled and style_hashes_enabled: Joomla collects the inline scripts and styles registered through its API and generates SHA hashes. Use {script-hashes} and {style-hashes} in directives. The hashes change whenever the inline code changes.

  • strict_dynamic_enabled: scripts trusted by a nonce or hash may load further scripts (this overrides 'self' and 'unsafe-inline' for those children).

  • frame_ancestors_self_enabled (default Yes): adds frame-ancestors 'self'.

  • contentsecuritypolicy_values: a repeatable list of directive, value and client. The User Guide describes 21 of the directives, among them default-src, script-src, style-src, img-src, connect-src, font-src, object-src, media-src, frame-src, child-src, form-action, frame-ancestors, base-uri, worker-src, manifest-src, report-uri and report-to. report-uri is deprecated in CSP level 3 in favour of report-to, but browser support for report-to is still uneven.

A starting policy for Tallpine, assuming map tiles from a tile server and no third-party scripts, is entered as separate rows of the directive list. The block below is the resulting header, for reference.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-RANDOM'; style-src 'self' 'nonce-RANDOM'; img-src 'self' data: https://tiles.tallpine.example; connect-src 'self' https://tiles.tallpine.example; frame-ancestors 'self'; base-uri 'self'; form-action 'self'
Strict-Transport-Security: max-age=31536000
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: geolocation=(self), camera=(), microphone=()

Run the site in report-only mode for at least a week while editors use TinyMCE, the Media Manager and the Smart Search module, then switch Report-Only off. Typical breakages are inline event handlers, editor iframes, third-party analytics and fonts. Test the result with an external security-headers scanner. Enabling CSP can change front-end behaviour, especially with inline code, so keep it separate from other changes in the release.

The Proof-of-Work captcha

Joomla 5.0 removed the reCAPTCHA plugin, which left sites with no core captcha for a release cycle. Joomla 6.1 (released 2026-04-14) brought spam protection back with Captcha - Proof of Work (plg_captcha_powcaptcha). It needs no account and no external API, which makes it privacy-friendly, and it asks the visitor’s browser to solve a small computational puzzle in the background so the visitor sees nothing. A bot that submits a form without doing the work is rejected.

To use it, open System > Global Configuration > Site > Default Captcha and select it, then make sure forms that support a captcha (contact, registration, password reset, and any extension that uses the captcha field) are set to use the default captcha. The plugin parameters are:

Parameter Default Meaning

difficulty

moderate

easy, moderate, hard or custom. Harder puzzles cost bots more and slow slower phones. The manifest stores the default in upper case while the options are lower case, so save the plugin once explicitly to be sure a value is stored.

maxnumber

250000

Between 10000 and 1000000. Shown only when difficulty is custom.

autosolve

onfocus

When the puzzle starts: off, onfocus, onload or onsubmit.

expiration

300

How long a solution is valid, in seconds: 60, 300, 600 or 3600.

Proof of work raises the price of automated spam, it does not identify humans. A determined attacker with real compute can still pass. Combine it with the contact form’s email rate limits, the registration moderation settings and, for public forms, a honeypot or moderation queue. Third-party captcha extensions that use the same on…Setup events (for example hCaptcha or Turnstile integrations) remain an option.

Server hardening

File permissions and ownership

After installation the recommended permissions are 644 for files and 755 for directories. The Web Installer needs some directories to be writable, and the official guide says to reset them to safer values afterwards. Never leave 777 (or 757) on a directory or on configuration.php.

# run from the Joomla root as the account that owns the site (not as root)
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;

# the web server must write only here
chmod 775 cache administrator/cache tmp administrator/logs

Points that cause trouble:

  • Ownership matters more than modes. On a host where PHP runs as the file owner (suExec, PHP-FPM pools per account), 644 and 755 are enough. When PHP runs as www-data and files are owned by a deploy user, grant write access only through group ownership of the writable folders.

  • The files PHP must write: cache, administrator/cache, tmp, the log folder ($log_path), images (for the Media Manager) and, if you install extensions from the web interface, the extension folders. Writing to images is also an upload risk: do not allow PHP execution there (see below).

  • 640 or 750 can make the web server unable to read files when it falls under "other".

  • In containers use a read-only root filesystem with volumes for images, tmp, cache and logs. See local development with Docker and the site pages on Docker security and Kubernetes admission control and policy.

Protecting configuration.php

configuration.php holds the database credentials and the site $secret. It is a PHP file, so a request for it returns an empty page, but anything that lets an attacker read files (a path traversal, a backup left in the web root, a misconfigured server that serves PHP as text) discloses it. Hardening steps:

  1. Make it read-only for everyone but the owner after configuration is finished, for example chmod 444 configuration.php (or 440 with a matching group). If a Global Configuration save is needed, temporarily make it writable and revert.

  2. Never commit it to Git. Keep a configuration.php template with placeholders in the repository and generate the real one at deploy time.

  3. Prefer to place it outside the web root. The optional public folder layout lets the document root contain only the front controllers and assets (see installation and the public folder and configuration.php and .htaccess).

  4. Do not leave configuration.php.bak, configuration.php~, .orig or editor swap files next to it. Block those names in the server configuration (below).

  5. Keep $debug at 0 and $error_reporting at none or default on production, so stack traces do not leak paths.

  6. Set $force_ssl to 2 (whole site) only when HTTPS is certain, and leave $live_site empty so Joomla derives it from the request.

<?php
// configuration.php (extract) for Tallpine production. Every value shown here is a placeholder.
class JConfig
{
    public $offline = false;
    public $sitename = 'Tallpine Hiking Club';
    public $debug = false;
    public $error_reporting = 'none';

    public $dbtype = 'mysqli';
    public $host = 'db';
    public $user = 'tallpine';
    public $password = 'CHANGE_ME_DB_PASSWORD';
    public $db = 'tallpine';
    public $dbprefix = 'tp_';

    // Used to derive encryption keys (for example for stored WebAuthn credentials). Generated by the installer.
    public $secret = 'CHANGE_ME_GENERATED_BY_INSTALLER';

    public $live_site = '';
    public $force_ssl = 2;

    public $session_handler = 'database';
    public $lifetime = 30;

    public $log_path = '/var/www/html/administrator/logs';
    public $tmp_path = '/var/www/html/tmp';
}

A generated configuration.php is created by the installer, which fills $secret with a random value. Deployment tooling should treat that file like a secret: in Docker or Kubernetes, mount it from a secret store or render it from environment variables at container start instead of baking it into an image. The $secret value must be kept stable, because rotating it invalidates sessions and encrypted data such as stored WebAuthn credentials. The database table prefix (dbprefix) is a convenience for sharing a database, not a security feature.

Hardening with .htaccess

Joomla ships htaccess.txt in the root. To use it on Apache, rename it to .htaccess (if the host already supplies one, merge the two). The shipped file already contains:

  • Options -Indexes and IndexIgnore *, which disable directory listings;

  • Header always set X-Content-Type-Options "nosniff";

  • a rule that sets Content-Security-Policy "script-src 'none'" for SVG files, so scripts embedded in an uploaded SVG do not run;

  • rewrite rules that reject requests whose query string contains patterns such as base64_encode(, a script tag, a GLOBALS assignment or a _REQUEST modification (they answer 403; comment them out if they block legitimate URLs);

  • the SEF rewrite to index.php, routing of /api/ to api/index.php and an HTTP_AUTHORIZATION forwarding rule for PHP over FastCGI;

  • optional pre-compressed asset serving, which must be disabled if the server already compresses (otherwise browsers show ERR_CONTENT_DECODING_FAILED).

The shipped file has no HTTPS redirect and no canonical www redirect. The official guide says to add custom redirects in the marked section. Keep your own edits in .htaccess, never in htaccess.txt, because an update can overwrite the text file and the file differs between versions, so compare it after every upgrade.

# .htaccess additions for Tallpine (Apache 2.4), placed in the custom section

# 1. Redirect HTTP to HTTPS
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

# 2. Never serve backups, dumps or editor leftovers
<FilesMatch "(^#.*#|\.(bak|old|orig|sql|dist|swp|log)|~)$">
    Require all denied
</FilesMatch>

# 3. Never execute PHP in upload folders
<IfModule mod_rewrite.c>
    RewriteRule ^images/.*\.(php|phtml|phar)$ - [F,L]
</IfModule>

On nginx there is no .htaccess. The equivalent protections go in the server block.

# nginx server block fragment for the same rules
location ~* ^/(images|media|tmp|cache)/.*\.(php|phtml|phar)$ { return 403; }
location ~* \.(bak|old|orig|sql|dist|swp|log)$ { return 403; }
location ~ ^/(configuration\.php|htaccess\.txt|README\.txt|LICENSE\.txt)$ { return 404; }

Restricting access to the administrator area

The most effective step after MFA is to limit who can even reach /administrator. Choose one or more of these:

  1. By IP address, with an .htaccess file in the administrator folder (or the equivalent nginx allow and deny rules). This suits clubs whose editors share a VPN or a fixed address.

  2. By HTTP basic authentication in front of /administrator as a second password, kept separate from the Joomla login.

  3. By network: serve the administrator only on an internal hostname or through the reverse proxy, or require a corporate identity provider at the proxy.

  4. By permissions: only groups with the core.login.admin permission (Administrator Login) can log in to the backend. Grant it to the smallest set of groups in Global Configuration. A related permission, core.login.api, controls access to the Web Services API. See users, groups, access levels and ACL.

# administrator/.htaccess (Apache 2.4): only the club office and the VPN may reach the backend
<RequireAny>
    Require ip 203.0.113.10
    Require ip 198.51.100.0/24
</RequireAny>

Do not restrict /api this way unless the headless front end has a fixed address; use token authentication instead (next section). Also give each person a personal account, never share a Super User, avoid the username admin, keep the number of Super Users to two or three, and give everyone else only the role they need: Managers manage content but cannot install extensions, Administrators manage users, menus and extensions but not Global Configuration, and only Super Users change Global Configuration and other Super Users.

API authentication defaults

The Web Services API (under /api) authenticates through plugins of the api-authentication group. On a fresh 6.2 install API Authentication - Token is enabled and API Authentication - Basic Auth is disabled. Two details are easy to get wrong:

  • The token is sent in an X-Joomla-Token header together with Accept: application/vnd.api+json. A user can view or reset their token on the profile page, and the matching User - Joomla API Token plugin (plg_user_token) provides that tab. The account needs the core.login.api and core.login.site permissions (Super Users have them). Store tokens in a password manager or secrets vault.

  • Basic Auth sends the username and password with every request. Leave it disabled unless a legacy client needs it, and if you must enable it, require HTTPS and give the integration its own low-privilege account. Older tutorials enable Basic Auth to "get started"; with tokens that is no longer necessary.

# The headless trail-map build calls the API with a token read from the environment
curl -sS https://tallpine.example/api/index.php/v1/content/articles \
  -H "Accept: application/vnd.api+json" \
  -H "X-Joomla-Token: ${TALLPINE_API_TOKEN}"

Create a dedicated API user in a group with read-only permissions for the content the front end needs, never a Super User token. See the Web Services API and headless Joomla for the full set-up.

Security Centre, Vulnerable Extensions and the monthly update routine

The Joomla Security Centre

The Security Centre at developer.joomla.org/security-centre.html is the project’s official security hub. It explains how to report a vulnerability to the Joomla Security Strike Team (JSST), which handles the CMS, the Joomla Framework and *.joomla.org sites, and it lists the Security Announcements feed, available as an RSS feed. Each announcement names the affected and fixed versions, a CVE number where one exists and the reporter. Recent entries include fixes shipped in 6.1.4 and 5.4.9 on 2026-09-25 and in 6.1.3 and 5.4.8 in August 2026. Subscribe an administrator mailbox or a team channel to the feed so that nobody depends on remembering to look.

Vulnerable Extensions List

The Vulnerable Extensions List (VEL), linked from the Security Centre and from the Extensions Directory, lists extensions known to contain vulnerabilities. For each extension on your site:

  1. Check it against the VEL before installing and again during each monthly review.

  2. Remove or replace listed extensions with no patch. Unpublishing a module or deleting a menu item does not protect the site, because the files stay on the server and can be requested directly.

  3. Uninstall through the Installer, compare the files with the extension’s manifest, and delete leftovers. Database tables may be kept if you plan to reuse the data.

  4. Run the JED Checker on an extension zip that is not on the VEL, as a coarse compliance check.

Since Joomla 6.2 extension developers can mark an update as a security update with a severity from None to Critical. Flagged updates are highlighted under System > Update > Extensions and on the dashboard quick icon, so triage security updates first. See installing, updating and update sites and security in extensions.

Core updates

Joomla releases patch and security fixes for the current minor and, for a limited time, the previous major’s last minor. At the check date 6.2 is current, 5.4.x receives security fixes until 2027-10-12, and 4.x and 3.x are end of life. Core updates are verified with The Update Framework (TUF) since 5.1. Automated Core Updates (5.4 and 6.0 onward) lets joomla.org trigger the update on your site for minor, patch and security releases. They require a publicly reachable site, the Default update channel and Stable minimum stability, and are on for new installs and off for upgraded sites. Major upgrades such as 5.4 to 6.x are always manual. If the server is behind a firewall or HTTP basic authentication, automated updates cannot be triggered, and the staging-first routine below is your only update path.

A monthly routine for the Tallpine site

  1. Subscribe to the Security Announcements feed and note any release that fixes a vulnerability. Treat those as out-of-cycle updates, not as part of the monthly slot.

  2. Back up files and database, download the archive and test-restore it on staging (see backups and recovery). Akeeba Backup and similar tools are third-party.

  3. Update staging first: core, then extensions (Extensions > Manage > Update, with security-flagged updates first), then templates. Clear the cache.

  4. Read the post-installation messages on staging and production. They often describe security changes that an upgrade cannot apply automatically.

  5. Check the VEL against the list in System > Information.

  6. Review users: remove stale accounts, check Super Users and the forced-MFA groups, and review the action log for unusual logins (see privacy and action logs).

  7. Review the plugin state: MFA methods enabled, Fixed Code and Basic Auth disabled, HTTP Headers enabled.

  8. Update PHP within the supported branch used by the host, after checking extension compatibility. Joomla 6.2 needs PHP 8.3 or newer in the versions listed in requirements and hosting.

  9. Promote to production during a quiet period and run a smoke test (login with MFA, passkey login, a contact form with the captcha, an API call with a token).

# Check available core and extension updates and list the accounts (full list on the CLI page)
php cli/joomla.php core:check-updates
php cli/joomla.php update:extensions:check
php cli/joomla.php user:list

The site:down and site:up commands are also handy for containment during an incident. See the command-line interface for the full list.

Incident response

Plan before an incident, because the first hour decides how much has to be rebuilt. Typical indicators are an unknown Super User or extension, modified PHP files, defaced pages, spam mail from the server, new files in images, unexpected redirects (including only for search-engine visitors), and a hosting-provider abuse notice.

flowchart TD A[Suspicion or alert] --> B[Contain: maintenance page or block at the firewall] B --> C[Preserve evidence: copy logs and a disk snapshot] C --> D[Find the entry point: VEL, outdated extension, weak account, leaked file] D --> E{Clean restore possible?} E -- yes --> F[Restore a known good backup into a clean environment] E -- no --> G[Rebuild from a fresh Joomla and re-import content] F --> H[Patch the cause and update everything] G --> H H --> I[Rotate secrets: passwords, secret, tokens, MFA] I --> J[Monitor and write a post-incident review]
  1. Contain. Switch the site offline in Global Configuration (or run site:down), or block traffic at the firewall or proxy. Take the site off the network if the host is also used for other projects.

  2. Preserve evidence. Save web server, PHP and Joomla logs (the $log_path folder), the action log and a snapshot of the files before cleaning anything.

  3. Find how they got in. Check the VEL for each extension, compare core files against a clean download of the same version, search for recently changed PHP files with find . -name '*.php' -mtime -7, inspect the images, tmp and cache folders for scripts, and list Super User accounts and the most recent logins from the action log.

  4. Clean or rebuild. The safest recovery is a restore of a backup made before the intrusion into a freshly provisioned server, followed by the patches. If you clean in place, you must replace every core file and review every extension, because one overlooked back door brings the intruder back.

  5. Rotate secrets. Change the database password, the $secret, all user passwords that have a path to the backend, and every API token. Regenerate MFA backup codes and re-register passkeys for administrators. Rotate SMTP credentials and any keys stored in extensions. After changing the secret, sessions and stored encrypted data are invalidated.

  6. Patch and verify. Apply all updates, remove the vulnerable component, re-run the permissions commands and re-check the HTTP headers.

  7. Review. Write down the timeline, the cause and one change that would have detected it earlier (alerting on new Super Users, file-change monitoring, an off-site backup). Report new core vulnerabilities to the Joomla Security Strike Team through the Security Centre, not in public forums.

Pulling it together: a hardening checklist

Control Where Since

Forced MFA for Super Users, Administrators, Editors, Trail Wardens

Users > Options > Multi-factor Authentication

4.2

Fixed Code method disabled; at least two methods or authenticators per administrator; backup codes stored offline

System > Plugins; profile

4.2

Passkey registered on each administrator account

Edit Account > WebAuthn Login

4.2

Minimum password length 12 and character-class minimums of 1

Users > Options > Password Options

4.x, symbol handling fixed 5.2

HSTS, Referrer-Policy, COOP, X-Frame-Options, CSP in report-only then enforced

System > Plugins > System - HTTP Headers

4.0, per-directive options expanded since

Proof-of-Work captcha selected as the default captcha

Global Configuration > Site

6.1

644 files, 755 folders, configuration.php read-only, no backups in the web root

Server

all

Administrator reachable only from known addresses or VPN

administrator/.htaccess or the proxy

all

API token authentication on, Basic Auth off, a read-only API user

System > Plugins; Users

4.0, defaults confirmed in 6.2

Security Announcements feed followed; VEL checked monthly; security-flagged extension updates first

Security Centre; System > Update

flag in 6.2

Automated Core Updates decided on purpose; tested backups; written incident procedure

Joomla Update; backup tool

5.4 and 6.0