/ โ† Security Audit Fixes / Security Headers on IIS
In this guide
  1. How IIS adds custom response headers
  2. Adding security headers in web.config
  3. How to set Content-Security-Policy in web.config
  4. Three ways to add HSTS on IIS
  5. Why HSTS is not showing in IIS
  6. How to check the headers IIS sends
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping the headers in place
  10. Questions and answers

How IIS adds custom response headers

Windows IIS adds extra response headers through the customHeaders collection in the system.webServer/httpProtocol section of the configuration. You can set it in three places, and they stack from the top down:

Lower levels inherit from higher ones unless they clear the collection. When you add a header in IIS Manager under HTTP Response Headers, IIS Manager simply writes the same customHeaders entry into the config file for the level you selected. So whether you edit web.config by hand or click through the interface, the result is the same XML.

Custom headers are added to responses for static files and for applications IIS hosts, including ASP.NET and ASP.NET Core sites behind the ASP.NET Core Module. That makes web.config a good single place to own your security headers on a Windows server, as long as the application does not also set them.

๐Ÿ’ก Back up web.config before editing it. A single XML mistake makes IIS return a 500.19 configuration error for the whole site, and restoring the copy is the quickest way back.

Adding security headers in web.config

Open the web.config in the site root and add a httpProtocol block inside system.webServer. If the file already has a system.webServer element, add the block inside it rather than creating a second one.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="X-Frame-Options" value="SAMEORIGIN" />
        <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
        <add name="Permissions-Policy" value="camera=(), microphone=(), geolocation=()" />
        <add name="Content-Security-Policy-Report-Only" value="default-src 'self'; img-src 'self' data:; frame-ancestors 'self'" />
      </customHeaders>
    </httpProtocol>
    <security>
      <requestFiltering removeServerHeader="true" />
    </security>
  </system.webServer>
</configuration>

The remove line drops the X-Powered-By: ASP.NET header that IIS adds by default, and removeServerHeader drops the Server header on IIS 10 version 1709 and later (Windows Server 2019 onwards). Neither is a security fix in itself, but scanners often flag them as information disclosure.

The same thing in IIS Manager

  1. Open IIS Manager, expand Sites and select your site.
  2. Double-click HTTP Response Headers.
  3. Click Add in the Actions pane, enter the header name and value, and click OK.
  4. Repeat for each header. To remove X-Powered-By, select it and click Remove.

Changes apply straight away; you do not need to restart the site. For a full list of headers and valid values, see the missing security headers guide.

How to set Content-Security-Policy in web.config

A Content Security Policy goes in customHeaders like any other header, but the XML format adds three rules:

<add name="Content-Security-Policy"
     value="default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" />

Start with the header name Content-Security-Policy-Report-Only. The browser then reports what the policy would block without blocking it, so you can watch the DevTools console and your reports for a week or two, add the sources your site really uses, and switch the name to Content-Security-Policy when nothing legitimate is being reported. The CSP report-only guide covers reading the reports.

ASP.NET sites often rely on inline scripts, for example WebForms postback scripts, which a strict policy blocks. If you cannot remove them, you may need 'unsafe-inline' for scripts at first, then work towards nonces. Framework-specific advice is in the CSP and frameworks guide.

Three ways to add HSTS on IIS

Strict-Transport-Security should only be sent on HTTPS responses. Browsers ignore it over plain HTTP, and some scanners flag it there as invalid. That is why the simple customHeaders method is not ideal for HSTS: it adds the header to every response, HTTP included. There are better options.

MethodWorks onSends on HTTPS only
Native HSTS site settingIIS 10.0 version 1709 and later (Windows Server 2019 onwards)Yes
URL Rewrite outbound ruleAny IIS with the URL Rewrite module installedYes, with a condition
customHeaders in web.configAny IISNo; also sent over HTTP

Native HSTS

On a recent IIS, select the site in IIS Manager and look for HSTS in the Actions pane, or set it from an elevated command prompt:

%windir%\system32\inetsrv\appcmd.exe set config -section:system.applicationHost/sites "/[name='Example Site'].hsts.enabled:True" "/[name='Example Site'].hsts.max-age:31536000" "/[name='Example Site'].hsts.includeSubDomains:True" "/[name='Example Site'].hsts.redirectHttpToHttps:True" /commit:apphost

This writes to applicationHost.config, not web.config, so you will not see it in the site folder.

URL Rewrite outbound rule

<rewrite>
  <outboundRules>
    <rule name="Add HSTS on HTTPS">
      <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
      <conditions>
        <add input="{HTTPS}" pattern="on" ignoreCase="true" />
      </conditions>
      <action type="Rewrite" value="max-age=31536000; includeSubDomains" />
    </rule>
  </outboundRules>
