Docs

Turn on HSTS

Turn on HSTS

A redirect from http:// to https:// protects visitors only after the first request, and that first request travels unencrypted. On a public Wi-Fi network someone can intercept it and keep the visitor on a fake http:// version of your site. HSTS (HTTP Strict Transport Security) closes that gap: one response header tells the browser to use HTTPS for your domain for a set time, without ever trying http:// again.

The header costs nothing and every modern browser honours it. The site checklist expects it with a max-age of at least 180 days.

What the header looks like

Strict-Transport-Security: max-age=31536000; includeSubDomains
Part Meaning
max-age how long, in seconds, the browser remembers to use HTTPS. The checklist wants at least 15552000 (180 days); one year is 31536000.
includeSubDomains the rule also applies to every subdomain: shop.example.com, mail.example.com, intranet.example.com …
preload asks to be built into browsers (see the warning below). Optional.

Browsers only accept the header over HTTPS; on http:// responses it is ignored. Make sure the HTTP to HTTPS redirect works first.

Warning: HSTS is a promise the browser holds you to. For as long as max-age lasts, a visitor's browser refuses to open the site over plain HTTP or past a certificate error, and there is no "continue anyway" button. Before adding includeSubDomains, make sure every subdomain has a valid certificate, including old ones that only work on http://.

Start low, then raise it

  1. Start with max-age=300 (five minutes) and check the site and its subdomains.
  2. Raise it to 86400 (one day) for a few days.
  3. Then set 31536000 (one year).

If something breaks with a short value, you can remove the header and browsers forget it within minutes. With a year, they remember for a year.

About preload

The preload keyword and the list at hstspreload.org build your domain into Chrome, Firefox, Safari and Edge, so even the very first visit uses HTTPS. The list requires max-age of at least 31536000, includeSubDomains and preload. Getting off the list again takes months and only reaches visitors with updated browsers. Add preload only when you are sure the whole domain, with every subdomain, will stay on HTTPS for good.

Check it yourself

Replace example.com with your site:

curl -sI https://example.com | grep -i '^strict-transport-security'
curl -sI https://example.com/no-such-page | grep -i '^strict-transport-security'

Fixed: both lines print the header, with max-age=15552000 or more. The second command checks that error pages carry it too; that is what always does in the nginx and Apache examples below.

How to fix

Set the header in one place, as close to the visitor as possible: the CDN or web server covers every response, including images and cached pages. Pick what you have access to.

WordPress

If you can, set the header in the hosting panel or .htaccess (see those tabs): it then also applies to cached pages and files, which never reach PHP.

If you only have WordPress admin access, add this with the free Code Snippets plugin (Snippets → Add New, run everywhere) or to your child theme's functions.php:

// HSTS on every page WordPress generates over HTTPS.
add_action('send_headers', function () {
    if (is_ssl()) {
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
    }
});

Then clear the page cache. Pages served straight from a cache (WP Rocket, LiteSpeed Cache) may still come without the header; if the curl check shows it missing, use the server-level tab.

cPanel

Shared cPanel hosting has no HSTS switch for most accounts, but .htaccess works. Add this to the .htaccess in the site's root (File Manager, with Show Hidden Files on):

<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>

Browsers ignore the header on http:// responses, so it does no harm there; what counts is that it is on every https:// response.

Plesk

  1. Open Websites & Domains → example.com → SSL/TLS Certificates.
  2. Turn on HSTS and set its max-age to at least 6 months (one year is better). Tick Include subdomains only after checking them, and leave Preload off unless you mean it.

If your Plesk version has no HSTS switch there, add the header under Websites & Domains → example.com → Apache & nginx Settings, in Additional nginx directives:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

nginx

In the HTTPS server block of your site (/etc/nginx/sites-available/example.com on Debian and Ubuntu, /etc/nginx/conf.d/example.com.conf on Rocky, Alma and CentOS):

server {
    listen 443 ssl;
    server_name example.com;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

Without always, nginx leaves the header off error responses. One trap: a location block that has its own add_header does not inherit any from the server block, so repeat the line there.

sudo nginx -t && sudo systemctl reload nginx

Apache and LiteSpeed

With server access (Apache). In the <VirtualHost *:443> of your site (/etc/apache2/sites-available/example.com-le-ssl.conf or similar on Debian and Ubuntu, /etc/httpd/conf.d/ on Rocky, Alma and CentOS):

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

It needs mod_headers (on Debian and Ubuntu: sudo a2enmod headers).

sudo apachectl configtest && sudo systemctl reload apache2

(httpd instead of apache2 on Rocky, Alma and CentOS.)

Without server access (shared hosting, LiteSpeed). Use the .htaccess lines from the cPanel tab; LiteSpeed reads them the same way.

Cloudflare

  1. Open the domain and go to SSL/TLS → Edge Certificates.
  2. Under HTTP Strict Transport Security (HSTS), click Enable HSTS and confirm the warning.
  3. Set Max Age Header to 6 months or 12 months. Turn on Apply HSTS policy to subdomains only after checking them; leave Preload off unless you mean it.
  4. Save.

Cloudflare adds the header to every response it serves. If your server also sends one, check with curl that the result is a single header with the value you want.

Node.js and Next.js

Next.js (self-hosted), in next.config.js:

module.exports = {
    async headers() {
        return [{
            source: '/:path*',
            headers: [
                { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
            ],
        }];
    },
};

On Vercel, custom domains already get an HSTS header from the platform; check with curl before adding your own.

Express: the helmet package sends Strict-Transport-Security with a one-year max-age by default (app.use(helmet())). If nginx sits in front of the app, setting it there (nginx tab) is enough.

Laravel

Create a middleware, for example app/Http/Middleware/StrictTransportSecurity.php:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class StrictTransportSecurity
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        if ($request->isSecure()) {
            $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        }

        return $response;
    }
}

Register it in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\App\Http\Middleware\StrictTransportSecurity::class);
})

Behind a load balancer or proxy, isSecure() is only true when Laravel trusts the proxy ($middleware->trustProxies(at: '*') or your proxy's IP). Otherwise the header never appears.

IIS and ASP.NET

In the site's web.config:

<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

In ASP.NET Core, app.UseHsts() sends only 30 days by default, which is below the checklist's 180. Set the age explicitly:

builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
});

Hosted platforms

On Shopify, Wix, Squarespace and Webflow the platform sets the response headers. If HSTS is missing or shorter than 180 days, there is no setting on your side; you can ask the platform's support, or put the site behind Cloudflare and set it there.

Confirm the fix

Run the curl commands from Check it yourself again, then open the target in PulseKeeper and click Check now on the Checklist tab. The item shows the max-age it found, in days.