Hardening for hosts
This page is for system administrators and security staff of a host who build the server template that every Exponential site is created from. It goes through the layers of a site from the outside in, says what Exponential does by default, and names the setting or command that the host sets or checks. It follows the order of chapter 13 of the installation book, which has every detail, and the threat-by-threat view of Security hardening.
Warning
.htaccess slows down every request. When .htaccess files are enabled (AllowOverride anything but None), let alone
used, Apache looks for them and reads and evaluates them on every request, in every directory of the path, and
page loading times go up drastically (measured). The only real fix is to switch the feature off for
the virtual host: AllowOverride None in the HTTPS virtual host, and the rules in the virtual host's own
configuration as mod_rewrite rules. That is far faster in production speed tests and well worth the time it takes
to optimize a production configuration. Switch over right away: copy the rules of the
example HTTPS virtual host into your virtual host (with the static cache, also
staticcache_offload.conf), set AllowOverride None, run apachectl configtest, reload Apache and
test the site. The same rules for other servers: nginx, LiteSpeed,
Caddy, IIS, Velocity (built in); where virtual host rules go in a control
panel: Plesk, cPanel, DirectAdmin, ISPConfig.
On LiteSpeed or OpenLiteSpeed, the server's own page cache can also answer anonymous visitors and crawlers without PHP: Page caching with LiteSpeed and OpenLiteSpeed.
Note
Settings are changed in overrides, never in the shipped files: settings/override/<file>.ini.append.php for the
whole installation, settings/siteaccess/<siteaccess>/<file>.ini.append.php for one siteaccess. After a change,
clear the INI cache (php bin/php/ezcache.php --clear-tag=ini), and under Velocity run exp:velocity deploy
(chapter 13).
Contents
- The principles
- Served files
- Secrets
- Debug output
- Administration, passwords and sessions
- Headers, TLS and proxies
- Files, users and processes
- Watching
- A shield in front
- Updates
- Server-template checklist
The principles
Security hardening measures Exponential against the OpenBSD way of working: secure by default, least privilege, privilege separation, minimal attack surface, audit, and fixing forward. For a host that means one rule above all: do not loosen a default to make something work; find the cause. A support ticket that ends with "we switched the rewrite rule off" has opened a hole on that site.
Served files
The document root of Exponential is the installation root, so settings/, var/, vendor/ and the SQLite database
lie inside it. Safety comes from routing: only a fixed list of asset paths is served as files, everything else goes
to index.php (13.2).
| Web server | What the host does |
|---|---|
Apache, LiteSpeed with .htaccess | cp .htaccess_root .htaccess or ./console exp:setup:htaccess --apply; compare after every upgrade and merge new rules (./console exp:setup:htaccess prints the diff and exits 1 while the file differs, a drift check for monitoring) |
| nginx | the derived server block of 8.6, with the dot-file and script rules ahead of every static location |
| Velocity | nothing; FollowSymlinks=disabled (the default) also refuses links out of the root |
| any | never a generic "serve the file if it exists" configuration |
Check from outside after every change of the web server's configuration:
for p in /.git/config /.env /settings/site.ini /composer.json /kernel/classes/ezini.php /var/log/error.log; do
printf '%-32s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://example.com$p)"
doneEach line must show 404, or 200 with an Exponential page and never the file's content. Web server details:
Choosing a web server and
Chapter 8.
Secrets
settings/override/ holds the database password (site.ini.append.php), the audit signing and pseudonym keys
(audit.ini.append.php), the site secret of the mail preferences (mailpreferences.ini.append.php, which also keys
the session password stamp) and, when set, Velocity's dashboard token
(13.3).
- Mode
0640or stricter, never in version control, never in a public place. - Back it up as a secret, encrypted, with the database (Backups: what to back up).
- Copy it with every move of the site. A copy without
mailpreferences.ini.append.phpgenerates a new site secret: old unsubscribe links stop matching and every user is signed out once. - Remove
kickstart.iniandvar/log/initial-admin-passwordafter provisioning (First sign-in and default passwords).
config.php in the installation root can hold the cluster database password (CLUSTER_STORAGE_PASS). Treat it like
settings/override/:
- Mode
0640with the web server's group (chmod 0640 config.php); later edits withexp:config:setkeep that mode. Setting a secret in aconfig.phpevery user can read prints aWARN:line. - Never in version control:
/config.phpin the project's.gitignore(the Exponential repository tracks onlyconfig.php-RECOMMENDED). - The console masks the values of names with
PASS,SECRET,KEY,TOKEN,CREDENTIALorPWDas********;--show-secretsshows them, so do not paste such output into a ticket. exp:setup:*andexp:config:*back up every file they replace or edit tovar/backup/setup/<time>/orvar/backup/config/<time>/(directories0700, never deleted by the commands). Includevar/backup/in what you protect and clean it up (Setting up .htaccess and config.php from the console).
Debug output
[DebugSettings] DebugOutput=disabled and [TemplateSettings] Debug=disabled ship off. Keep them off. When support
must debug a live site, limit it to the support desk's address and remove the entry again:
# settings/override/site.ini.append.php
[DebugSettings]
DebugOutput=enabled
DebugByIP=enabled
DebugIPList[]
DebugIPList[]=203.0.113.10In PHP-FPM's php.ini or pool: display_errors=Off, log_errors=On, expose_php=Off
(13.4). Behind a proxy, DebugByIP
is only as reliable as TrustedProxies[] (see TLS and HTTPS).
Administration, passwords and sessions
| Topic | Default | Host's part |
|---|---|---|
| Administration siteaccess | RequireUserLogin=true | its own host name, network limits: Restricting the administration |
| Setup wizard | closed on an installed site; CheckValidity=false written by the installer | keep [SetupSettings] AllowWizardOnInstalledSite=disabled |
| Passwords | MinPasswordLength=10, WeakPasswordCheck=enabled, bcrypt | offer a policy such as MinPasswordLength=15, ForbidLogin, ForbidCurrentPassword (13.6) |
| Lockout | MaxNumberOfFailedLogin=0 (off) | decide with the customer; a lockout also lets anyone lock a known account |
| Two-step sign-in | extension required by composer.json and activated by the installers; Enforce2FA=disabled | recommend Enforce2FA for administrators: Two-step sign-in |
| Session cookie | HttpOnly, SameSite=Lax, CookieSecure=auto | CookieSecure=true on HTTPS-only sites (13.7) |
| Form tokens | ezformtoken active | keep it in ActiveExtensions[] (13.8) |
| Configuration from Git | ManagedByDeployment=disabled | enabled on production administration siteaccesses when settings and templates come from version control |
A password change signs out every other session of that user ([PasswordSettings] EndOtherSessions=enabled). Keep it
on: it is what makes a password change useful after an account was taken over.
Headers, TLS and proxies
Five security headers go out on every page (X-Content-Type-Options, X-Frame-Options,
Content-Security-Policy: frame-ancestors 'self', Referrer-Policy, Permissions-Policy); HSTS is off until it is
switched on. Apache and nginx add nothing to static files by themselves. The host's part:
- HTTPS on every name, HTTP redirected, HSTS raised once everything works: TLS and HTTPS.
- The full policy (
script-src,style-src) is report-only on the administration as shipped: Content Security Policy. - Behind a load balancer, CDN or panel proxy: list exactly your proxies in
site.ini [HTTPHeaderSettings] TrustedProxies[](Velocity:Q.webserver.proxy.trusted), and make sure the application server cannot be reached around them (13.10).
Files, users and processes
"If the PHP process can change only var/ and settings/, and the web server refuses to run anything below the
root, a flaw in an extension cannot plant code"
(13.11).
- One user writes: PHP-FPM pool, Velocity workers, cronjobs and console scripts run as the site's user.
- The code (
kernel/,lib/,vendor/,index.php) belongs to another user and is read-only to the site's user. - No world-writable files. The shipped
[FileSettings]values are0777and0666; set the hardened ones:
# settings/override/site.ini.append.php
[FileSettings]
StorageDirPermissions=0770
StorageFilePermissions=0660
TemporaryPermissions=0770
LogFilePermissions=0660TemporaryDirships as/tmp/, shared with every user of the machine; give each site one of its own belowvar/.- Velocity:
[ServerSettings] Host=127.0.0.1when a proxy is in front,UserandGroupset to the site's user,AllowRootWorkers=disabled, dashboard token and control panel password set or the views kept local (13.12).
How this scales to many customers: Per-customer users and pools and Writable directories and permissions.
Watching
Hardening without watching only delays the moment you learn of an attack.
| Source | What it records | Host's page |
|---|---|---|
var/log/security.log | the request classifier's verdicts: probes, traversal, injection, attack tools, 404 bursts | Security log |
| the audit trail | sign-ins, role and setting changes, removals, commands; hash-chained | Audit trail |
var/log/auth.log | every step of two-step sign-in (never a code or a secret) | Two-step sign-in |
var/log/csp.log | Content Security Policy violations | Content Security Policy |
Run the frequent cronjob group so the audit's alert rules run, forward the trail off the machine with a sink, and
read security.log weekly (Security hardening).
A shield in front
request-shield, a small web application firewall for
PHP by the CJW Network, refuses scanners and floods before the kernel starts. It is recommended for every site that
can be reached from the internet and is not part of Exponential's composer.json. A host can offer it as part of a
managed plan: request-shield and request rules.
Updates
Only the 6.0 line receives security fixes, published as the next 6.0.x release. Follow releases, update a copy first, check file consistency after every update, and keep PHP, the database server, the operating system and Velocity's engine current as well: Security updates and disclosure.
Server-template checklist
- To do: Rewrite rules: a current
.htaccess_rootcopy, the derived nginx block, or Velocity; the outside check returns no file. - To do: SOAP only where a customer needs it:
index_soap.phplet through (it wassoap.phpbefore 6.0.15) and limited to the callers' addresses;soap.phprefused once nothing calls it (the SOAP entry script is now index_soap.php). - To do:
settings/override/mode0640or stricter, out of version control, backed up encrypted. - To do:
config.phpmode0640, out of version control;var/backup/protected and cleaned. - To do: Provisioning removes
kickstart.iniandvar/log/initial-admin-password, and never leaves a known password. - To do:
DebugOutput=disabled; PHPdisplay_errors=Off,expose_php=Off. - To do: Administration on its own host name, restricted by network, VPN or the shield's browser check.
- To do:
CheckValidity=false,AllowWizardOnInstalledSite=disabled. - To do:
ezformtokenactive;EndOtherSessions=enabled. - To do: HTTPS on every name, HTTP redirected,
CookieSecure=true, HSTS raised once stable. - To do:
TrustedProxies[]lists exactly your proxies, or stays at the loopback default. - To do: One operating-system user per site; code read-only to it; hardened
[FileSettings]; privateTemporaryDir. - To do: Velocity bound to
127.0.0.1behind a proxy; workers never root. - To do:
frequentcronjob group running as the site's user; audit alert recipients checked; a sink configured. - To do: An update routine with named owners.
The setting-by-setting go-live list is 13.16; the recurring review is the Security review checklist.
Demo mode stays off. EXP_DEMO_MODE=1 installs and keeps a known administrator password; set it only for a public demo's own pool, virtual host or unit, never server-wide or in a template, and alert on demo/mode lines in the security.log of customer sites. Demo mode for hosts.
Related pages
- Security for hosts: overview
- Security hardening
- Chapter 13: Security hardening for production
- New server checklist
- Compromised site
- Setting up .htaccess and config.php from the console
- Free documentation: this page is free to copy, translate and improve under the GNU FDL 1.3.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.