</rewrite>

This goes inside system.webServer. Start with a short max-age, such as 300, until you are sure every subdomain works over HTTPS; the disabling HSTS guide explains why a long one is hard to undo.

Why HSTS is not showing in IIS

If you have set HSTS and a scanner or curl still says it is missing, one of these is usually the reason:

CauseHow to tellFix
You are testing the HTTP addressThe test URL starts with http://Native HSTS and rewrite rules only send it on HTTPS; test the https URL
No HSTS option in IIS ManagerOlder Windows Server, before IIS 10.0 version 1709Use the URL Rewrite rule instead
URL Rewrite not installedThe rewrite section causes a 500.19 error, or is ignoredInstall the URL Rewrite module, then reload the site
Set on the wrong site or bindingSeveral sites share the serverCheck the site name in appcmd list site and its https binding
A proxy or CDN in frontResponses show the proxy's headersSet HSTS on the load balancer, Azure Front Door or CDN that terminates TLS
ASP.NET Core UseHsts on localhostTesting on the server itselfThe middleware skips localhost by default; test the public host name
A clear element in a lower web.configHeaders set at server level vanish for one siteRemove the clear or add the headers again at that level

A certificate or binding problem can also stop HTTPS working at all, so no HTTPS response, and no HSTS, ever reaches the visitor. The IIS SSL bindings guide covers that case.

How to check the headers IIS sends

Windows 10 and later, and Windows Server 2019 and later, include curl.exe. Use it from Command Prompt or PowerShell, typing curl.exe rather than curl, because in Windows PowerShell 5.1 curl is an alias for a different command:

curl.exe -sI https://www.example.com/
curl.exe -sI http://www.example.com/

Or use PowerShell directly:

