“HSTS missing from HTTPS server” means the web server answers HTTPS requests without a Strict-Transport-Security header, so browsers are never told to refuse plain HTTP for that host. Fix it by sending Strict-Transport-Security: max-age=31536000; includeSubDomains on every HTTPS response, including errors and redirects, then rescan.
What the scanner is actually detecting
All three major scanners report the same underlying condition: an HTTPS response came back without a usable HSTS header. The titles and scoring differ, which is why the same host often appears three times in a merged report.
| Scanner | Finding title | ID | How it is scored |
|---|---|---|---|
| Nessus | HSTS Missing From HTTPS Server (RFC 6797) | Plugin 142960 | Medium (CVSS v3 6.5), family Web Servers |
| Nessus | HSTS Missing From HTTPS Server | Plugin 84502 | Info, an older informational version of the same check |
| Greenbone / OpenVAS | SSL/TLS: HTTP Strict Transport Security (HSTS) Missing | OID 1.3.6.1.4.1.25623.1.0.105879 | Log level (CVSS 0.0), family SSL and TLS |
| Qualys | HTTP Security Header Not Detected | QID 11827 | Severity 2, category CGI |
The OpenVAS check is worth understanding in detail because its logic is public. It reads the response headers of the HTTPS service and flags the port when the Strict-Transport-Security header is absent, when the header lacks a max-age= directive, or when it is set to max-age=0. A 304 Not Modified response is deliberately skipped. Qualys QID 11827 is broader than HSTS: its published detection logic also looks for X-Content-Type-Options: nosniff, so fixing HSTS alone may not close that QID.
Real-world risk
HSTS protects browser users against one specific attack: an on-path attacker (hostile Wi-Fi, a compromised router, ARP spoofing on a LAN) intercepting the first plain HTTP request when a user types a bare hostname or follows an http:// link, then keeping the victim on HTTP (SSL stripping). Once a browser has seen the header, it rewrites those requests to HTTPS locally and, per RFC 6797, removes the option to click through certificate warnings.
Keep the limits in mind. HSTS does nothing for the very first visit unless the domain is preloaded into browsers, it has no effect on scripts, API clients or agents that do not implement it, and it does not fix weak ciphers or bad certificates. On a public login page it is a genuine, cheap hardening step. On an internal appliance console reached only through bookmarked HTTPS URLs, the practical exposure is low, even though the scanner still reports it.
How to confirm it on the host
Check the root, an error path and a redirect, because many servers add the header only to successful responses:
curl -sI https://www.example.com/ | grep -i strict-transport-security
curl -sI https://www.example.com/this-page-does-not-exist | grep -i strict-transport-security
curl -skI https://10.0.0.15:8443/ | grep -i strict-transport-security
The -k flag in the last line skips certificate validation, which is useful for internal hosts with self-signed certificates but should only be used for testing. Qualys suggests reproducing its request with curl -lkL –verbose. On Windows without curl:
$r = Invoke-WebRequest -Uri https://www.example.com/ -Method Head -UseBasicParsing
$r.Headers['Strict-Transport-Security']
Then look at the configuration itself:
sudo nginx -T | grep -i strict-transport
sudo grep -Ri strict-transport /etc/apache2/ /etc/httpd/ 2>/dev/null
%windir%system32inetsrvappcmd.exe list config -section:system.applicationHost/sites
In the IIS output, look for an <hsts> element under the affected <site>.
How to fix it
Send the header only on HTTPS. RFC 6797 section 7.2 says an HSTS host must not include it over plain HTTP, and browsers ignore it there anyway. Handle port 80 with a redirect to HTTPS instead.
Apache HTTP Server (mod_headers)
Enable mod_headers (on Debian and Ubuntu: sudo a2enmod headers; RHEL-family builds usually load it already), then add the directive inside the port 443 virtual host:
<VirtualHost *:443>
ServerName www.example.com
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
Use always, not the default onsuccess table. The Apache documentation states that headers in the always table are added even on errors and persist across internal redirects, which is exactly where scanners catch missing headers. Test and reload:
sudo apachectl configtest && sudo systemctl reload apache2 # httpd on RHEL-family systems
nginx
server {
listen 443 ssl;
server_name www.example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
Without always, nginx adds the header only to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses. Also watch inheritance: add_header directives are inherited from the enclosing level only if the current level defines none, so any location block with its own add_header silently drops HSTS. Repeat the line in those blocks, or on nginx 1.29.3 and later use add_header_inherit merge;. Apply with sudo nginx -t && sudo systemctl reload nginx.
IIS 10.0 version 1709 and later (Windows Server 2019 and newer)
IIS has a native <hsts> element per site. Microsoft’s documented AppCmd commands (replace Contoso with your site name):
%windir%system32inetsrvappcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.enabled:True" /commit:apphost
%windir%system32inetsrvappcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.max-age:31536000" /commit:apphost
%windir%system32inetsrvappcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.includeSubDomains:True" /commit:apphost
The same settings through the IISAdministration module:
Import-Module IISAdministration
Start-IISCommitDelay
$sites = Get-IISConfigSection -SectionPath "system.applicationHost/sites" | Get-IISConfigCollection
$site = Get-IISConfigCollectionElement -ConfigCollection $sites -ConfigAttribute @{"name"="Contoso"}
$hsts = Get-IISConfigElement -ConfigElement $site -ChildElementName "hsts"
Set-IISConfigAttributeValue -ConfigElement $hsts -AttributeName "enabled" -AttributeValue $true
Set-IISConfigAttributeValue -ConfigElement $hsts -AttributeName "max-age" -AttributeValue 31536000
Set-IISConfigAttributeValue -ConfigElement $hsts -AttributeName "includeSubDomains" -AttributeValue $true
Stop-IISCommitDelay
The element also has redirectHttpToHttps. Microsoft notes that this redirect replaces the scheme and ignores the port, so only enable it when HTTPS is served on 443.
IIS on Windows Server 2016 and older
Native HSTS does not exist before IIS 10.0 version 1709. For a site with only an HTTPS binding, add a custom header in its web.config:
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
If the site has both HTTP and HTTPS bindings, Microsoft’s alternative is a URL Rewrite outbound rule that sets RESPONSE_Strict_Transport_Security only when {HTTPS} is on.
Load balancers, CDNs and appliances
Set the header on whatever component terminates TLS for the address the scanner tests. If a reverse proxy terminates TLS, configure it there. For vendor appliances with fixed web consoles, Qualys itself notes that management interfaces often lack these headers and recommends contacting the vendor.
How to verify the fix and rescan
Repeat the curl checks against the root, a 404 path and any login redirect. Every HTTPS response should show the header, and the HTTP (port 80) response should show a redirect without it. Then rescan the same targets with the same credentials and scan policy. For OpenVAS, the detection runs as part of the SSL and TLS family, so a policy that excludes that family will not re-evaluate it. Rescan by the same IP or hostname the original finding used, because a different virtual host can return different headers.
What can break and how to roll back
- includeSubDomains applies the policy to every subdomain below the host that sent it. If you set it on the parent domain, any subdomain still served only over HTTP (old intranet apps, printers, test sites) becomes unreachable in browsers that cached the policy.
- Certificate problems become hard failures. RFC 6797 requires no user recourse, so an expired or self-signed certificate on an HSTS host blocks users entirely.
- preload should only be added after you deliberately submit the domain to the browser preload list; Microsoft’s IIS reference gives the same warning.
- If both the application and a proxy send the header, browsers process only the first one, which can make your configured value appear not to take effect.
Rollback is asymmetric. Removing the directive does not clear browsers that already cached the policy; they keep enforcing it until max-age expires. To actively withdraw it, serve max-age=0 over HTTPS, which clears the policy only for browsers that return to the site. A cautious rollout starts with a short value such as max-age=300 without includeSubDomains, then raises it once nothing breaks. Some scanners may still flag very short values, so finish at one year.
Common false positive reasons
- Header set only on success. The scanned path returned a redirect, 401 or 404, and Apache or nginx was configured without always. Technically a true finding for those responses.
- Wrong layer. The header is set on the CDN or load balancer, but the scanner hit the origin IP directly, or the reverse.
- Default virtual host. Scanning by IP lands on the default server block or site, which lacks the header even though the named site has it.
- Header with max-age=0 or no max-age. OpenVAS treats both as missing.
- Middleboxes. A WAF can add or strip response headers, which changes what the scanner sees; see how a WAF changes what your web scanner reports.
Because this is a runtime check against the live server, it is the kind of issue only dynamic testing sees; the differences between SAST, DAST and SCA explain why source code review will not catch it.
FAQ
Do I need includeSubDomains to clear the finding?
Not by RFC 6797, where max-age is the only required directive. The OpenVAS check fires only when the header or max-age is missing or zero, and Qualys lists includeSubDomains as optional. Add it when every subdomain supports HTTPS.
Should I also send the header on port 80?
No. The RFC forbids it over plain HTTP and browsers ignore it there. Redirect port 80 to HTTPS and send the header on the HTTPS response.
Why does Qualys QID 11827 still appear after I added HSTS?
QID 11827 also checks for X-Content-Type-Options: nosniff. Add it the same way, for example Header always set X-Content-Type-Options “nosniff” in Apache.
Does HSTS protect the first visit?
No. The browser must receive the header over HTTPS at least once. Only preloading covers the first connection.
Tracking this finding across many hosts
HSTS findings usually arrive in bulk, and the same host can show up as Nessus 142960, an OpenVAS log entry and Qualys QID 11827 at once. If you manage that volume in SITEY, a self-hosted vulnerability management platform, it imports findings from 16 scanners and can launch Nessus and OpenVAS scans directly, although duplicates are merged per scanner rather than across scanners, so expect one record per tool. Per-finding retest is available for Nessus, Acunetix and Burp findings, which suits a header change you want confirmed host by host; for other scanners, rerun the scan.