How-to
.htaccess support
Turn on Apache .htaccess files for a site and know which directives ScalWS applies,
which it ignores and which make it deny access.
ScalWS reads .htaccess files itself, without Apache, for applications written for Apache
hosting (WordPress, Joomla, Drupal, single-page applications, redirects maintained by the
site owner). The decision and its limits are in
ADR-0031; the parser and evaluator are the scalws-htaccess
crate.
Turn it on
Per site, with htaccess: true (in scalws.yaml:
applications[].htaccess):
# /etc/scalws/sites.d/example.com.yaml
domain: example.com
root: /srv/www/example.com
php: {fastcgi: "unix:/run/php/php8.3-fpm.sock"}
htaccess: trueor scalwsctl site add example.com --root /srv/www/example.com --htaccess. It is off by
default and needs the site’s root.
- Files are read from
rootdown to the directory of the request. The lookup starts at the site’sroot, not at a runtime’s own root (for Laravel,/srv/shop, not/srv/shop/public). - The rules run for every request of the site, whatever the runtime (static, PHP, proxy,
Node.js, …), after denied paths, rate limits and
https_redirect, and before the route is chosen. A rewritten path selects the route again, and denied paths are checked again on it. - Parsed files are cached per directory and re-checked at most every 2 seconds: edits take effect within 2 seconds, without a reload.
.htaccessitself is never served (dotfiles answer404).
What is supported
| Module | Directives |
|---|---|
| mod_rewrite | RewriteEngine, RewriteBase, RewriteCond (server variables, %{HTTP:Header}, -f, -d, -s, =, <, >, regular expressions, [NC,OR]), RewriteRule with [L,END,R=code,NC,QSA,QSD,NE,F,G,S=n,C], $n and %n back-references |
| mod_alias | Redirect, RedirectPermanent, RedirectTemp, RedirectMatch |
| Access | Require all granted|denied, Require [not] ip ..., Order/Allow/Deny, also inside <Files> and <FilesMatch> |
| Other | ErrorDocument, DirectoryIndex, Header set|append|add|unset, ExpiresActive, ExpiresByType, ExpiresDefault, <IfModule>, Options (no effect: directories are never listed) |
Server variables available in RewriteCond: REQUEST_URI, QUERY_STRING,
REQUEST_FILENAME, SCRIPT_FILENAME, DOCUMENT_ROOT, HTTP_HOST, SERVER_NAME,
SERVER_PORT, HTTPS, REQUEST_SCHEME, REQUEST_METHOD, REMOTE_ADDR, THE_REQUEST,
HTTP_USER_AGENT, HTTP_REFERER, HTTP_COOKIE, HTTP_ACCEPT, HTTP_AUTHORIZATION,
REDIRECT_STATUS, and any request header as %{HTTP:Name}. Other variables are empty.
Apache’s semantics are kept: rewrite rules apply from the deepest .htaccess that has
them, per-directory patterns match the path relative to that directory, an internal
rewrite restarts rule processing (at most 10 rounds) unless [END], and other directives
are merged with deeper files winning. PHP still sees the original REQUEST_URI.
What is ignored, and what denies
Ignored, with a log line naming the file:
- proxying with
[P],RewriteMap,SetHandler,AddHandler; php_value,php_flag,php_admin_*(set PHP options in the pool or inphp.fpm.iniinstead);- a rule whose regular expression the linear-time engine cannot run (look-around, back-references inside a pattern): that rule is inactive;
- directives of modules ScalWS does not model.
Denied with 403: access rules ScalWS cannot evaluate are never skipped, so nothing
meant to be protected is published by accident. That covers HTTP authentication
(AuthType, Require valid-user|user|group), Require host|env, access rules inside
<RequireAll>, <RequireAny>, <Limit>, <LimitExcept> or <If>, and access rules in
an <IfModule> for a module ScalWS does not model. The whole directory (or, for a
<Files> block, every name) then answers 403. Protect such areas another way, for
example with a route’s basic_auth, in the
application, or with security.deny_paths.
Limits: 64 KiB per file and 1000 directives (access rules beyond them deny), 10 rewrite
rounds, at most 16 DirectoryIndex names, and bounded regular-expression size and
scanning per request (a request over the scanning budget answers 500).
Examples
WordPress’s own block works as written:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>Redirects kept by the site owner:
Redirect 301 /old-page https://example.com/new-page
RedirectMatch 301 ^/blog/(\d+)/(.*)$ https://example.com/news/$2Close a directory to everyone but one address, and set a header:
# /srv/www/example.com/internal/.htaccess
Require ip 192.0.2.10Header set Strict-Transport-Security "max-age=31536000"Check it works
curl -sI -H 'Host: example.com' http://127.0.0.1/old-page # 301
curl -sI -H 'Host: example.com' http://127.0.0.1/internal/ # 403 from other addresses
journalctl -u scalwsd | grep htaccess # ignored lines
sudo grep htaccess /var/lib/scalws-controller/logs/web-0.log # under scalws-controllerCommon problems
- Rules have no effect:
htaccess: trueis missing, the file is outsideroot, orscalwscannot read it. Look for ignored lines in the log. - A directory answers 403 for everyone: it (or a parent) has an access rule ScalWS
cannot evaluate, such as
AuthType BasicwithRequire valid-user. - A rewrite loops or answers 500: more than 10 internal rewrite rounds; end the rule
with
[END]or add aRewriteCondthat stops it. - A
[P]proxy rule is ignored: use a route with aproxyruntime. php_value upload_max_filesizeis ignored: set it in the PHP-FPM pool or inphp.fpm.ini.
See also: applications: .htaccess, security: .htaccess.