IIS Host Header Bindings: Serving Exactly One Canonical Hostname on a Windows Server

An IIS site that answers on every hostname your VPS responds to is a common and avoidable misconfiguration. On a Windows Server VPS the default site binds to *:80 — all unassigned IPs, all hostnames. That is fine on a laptop. In production it means https://windows-vps.org/blog/-style canonical hostnames, an IP address, and random subdomains all serve the same content, which splits crawl budget, breaks cookies, and leaks internal hostnames to scanners.

For a working reference architecture, see the hosting context on windows-vps.org. This article is the technical how-to: one bindings model that makes every request resolve to exactly one canonical hostname, then redirect anything that does not match.

Why a catch-all binding is a real problem

When IIS has no bindings that match the incoming Host header, it falls back to the first site that has a matching IP and port. Attackers and crawlers exploit this consistently:

  • Duplicate content: https://203.0.113.10/, https://www.example.com/, and https://example.internal/ all return HTTP 200 with identical HTML.
  • Cookie leakage: cookies scoped to .example.com are sent to any subdomain pointing at the same server, including ones you never intended to expose.
  • TLS misrouting: with SNI, a request for an unknown hostname may land on the wrong certificate and produce a name-mismatch warning that looks like your fault.
  • SEO dilution: search engines see several URLs for one page and split ranking signals between them.

Step 1: Inventory what IIS is actually serving

Start in an elevated PowerShell session. Enumerate every site and binding before you change anything:

Import-Module WebAdministration
Get-Website | Select-Object Name, State, PhysicalPath, Bindings |
    Format-List

Get-WebBinding | Select-Object ItemXPath, protocol, bindingInformation |
    Sort-Object bindingInformation

Look for bindings where the hostname portion (the third field after the IP and port) is empty. An empty hostname is the catch-all. On a fresh VPS template you will usually see one site named Default Web Site with *:80: and nothing else.

Step 2: Create explicit bindings for the canonical host only

Decide on exactly one canonical hostname — the one you will publish, link, and put in your sitemap. Pick one form and never publish the other. Then add explicit bindings:

New-WebBinding -Name "MyApp" -Protocol http  -Port 80  -HostHeader "app.example.com"
New-WebBinding -Name "MyApp" -Protocol https -Port 443 -HostHeader "app.example.com" `
    -SslFlags 1   # 1 = SNI required

The SslFlags 1 parameter is important on any VPS hosting more than one TLS site: it forces Server Name Indication so IIS selects the certificate by hostname rather than by IP address alone.

Step 3: Catch everything else and return 444 or a redirect

The cleanest pattern is three layers: the canonical site, a redirect site for the alternate form (www to apex, or the reverse), and an empty “reject” site bound to the catch-all binding that terminates unmatched traffic rather than serving content.

Incoming HostMatching siteResult
app.example.comMyApp200 — canonical, served
www.app.example.comRedirectSite301 to https://app.example.com
203.0.113.10 (raw IP)RejectSiteConnection closed / 404
unknown.example.comRejectSiteConnection closed / 404

IIS has no native 444 like nginx, but the equivalent behaviour is a catch-all site whose root is an empty folder holding a bare web.config that returns HTTP 404 with a null body and no server banner. Attackers who scan by IP stop learning anything useful about your deployment.

Step 4: Handle the port-443 blind spot

Catch-all HTTPS is subtler than HTTP. A client requesting an unknown hostname over TLS will fail during the handshake before IIS can route it, unless a default certificate is configured. Bind the catch-all HTTPS site to a self-signed or generic certificate so the handshake completes and your rejection site can answer cleanly. Keep the certificate out of any path the public canonical site uses.

HTTP/3 and the host-header edge case

If you enable HTTP/3 on IIS (Windows Server 2022 and later), the QUIC listener binds per-site and interacts with SNI differently from TCP. A site with a catch-all binding and HTTP/3 enabled can behave inconsistently across clients — some browsers reach the reject site by accident. Keep HTTP/3 enabled only on the canonical site, and re-run the host-header tests over both TCP and UDP after any protocol change.

Keeping the canonical set consistent across layers

IIS bindings are only one of three places a hostname is enforced. A hostname change that is not propagated everywhere creates exactly the redirect chains you set out to eliminate. Audit all three whenever you add a domain:

LayerWhat to checkTool
DNSA/AAAA records point only at serving hostsResolve-DnsName
IISExplicit host-header bindings, SNI flagGet-WebBinding
AppGenerated absolute URLs use canonical hostApp config / runtime test

An application that generates redirect URLs from the incoming Host header rather than a configured value is the most common source of accidental non-canonical URLs. Pin the public base URL in configuration so an attacker cannot force your app to emit links to a hostname they control.

Verification checklist

  1. Request the canonical host — expect HTTP 200 and correct content.
  2. Request the alternate (www or apex) — expect a single 301, never a chain of redirects.
  3. Request the raw IP — expect a rejection or 404, not your site.
  4. Request a made-up hostname with curl -H "Host: bogus.example.com" https://your-ip/ -k — expect rejection.
  5. Confirm curl -I https://canonical/ returns no redirect and the expected Strict-Transport-Security header.

Host-header hygiene is one of those fixes that costs twenty minutes and closes an entire class of duplicate-content and information-disclosure issues. Do it before you point a real domain at the box, and it never becomes a problem later.

Leave a Comment