NGINX: set Cache-Control via a map, not a nested location, so security headers survive
The Cache-Control rule I added in the previous commit used a nested `location ~* \.(html|css|js|webmanifest)$`. nginx does NOT inherit add_header into a block that declares its own add_header, so every HTML, CSS and JS response would have been served WITHOUT the CSP, HSTS, X-Frame-Options, Referrer-Policy and nosniff headers from the Phase S hardening — the headers dropped for exactly the files that matter most, and silently, since the pages would still work. Now computed by `map $uri $wp_cache_control` at http level and applied with one server-level add_header alongside the security headers, so nothing is scoped away. An empty value makes nginx omit the header entirely, so images and fonts stay cacheable. Applied to both the Docker config (nginx/conf.d/wp-suite.conf) and the bare-metal one (nginx-wp-suite.conf), which carries the same header set. Caught while checking whether the stack was safe to redeploy. Not verified with `nginx -t` — this machine has neither nginx nor docker — so DEPLOYMENT.md now records the rule and the one-line curl that confirms both headers are present after a deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,22 @@
|
||||
# 5. sudo nginx -t && sudo systemctl reload nginx
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Cache-Control per file type. Computed in a map rather than a nested location
|
||||
# because nginx's add_header is NOT inherited into a block that declares its own —
|
||||
# a `location ~* \.(html|css|js)$` setting only Cache-Control would silently drop the
|
||||
# CSP / HSTS / X-Frame-Options / nosniff headers below for exactly those files. An
|
||||
# empty value makes nginx omit the header, so images and fonts stay cacheable.
|
||||
#
|
||||
# Code must revalidate on every load: with no Cache-Control the browser applies
|
||||
# HEURISTIC freshness (~10% of the file's age), so the least recently changed file
|
||||
# gets the LONGEST lifetime — which is how a page ends up running against a
|
||||
# stylesheet or script from a previous deploy. ETag/Last-Modified keep it a 304.
|
||||
map $uri $wp_cache_control {
|
||||
default "";
|
||||
~*\.(?:html|css|js|webmanifest)$ "no-cache";
|
||||
~*/$ "no-cache"; # directory index -> index.html
|
||||
}
|
||||
|
||||
# Redirect plain HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
@@ -40,6 +56,8 @@ server {
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self'" always;
|
||||
# Empty for anything that isn't code, in which case nginx omits the header.
|
||||
add_header Cache-Control $wp_cache_control always;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
|
||||
Reference in New Issue
Block a user