deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Four nginx misconfigurations that pass config checks and silently break production

A dev.to guide by the gixy-ng maintainer details four nginx configs that validate cleanly yet quietly disable OCSP stapling, access control, and HTTP/3 connections.

Four nginx misconfigurations that pass config checks and silently break production

A guide published on dev.to by the maintainer of gixy-ng, a static analyzer for nginx configurations, catalogs four misconfigurations that pass nginx -t, boot without complaint, and produce no runtime errors — while quietly disabling the exact behavior they were meant to enable. The author's argument is that this class of failure is why static analysis matters for nginx: a mistake that generates no signal at runtime cannot be caught by watching logs.

OCSP stapling that never staples

Turning on ssl_stapling accomplishes nothing unless nginx can resolve the certificate authority's OCSP responder. As the post explains, nginx does not use the system resolver for runtime lookups; it performs them only when a resolver directive is in scope. With no resolver configured, the revocation-status fetch never happens and stapling simply does not occur, leaving clients to run their own OCSP queries — the work stapling was supposed to take off their plate.

The fix is a resolver directive pointing at local or cloud-internal DNS; the author cautions that a public resolver such as 8.8.8.8 sends every internal lookup off your network in cleartext. You can verify with openssl s_client and its -status flag, checking for a successful OCSP response — and run the check twice, because the first handshake after a reload often goes out unstapled while the fetch happens in the background. One modern wrinkle: according to the post, Let's Encrypt stopped serving OCSP in August 2025, so for its certificates the correct move is removing ssl_stapling entirely rather than adding a resolver.

Access rules that admit everyone

The ngx_http_access_module evaluates rules in order and stops at the first match — and grants access when nothing matches at all. An allow list with no trailing deny all; therefore serves any address that matches neither rule, even though the intent reads clearly to a human.

The subtler trap is inheritance. Access rules carry down from an outer context only when the inner context defines none of its own, so a single allow inside a location throws away the entire server-level rule set, including its deny all. The post's example adds one allow to a /metrics/ location and inadvertently makes the endpoint public — tightening access made things looser.

return running before access checks

Position in the file does not decide execution order. Directives from different modules run in different request phases: return belongs to the rewrite phase, while allow and deny belong to the access phase. A return 200 inside a location ends the request before the access rules are ever consulted, so a complete, correct allow list guarding a health endpoint is effectively dead code.

The workaround routes the canned response through an internal redirect — try_files into a named location holding the return — because try_files runs in the content phase, after access evaluation. The author pairs it with open_file_cache settings so the redirect does not pay a filesystem lookup per candidate on every request, avoiding the trap of fixing one finding by creating another.

QUIC connections dropped on every reload

The final case combines three individually harmless settings: quic_bpf on, reuseport on a QUIC listener, and more than one worker. The eBPF program that quic_bpf attaches routes packets to the correct worker by QUIC connection ID, but on reload the BPF socket map retains entries for workers that are shutting down. The result, per the author, is that roughly half of live QUIC connections are dropped with nothing appearing in logs or counters — clients time out and slip back to HTTP/2 over TCP. The post identifies this as a known upstream issue (nginx/nginx#425) that remains open in mainline, and recommends quic_bpf off as the trade almost every deployment should take. Anyone reloading to pick up renewed 90-day certificates hits the bug every couple of months.

Scanning for these

gixy-ng installs via pip. The author recommends scanning the output of nginx -T rather than whichever file you happen to be editing, because that dump resolves every include and exposes problems that only exist once the pieces are combined. Useful flags include -l 2 for medium-severity findings and above, -f for CI pipelines, and --nginx-version, which enables version-specific CVE checks — the tool is config-only and has no view of your binary, so the CVE check stays quiet unless told what version is running.

Why it matters

nginx's fast validation loop catches typos, not semantics. These four patterns share a property that makes them dangerous at scale: no start-up error, no log line, no metric — just security exposure (public admin paths, skipped revocation checks) or quiet regressions (HTTP/3 traffic sagging after routine reloads). Folding config-only static analysis into CI is, for now, the only practical way to detect them before production traffic does.

  • #nginx
  • #web-server
  • #devops
  • #static-analysis
  • #http3

Related posts