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 shortmax-age(300 seconds) - Setting
includeSubDomainsmakes 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)