HTTP Strict Transport Security (HSTS) in .htaccess
Even when your server correctly redirects all HTTP traffic to HTTPS, there is a brief moment of vulnerability during that very first connection. When a user types example.com into their browser or clicks an HTTP link, the initial request travels over unencrypted HTTP before the server can respond with a redirect. In that window, an attacker on the same network can intercept the request, modify the response, or perform an SSL stripping attack that keeps the user on HTTP indefinitely without them realizing it.
HTTP Strict Transport Security (HSTS) eliminates this vulnerability by telling the browser to never even attempt an HTTP connection to your domain. This guide explains what HSTS is, how to configure it through the Header directive, what each parameter does, how to get on the browser preload list, and the risks you should carefully consider before enabling it.
What Is HSTS
HTTP Strict Transport Security is a security mechanism defined in RFC 6797 that lets a web server declare to browsers: "Only communicate with me over HTTPS. If anyone tries to connect over HTTP, upgrade the connection to HTTPS automatically before sending any data."
Once a browser receives a valid HSTS header over an HTTPS connection, it remembers this policy for the specified duration. During that period, the browser performs the following behavior automatically:
- Any HTTP link to the domain (
http://example.com/page) is internally converted to HTTPS (https://example.com/page) before the request is sent. No HTTP request ever leaves the browser. - If the TLS certificate is invalid, expired, or misconfigured, the browser shows a hard error with no option to bypass it. The user cannot click "proceed anyway."
- The policy persists even after browser restarts, clearing browsing history, or reconnecting from a different network.
HSTS vs HTTPS Redirect
An HTTPS redirect and HSTS solve related but different problems:
| Aspect | HTTPS Redirect (mod_rewrite) | HSTS (Strict-Transport-Security header) |
|---|---|---|
| First visit | HTTP request is sent, then redirected | HTTP request is sent, then redirected (same) |
| Subsequent visits | HTTP request is sent, then redirected again | Browser converts to HTTPS internally, no HTTP |
| Network exposure | Every first request per session is vulnerable | Only the very first visit ever is vulnerable |
| SSL stripping defense | None. Attacker can intercept the redirect. | Strong. Browser refuses to use HTTP. |
| Requires server contact | Yes, every time | No, browser enforces locally after first visit |
HSTS does not replace the HTTPS redirect. You need both:
- The redirect catches the first-ever HTTP request and sends the user to HTTPS.
- The HSTS header (delivered with the HTTPS response) tells the browser to never use HTTP again.
First visit:
Browser ──HTTP───▶ Server ──301 Redirect──▶ HTTPS
Browser ──HTTPS──▶ Server ──Response + HSTS header──▶ Browser remembers
All future visits:
Browser ──HTTPS──▶ Server (HTTP is never attempted)
The Header Directive (mod_headers)
HSTS is configured by sending the Strict-Transport-Security HTTP response header. In .htaccess, this is done using the Header directive from mod_headers.
The basic syntax:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
</IfModule>
Let's examine each part:
Header always set: Thealwayskeyword ensures the header is sent even on error responses (403, 404, 500, etc.). Withoutalways, the header would only be added to successful (2xx) and redirect (3xx) responses. Usingalwaysis important because a browser that receives a 404 error should still learn the HSTS policy.Strict-Transport-Security: The header name, as defined by the HSTS specification."max-age=31536000": The header value, containing the policy directives (covered in detail below).env=HTTPS: A condition that ensures the header is only sent when the response is served over HTTPS.
Why Only Send Over HTTPS
The HSTS specification requires that browsers ignore the Strict-Transport-Security header when received over an insecure HTTP connection. This is a security measure: if an attacker can intercept your HTTP traffic, they could inject a fake HSTS header to cause a denial of service.
The env=HTTPS condition prevents Apache from adding the header to HTTP responses, which would be pointless and wasteful.
An alternative syntax using Apache expressions achieves the same result:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000" "expr=%{HTTPS} == 'on'"
</IfModule>
Both env=HTTPS and the expression expr=%{HTTPS} == 'on' are functionally equivalent. The env=HTTPS syntax is shorter and more commonly seen in .htaccess configurations.
Common Mistake: Missing the HTTPS Condition
Wrong approach:
# Sent on ALL responses, including HTTP
Header always set Strict-Transport-Security "max-age=31536000"
While browsers should ignore HSTS over HTTP, sending unnecessary headers on HTTP responses wastes bandwidth and can cause confusion during debugging. Always include the HTTPS condition.
Correct approach:
Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
max-age
The max-age directive is the only required parameter in the HSTS header. It specifies how long, in seconds, the browser should remember the HTTPS-only policy.
# Browser remembers the policy for 1 year
Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
Once the browser receives this header, it starts a countdown. For the next 31,536,000 seconds (365 days), every connection to this domain is forced to HTTPS. Each time the browser receives the header again (on any subsequent HTTPS visit), the countdown resets to the full duration.
Choosing a max-age Value
| Duration | Seconds | Use Case |
|---|---|---|
| 5 minutes | 300 | Initial testing. If something breaks, wait 5 minutes. |
| 1 hour | 3600 | Short-term testing with slightly more confidence. |
| 1 day | 86400 | Moderate testing before committing. |
| 1 week | 604800 | Extended testing. Comfortable that HTTPS works. |
| 1 month | 2592000 | Pre-production validation. |
| 6 months | 15768000 | Production deployment with room for rollback. |
| 1 year | 31536000 | Recommended production value. Required for preload eligibility. |
| 2 years | 63072000 | Maximum commonly used. Extra insurance. |
Start small, increase gradually:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=300" env=HTTPS
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=604800" env=HTTPS
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=2592000" env=HTTPS
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
</IfModule>
This gradual approach lets you catch problems early. If your SSL certificate expires or you discover a subdomain that does not support HTTPS, the short max-age means users only experience issues briefly before the policy expires and they can access your site over HTTP again.
What Happens When max-age Expires
If the browser has not visited your site within the max-age period, the HSTS policy expires. The next visit behaves as if HSTS was never set: the browser allows HTTP and does not automatically upgrade. This is why a max-age of at least one year is recommended for production, as it ensures that even infrequent visitors remain protected.
includeSubDomains
The includeSubDomains directive extends the HSTS policy to all subdomains of the current domain.
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
</IfModule>
Without includeSubDomains, the policy only applies to the exact domain that sent the header. With it, every subdomain is also forced to HTTPS:
| Subdomain | Without includeSubDomains | With includeSubDomains |
|---|---|---|
example.com | HSTS enforced | HSTS enforced |
www.example.com | Not affected | HSTS enforced |
api.example.com | Not affected | HSTS enforced |
staging.example.com | Not affected | HSTS enforced |
mail.example.com | Not affected | HSTS enforced |
internal.dev.example.com | Not affected | HSTS enforced |
When to Use includeSubDomains
Enable it when:
- All your subdomains support HTTPS with valid certificates.
- You have audited every subdomain to confirm HTTPS works correctly.
- You want comprehensive protection against cookie-based attacks on subdomains.
Do not enable it when:
- Any subdomain does not support HTTPS (it will become completely inaccessible).
- You use subdomains for internal tools that only work over HTTP.
- Third-party services run on your subdomains and you do not control their SSL configuration.
Enabling includeSubDomains when any subdomain lacks HTTPS support will make that subdomain completely unreachable for every user whose browser has received the HSTS header. There is no workaround on the server side. You must wait for the max-age to expire or have each affected user manually clear their browser's HSTS cache.
Checking Your Subdomains
Before enabling includeSubDomains, verify that all your subdomains have valid HTTPS:
# Check each subdomain
curl -I https://www.example.com
curl -I https://api.example.com
curl -I https://staging.example.com
curl -I https://mail.example.com
Each response should return a valid HTTPS response without certificate errors. You can also use online tools like SSL Labs to perform a thorough certificate check.
Preload
HSTS has one remaining gap: the very first visit to your site ever. Before the browser has received the HSTS header for the first time, it does not know about your policy. That initial request goes over HTTP (if the user typed example.com without https://), and for that single request, the SSL stripping vulnerability exists.
HSTS preloading closes this gap by hardcoding your domain's HSTS policy directly into the browser's source code. Browsers ship with a built-in list of domains that should always use HTTPS, even on the very first visit, even before any connection to the server has been made.
To signal that your domain is eligible for preloading, add the preload directive:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=HTTPS
</IfModule>
The preload directive alone does not add your domain to the list. It signals your intent. You must also submit your domain through the official process.
Submitting to the Preload List
The preload list is maintained by the Chromium project and used by all major browsers (Chrome, Firefox, Safari, Edge, Opera). To submit your domain:
Step 1: Meet the requirements.
Your domain must satisfy all of the following:
- Serve a valid SSL/TLS certificate.
- Redirect all HTTP traffic to HTTPS on the same host (no intermediate hops to a different domain).
- Serve the HSTS header on the HTTPS response from the base domain with:
max-ageof at least 31536000 (one year).includeSubDomainsdirective present.preloaddirective present.
- All subdomains must support HTTPS.
Step 2: Verify your configuration.
Ensure your complete .htaccess setup looks like this:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=HTTPS
</IfModule>
Step 3: Test with the preload checker.
Visit hstspreload.org and enter your domain. The tool checks all requirements and reports any issues that need to be fixed before submission.
Step 4: Submit your domain.
Once all checks pass, submit your domain through the same site. The domain will be reviewed and added to the Chromium preload list.
Step 5: Wait for browser releases.
After acceptance, your domain is added to the Chromium source code. It then propagates to Chrome, Edge, and other Chromium-based browsers in their next release cycle. Firefox and Safari maintain their own lists that are generally synchronized. The entire process from submission to appearing in stable browser releases can take several months.
Preload Status Verification
After submission, you can check the status of your domain:
# Check if the header is correctly configured
curl -I https://example.com | grep -i strict-transport-security
Expected output:
strict-transport-security: max-age=31536000; includeSubDomains; preload
You can also check the Chromium source directly to see if your domain has been added: Chromium HSTS preload list source.
Risks and Rollback
HSTS is a powerful security mechanism, but its strength is also its greatest risk. Once enabled, reversing it is difficult and time-consuming. Understanding the risks before enabling HSTS is essential.
Risk: Expired or Misconfigured Certificate
If your SSL/TLS certificate expires and HSTS is active, your site becomes completely inaccessible. The browser refuses to connect over HTTP (because of HSTS) and refuses to connect over HTTPS (because the certificate is invalid). There is no "proceed anyway" button. The user sees a hard error with no workaround.
NET::ERR_CERT_AUTHORITY_INVALID
You cannot visit example.com right now because the website
sent scrambled credentials that cannot be verified.
This is usually caused by an error on the server's
certificate, which cannot be overridden.
Mitigation:
- Set up automatic certificate renewal (Let's Encrypt certificates auto-renew every 60-90 days).
- Monitor certificate expiration dates with alerting tools.
- Test certificate renewal in a staging environment before production.
Risk: Subdomain Without HTTPS
If you enable includeSubDomains and a subdomain does not have HTTPS configured, that subdomain becomes unreachable. This is particularly dangerous for:
- Legacy internal tools running on subdomains.
- Third-party services using your subdomains (e.g.,
helpdesk.example.comhosted by a vendor). - Development or staging subdomains (
dev.example.com).
Mitigation:
- Audit all subdomains before enabling
includeSubDomains. - Use wildcard certificates to cover all subdomains.
- Avoid
includeSubDomainsif you are not certain about every subdomain's HTTPS support.
Risk: Preload Removal Delay
If you submit your domain to the preload list and later need to remove it (because you can no longer support HTTPS for all subdomains, for example), the removal process is slow:
- Submit a removal request at hstspreload.org.
- Wait for the Chromium team to process the removal.
- Wait for the change to be included in a Chromium release.
- Wait for that release to reach stable channel across all browsers.
This process can take 3 to 6 months or longer. During that time, your domain remains in the preload list, and any subdomain without HTTPS remains inaccessible to all users with an updated browser.
Rolling Back HSTS
If you need to disable HSTS (without preload), set max-age to zero:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=0" env=HTTPS
</IfModule>
A max-age=0 tells browsers to immediately forget the HSTS policy. However, this only works for users who visit your site again and receive the updated header. Users who do not return before the original max-age expires will continue to have the old policy enforced.
To also remove the subdomain policy:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=0; includeSubDomains" env=HTTPS
</IfModule>
Manually Clearing HSTS in Browsers
For testing or emergency situations, users can manually clear the HSTS policy for a domain:
Chrome:
Navigate to chrome://net-internals/#hsts, enter the domain in the "Delete domain security policies" section, and click Delete.
Firefox:
Close the browser, find the SiteSecurityServiceState.txt file in the Firefox profile directory, and remove the line containing your domain. Alternatively, clear the browsing history for the specific domain.
Safari:
Clear the entire browser history (there is no domain-specific HSTS clearing).
The gradual rollout approach described in the max-age section is the best protection against these risks. Start with a 5-minute max-age, verify everything works, increase to a week, verify again, increase to a month, and finally set it to one year. Only consider preloading after the one-year policy has been running without issues for several months.
Decision Checklist
Before enabling HSTS, confirm each item:
- SSL/TLS certificate is valid and set to auto-renew.
- All HTTP traffic is redirected to HTTPS with a 301 redirect.
- The site works correctly over HTTPS (no mixed content warnings).
- (For
includeSubDomains) Every subdomain has a valid HTTPS configuration. - (For
preload) You understand the removal process takes months. - (For
preload) You are committed to HTTPS permanently for this domain and all subdomains. - You have started with a short
max-ageand tested before increasing.
HSTS is one of the most effective security headers available, but it demands a commitment to maintaining valid HTTPS across your entire domain infrastructure. When properly configured with a gradual rollout, it provides strong protection against protocol downgrade attacks with minimal risk. Treat it with the respect it deserves, test thoroughly, and increase your commitment incrementally.