(Invoke-WebRequest -Uri https://www.example.com/ -Method Head -UseBasicParsing).Headers

To see what the configuration says, rather than what the response says, list the section for the site:

%windir%\system32\inetsrv\appcmd.exe list config "Example Site" -section:system.webServer/httpProtocol

If the config lists a header that the response lacks, something after IIS, such as a proxy, is removing it, or the application is overwriting it. If the response has a header twice, both IIS and the application are adding it; remove one. You can also run the site through our security audit to see every header on several pages at once.

The fix, step by step

Step 1

Back up web.config

Copy the site's web.config to a safe place before you change anything, and note the IIS version from IIS Manager's Help, About. The version decides whether native HSTS is available. Check whether the application or a proxy already sets any security headers by running curl.exe -sI on the HTTPS address, so you know your starting point and can spot duplicates after the change. Keep the curl output in a text file for comparison later.

Step 2

Add the basic headers

Inside system.webServer, add an httpProtocol block with a customHeaders collection, and add X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy. Save the file. If the site returns a 500.19 error, the XML is malformed or the section is locked, so restore the backup and check the syntax. Otherwise run curl.exe -sI on the HTTPS address and confirm each header appears exactly once. If you prefer the interface, add the same headers in IIS Manager under HTTP Response Headers, which writes identical entries to the file.

Step 3

Remove information headers

Add a remove element for X-Powered-By to the customHeaders collection and set removeServerHeader to true under security/requestFiltering on IIS 10 version 1709 or later. For ASP.NET sites, also set enableVersionHeader to false on httpRuntime in system.web. Reload the page with curl and confirm that X-Powered-By, Server and X-AspNet-Version no longer appear in the response headers. Scanners often report these as information disclosure, so removing them closes those findings, although it does not fix any weakness by itself.

Step 4

Add CSP in report-only mode

Add a Content-Security-Policy-Report-Only header with your starting policy on a single line, with keywords such as 'self' in single quotes. Browse the site with DevTools open and note every report in the console. Add the real sources your pages use, such as a CDN, analytics or payment provider, and keep the policy in report-only mode for one to two weeks so that rarely used pages are visited too.

Step 5

Switch CSP to enforcing

When the reports show only things you want blocked, rename the header to Content-Security-Policy, keeping the same value. Test the pages that matter most, such as checkout, contact forms, logins and embedded maps, with the browser console open, and look for any blocked resource messages. Keep a copy of the report-only version, so you can switch back quickly if a page breaks after the change goes live.

Step 6

Add HSTS for HTTPS only

On IIS 10.0 version 1709 or later, enable the site's native HSTS setting in IIS Manager or with appcmd, starting with a max-age of 300. On older servers, install URL Rewrite and add the outbound rule that sets RESPONSE_Strict_Transport_Security when {HTTPS} is on. Do not also add it in customHeaders, or HTTPS responses will carry it twice. Confirm the setting saved by listing it with appcmd.

Step 7

Test HTTPS and HTTP separately

Run curl.exe -sI on the https address and confirm Strict-Transport-Security appears with your value. Then run it on the http address: it should redirect to HTTPS, and it is normal for HSTS to be absent there. If HSTS is still missing on HTTPS, check the site name and binding, whether a proxy sits in front, and whether a lower web.config clears the headers for that folder.

Step 8

Raise max-age and rescan

After a few days with no problems, and once every subdomain is confirmed to work over HTTPS, raise max-age to 31536000 and add includeSubDomains if it suits. Run the original scanner again on the same URLs and confirm the findings are closed. Commit the final web.config to source control, so the next publish or server rebuild keeps your headers in place without anyone having to remember them.

Edge cases worth knowing

Keeping the headers in place

Keep the security header block in source control with the site, so every deployment carries it. After each Windows update, IIS change or publish, run curl.exe -sI on the home page, a deep page and a 404, and compare against your list. Rescan monthly, and keep the reports for clients or auditors. If the site sits behind a proxy, check the public address, not the server's own name, because that is what visitors and scanners see. Redirect rules affect HSTS too; see the HTTPS redirects guide.

Questions and answers

1. How do I set Content Security Policy in IIS?

You set a Content Security Policy in IIS by adding a custom response header named Content-Security-Policy, either in IIS Manager under HTTP Response Headers or in web.config under system.webServer, httpProtocol and customHeaders. Write the whole policy on one line, with directives separated by semicolons and keywords such as self in single quotes. Start with the Content-Security-Policy-Report-Only header name, watch the browser console for a week or two, then switch to the enforcing name once only unwanted resources are reported.

2. How do I add a Content Security Policy header in web.config?

Add an add element inside the customHeaders collection, with name set to Content-Security-Policy and the policy as its value. The collection sits inside httpProtocol, which sits inside system.webServer. Keep the value on one line, use single quotes around CSP keywords, and escape any ampersand as an XML entity, because an invalid file makes IIS return a 500.19 error. Save the file and check the response with curl.exe -sI to confirm the header appears once and with the value you expect.

3. Why is HSTS not showing in IIS?

HSTS usually does not show in IIS because you are testing the http address, where native HSTS and rewrite rules deliberately do not send it, or because your IIS version is older than 10.0 version 1709 and has no HSTS option in IIS Manager. Other causes are a proxy or CDN in front of IIS that terminates TLS, the setting applied to a different site, ASP.NET Core skipping HSTS on localhost, and a clear element in a lower web.config.

4. Where is the HSTS setting in IIS Manager?

On IIS 10.0 version 1709 and later, which includes Windows Server 2019 and newer versions, select the site in IIS Manager and look for HSTS in the Actions pane on the right. There you can enable it, set max-age, include subdomains and redirect HTTP to HTTPS. On older versions the option does not exist, so use a URL Rewrite outbound rule that sets the header only when the HTTPS server variable is on, and test the result with curl.exe.

5. Should I add HSTS with customHeaders in web.config?

It works, but it is not the best method, because customHeaders adds the header to every response, including plain HTTP ones, where browsers ignore it and some scanners mark it as invalid. Native HSTS on IIS 10.0 version 1709 or later, or a URL Rewrite outbound rule with an HTTPS condition, sends it only over HTTPS. Use one method only, because using two at once gives duplicate headers on your HTTPS responses and can confuse scanners.

6. Why does my web.config change give a 500.19 error?

A 500.19 error after editing web.config means IIS cannot read the configuration, usually because of malformed XML or a locked section. Common causes are a missing closing tag, a second system.webServer element, an unescaped ampersand inside a header value, or a rewrite section on a server without URL Rewrite installed. On shared hosting, the httpProtocol section may be locked at server level. Restore your backup, then add the change again in small steps, testing after each one.

7. How do I remove the X-Powered-By and Server headers in IIS?

Remove X-Powered-By by adding a remove element with that name inside customHeaders in web.config, or by selecting it in HTTP Response Headers in IIS Manager and clicking Remove. On IIS 10 version 1709 and later (Windows Server 2019 onwards), remove the Server header by setting removeServerHeader to true under requestFiltering in the security section. For ASP.NET, also turn off the X-AspNet-Version header with enableVersionHeader set to false. These steps hide version details that scanners report as information disclosure, but they do not fix any weakness in the software itself, so keep IIS and Windows updated as well.

Check the security headers your IIS site sends

Our security audit fetches your pages over HTTPS and HTTP and lists each security header with its value, so you can see what IIS really sends and which ones are missing or invalid.

Open Security Audit Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Security Audit Fixes  ·  Fixing missing security headers  ·  Fixing SSL bindings on IIS  ·  Fixing HSTS missing from an HTTPS server  ·  Rolling out CSP in report-only mode  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.