
Self-Hosted Web: nginx, HTTP/2 and HTTP/3
I run the web server for gnpk.zip on the same VPS as the rest of AS218248. nginx serves a small static site — the peering policy, the ASN guide, and the service pages — over HTTP/2 and HTTP/3, with strict security headers and a clean bill from the public testers. It is a simple setup, but every part of it is there on purpose, and the whole stack is verifiable from the outside.
Why this exists
I run a web server to learn about the web. The site is the public face of the network: the peering policy, the how-to guide, the pages that document DNS, NTP, and mail. Running it myself means I control the whole stack, from the TLS certificate to the response headers, and I can see exactly what the public testers think of it. It is also a working example: if you are setting up your own site, this page shows what a hardened static site looks like.
The setup
| Web server | nginx 1.26.3, built from source |
|---|---|
| OS | Debian 13 (trixie) on the VPS (gnpk-as) |
| TLS | Let’s Encrypt ECDSA, TLS 1.2 and 1.3 |
| HTTP/2 | On |
| HTTP/3 | On (QUIC) |
| Compression | gzip + Brotli (level 6) |
The site is static HTML with an external stylesheet — no database, no logins, no forms, no user input. The config lives at /etc/nginx/sites-available/gnpk (symlinked from sites-enabled). The .zip TLD requires HTTPS (HSTS preload), so there is no HTTP fallback: plain HTTP redirects to HTTPS.
HTTP/2 and the custom 404
HTTP/2 went live Nov 13, 2026, along with a custom 404 page. The 404 page matches the site design (logo, nav, short message, link home) and returns a proper 404 status. The first attempt to enable HTTP/2 failed silently — the config edit never actually landed — and the fix was to re-apply it cleanly and verify with curl --http2, which returned version 2.
HTTP/3: the source build
HTTP/3 went live Nov 13, 2026, the same day. Debian’s nginx package does not ship the HTTP/3 module, so I built nginx 1.26.3 from source with --with-http_v3_module, against the system OpenSSL 3.5.6 — which has native QUIC support, so no QuicTLS or BoringSSL was needed. The configure flags matched the Debian layout (prefix, sbin, module paths, log paths), plus the HTTP/2, HTTP/3, SSL, gzip-static, and stub-status modules.
Two gotchas from the build, both fixed the same day:
- Brotli modules wiped.
make installremoved the dynamic Brotli module files, songinx -tfailed on the missing.so. Fixed by rebuildingngx_brotlifrom source as dynamic modules (--add-dynamic-module,make modules, copy the.sofiles to/etc/nginx/modules/). - sites-enabled symlink broken.
make installreplaced thesites-enabled/gnpksymlink with a separate regular file, so the two copies diverged — the available file had the HTTP/3 lines, the enabled file did not, and nginx kept serving HTTP/2 only. Fixed by restoring the symlink (rmthe file,ln -sto the available file) and reloading.
Verified live: curl --http3 returns version 3, and the response carries Alt-Svc: h3=":443"; ma=86400. HTTP/2 stays on alongside HTTP/3 for discovery and fallback. The benefit for a tiny static site is small — no head-of-line blocking, 1-RTT handshakes, connection migration — but it was a learning exercise, and the stack is now current.
One consequence of the source build: nginx no longer gets security updates via apt. I own keeping it current — when a new nginx version appears, rebuild from /usr/local/src with the same configure flags (plus the Brotli dynamic module), then make, make modules, make install, and copy the .so files.
Security headers
The site sends strict headers on every response:
| HSTS | max-age=63072000; includeSubDomains; preload |
|---|---|
| Content-Security-Policy | default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' (hashed); object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; upgrade-insecure-requests; manifest-src 'self'; |
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | no-referrer |
| Permissions-Policy | all disabled |
| Cross-Origin-Embedder-Policy | require-corp |
| Cross-Origin-Opener-Policy | same-origin |
| Cross-Origin-Resource-Policy | same-origin (cross-origin for /bimi-logo.svg only) |
The CSP is strict: no inline scripts at all, styles only from the external stylesheet plus hashed inline styles. img-src 'self' was added when the favicon was blocked; manifest-src 'self' was added when the browser blocked the PWA manifest. No Cloudflare, no CDN — the site is served directly from the VPS.
TLS 1.3 0-RTT: tried and turned off
On Nov 13, 2026, I enabled TLS 1.3 0-RTT (ssl_early_data on;) to test it. It worked — internet.nl confirmed 0-RTT on both IPs — but the tester rates 0-RTT as “Insufficient” (no replay protection at the TLS layer). The practical replay risk for a GET-only static site is basically zero, but I turned it off the same day to keep the clean 100% score (ssl_early_data off;). I will not re-enable it unless there is a reason to.
Encrypted Client Hello: deferred
ECH (RFC 9849) was researched Nov 13, 2026, and deferred. It needs nginx 1.29.4+ (for ssl_ech_file) and an ECH-capable OpenSSL (4.0, or a DEfO backport on 3.5); my nginx 1.26.3 and stock OpenSSL 3.5.6 do not support it. nginx also only supports ECH in shared mode, so the outer SNI still reveals gnpk.zip in cleartext — the privacy benefit for a single-host site is modest. Like RequireTLS for mail, ECH is a “when Debian ships it” item.
How it scores
| PageSpeed | 100 on all pages |
|---|---|
| Mozilla Observatory | A+, 155/100, 12/12 |
| internet.nl website | 100% — Hall of Fame |
| Hardenize | All green (DNS, email, web) |
| Lighthouse Accessibility | 100 |
The site is also a manifest-only PWA (no service worker, by choice): a manifest.json with home-screen icons, so browsers offer an install prompt. AMP is not used — Google dropped the AMP requirement for Top Stories in 2021, and PageSpeed 100 already meets Core Web Vitals.
Find AS218248 around the internet: PeeringDB · bgp.tools · RIPEstat · Hurricane Electric · Network · Peering policy