2026年11月12日

2026年11月12日

WordPress HSTS: How to Set the Strict-Transport-Security Header

This article is a localized edition of the Japanese original. Read the Japanese original

Introduction

HSTS (HTTP Strict Transport Security) is a security feature that tells browsers "this site always communicates over HTTPS." Once a browser has received the HSTS header, it automatically switches to HTTPS even when you try to access the site over HTTP, which prevents man-in-the-middle attacks.

Symptoms and Causes

  • HTTPS is configured, but HSTS is not
  • A security scan flagged "HSTS not configured"
  • Browsers connect over HTTP every time and then get a 301 redirect
  • You want an "A+" score on SSL Labs

Solution Steps

Step 1: What to check before setting up HSTS

# (1) Confirm that HTTPS works fully (set up HSTS afterwards)
curl -I https://example.com
# Confirm that HTTP/2 200 is returned

# (2) Check for mixed content
curl -s https://example.com/ | grep -oP 'src="http://[^"]+"|href="http://[^"]+"' | head -10
# Confirm that no HTTP resources remain

# (3) Confirm that www and all subdomains also use HTTPS
curl -I https://www.example.com
curl -I https://subdomain.example.com

# (4) Check the SSL certificate's expiry date
certbot certificates | grep "Expiry Date"

Step 2: Set the HSTS header in Apache

# /etc/apache2/sites-available/example.com-ssl.conf
<VirtualHost *:443>
    # Test first with a short max-age (5 minutes = 300 seconds)
    Header always set Strict-Transport-Security "max-age=300"

    # After testing, if there are no problems, use a long setting (1 year = 31536000 seconds)
    # Header always set Strict-Transport-Security "max-age=31536000"

    # To include subdomains (all subdomains must then use HTTPS)
    # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

    # To apply for the preload list (hard to undo once you apply)
    # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>
# Enable mod_headers
sudo a2enmod headers
sudo systemctl reload apache2

# Check that the header is set
curl -I https://example.com | grep -i strict
# Strict-Transport-Security: max-age=300

Step 3: Set the HSTS header in Nginx

# /etc/nginx/sites-available/example.com
server {
    listen 443 ssl http2;
    server_name example.com;

    # Test first with a short max-age
    add_header Strict-Transport-Security "max-age=300" always;

    # Production setting (1 year)
    # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com | grep -i strict

Step 4: Set HSTS in WordPress functions.php

// functions.php: add the HSTS header at the WordPress level
// Note: if you can set it at the web server level, that is recommended
add_action('send_headers', function(): void {
    if (!is_ssl()) return;

    // Stage 1: test (max-age=300, 5 minutes)
    header('Strict-Transport-Security: max-age=300');

    // Stage 2: short term (max-age=86400, 1 day)
    // header('Strict-Transport-Security: max-age=86400');

    // Stage 3: production (max-age=31536000, 1 year)
    // header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
});
// functions.php: set the security headers together
add_action('send_headers', function(): void {
    if (!is_ssl()) return;

    // HSTS
    header('Strict-Transport-Security: max-age=31536000; includeSubDomains');

    // Prevent clickjacking
    header('X-Frame-Options: SAMEORIGIN');

    // Prevent MIME type sniffing
    header('X-Content-Type-Options: nosniff');

    // XSS protection (for older browsers)
    header('X-XSS-Protection: 1; mode=block');

    // Referrer policy
    header('Referrer-Policy: strict-origin-when-cross-origin');

    // Permissions Policy (restricts camera, microphone, etc.)
    header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
});

Step 5: Submit to the HSTS preload list

# HSTS preload list requirements:
# 1. A valid SSL certificate
# 2. A 301 redirect from HTTP → HTTPS
# 3. All subdomains on HTTPS
# 4. max-age of 31536000 or more
# 5. includeSubDomains is included
# 6. The preload directive is included

# Set a preload-ready header
Header always set Strict-Transport-Security \
    "max-age=31536000; includeSubDomains; preload"

# Check the requirements
# Enter example.com at https://hstspreload.org/ and check it

# After applying, keep in mind:
# - Removal takes several months
# - All subdomains permanently require HTTPS
# - Think it through carefully before applying

Caveats

  • Once HSTS is set, browsers refuse HTTP connections for the period specified in max-age. If a problem occurs with your certificate, visitors will be unable to reach the site. Test first with a short max-age (300 seconds)
  • Setting includeSubDomains makes HTTPS mandatory for all subdomains too. Any HTTP-only subdomain will become unreachable
  • Getting on the HSTS preload list is like having your site "baked into the browser's source code." Once listed, removal takes anywhere from several months to a year

Summary

To set up HSTS: (1) do it only after you have fixed mixed content and confirmed HTTPS works fully, (2) add Strict-Transport-Security: max-age=300 to your Apache/Nginx configuration (short-term test), (3) if there are no problems, extend it to max-age=31536000, (4) add includeSubDomains once all subdomains are on HTTPS, and (5) on the WordPress side, it is efficient to set it together with other security headers using the send_headers hook.

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