Skip to content

fix(nginx): restore /.well-known/, de-duplicate security headers, HSTS on www - #90

Merged
wjm2202 merged 3 commits into
mainfrom
nextjs-16-upgrade
Sep 10, 2026
Merged

wjm2202 merged 3 commits into
mainfrom
nextjs-16-upgrade

Conversation

@wjm2202

@wjm2202 wjm2202 commented Sep 10, 2026

Copy link
Copy Markdown
Owner
  1. /.well-known/ was 404ing. location ~ /\. treats .well-known as a
    dot-directory and denied it. The block sat inert in the repo for months
    until af9a16a (2026-08-23) synced nginx.conf to the droplet for the first
    time — the same deploy that broke /brand/*. That one was visible and got
    fixed; this one was silent, because an unreachable JWKS does not fail
    verification, it degrades keySource to embedded-fallback-jwks-unreachable
    and still prints "VERIFIED — CRYPTOGRAPHICALLY INTACT".

    Proven by same-file A/B: the snapshot at mmpm-cli/snapshots/
    silver-flat-6czf-...-2026-07-17T03-20-38Z.json verified with keySource=jwks
    on 2026-07-17 and reported the fallback on 2026-09-10 — same machine, same
    verifier build, byte-identical input. Only the endpoint changed.

    Blast radius: the compute provisioner passes MMPM_JWKS_URI= into
    every provisioned customer substrate, so every customer's verifier was
    falling back, with three SPEC-SNAPSHOT.md §6.4 case-(b) guarantees inactive
    — JWKS revocation markers, the embedded-vs-published key byte-comparison,
    and case-(c) kid-absent rejection.

    Fixed with location ^~ /.well-known/ before the deny block. The ^~ is
    load-bearing: regex locations otherwise take precedence over prefix
    locations, so a plain location /.well-known/ would still lose.

  2. Every response carried four security headers twice. nginx and
    next.config.ts both set them: "DENY, DENY", "nosniff, nosniff", doubled
    Referrer-Policy and Permissions-Policy. Not cosmetic — "nosniff" must be
    the entire X-Content-Type-Options value, and browsers may discard a
    multi-valued X-Frame-Options, so the duplication risked disabling the
    protections it appeared to double. seo-headers.test.ts:61 already pinned
    these four to the app, so the app keeps ownership and nginx's copies are
    removed. nginx now owns only X-XSS-Protection, HSTS and CSP.

  3. www→apex 301 served no HSTS. hstspreload.org requires redirects to carry
    it and this domain is on the preload list.

  4. Removed dead OCSP stapling. Let's Encrypt dropped OCSP URLs from certs on
    2025-05-07 and shut its responders down on 2025-08-06; our certs carry no
    OCSP URL, so ssl_stapling was a no-op and the resolver existed only to
    serve it. Every proxy_pass here targets literal 127.0.0.1.

/_next/static/ additionally repeats X-XSS-Protection and HSTS: it declares
its own add_header, and per the nginx docs that suppresses inheritance
entirely. CSP is deliberately not repeated there — it is inert on a
subresource response.

Tests: src/nginx-security-headers.test.ts (16, new) and
src/nginx-wellknown-jwks.test.ts (7, new). Both proven load-bearing by
running them against the reconstructed pre-fix config — 10 of 16 fail.
Full suite green: 158 files, 2155 passed, 3 skipped.

Not included, each needing its own change:

  • rate limiting — limit_req_zone is http-context only, so recovering the
    lost 2026-06-29 zones needs a coordinated edit to the droplet's main
    nginx.conf; adding limit_req alone fails nginx -t
  • listen ... http2 → http2 on — needs the droplet's nginx version first
    (directive added in 1.25.1; unknown directive on anything older)
  • CSP still allows 'unsafe-inline'/'unsafe-eval' — needs Next.js nonce work

nginx -t not run locally (no nginx available); the deploy step validates
before reloading and restores on failure.

@wjm2202
wjm2202 merged commit 437942b into main Sep 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant