# Security headers on IIS and Nginx: HSTS, CSP and the ones that matter

> Add HSTS, CSP, nosniff, frame protection and Referrer-Policy with IIS/Nginx, test them, and avoid breaking the app with a copied CSP.

**Tested on**

- Build: 26100.33650
- Edition: Standard / Datacenter
- Architecture: x64
- Evidence: E1
- Verified: 2026-10-29

## The short answer

Start with headers that rarely break apps:

```http
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: frame-ancestors 'self'
```

Add a full CSP only after inventorying scripts/styles/frames. A copied CSP will break the app and get removed.

## IIS

`web.config`:

```xml
<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
      <add name="X-Content-Type-Options" value="nosniff" />
      <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
      <add name="Content-Security-Policy" value="frame-ancestors 'self'" />
      <remove name="X-Powered-By" />
    </customHeaders>
  </httpProtocol>
</system.webServer>
```

IIS may add `X-Powered-By`; remove it. Do not add HSTS to HTTP responses; browsers ignore it, but it signals a config mistake.

## Nginx

```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
server_tokens off;
```

`always` applies to error responses too.

## HSTS

- Only after every name on the host works over HTTPS.
- `includeSubDomains` affects all subdomains; be sure.
- Start with a short `max-age` in a pilot; raising it is easy, rolling back is slow (browsers cache it).
- Do not add `preload` unless you understand the removal process.

## CSP

Use report-only first:

```http
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
```

Then enforce with nonces/hashes for inline scripts. `frame-ancestors` is the modern replacement for `X-Frame-Options`; keep XFO only for old clients if needed.

## Verify

```powershell
curl.exe -I https://lab.example/
curl.exe -I https://lab.example/assets/app.js
```

Check headers on HTML, static assets and error pages. A header added only on 200 responses is incomplete.

> **Caution: CSP is application-specific**
>
> A CSP for a static site is not a CSP for a SPA with inline bootstrap, third-party analytics and WebSockets. Inventory sources; use report-only; change one directive at a time.

## Lab boundary

E1. The lab did not apply these headers or run a CSP report collector; the snippets are the starting point to validate on Server 2025.

## Sources

- [OWASP — Secure headers](https://owasp.org/www-project-secure-headers/)
- [MDN — Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)
- [MDN — Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)

---

Source: https://winbook.fyi/en/server/security/headers/
