M4: security pass — strict CSP, header split, build-time scan
All JS moved to external /site.js → script-src 'self' with no inline JS, hashes or eval. Full header set via nginx (CSP, nosniff, frame-deny, referrer, permissions, COOP/CORP); HSTS stays at the CF edge. Shared headers include avoids the location add_header reset footgun. Build-time secret/inline-script/third-party scan gate. SECURITY.md documents posture.
This commit is contained in:
+12
-12
@@ -11,28 +11,28 @@ server {
|
||||
# Don't leak the nginx version.
|
||||
server_tokens off;
|
||||
|
||||
# ---- Security headers (origin) -------------------------------------------
|
||||
# These travel with the artifact. HSTS + HTTPS redirect are set at the
|
||||
# Cloudflare edge (the tunnel terminates TLS), so they are NOT set here.
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
|
||||
|
||||
# ---- Content-Security-Policy ---------------------------------------------
|
||||
# FILLED IN M4: Astro computes the inline script/style hashes at build; the
|
||||
# final policy is emitted here as a header (a <meta> CSP can't set
|
||||
# frame-ancestors). Until then, a conservative baseline:
|
||||
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||
# Security headers (CSP, nosniff, frame, etc.) — applied site-wide.
|
||||
# Re-included in each location below that sets its own add_header, because
|
||||
# a location-level add_header drops all inherited ones.
|
||||
include /etc/nginx/security-headers.conf;
|
||||
|
||||
# ---- Caching -------------------------------------------------------------
|
||||
# Astro emits content-hashed assets under /_astro/ — cache them hard.
|
||||
location /_astro/ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
}
|
||||
|
||||
# Non-fingerprinted top-level script — revalidate so updates propagate.
|
||||
location = /site.js {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
# HTML is revalidated so deploys show up immediately.
|
||||
location ~* \.html$ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Shared security headers. `include`d in the server block AND in every location
|
||||
# that sets its own add_header (because add_header in a location REPLACES, not
|
||||
# merges, inherited headers — the classic nginx footgun).
|
||||
#
|
||||
# HSTS + HTTP->HTTPS redirect are set at the Cloudflare edge (TLS terminates
|
||||
# there), so they are intentionally NOT here.
|
||||
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; worker-src 'self'; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
Reference in New Issue
Block a user