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 year
  • includeSubDomains — 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:

  1. Make sure HTTPS is solid
  2. Start with a short max-age
  3. Increase it once you’re confident
  4. Only then consider includeSubDomains
  5. 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 just set
  • 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.

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.com
  • www.example.com
  • api.example.com
  • blog.example.com
  • every other subdomain under example.com

This is where people shoot themselves in the foot.

If you have something like:

  • old.example.com still serving only HTTP
  • mail.example.com with weird legacy TLS
  • dev.example.com using 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.

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:

  1. Enable mod_headers
  2. Add the Strict-Transport-Security header to your HTTPS virtual host
  3. Reload Apache
  4. 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 includeSubDomains only when you’re sure
  • treat preload as a one-way door

That approach avoids most of the horror stories and still gets you the security benefit you want.