There are two aspects to HTTP Strict Transport Security (HSTS):
- Implementing HSTS on your site.
- Submitting your already HSTS enabled site to the HSTS preload list. This is where the "list of HSTS sites" are compiled directly into the browser, which avoids the first request ever being over HTTP (where the request is potentially vulnerable to MITM attacks).
You seem to be going straight for #2. This is not necessarily recommended. Think of the "preload list" as a one-way trip. Technically it's possible to be removed from the preload list; realistically that's not something you want to even be thinking about. (It's hard - slow - enough stepping back from HSTS-only.)
The preload list submission page itself does not recommend going straight to "preload list submission". The recommendation is to increase the max-age
parameter over a period of time (months), before taking the final step to submitting to the preload list. Test test test in the meantime to ensure SSL certs are reliably renewed, no mixed content warnings etc. etc.
I would also be wary about HSTS preload list submission (or even HSTS itself to some degree) on a shared server, where you don't have full control over the SSL configuration. You don't actually state whether you are on a shared server or not, but since you are doing all this config in .htaccess
, I assume that you are. If you have your own server and access to the server config then most of this should be configured in the server/virtualhost config (and it is arguably easier and more reliable to do so).
Remember, that once you have gone the HSTS route (and users have accessed the HTTPS site or you are on the "preload list") then your site can only be accessed over HTTPS. This doesn't just apply to your site, but also to any 3rd party services you might be using (mixed content browser warnings etc.).
#IMPLEMENT HSTS
<IfModule mod_headers.c>
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</IfModule>
This sets the required HSTS HTTP response header on "most"*1 responses (but note the preload
parameter, which should probably be omitted initially).
*1 However, this directive does not necessarily set the required header on all responses. A requirement of HSTS is that you also set the header on "redirect" responses (eg. www to non-www on HTTPS). Currently the above does not do this. You should be using the always
condition on the Header
directive to set the header on responses other than 200 OK. For example:
# Use "always" condition to set on "redirects" as well.
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
(I've also removed the preload
parameter for now.)
You do not need the <IfModule>
wrapper and should be removed. mod_headers should be enabled by default. This is your server, you know whether mod_headers is enabled or not. mod_headers must be enabled for this to work. You don't want this to silently fail should mod_headers not be available - you need notification as soon as this fails with an error in your logs.
Many articles state that you should only set the Strict-Transport-Security
header on secure (HTTPS) responses. And the "preload list submission" does indeed issue a "warning" (not strictly an "error" I believe) if you send the header over HTTP as well. However, whilst it only needs to be set on the HTTPS response, compliant browsers ignore this header when sent over an unencrypted HTTP connection (to prevent MITM attacks) so it should not matter whether the header is "unnecessarily" sent over HTTP as well. This would be easier to manage in the appropriate <VirtualHost>
containers in the main server config. Sending this header only on HTTPS responses in .htaccess
is more complex (and hence more prone to error). You would need to employ an additional environment variable that you can use to set the HSTS response header conditionally.
For example:
# Set environment var "HSTS" if accessed over HTTPS connection
RewriteCond %{HTTPS} on
RewriteRule ^ - [E=HSTS:1]
# Conditionally set only on HTTPS connections (ie. when "HSTS" env var is set)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains" env=HSTS
Otherwise, I believe your remaining directives are OK with regards to HSTS. But as always, test test test.
Resolve multiple (unnecessary) redirects
#FORCE WWW TO NON-WWW
RewriteCond %{HTTP_HOST} ^www.example.com [NC]
RewriteRule ^(.*)$ https://example.com/$1 [L,R=301]
#URL EXTENSION REMOVAL
RewriteCond %{THE_REQUEST} /([^.]+)\.html [NC]
RewriteRule ^ /%1 [NC,L,R]
A potential issue here (with the 2nd and 3rd canonical redirects above, after the HTTP to HTTPS redirect) is that this potentially results in two additional redirects if www
+ .html
is requested. This could be resolved by simply reversing the two redirects and including the canonical hostname in the "URL EXTENSION REMOVAL" redirect (as mentioned in my answer to your earlier question).
For example:
#URL EXTENSION REMOVAL
RewriteCond %{THE_REQUEST} /([^.]+)\.html [NC]
RewriteRule ^ https://example.com/%1 [R=301,L]
#FORCE WWW TO NON-WWW
RewriteCond %{HTTP_HOST} ^www\.example\.com [NC]
RewriteRule (.*) https://example.com/$1 [R=301,L]
See my previous answer for an alternative approach to the "URL EXTENSION REMOVAL" redirect that addresses some potential issues.
If you are not using the www
subdomain on other hostnames (eg. www.subdomain.example.com
) then you could simplify the CondPattern on the www to non-www redirect to simply ^www\.
, ie. any requested hostname that simply starts www.
, rather than checking the entire hostname.
Additional Fixes
URL Extension Removal / Rewrite Loop (500 Internal Server Error)
There are a couple of other issues that I did not get round to addressing in your earlier question, not relating to HSTS, which I'll cover below...
#URL EXTENSION REMOVAL
:
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^ %{REQUEST_URI}.html [NC,L]
This (and similar) is one of those code snippets that is "blindly" copy/pasted everywhere (and I mean everywhere) as the "standard" way to append (URL-rewrite) the file-extension when using extensionless URLs. However, whilst it probably "works" for your valid-URLs, it has a serious flaw when requesting invalid-URLs...
If /about.html
is a valid file that you wish to serve when requesting the extensionless URL /about
then it works OK. However, if I (maliciously) requested /about/
or /about/<anything>
it will send your server into a spiralling rewrite-loop, resulting in 500 Internal Server Error response. The end-user should not be able to invoke such a response (potentially more vulnerable to DDOS attacks and other hostile behaviour).
This is because REQUEST_FILENAME
(the mapped filesystem path) does not necessarily refer to the same public URL-path as the REQUEST_URI
(requested URL-path) variable.
To resolve this, use REQUEST_URI
throughout. For example:
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.html -f
RewriteRule ^ %{REQUEST_URI}.html [L]
(The NC
flag is superfluous on the RewriteRule
directive here.)
See my answer to the following ServerFault question for more detail on this.
Hotlink Protection - Simplify Condition
#HOTLINKING PROTECTION
RewriteCond %{HTTP_REFERER} !^https://(www\.)?example\.com(/.*)*$ [NC]
This is relatively minor. The regex in the above condition can be simplified. At this point in the .htaccess
file, the hostname has already been canonicalised to remove the www the subdomain, so the (www\.)?
subpattern in the above is superfluous. As is the trailing (/.*)*$
subpattern, which simply matches everything else. You don't need to actually match anything here, you just need to assert that the Referer
header starts with the appropriate (scheme+)hostname.
For example:
RewriteCond %{HTTP_REFERER} !^https://example\.com
The NC
flag is also superfluous here. Forcing a case-insensitive match when it's not required just creates (a tiny bit) more work for your server and in some cases can open you up to vulnerabilities ("duplicate content" being a common one - although that is not an issue here).
Enabling unnecessary Options
#PREVENT DIRECTORY BROWSING
Options All -Indexes
This doesn't only prevent "directory browsing". The All
argument enables a bunch of other stuff that you probably don't need, such as server-side includes (Includes
) and the ability to execute CGI scripts (ExecCGI
). (Incidentally, this is the only time when you can mix arguments with a +
or -
with those without.) To only prevent directory browsing (ie. the auto generating of directory indexes by mod_autoindex) then remove the All
argument.
However, you probably only need FollowSymLinks
(which might already be set in the server config), so you could set the following instead:
Options FollowSymLinks
Note the absence of +
or -
. This only sets FollowSymLinks
, so disabling Indexes
("directory browsing") if it was already set.