Why IIS Security Hardening Matters
A default IIS installation on Windows Server 2022 is functional but not fully hardened. Out of the box, IIS reveals version information in response headers, serves directory listings for unconfigured virtual directories, may support legacy TLS versions, and lacks the HTTP security headers that modern browsers rely on to prevent cross-site scripting, clickjacking, and MIME-type sniffing attacks. Attackers routinely scan for these misconfigurations because they require no credentials to exploit and can reveal attack surface or enable client-side attacks against your users.
This guide provides concrete, command-line-driven steps to harden IIS on Windows Server 2022 across six areas: HTTP security headers, HSTS, TLS 1.3 enablement, server banner removal, directory browsing and request filtering, and the URL Scan module.
Adding HTTP Security Headers via web.config
HTTP security headers are sent in every response and instruct the browser on how to handle the content securely. They are the first line of defence against several categories of attack. All headers are configured in the <httpProtocol> section of web.config.
X-Frame-Options
Prevents the page from being embedded in an <iframe>, <frame>, or <object> on another origin, protecting against clickjacking attacks where attackers trick users into clicking invisible page elements:
Use SAMEORIGIN to allow embedding only from the same origin, DENY to block all embedding, or ALLOW-FROM https://trusted.example.com to permit a specific origin (note: ALLOW-FROM is deprecated in modern browsers; use CSP frame-ancestors instead).
X-Content-Type-Options
Prevents browsers from MIME-type sniffing — inferring a content type different from what the server declared. This stops attacks where an uploaded file with a misleading extension is executed as HTML or JavaScript:
With nosniff, browsers refuse to execute a response as a script or stylesheet unless the server’s Content-Type header matches the expected type. Always pair this with correct Content-Type headers on all responses.
X-XSS-Protection
Enables the legacy reflected XSS filter built into older versions of Chrome and IE/Edge. Modern browsers have deprecated this header in favour of Content Security Policy, but it remains valuable for users on older browsers:
The value 1; mode=block tells the browser to block rendering of the page if a reflected XSS attack is detected, rather than sanitizing the attack in place (which can itself introduce vulnerabilities).
Content Security Policy (CSP)
CSP is the most powerful security header. It defines a whitelist of trusted sources for every type of content the page can load: scripts, stylesheets, images, fonts, frames, and more. A properly configured CSP makes it extremely difficult for injected scripts (whether via XSS or injected third-party content) to execute:
This policy allows scripts only from the same origin and cdn.jsdelivr.net, allows inline styles (needed for many frameworks), permits images from any HTTPS source and data URIs, restricts frames to same-origin, and limits form submissions to the same origin. Start with a report-only policy during development to identify violations without breaking the site:
Referrer-Policy
Controls how much referrer information is sent in the Referer header when users navigate from your site to external sites. Overly permissive referrer settings can leak authenticated URLs, user IDs, or session tokens embedded in query strings:
With strict-origin-when-cross-origin, the browser sends the full URL as the referrer for same-origin requests, but only the origin (no path or query string) for cross-origin requests, and nothing when downgrading from HTTPS to HTTP.
Permissions-Policy
The Permissions-Policy header (formerly Feature-Policy) restricts which browser features the page and embedded frames can use. Disable features your application does not need to reduce attack surface:
HSTS: HTTP Strict Transport Security
HSTS instructs browsers to only ever connect to your site over HTTPS — never HTTP — for a specified period. Once a browser receives an HSTS header, it will refuse to make plain HTTP connections to that hostname and will automatically upgrade requests to HTTPS, even before any server response is received. This prevents SSL-stripping attacks.
Breaking down the value: max-age=31536000 means the HSTS policy is cached by the browser for one year (31,536,000 seconds); includeSubDomains applies the policy to all subdomains; preload signals that the site can be included in browser preload lists (hardcoded lists of HSTS sites shipped with Chrome, Firefox, and Edge).
Important warnings before enabling HSTS: HSTS is difficult to undo once deployed because browsers cache the policy for the full max-age duration. Before setting a one-year max-age, test with a short value (e.g., max-age=300). Ensure HTTPS is fully functional on all site URLs, all subdomains if using includeSubDomains, and that your TLS certificate will not expire before the max-age expires. Only submit to the preload list after extensive testing.
Enabling TLS 1.3 via Registry
TLS 1.3 provides significant security and performance improvements over TLS 1.2: it removes several weak cipher suites, reduces the handshake to one round-trip, and encrypts more of the handshake itself to prevent downgrade attacks. On Windows Server 2022, TLS 1.3 is supported by the OS but may need to be explicitly enabled for IIS via SChannel registry settings:
# Enable TLS 1.3 for both server and client roles
$tls13Path = "HKLM:SYSTEMCurrentControlSetControlSecurityProvidersSCHANNELProtocolsTLS 1.3"
$serverPath = "$tls13PathServer"
$clientPath = "$tls13PathClient"
New-Item -Path $serverPath -Force
New-ItemProperty -Path $serverPath -Name "Enabled" -Value 1 -PropertyType DWORD -Force
New-ItemProperty -Path $serverPath -Name "DisabledByDefault" -Value 0 -PropertyType DWORD -Force
New-Item -Path $clientPath -Force
New-ItemProperty -Path $clientPath -Name "Enabled" -Value 1 -PropertyType DWORD -Force
New-ItemProperty -Path $clientPath -Name "DisabledByDefault" -Value 0 -PropertyType DWORD -Force
While you are in the SChannel registry, explicitly disable TLS 1.0 and TLS 1.1 which are deprecated and insecure:
# Disable TLS 1.0
$tls10 = "HKLM:SYSTEMCurrentControlSetControlSecurityProvidersSCHANNELProtocolsTLS 1.0Server"
New-Item -Path $tls10 -Force
New-ItemProperty -Path $tls10 -Name "Enabled" -Value 0 -PropertyType DWORD -Force
New-ItemProperty -Path $tls10 -Name "DisabledByDefault" -Value 1 -PropertyType DWORD -Force
# Disable TLS 1.1
$tls11 = "HKLM:SYSTEMCurrentControlSetControlSecurityProvidersSCHANNELProtocolsTLS 1.1Server"
New-Item -Path $tls11 -Force
New-ItemProperty -Path $tls11 -Name "Enabled" -Value 0 -PropertyType DWORD -Force
New-ItemProperty -Path $tls11 -Name "DisabledByDefault" -Value 1 -PropertyType DWORD -Force
# Restart is required for SChannel changes to take effect
Restart-Computer -Confirm
Removing the Server Version Header
By default, IIS includes a Server: Microsoft-IIS/10.0 header in every response. This tells attackers exactly which web server and version they are dealing with, making it trivial to look up known vulnerabilities. Remove this header:
# Remove the Server header at the server level (IIS 10, Windows Server 2016+)
Set-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST" `
-filter "system.webServer/security/requestFiltering" `
-name "removeServerHeader" -value "true"
Also remove the X-Powered-By header which reveals the ASP.NET version:
For ASP.NET applications, also suppress the X-AspNet-Version and X-AspNetMvc-Version headers in your application’s Global.asax or Startup.cs:
# In web.config for ASP.NET WebForms/MVC
Disabling Directory Browsing
Directory browsing displays a file listing when no default document exists in a directory. This can expose internal file names, backup files, configuration samples, and upload directories to anyone who knows the URL. It should be disabled globally:
# Disable directory browsing globally
Set-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST" `
-filter "system.webServer/directoryBrowse" `
-name "enabled" -value "false"
# Verify
Get-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST" `
-filter "system.webServer/directoryBrowse" -name "enabled"
Request Filtering
The IIS Request Filtering module blocks requests that violate defined rules before they reach the application, providing a first layer of protection against common attack patterns:
Blocking .config, .bak, and .sql file extensions prevents accidental exposure of web.config files, database backups, and SQL scripts that might be placed in web-accessible directories. Always apply the principle of least privilege: only allow HTTP verbs that your application actually uses.
URL Scan Module
UrlScan is a legacy ISAPI filter that predates Request Filtering but provides some additional pattern-matching capabilities. On IIS 10, the built-in Request Filtering module covers most UrlScan use cases. However, UrlScan can still be installed for its logging and reporting capabilities. Download and install from Microsoft’s IIS downloads:
Invoke-WebRequest -Uri "https://www.iis.net/downloads/microsoft/urlscan" -OutFile "C:TempUrlScan.msi"
Start-Process msiexec.exe -ArgumentList "/i C:TempUrlScan.msi /quiet" -Wait
UrlScan is configured through %WinDir%System32inetsrvurlscanUrlScan.ini. Key settings include blocking specific file extensions, restricting HTTP verbs, limiting header length, and rejecting high-bit characters in URLs.
IIS Lockdown Best Practices Summary
Consolidating the settings above into a hardening checklist:
# 1. Verify all security headers are present
Invoke-WebRequest -Uri "https://yourdomain.com" -UseBasicParsing | Select-Object -ExpandProperty Headers
# 2. Verify TLS configuration
Get-TlsCipherSuite | Where-Object { $_.Name -match "ECDHE" } | Format-Table Name
# 3. Test HSTS
curl -sI https://yourdomain.com | grep -i strict-transport
# 4. Confirm Server header is removed
curl -sI https://yourdomain.com | grep -i server
# 5. Verify directory browsing is off
Invoke-WebRequest -Uri "https://yourdomain.com/uploads/" -UseBasicParsing
# Should return 403, not a directory listing
# 6. Test blocked extensions
Invoke-WebRequest -Uri "https://yourdomain.com/web.config" -UseBasicParsing
# Should return 404 or 403
Run a free online scan at securityheaders.com and ssllabs.com/ssltest after making these changes to verify the headers are set correctly and the TLS configuration receives an A or A+ rating. Mozilla’s SSL Configuration Generator at ssl-config.mozilla.org is an excellent reference for generating IIS-compatible SChannel and cipher suite configurations. Review headers and TLS settings quarterly or whenever IIS, Windows, or your application stack is updated, as new vulnerabilities and deprecations can affect previously adequate configurations.