How to Secure IIS: Security Headers, TLS 1.3, HSTS on Windows Server 2025

A freshly installed IIS server on Windows Server 2025 exposes several attack surfaces that default configuration does not address: version disclosure headers that help attackers fingerprint your stack, missing HTTP security headers that leave browsers unprotected against cross-site scripting and clickjacking, weak TLS protocol versions that enable downgrade attacks, and unnecessary IIS modules that increase attack surface. This tutorial covers every layer of IIS hardening — from adding the complete set of modern security headers in web.config through enforcing TLS 1.3 and HSTS, using the IIS Crypto tool for cipher suite management, and removing modules your application does not need.

Prerequisites

  • Windows Server 2025 with IIS 10 and at least one HTTPS-enabled site
  • A valid SSL/TLS certificate bound to port 443
  • PowerShell 5.1 or later running as Administrator
  • Optional: IIS Crypto 3.x by Nartac Software (free download from nartac.com/Products/IISCrypto)
  • A test client or browser for verifying headers (curl, SecurityHeaders.com)

Step 1: Add Security Headers via web.config

HTTP security headers instruct the browser to apply protective policies regardless of what the application itself does. All headers can be applied globally to an IIS site by adding them to the site’s root web.config:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>

        <!-- Prevent MIME-type sniffing -->
        <add name="X-Content-Type-Options" value="nosniff" />

        <!-- Deny framing by other origins (clickjacking protection) -->
        <add name="X-Frame-Options" value="SAMEORIGIN" />

        <!-- Legacy XSS filter for older browsers (modern browsers ignore it) -->
        <add name="X-XSS-Protection" value="1; mode=block" />

        <!-- Control referrer information sent to third-party origins -->
        <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />

        <!-- Content Security Policy — restrict asset origins -->
        <add name="Content-Security-Policy"
             value="default-src 'self'; script-src 'self' 'nonce-{NONCE}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" />

        <!-- Permissions Policy — disable unused browser features -->
        <add name="Permissions-Policy"
             value="camera=(), microphone=(), geolocation=(), payment=(), usb=()" />

      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Content-Security-Policy is the most powerful header and requires careful tuning for your specific application. Start with a permissive policy in report-only mode (Content-Security-Policy-Report-Only) and review violations before switching to enforcement mode. Replace 'nonce-{NONCE}' with a server-generated per-request nonce for inline scripts.

Step 2: Remove IIS Version Disclosure Headers

By default, IIS adds Server: Microsoft-IIS/10.0 and ASP.NET adds X-Powered-By: ASP.NET to every response. These headers reveal your stack to attackers. Remove them:

<!-- Remove X-Powered-By -->
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Removing the Server header requires a URL Rewrite outbound rule because IIS adds it at the kernel level. Install the URL Rewrite module then add:

<rewrite>
  <outboundRules>
    <rule name="Remove Server Header">
      <match serverVariable="RESPONSE_Server" pattern=".+" />
      <action type="Rewrite" value="" />
    </rule>
  </outboundRules>
</rewrite>

Alternatively, use the removeServerHeader attribute added in IIS 10 on Windows Server 2019 and later:

Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.webServer/security/requestFiltering" `
  -Name "removeServerHeader" `
  -Value $true

Step 3: Configure HSTS (HTTP Strict Transport Security)

HSTS instructs browsers to only connect to your domain over HTTPS for a specified period, even if the user types a plain HTTP URL. It eliminates SSL-stripping attacks. On Windows Server 2025 with IIS 10, HSTS can be configured in web.config or via IIS Manager’s HSTS feature:

<!-- Add HSTS header via customHeaders -->
<add name="Strict-Transport-Security"
     value="max-age=31536000; includeSubDomains; preload" />

Or use the IIS 10 native HSTS configuration (Windows Server 2019+):

# Enable HSTS natively in IIS (applies at the site level)
Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.applicationHost/sites/site[@name='Default Web Site']/hsts" `
  -Name "enabled" -Value $true

Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.applicationHost/sites/site[@name='Default Web Site']/hsts" `
  -Name "max-age" -Value 31536000

Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.applicationHost/sites/site[@name='Default Web Site']/hsts" `
  -Name "includeSubDomains" -Value $true

Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.applicationHost/sites/site[@name='Default Web Site']/hsts" `
  -Name "preload" -Value $true

Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.applicationHost/sites/site[@name='Default Web Site']/hsts" `
  -Name "redirectHttpToHttps" -Value $true

Do not set includeSubDomains or preload until you have confirmed that every subdomain of your domain serves valid HTTPS — otherwise you will lock users out of HTTP-only subdomains for the entire max-age duration.

Step 4: Disable TLS 1.0 and TLS 1.1

