# Nginx TLS on Windows: certificates, HTTP/2 and sane defaults

> Configure PEM certificates, TLS 1.2/1.3, HTTP/2, SNI and HSTS on Windows Nginx, and test the handshake instead of trusting a browser padlock.

**Tested on**

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

## The short answer

Nginx on Windows reads **PEM files**, not the Windows certificate store:

```nginx
server {
    listen 443 ssl;
    http2 on;
    server_name lab.local;

    ssl_certificate     C:/nginx/certs/lab.crt;
    ssl_certificate_key C:/nginx/certs/lab.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
}
```

Export the certificate + private key as PEM (full chain first), restrict the key’s ACL, and test with `nginx -t`.

## HTTP to HTTPS

```nginx
server {
    listen 80;
    server_name lab.local;
    return 301 https://$host$request_uri;
}
```

Do not add HSTS until HTTPS works on every name; HSTS is cached by browsers.

## Headers

```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;
```

CSP is application-specific; do not paste a random CSP into every site. A wrong CSP breaks the app and gets removed, which teaches nothing.

## SNI and multiple names

One `server` block per certificate/name group. Nginx selects the block by SNI. A client that does not send SNI gets the `default_server` certificate.

```nginx
server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     C:/nginx/certs/default.crt;
    ssl_certificate_key C:/nginx/certs/default.key;
    return 444;
}
```

`return 444` closes the connection for unmatched names; do not serve a random site’s certificate.

## Verify

```powershell
cd C:\nginx
.\nginx.exe -t
.\nginx.exe -s reload
curl.exe -I --resolve lab.local:443:127.0.0.1 https://lab.local/ -k
openssl s_client -connect 127.0.0.1:443 -servername lab.local -tls1_3
```

Check:

- certificate subject/SAN matches the hostname;
- TLS 1.0/1.1 are not offered;
- HTTP/2 is negotiated (`curl --http2` shows `HTTP/2`);
- redirect goes to the public hostname, not an internal one.

> **Caution: HTTP/2 is not HTTP/3**
>
> `http2 on` gives HTTP/2 over TCP. HTTP/3 uses QUIC/UDP and is not available in the Windows Nginx build. Do not label an HTTP/2 site as HTTP/3.

## Windows certificate notes

- Nginx does not read `Cert:\LocalMachine\My`; export PFX, then convert to PEM/key.
- Keep the private key off the web root; ACL it to the Nginx service account + Administrators.
- Renewal must reload Nginx (`nginx -s reload`), not just replace the file.
- Self-signed is lab-only; clients must trust it explicitly.

## Lab boundary

E1. The lab served Nginx over HTTP on 8081; TLS, SNI and HTTP/2 were not reproduced on Windows. Validate on Server 2025 with a real certificate before claiming E2.

## Sources

- [nginx — HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html)
- [nginx — SSL module](https://nginx.org/en/docs/http/ngx_http_ssl_module.html)
- [nginx — HTTP/2 module](https://nginx.org/en/docs/http/ngx_http_v2_module.html)

---

Source: https://winbook.fyi/en/server/web/nginx-tls/
