HSTS Missing From HTTPS Server: How to Fix It
Security scanners report HSTS missing from HTTPS server when your site works over https but its responses do not include a Strict-Transport-Security header. Without that header, a browser that is sent to the http address of your site, by a typed address, an old link or a bookmark, makes that first request unencrypted before your redirect moves it to https. That first hop can be intercepted on a hostile network. The fix is a single header line, but it changes how browsers treat your domain for months, so it needs to be added carefully. This guide explains what the header does, how to add it on nginx, Apache, Plesk, Cloudflare and IIS, how to test it, and how to avoid locking visitors out of a subdomain.
What the finding means
HTTP Strict Transport Security, defined in RFC 6797, is a response header that tells a browser to use only https for your host for a set number of seconds. Once a browser has seen it, it rewrites any http request for your site to https internally, before anything leaves the device, and it refuses to let the visitor click through a certificate warning.
Scanners name the finding in slightly different ways. Nessus reports HSTS Missing From HTTPS Server, OWASP ZAP reports Strict-Transport-Security Header Not Set, and other audits simply list the header as missing. They all mean the same thing: an https response from your server had no Strict-Transport-Security header.
A typical header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
| Directive | What it does | When to use it |
|---|---|---|
max-age | How long, in seconds, the browser remembers the rule. 31536000 is one year | Always; start low, then raise it |
includeSubDomains | Applies the rule to every subdomain of the host that sent it | Only once every subdomain works over https |
preload | Signals consent to be added to the browser preload list | Only if you intend to submit the domain and can commit to https permanently |
Browsers ignore the header when it arrives over plain http, because an attacker could inject or strip it there. It only takes effect when received over a valid https connection. That is why the finding is about your https server, and why your http to https redirect is still needed.
Why a redirect alone is not enough
Most sites already redirect http:// to https://. The weakness is the request that comes before the redirect. When someone types your domain without a scheme, or follows an old http link, the browser sends that first request in clear text. On public Wi-Fi or any network someone else controls, an attacker can answer it instead of your server, keep the visitor on an http copy of the site and read or change what they send. This is known as SSL stripping.
With HSTS in place, a returning visitor's browser never makes that clear-text request. It upgrades the address itself, which also saves a redirect round trip on every visit. The remaining gap is the very first visit, before the browser has seen the header, which is the gap the preload list closes.
| Situation | Without HSTS | With HSTS |
|---|---|---|
| Visitor types example.com | http request sent, then redirected | Browser goes straight to https |
| Old http link clicked | Clear-text request can be intercepted | Upgraded before leaving the device |
| Certificate error | Visitor can click through the warning | Warning cannot be bypassed |
| First visit ever | Exposed | Still exposed unless the domain is preloaded |
If your redirects themselves are messy, with several hops or a hop back to http, fix those first. The https redirects guide and the guide to a redirect chain that contains http cover them.
How to check your current headers
Ask the server directly with curl, so you see exactly what it sends rather than what a plugin claims to send:
curl -sI https://example.com/ | grep -i strict-transport
curl -sI https://www.example.com/ | grep -i strict-transport
curl -sI https://example.com/no-such-page-404 | grep -i strict-transport
curl -sI http://example.com/
The first three commands should each print one strict-transport-security line once the fix is in. Testing a missing page matters, because many configurations only add headers to successful responses. The last command checks the plain http response: you want a 301 or 308 redirect to the https address of the same host, and it does not matter whether the header appears there.
Before adding includeSubDomains, list every subdomain in your DNS zone, including old ones such as mail, webmail, staging, shop or intranet, and test each over https:
for h in www mail webmail staging shop; do
printf '%s: ' "$h"
curl -s -o /dev/null -w '%{http_code}\n' "https://$h.example.com/" || echo failed
done
Any subdomain that fails, shows a certificate error or only serves http will become unreachable for visitors who have seen an includeSubDomains header from the main domain. Fix its certificate first, using the SSL errors guide if needed, or leave includeSubDomains out for now.
chrome://net-internals/#hsts. The same page lets you delete the rule from your own browser while testing.Adding the header on each server
Add the header in one place only, on the https side. The examples start with a five-minute max-age so a mistake costs little; you raise it later.
nginx
server {
listen 443 ssl;
server_name example.com www.example.com;
add_header Strict-Transport-Security "max-age=300" always;
# ...
}
The always parameter sends the header on error responses as well as successful ones. Test and reload with sudo nginx -t && sudo systemctl reload nginx. More examples are in the nginx security headers guide.
Apache
<VirtualHost *:443>
ServerName example.com
Header always set Strict-Transport-Security "max-age=300"
</VirtualHost>
This needs mod_headers. On Debian and Ubuntu enable it with sudo a2enmod headers, then run sudo apachectl configtest and reload Apache. On shared hosting you can put the same Header always set line in .htaccess. See the Apache security headers guide.
Plesk
Either turn on HSTS in the SSL It! extension for the domain, or go to Websites & Domains, then Hosting & DNS, then Apache & nginx Settings, and add the nginx line to the Additional nginx directives box. Use one method, not both. The Plesk security headers guide walks through the screens.
Cloudflare
In the dashboard, open SSL/TLS, then Edge Certificates, and enable HTTP Strict Transport Security. Choose the max-age and whether to apply it to subdomains. Cloudflare then adds the header at the edge for proxied hostnames. See the Cloudflare security headers guide.
IIS
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=300" />
</customHeaders>
</httpProtocol>
</system.webServer>
Newer IIS versions also have a built-in HSTS setting on the site, which is the cleaner choice where it is available.
Rolling out max-age in stages
A browser that receives a one-year max-age keeps the rule for a year from the last time it saw the header, even if you remove it from the server. If something then breaks on https, those visitors cannot fall back to http. Raising the value in stages limits the damage of a mistake.
| Stage | Header value | How long to watch |
|---|---|---|
| 1 | max-age=300 | A day: check pages, forms, logins and payment pages |
| 2 | max-age=86400 | About a week |
| 3 | max-age=2592000 (30 days) | A few weeks, then add includeSubDomains if every subdomain passed |
| 4 | max-age=31536000; includeSubDomains | Leave in place |
Many scanners treat a max-age below about six months as weak, so the finding may change from missing to a warning about a short value until you reach stage four. That is expected.
max-age=0 over https. Each browser only forgets the rule when it next visits and sees the new value, so visitors who do not return stay on the old rule until it expires.A worked example
A small agency's site on a Plesk server running nginx is flagged with HSTS missing from HTTPS server. curl shows the header absent on both the home page and a 404 page. The DNS zone has www, mail and a forgotten dev subdomain.
- They add
add_header Strict-Transport-Security "max-age=300" always;to the Additional nginx directives for the main domain and apply the change. - curl now shows the header on the home page and the 404 page. A day of normal use shows no problems.
- Testing subdomains shows
devhas an expired certificate. They issue a new certificate for it in Plesk and confirm it loads over https. - They raise the value to
max-age=86400, then to 30 days, then tomax-age=31536000; includeSubDomains. - A rescan clears the finding. They leave
preloadout, because they are not yet sure every future subdomain will run on https.
The fix, step by step
Step 1
Confirm the header is missing
Run curl -sI https://example.com/ | grep -i strict-transport against your bare domain, your www address and a page that returns 404. Note which responses lack the header. Also run curl -sI http://example.com/ and confirm it returns a 301 or 308 redirect to https on the same host. If the redirect is missing or goes through several hops, fix that first. You have finished this step when you know exactly which hosts and response types need the header.
Step 2
Check every subdomain works over https
List every hostname in your DNS zone, including old mail, staging and development subdomains, and load each one over https with curl or a browser. Each must return a page with a valid certificate and no warning. Fix or remove any that fail before you plan to use includeSubDomains. If you cannot fix one yet, leave includeSubDomains out of the header for now, because a failing subdomain would become unreachable for visitors holding the rule.
Step 3
Choose one place to set the header
Decide whether the header will come from the web server, the hosting panel or the CDN, and use only that one. On nginx that is the https server block, on Apache the port 443 virtual host or .htaccess, on Plesk either SSL It! or the Additional nginx directives, and on Cloudflare the HSTS setting under Edge Certificates. Check that no plugin or application already sends the header, so responses never carry two different copies.
Step 4
Add the header with a short max-age
Add the header with max-age=300 first. On nginx write add_header Strict-Transport-Security "max-age=300" always; and on Apache write Header always set Strict-Transport-Security "max-age=300" with mod_headers enabled. Test the configuration with nginx -t or apachectl configtest before reloading. Keeping the first value at five minutes means that if a page, login or payment form breaks on https, browsers forget the rule quickly while you fix it.
Step 5
Verify the header on every response type
Repeat the curl commands for the home page, an inner page, a 404 page, an image or stylesheet, and the www address. Each should show exactly one strict-transport-security line with the value you set. If it is missing on error pages, add the always flag. If it is missing on some paths in nginx, look for add_header lines in location blocks that stop inheritance. Open the Network panel in Chrome DevTools to confirm the browser sees it too.
Step 6
Raise max-age in stages
After a day with no problems, raise the value to max-age=86400, then after a week to 30 days, reloading the server each time and checking with curl. Use the time to test logins, checkout pages, embedded content and any subdomains. When everything has worked at 30 days, move to max-age=31536000. A year is the value most scanners expect, and anything much shorter may still be flagged as weak rather than missing.
Step 7
Add includeSubDomains when ready
Once every subdomain has passed your https checks, change the header to max-age=31536000; includeSubDomains and send it from the bare domain, not only from www. Reload and confirm the new value with curl. This protects subdomains that a visitor has never visited directly, and it is required if you later choose to preload. Keep preload out unless you have decided to submit the domain and can keep every subdomain on https permanently.
Step 8
Rescan and record the change
Run the security scan again and confirm the HSTS finding has cleared. If the scanner now reports a short max-age, check that the final value is live and that no cache or CDN is serving an older response. Write down where the header is set, its value and the date, so anyone rebuilding the server or moving the site keeps it. Add a curl check for the header to your monitoring or deployment notes.
Edge cases worth knowing
- nginx inheritance. If a
locationblock contains anyadd_headerline of its own, it does not inherit the ones from theserverblock. The header can vanish from some paths. Repeat it in that block or use a shared include file. - Two headers with different values. If both the server and the application, or both Plesk and Cloudflare, set the header, responses may carry two copies. Browsers use the first, and a scanner may report the other. Set it in one place.
- Header set on www only.
includeSubDomainsonwww.example.comcovers subdomains of www, not the bare domain or other subdomains. Send the header from the bare domain too. - Preload commitments. The preload list at hstspreload.org needs a
max-ageof at least one year,includeSubDomainsandpreload, plus a redirect from http to https on the same host. Removal relies on browser releases and can take months. - WordPress and other applications. Security plugins can set the header from PHP, but static files served directly by the web server will not carry it. Setting it on the server covers everything; see the WordPress security headers guide.
Keeping it fixed
HSTS findings come back when a server is rebuilt, a site is migrated, or a new proxy or CDN sits in front of the old configuration. A few checks prevent that:
- Keep the header line in your server configuration under version control or in your hosting notes, so a rebuild includes it.
- Add a curl check for the header to any deployment or monitoring script you run.
- Before creating a new subdomain, make sure it will have a valid certificate from the first day, because the browser rule already applies to it.
- Rescan after migrations and after changing CDN or proxy settings.
HSTS is one of several headers audits look for. The security audit fixes cover the others, including Content-Security-Policy and X-Content-Type-Options.
1. What does HSTS missing from HTTPS server mean?
It means a scanner connected to your site over https and the response did not include a Strict-Transport-Security header. The site itself is encrypted, but browsers have not been told to always use https for it, so a visitor who follows an http link makes one unencrypted request before your redirect. That request can be intercepted on an untrusted network. Adding the header to your https responses tells browsers to upgrade those requests themselves and clears the finding.
2. Is a missing HSTS header a serious security risk?
It is usually rated as low or medium severity rather than critical. The risk is real but specific: an attacker on the same network, such as public Wi-Fi, can intercept the first http request and keep the visitor on an unencrypted copy of the site. Sites with logins, forms or payments benefit most from fixing it. Because the fix is a single header line, it is worth doing on any site that already works fully over https.
3. What max-age should I use for HSTS?
Aim for 31536000 seconds, which is one year, once you have tested the site. Start with a short value such as 300 seconds, raise it to a day, then a month, and only then to a year. Many scanners treat values under about six months as weak, and the preload list requires at least a year. The short start matters because browsers remember the rule for the full max-age, even if you later remove the header.
4. Should I add includeSubDomains to my HSTS header?
Add it only once every subdomain of the domain works over https with a valid certificate. With includeSubDomains, browsers apply the rule to all subdomains, including mail, staging and old test hosts you may have forgotten. Any that only work over http, or have certificate problems, become unreachable for those visitors until the rule expires. List your DNS records and test each hostname first. It is worth adding when you can, because it protects subdomains visitors have never visited directly.
5. Does the HSTS header need to be sent over http as well?
No. Browsers ignore the Strict-Transport-Security header when it arrives over plain http, because an attacker could add or remove it on an unencrypted connection. Send it on your https responses, and keep a permanent redirect from every http address to the https address of the same host. Sending it on http responses as well does no harm, so a single .htaccess line that applies to both is acceptable, but it has no effect there.
6. Should I submit my site to the HSTS preload list?
Only if you are certain the domain and every subdomain will stay on https permanently. Preloading builds your domain into browsers, which protects even the very first visit, but removal depends on browser release cycles and can take months. The list requires a max-age of at least one year, includeSubDomains, the preload directive and a correct http to https redirect. Most small business sites are well protected by a one-year header without preloading.
7. How do I remove or undo HSTS if something breaks?
Change the header to max-age=0 and keep serving it over https. Each browser that visits again sees the new value and forgets the rule. Visitors who do not return keep the old rule until it expires, which is why starting with a short max-age is safer. In your own Chrome browser you can delete the rule straight away from the HSTS section of chrome://net-internals. If the domain is preloaded, you also need to request removal from the preload list.
Our security audit reads the headers your https responses actually send, flags a missing or weak Strict-Transport-Security value, and shows the other headers that need attention.
Open Security Audit Fixes โ