TLS 1.0 is vulnerable to BEAST and POODLE attacks. TLS 1.1 lacks modern cipher suites. Both should be disabled on any internet-facing IIS server:

$protocols = @("TLS 1.0", "TLS 1.1")
$roles     = @("Server", "Client")

foreach ($proto in $protocols) {
  foreach ($role in $roles) {
    $path = "HKLM:SYSTEMCurrentControlSetControlSecurityProvidersSCHANNELProtocols$proto$role"
    if (-not (Test-Path $path)) {
      New-Item -Path $path -Force | Out-Null
    }
    Set-ItemProperty -Path $path -Name "Enabled"          -Value 0 -Type DWord
    Set-ItemProperty -Path $path -Name "DisabledByDefault" -Value 1 -Type DWord
    Write-Host "Disabled: $proto $role"
  }
}

Step 5: Enable TLS 1.3 via Registry

TLS 1.3 is the current standard, offering improved security (0-RTT optional, fewer negotiation round trips, and removal of vulnerable cipher suites). On Windows Server 2025, TLS 1.3 is supported natively by Schannel and should already be enabled, but you can ensure it explicitly:

$protocols = @("TLS 1.2", "TLS 1.3")
$roles     = @("Server", "Client")

foreach ($proto in $protocols) {
  foreach ($role in $roles) {
    $path = "HKLM:SYSTEMCurrentControlSetControlSecurityProvidersSCHANNELProtocols$proto$role"
    if (-not (Test-Path $path)) {
      New-Item -Path $path -Force | Out-Null
    }
    Set-ItemProperty -Path $path -Name "Enabled"          -Value 1 -Type DWord
    Set-ItemProperty -Path $path -Name "DisabledByDefault" -Value 0 -Type DWord
    Write-Host "Enabled: $proto $role"
  }
}

Write-Host "Reboot required for SCHANNEL changes to take effect."

After rebooting, verify TLS version negotiation using:

curl -v --tlsv1.3 https://example.com/ 2>&1 | Select-String "TLS|SSL"

Step 6: Harden Cipher Suites with IIS Crypto

While registry edits handle TLS protocol versions, cipher suite ordering and individual suite selection require the HKLM:SOFTWAREPoliciesMicrosoftCryptographyConfigurationSSL0010002 key or Group Policy. The free IIS Crypto tool by Nartac Software provides a GUI and CLI for this:

# IIS Crypto CLI (run from its installation directory)
IISCryptoCli.exe /template best

# Available templates: best, pci40, fips140, default
# "best" enables TLS 1.2 + 1.3, disables TLS 1.0/1.1/SSL 3.0,
# enables only strong cipher suites (AES-GCM, CHACHA20), removes RC4, 3DES, NULL

After applying the template, IIS Crypto shows a summary of enabled protocols and cipher suites. Always test with SSL Labs after changes to confirm an A or A+ rating.

Step 7: Remove Unnecessary IIS Modules

Every IIS module that processes requests is a potential attack surface. Remove modules your application does not use. Common candidates for removal on a production site hosting a PHP or ASP.NET Core application:

<configuration>
  <system.webServer>
    <modules>
      <!-- Remove modules not needed by your application -->
      <remove name="WebDAVModule" />         <!-- WebDAV — only if not used -->
      <remove name="WindowsAuthentication" /> <!-- Only needed for intranet apps -->
      <remove name="CertificateMappingAuthenticationModule" />
      <remove name="AnonymousAuthenticationModule" />  <!-- Remove if using Forms auth -->
    </modules>
  </system.webServer>
</configuration>

To identify which modules are active on your server:

Get-WebConfiguration -Filter "system.webServer/modules/add" -PSPath "MACHINE/WEBROOT/APPHOST" |
  Select-Object Name, Type | Sort-Object Name

Step 8: Verify the Full Security Header Stack

After applying all changes, validate the response headers with curl:

curl -I https://example.com/ 2>&1 | Select-String `
  "Strict-Transport|Content-Security|X-Frame|X-Content-Type|Referrer|Permissions|Server|Powered"

Expected output should show HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy — and should not show Server: or X-Powered-By:. Use securityheaders.com for an automated grade and a detailed breakdown of missing or misconfigured headers.

Securing IIS on Windows Server 2025 is a layered process that requires attention at every level of the stack: from the HTTP response headers that instruct browsers how to handle your content, through the TLS configuration that protects data in transit, to the IIS module inventory that defines what code runs on every request. The steps in this tutorial — adding the complete security header set, removing version disclosure, enforcing HSTS, disabling legacy TLS, enabling TLS 1.3, hardening cipher suites with IIS Crypto, and pruning unnecessary modules — represent the current baseline for a production-ready, internet-facing IIS deployment. Revisit your configuration whenever a new CVE targets a TLS cipher suite or when your Content Security Policy requirements change as your application evolves.