HSTS in Apache is one of those things that’s surprisingly easy to turn on, but also easy to get wrong if you rush it.
If you’re serving a site over HTTPS, you should almost certainly be sending the Strict-Transport-Security header. It tells browsers: “from now on, only ever talk to me over HTTPS.” That closes off a whole class of downgrade and SSL-stripping attacks, and it helps make your HTTPS setup actually stick.
The Apache part is simple. The hard part is knowing when to start small, when to go all-in, and when not to enable it yet.
In this tutorial, I’ll show you exactly how to enable HSTS in Apache, how to verify it, and how to avoid the common mistakes that can lock users into a broken HTTPS setup.
What HSTS does
HSTS stands for HTTP Strict Transport Security. It’s an HTTP response header that tells browsers to automatically rewrite future requests to HTTPS for a period of time.
A typical header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
That means:
max-age=31536000— remember the rule for 1 yearincludeSubDomains— apply it to all subdomains too
You can also add preload, which is used for browser preload lists:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Important detail: browsers only honor HSTS headers sent over a valid HTTPS connection. If you send it over plain HTTP, it gets ignored.
Before you enable HSTS
Do this checklist first. Seriously.
You should enable HSTS only if all of these are true:
- Your site works correctly over HTTPS
- Your certificate is valid and trusted
- HTTP requests are redirected to HTTPS
- All important subdomains support HTTPS if you plan to use
includeSubDomains - You’re not relying on any HTTP-only subdomain that users still need
This matters because once a browser caches HSTS, it will refuse to connect over HTTP until the max-age expires. If your HTTPS setup breaks afterward, users can get stuck.
So the safest rollout is:
- Make sure HTTPS is solid
- Start with a short
max-age - Increase it once you’re confident
- Only then consider
includeSubDomains - Only much later consider
preload
That’s the sane path.
Make sure Apache can send headers
In Apache, HSTS is usually set with mod_headers. On many systems it’s already enabled, but not always.
Check whether it’s loaded:
apachectl -M | grep headers
Or on some systems:
httpd -M | grep headers
If you see headers_module, you’re good.
If not, enable it.
On Debian/Ubuntu:
sudo a2enmod headers
sudo systemctl restart apache2
On RHEL/CentOS/AlmaLinux/Rocky, mod_headers is often already available with Apache, and you may just need to ensure it’s loaded in config.
Basic HSTS configuration in Apache
The most common place to add HSTS is inside your HTTPS virtual host on port 443.
Example:
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
Header always set Strict-Transport-Security "max-age=31536000"
DocumentRoot /var/www/html
</VirtualHost>
That one line does the work:
Header always set Strict-Transport-Security "max-age=31536000"
A couple of notes:
- Use
always set, not justset - Put it in the HTTPS vhost, not the HTTP one
- Don’t send HSTS on non-TLS responses
always helps ensure the header is added even on error responses, redirects, and other non-200 cases where you still want the browser to learn the policy.
Recommended starting configuration
If this is your first rollout, don’t begin with a full year unless you’re very confident.
A safer starting value is something short, like 5 minutes:
Header always set Strict-Transport-Security "max-age=300"
Then, if nothing breaks, move to something like 1 week:
Header always set Strict-Transport-Security "max-age=604800"
And eventually to 1 year:
Header always set Strict-Transport-Security "max-age=31536000"
This gradual rollout is boring, but boring is good when you’re changing browser security behavior.
Enabling HSTS with includeSubDomains
Once every subdomain is HTTPS-only and working properly, you can expand the policy:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
This tells browsers to enforce HTTPS for:
example.comwww.example.comapi.example.comblog.example.com- every other subdomain under
example.com
This is where people shoot themselves in the foot.
If you have something like:
old.example.comstill serving only HTTPmail.example.comwith weird legacy TLSdev.example.comusing a broken cert
then includeSubDomains is not ready yet.
Be especially careful in larger organizations where random subdomains exist that your team doesn’t control.
Enabling HSTS preload
Preload is the most aggressive option.
Example:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
This signals that you want your domain included in browser preload lists. Once preloaded, browsers hardcode HTTPS-only behavior for your domain even before the first visit.
That’s powerful, but it comes with real commitment:
- You must serve HTTPS everywhere
- You must support all subdomains over HTTPS
- You need
includeSubDomains - You need a long enough
max-age - Rolling back is slow and annoying
My opinion: don’t rush into preload just because it sounds “more secure.” Regular HSTS already gives you a lot. Preload is great when you fully understand the operational commitment.
Apache config examples
Example 1: simple HTTPS virtual host
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.crt
SSLCertificateKeyFile /etc/ssl/private/example.key
Header always set Strict-Transport-Security "max-age=31536000"
</VirtualHost>
Example 2: with includeSubDomains
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
Example 3: full HTTP to HTTPS redirect plus HSTS on HTTPS
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
<Directory /var/www/example>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
That’s a pretty standard production setup.
Using .htaccess instead
You can sometimes set HSTS in .htaccess if AllowOverride permits it and mod_headers is enabled.
Example:
Header always set Strict-Transport-Security "max-age=31536000"
But I’ll be blunt: if you control the server config, put it in the virtual host instead. It’s cleaner, faster, and easier to audit. .htaccess is fine when shared hosting forces your hand, but it wouldn’t be my first choice.
Test your Apache config
Before reloading Apache, validate the config:
sudo apachectl configtest
If it looks good, reload:
sudo systemctl reload apache2
Or on RHEL-style systems:
sudo systemctl reload httpd
Now check the response headers with curl:
curl -I https://example.com/
You should see something like:
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains
You can also test redirects:
curl -I http://example.com/
That should return a redirect to HTTPS.
Test your HSTS configuration and other security headers at headertest.com - free, instant, no signup required.
Common mistakes
Sending HSTS over HTTP
Don’t do this. Browsers ignore it anyway, and it signals confusion in your config.
Set it only on HTTPS responses.
Using includeSubDomains too early
This is probably the biggest mistake. If even one important subdomain is not ready for HTTPS-only access, wait.
Starting with a huge max-age on day one
If you accidentally break cert renewal, misconfigure TLS, or discover a forgotten subdomain, a 1-year policy becomes painful fast. Start short and ramp up.
Enabling preload casually
Preload is not a checkbox feature. It’s a long-term commitment. Treat it that way.
Forgetting error responses and redirects
If you use Header set instead of Header always set, you may miss some responses. always set is the better option for HSTS.
How to disable or roll back HSTS
If you need to remove HSTS, deleting the header from Apache is only part of the story. Browsers that already cached the policy will keep enforcing it until max-age expires.
To tell browsers to clear the policy, serve this header over HTTPS:
Header always set Strict-Transport-Security "max-age=0"
That instructs browsers to forget the HSTS rule.
After that, reload Apache and verify:
curl -I https://example.com/
You should see:
Strict-Transport-Security: max-age=0
A few caveats:
- This only helps for users who can still reach your site over HTTPS
- It doesn’t instantly fix preload status
- If the domain is preloaded, removal is a separate process and takes time
So again: be careful before going all the way to preload.
Recommended production setup
If your whole site and all subdomains are cleanly on HTTPS, this is a good long-term Apache setting:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
If you are also intentionally pursuing preload:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
If you’re just getting started, use this first:
Header always set Strict-Transport-Security "max-age=300"
That gives you a safe testing window.
Final thoughts
Enabling HSTS in Apache is technically easy:
- Enable
mod_headers - Add the
Strict-Transport-Securityheader to your HTTPS virtual host - Reload Apache
- Verify with
curl
The real work is operational discipline. HSTS is one of those headers that actually changes browser behavior in a meaningful way, so it deserves a bit of respect.
My practical advice is simple:
- start with a short
max-age - verify HTTPS everywhere
- increase gradually
- add
includeSubDomainsonly when you’re sure - treat
preloadas a one-way door
That approach avoids most of the horror stories and still gets you the security benefit you want.