HSTS Missing From HTTPS Server: Why It's Still Flagged and How to Fix It
Your scan report says "HSTS Missing From HTTPS Server (RFC 6797)", you add one header, you rescan, and the finding is still there. That second part is why most people end up searching for it. The header itself is one lin
Your scan report says "HSTS Missing From HTTPS Server (RFC 6797)", you add one header, you rescan, and the finding is still there. That second part is why most people end up searching for it. The header itself is one line. The finding keeps coming back because a scanner checks the response you didn't look at: a redirect, a different hostname, an error page or another port.
I found two of those on this site while writing this, so the curl output below is real. Here's what the scanner actually checks, the fix for IIS, ASP.NET Core, Nginx, Apache, Next.js and Cloudflare, and how to make sure the next scan comes back clean.
What the finding means
HSTS (HTTP Strict Transport Security, defined in RFC 6797) is a response header:
Strict-Transport-Security: max-age=31536000
When a browser receives it over HTTPS, it remembers for max-age seconds (here one year) that this host must only be reached over HTTPS. After that, typing example.com or clicking an old http:// link never sends a plain-HTTP request, so there's no unencrypted first request for someone on the same Wi-Fi to intercept and downgrade (an "SSL stripping" attack). It also stops visitors from clicking through a certificate warning on that host.
Scanners report a missing header under different names:
- Nessus plugin 142960, "HSTS Missing From HTTPS Server (RFC 6797)": severity Medium, CVSS v3 6.5. This is the one that shows up in audit reports.
- Nessus plugin 84502, "HSTS Missing From HTTPS Server": the older check, rated Info.
- OWASP ZAP and similar tools: "Strict-Transport-Security Header Not Set".
Tenable's solution text is one sentence: "Configure the remote web server to use HSTS." The rest of this post fills in the details it leaves out.
What the scanner actually checks
Three things explain almost every "I fixed it and it's still flagged" case:
-
It reads the response to the exact URL it requested and doesn't follow the redirect. A GitLab admin found this the hard way: their server root answered with a redirect, the redirect had no HSTS header, and plugin 84502 failed the check even though the pages behind it had one. If
https://www.example.com/redirects tohttps://example.com/, the redirect itself needs the header. - It checks every HTTPS port and every hostname it knows. A management interface on 8443, a reporting service on 9080 or an old admin hostname are each a separate "HTTPS server" with their own finding.
-
The header only counts over HTTPS. Browsers ignore it on plain HTTP, and so do scanners. The
http://tohttps://redirect doesn't need the header (it can't help there). The HTTPS response it lands on does.
Tenable has also been tuning plugin 142960 recently. It shipped "avoid false positive" logic changes in July and August 2026, and version 1.16 (12 October 2026) changed when the check stops at an NTLM authentication challenge. That matters for Exchange, SharePoint and other Windows-authenticated sites, where the first response is a 401. If your results changed between two scans without a config change, an updated plugin is a likely reason.
Check it yourself first
Before you change anything, see which responses are missing the header. This loop prints the status, the redirect target and the HSTS header for each URL:
# -D - prints the response headers, -o /dev/null throws the body away.
# Check every URL a scanner might start from, not just the one you browse to.
for url in https://example.com/ https://www.example.com/ https://example.com/does-not-exist; do
echo "== $url"
curl -s -o /dev/null -D - "$url" | grep -iE '^HTTP|^location|^strict-transport-security'
done
Add every hostname and port from the scan report to that list. Here's what it showed for this site on 14 November 2026:
== https://knowledgemarkg.com/
HTTP/1.1 200 OK
strict-transport-security: max-age=31536000
== https://knowledgemarkg.com/no-such-page
HTTP/1.1 404 Not Found
strict-transport-security: max-age=31536000
== https://www.knowledgemarkg.com/
HTTP/1.1 301 Moved Permanently
location: https://knowledgemarkg.com/
<- no Strict-Transport-Security (sent by Cloudflare)
== https://knowledgemarkg.com/blog/filtered
HTTP/1.1 308 Permanent Redirect
location: /blog
<- no Strict-Transport-Security (sent by Next.js)
== https://api.knowledgemarkg.com/nope
HTTP/1.1 404 Not Found
strict-transport-security: max-age=2592000 <- 30 days, ASP.NET Core's default
Three problems in one check:
- The www redirect is answered by Cloudflare itself (the response carries
Server: cloudflareand none of the origin's headers), so nothing I set on the server can ever reach it. - A redirect defined in Next.js (
redirects()innext.config.ts) comes back without the header, even thoughheaders()sets it for/:path*and every normal page and the 404 page get it. - The API sends
max-age=2592000, which is 30 days. That's ASP.NET Core's default when you callUseHsts()without options. Nessus doesn't flag it, but many graders do (our own checker calls anything under 180 days weak).
A scan that started at https://www.knowledgemarkg.com/ would report this finding even though the site "has HSTS". Your setup will differ, but the pattern repeats: the pages are fine and a hop in front of them isn't.
Fix it on your server
Use max-age=31536000 (one year) and leave out includeSubDomains and preload unless you've read the section about them below. If you're nervous, start with max-age=300 for a day, confirm nothing breaks, then raise it. A browser that has seen a one-year value keeps it for a year.
IIS 10 (Windows Server 2019 and 2022)
IIS 10.0 version 1709 and later has built-in HSTS. In IIS Manager, select the site and open HSTS... in the Actions pane, or set it in applicationHost.config:
<!-- applicationHost.config, inside the <site> element. IIS 10.0 version 1709 and later. -->
<site name="example.com" id="2">
<!-- ... bindings, application ... -->
<hsts enabled="true" max-age="31536000" includeSubDomains="false" redirectHttpToHttps="true" />
</site>
redirectHttpToHttps="true" also handles the HTTP-to-HTTPS redirect, so you don't need a URL Rewrite rule for that. On older IIS, or on shared hosting where you only have web.config, add a custom header instead:
<!-- web.config in the site root: works on any IIS version, including shared hosting -->
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
This sends the header on HTTP responses as well. That's harmless, since browsers ignore it there. You'll also find a recipe that uses URL Rewrite outbound rules. At least one Stack Overflow asker still had Tenable reporting HSTS as disabled after using it. Whichever method you choose, run the curl loop again afterwards.
ASP.NET Core
// Program.cs
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365); // the default is 30 days
options.IncludeSubDomains = false; // true only when every subdomain serves HTTPS
options.Preload = false;
});
// Behind Nginx, a load balancer or anything else that terminates TLS, tell the app the
// original request was HTTPS - otherwise Request.IsHttps is false and UseHsts adds nothing.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
var app = builder.Build();
app.UseForwardedHeaders();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
Four things about UseHsts() catch people out:
- The default
max-ageis 30 days. - The project templates only call it outside Development, so a test server running with
ASPNETCORE_ENVIRONMENT=Developmentgets scanned without HSTS. - It never adds the header for
localhost,127.0.0.1or[::1]. - It only adds the header when the request it sees is HTTPS. Behind a proxy that terminates TLS, that's false unless forwarded headers are configured.
Hosted in IIS (in-process or behind the ASP.NET Core Module), the app sees the real scheme and none of the proxy setup is needed. For the rest of an IIS deployment, see deploying ASP.NET Core on Plesk.
Nginx
server {
listen 443 ssl;
server_name example.com www.example.com;
# "always" adds it to 4xx and 5xx responses too, not only to 2xx and 3xx.
add_header Strict-Transport-Security "max-age=31536000" always;
location /downloads/ {
# Any add_header in a block replaces ALL inherited add_header lines,
# so the HSTS line has to be repeated here or /downloads/ loses it.
add_header Cache-Control "public, max-age=86400";
add_header Strict-Transport-Security "max-age=31536000" always;
}
}
The location trap is the classic one. Nginx only inherits add_header lines from the outer block if the inner block has none of its own, so adding a single cache header to one location silently removes HSTS from every response that location serves.
Apache
# a2enmod headers (Debian/Ubuntu; mod_headers must be loaded)
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
# "always" also covers redirects and error responses that Apache generates itself.
Header always set Strict-Transport-Security "max-age=31536000"
</VirtualHost>
Without always, Header set only applies to successful responses, so redirects and error pages that Apache generates itself go out without it. If the www hostname has its own <VirtualHost *:443> that only redirects, it needs the line too.
Next.js
// next.config.ts
async headers() {
return [
{
source: "/:path*",
headers: [{ key: "Strict-Transport-Security", value: "max-age=31536000" }],
},
];
},
That covers pages, static files and the 404 page. As the output above shows, it didn't cover a redirect from redirects() on this site (Next.js 16, behind IIS). If a scanner starts at a redirecting URL, issue that redirect from proxy.ts and set the header yourself:
// proxy.ts (middleware.ts before Next.js 16): issue the redirect yourself and set the header on it
import { NextResponse, type NextRequest } from "next/server";
export default function proxy(request: NextRequest) {
if (request.nextUrl.pathname === "/old-page") {
const response = NextResponse.redirect(new URL("/new-page", request.url), 308);
response.headers.set("Strict-Transport-Security", "max-age=31536000");
return response;
}
return NextResponse.next();
}
Running Next.js behind IIS? The web.config custom header above should cover everything IIS sends, including Next's redirects, because IIS adds it on the way out. I haven't tested that combination yet, so check it with curl. The IIS setup itself is in deploying Next.js on IIS and Plesk.
Cloudflare and other CDNs
If your CDN answers a request itself (a Redirect Rule, "Always Use HTTPS", a cached response), your origin's headers aren't involved. In Cloudflare, HSTS is under SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS). Set the max-age there and leave "Apply HSTS policy to subdomains" and "Preload" off for now. Cloudflare's own warning screen explains that a mistake here can make the site unreachable for the length of the max-age.
That edge setting is the only place a header for this site's www redirect can come from. I haven't switched it on yet, so I can't confirm here that it covers Cloudflare-generated redirects. Check with curl after you enable it. If you're also chasing caching problems at the edge, see why Cloudflare shows cf-cache-status DYNAMIC.
Still flagged after the fix? Go through this list
- A redirect hop has no header. Check the www/apex redirect, trailing-slash redirects and the login redirect.
- Another port or hostname. Read the port column in the report. The finding may be about a management UI or an appliance, not your website.
-
Error responses. Nginx without
always, Apache withoutalways, or a custom error page served by a different handler. -
A
locationblock that drops it (Nginx inheritance, see above). - A proxy or load balancer in front that strips response headers or answers by itself.
- ASP.NET Core running as Development, or behind a TLS-terminating proxy without forwarded headers.
- The scan is stale. Rescan the exact asset, and check the plugin output (it shows the URL it tested) before assuming the fix failed.
includeSubDomains and preload: read this before adding them
Many guides and forum answers hand you max-age=31536000; includeSubDomains; preload as the fix. Neither extra directive is needed to clear plugin 142960, and both can lock users out.
- includeSubDomains forces HTTPS on every subdomain for the full max-age. This site deliberately leaves it out: the hosting panel and webmail run on subdomains, and if one of them ever served plain HTTP or a bad certificate, every browser that had seen the header would refuse to open it for a year. Turn it on only after you've checked every subdomain, including the internal ones.
-
preload asks for the domain to be built into browsers' HSTS lists via hstspreload.org. The requirements are a max-age of at least one year,
includeSubDomains,preload, and an HTTP-to-HTTPS redirect on the same host. Any redirect served over HTTPS must still carry the header, which is the same trap as above. Getting a domain off the list takes months to reach users' browsers. Treat it as a separate decision, not as part of fixing a scan finding.
Is it actually dangerous?
It's a real but narrow risk. Without HSTS, the first visit (or any visit from an old http:// link) can be intercepted and kept on HTTP by someone on the same network. Your redirect to HTTPS doesn't prevent that, because the attacker never lets it reach the browser. For a public website with logins or forms, add the header. It takes one line and costs nothing. For a machine-to-machine API or an appliance port no browser ever opens, HSTS protects very little. It's still cheap to add, but a documented risk acceptance is a reasonable answer there.
Verify
Run the curl loop for every hostname and port in the report, then rescan. For a quick graded view of HSTS and the other five security headers, along with the exact line to add for anything missing, try the free Security Headers Checker. It grades the final page after redirects, so use curl for the hops in between.
If a pen-test report has landed on your desk and you'd rather hand the fixes to someone, that's work I do.
FAQ
What does HSTS Missing From HTTPS Server mean?
Your HTTPS server did not send the Strict-Transport-Security response header on the response the scanner tested. Without it, browsers are not told to use HTTPS only, so a first visit or an old http:// link can be intercepted and downgraded. Nessus reports it as plugin 142960 (Medium) or 84502 (Info); ZAP calls it "Strict-Transport-Security Header Not Set".
How do I fix HSTS Missing From HTTPS Server?
Send Strict-Transport-Security: max-age=31536000 on every HTTPS response, including redirects and error pages. On IIS 10 use the site's built-in HSTS setting or a customHeaders entry in web.config, in ASP.NET Core call UseHsts() with a one-year MaxAge, on Nginx use add_header ... always, and on Apache use Header always set. Then check each hostname and port with curl and rescan.
Why does Nessus still report HSTS missing after I added the header?
Usually because the response it tested is not the one you checked. Nessus does not follow redirects, so a www-to-apex or login redirect without the header fails the check. Other causes are a different port or hostname, error responses without the header, an Nginx location block that drops inherited headers, or a proxy or CDN that answers the request itself.
How do I enable HSTS on IIS?
On IIS 10.0 version 1709 and later (Windows Server 2019 and 2022), select the site in IIS Manager and open HSTS in the Actions pane, or add to the site in applicationHost.config. On older IIS or shared hosting, add Strict-Transport-Security as a custom header under system.webServer/httpProtocol/customHeaders in web.config.
What max-age should I use for HSTS?
One year (31536000 seconds) is the common value and what the preload list requires. If you are unsure whether every page works over HTTPS, start with 300 seconds, check for a day, then raise it. ASP.NET Core's UseHsts() defaults to 30 days, which many graders treat as weak.
Do I need includeSubDomains and preload to fix the finding?
No. A plain max-age clears the scanner finding. includeSubDomains forces HTTPS on every subdomain for the whole max-age, and preload builds the rule into browsers, which takes months to undo. Add them only after checking that every subdomain serves valid HTTPS.
Should the HTTP to HTTPS redirect send the HSTS header?
It does not need to: browsers ignore Strict-Transport-Security on plain HTTP responses. What matters is that every HTTPS response sends it, including HTTPS redirects such as www to the bare domain, because scanners and the preload list check those responses directly.
Originally published on knowledgemarkg.com.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.