A freshly provisioned Windows Server with IIS can serve pages within minutes of first login, but the default configuration is tuned for convenience, not exposure. If the box is reachable from the public internet, work through this checklist before pointing a real domain at it. Everything below uses built-in IIS features or one free Microsoft module, so you do not need a third-party WAF to get a defensible baseline.
1. Remove modules you do not use
Every enabled IIS module is a potential attack surface. The most common offender is WebDAV, which is off by default on modern installs but easy to enable accidentally and frequently abused to upload web shells. In Server Manager, remove WebDAV Publishing, and drop CGI, ASP and Server Side Includes unless a legacy app needs them:
Remove-WindowsFeature Web-DAV-Publishing, Web-CGI, Web-ASP, Web-Includes
Then confirm what remains with Get-WindowsFeature Web-Server | Select-Object -ExpandProperty SubFeatures.
2. Tighten Request Filtering
Request Filtering is built into IIS 7 and later, and it is your first line of defense against path traversal, oversized requests and dangerous extensions. Start with sane limits and deny the file types that should never be served:
appcmd set config /section:requestFiltering /requestLimits.maxUrl:4096 /requestLimits.maxQueryString:2048
appcmd set config /section:requestFiltering /+fileExtensions.denyExtensions.[extension='.config',allowed='false']
appcmd set config /section:requestFiltering /+fileExtensions.denyExtensions.[extension='.cs',allowed='false']
appcmd set config /section:requestFiltering /+fileExtensions.denyExtensions.[extension='.bak',allowed='false']
appcmd set config /section:requestFiltering /+hiddenSegments.[segment='App_Data']
appcmd set config /section:requestFiltering /+verbs.[verb='TRACE',allowed='false']
web.config and bin are hidden segments by default; add App_Data and any folder that holds backups. Blocking the TRACE verb closes a cross-site tracing (XST) vector that scanners probe for constantly.
3. Strip and replace HTTP headers
Out of the box IIS announces itself with X-Powered-By: ASP.NET and a Server header. Remove the first from web.config and add the security headers that cost nothing:
<configuration>
<system.web>
<httpRuntime enableVersionHeader="false" />
</system.web>
<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" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Removing the Server header requires an outbound URL Rewrite rule; if you already have the URL Rewrite module installed, add a rule that matches Server in the RESPONSE_SERVER server variable and sets it to an empty value.
4. Force TLS 1.2+ and redirect HTTP to HTTPS
Disable TLS 1.0 and 1.1 at the Schannel level so no application can silently fall back:
foreach ($p in "TLS 1.0","TLS 1.1") {
New-Item "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$p\Server" -Force | Out-Null
New-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$p\Server" `
-Name Enabled -Value 0 -PropertyType DWord -Force | Out-Null
New-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$p\Server" `
-Name DisabledByDefault -Value 1 -PropertyType DWord -Force | Out-Null
}
Restart-Service W3SVC -Force
Then require SSL on the site binding and add a URL Rewrite rule that permanently redirects port 80 to HTTPS (a 301 from http://host/ to https://host/ with logRewrittenUrl disabled).
5. Enable IP and Domain Restrictions with dynamic blocking
IIS 8 and later ship with IP and Domain Restrictions built in. Static deny lists stop known bad ranges, but the dynamic restrictions are what save you from credential-stuffing floods. Set aggressive but survivable limits:
appcmd set config /section:ipSecurity /enableDynamicIpRestriction:true `
/dynamicIpRestriction.maxConcurrentRequests:10 `
/dynamicIpRestriction.maxRequestsPerTimeInterval:20 `
/dynamicIpRestriction.timePeriod:00:01:00 `
/dynamicIpRestriction.denyAction:Forbidden
An IP that exceeds 20 requests in 60 seconds gets a 403 for the configured window. Tune the numbers against real traffic before launch; login endpoints and APIs will need higher ceilings than static pages.
6. Clean up bindings and the default site
Delete the Default Web Site or repoint it at a placeholder page, then give every real site its own host header. When hosting multiple HTTPS sites on one IP, use SNI rather than a wildcard certificate with a shared binding. Disable directory browsing explicitly (directoryBrowse should be false) and drop the HTTP binding entirely if the redirect rule handles port 80.
7. Least-privilege app pool identities
Give each site its own application pool with the built-in ApplicationPoolIdentity, leave Load User Profile off, and grant the pool identity only the ACLs it needs:
icacls "D:\wwwroot\contoso" /grant "IIS APPPOOL\contoso:(OI)(CI)(RX)"
Avoid granting IIS_IUSRS write access to web roots: it covers every pool on the server, so one compromised site becomes a pivot into all of them.
8. Turn off detailed errors
Stack traces and file paths in HTTP responses are a gift to attackers. For .NET Framework apps set <customErrors mode="On" />; for ASP.NET Core set ASPNETCORE_ENVIRONMENT=Production and stdoutLogEnabled="false". At the IIS level, set <httpErrors errorMode="Custom" /> and serve a generic 404 and 500 page.
9. Log what matters, rotate it, watch it
W3C logging should include the fields that make incident response possible: client IP, method, URI, status, and crucially TimeTaken. Rotate logs daily or by size, and treat a burst of 404s from one IP as a scan in progress – that is exactly what the dynamic IP restriction in step 5 is for.
10. Back up the configuration before and after
appcmd add backup "PreHardening"
# verify:
appcmd list backups
The applicationHost.config backup takes seconds and gives you a clean rollback point when a hardening change breaks an app.
| Setting | Default | Recommended | Why it matters |
|---|---|---|---|
| WebDAV | Disabled | Disabled (remove feature) | Common web-shell upload vector |
| TRACE verb | Allowed | Blocked | XST / scanner noise |
| X-Powered-By | Present | Removed | Reveals ASP.NET to attackers |
| TLS 1.0 / 1.1 | Enabled | Disabled | Obsolete ciphers, downgrade risk |
| Directory browsing | Off | Confirmed off | Source/config exposure |
| Detailed errors | On (localhost) | Custom pages | Info leakage in production |
An hour with this checklist before you launch saves a lot of log-digging later. If you are still deciding where to run IIS, the Windows VPS comparison table on our site is a good starting point, and the feature checklist on our site lists the admin access you should confirm a provider gives you (registry, firewall, and full IIS control).



