# -------------------------------- # Base: safe in most environments # -------------------------------- Options -Indexes DirectoryIndex index.html Require all denied RewriteEngine On # Never redirect local/dev hosts RewriteCond %{HTTP_HOST} ^(localhost|127\.0\.0\.1|fr\.local|192\.168\.[0-9]+\.[0-9]+)$ [NC] RewriteRule ^ - [L] # --- HTTPS redirect --- # Use ONE of these blocks. # A) Direct TLS on this server (enable this if Apache terminates HTTPS here) #RewriteCond %{HTTPS} off #RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] # B) Behind a reverse proxy/CDN that sets X-Forwarded-Proto #RewriteCond %{HTTP:X-Forwarded-Proto} =http [OR] #RewriteCond %{HTTP:X-Forwarded-Proto} ^$ #RewriteCond %{HTTPS} !=on #RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] # Don't interfere with ACME/http-01 if you do your own certs #RewriteCond %{REQUEST_URI} ^/.well-known/acme-challenge/ #RewriteRule - - [L] # --- MIME types (fonts/SVG/ESM) --- AddType font/woff2 .woff2 AddType font/woff .woff AddType image/svg+xml .svg AddType application/javascript .mjs # --- Security headers --- Header always set X-Frame-Options "SAMEORIGIN" Header always set X-XSS-Protection "1; mode=block" Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" Header always set X-Download-Options "noopen" Header always set Expect-CT "max-age=86400, enforce" Header always set Cross-Origin-Resource-Policy "same-origin" Header always set X-Permitted-Cross-Domain-Policies "none" # HSTS only when actually on HTTPS Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'" # CSP (modules, blobs, workers, etc.) Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self' 'sha256-ajmGY+5VJOY6+8JHgzCqsqI8w9dCQfAmqIkFesOKItM='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'" # --- Caching (query-string based, no env vars needed) --- # HTML/PHP: no cache (only if PHP didn’t already set it) Header setifempty Cache-Control "no-cache, no-store, must-revalidate" Header setifempty Pragma "no-cache" Header setifempty Expires "0" # version.js: always non-cacheable Header set Cache-Control "no-cache, no-store, must-revalidate" Header set Pragma "no-cache" Header set Expires "0" # Unversioned JS/CSS: 1 hour Header set Cache-Control "public, max-age=3600, must-revalidate" "expr=%{QUERY_STRING} !~ /(^|&)v=/" # Unversioned static (images/fonts): 7 days Header set Cache-Control "public, max-age=604800" "expr=%{QUERY_STRING} !~ /(^|&)v=/" # --- Versioned assets (?v=...) : 1 year + immutable (override anything else) --- # Only when query string has v= Header unset Cache-Control "expr=%{QUERY_STRING} =~ /(^|&)v=/" Header unset Expires "expr=%{QUERY_STRING} =~ /(^|&)v=/" Header set Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable" "expr=%{QUERY_STRING} =~ /(^|&)v=/" # --- Compression --- BrotliCompressionQuality 5 AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml # --- Disable TRACE --- RewriteCond %{REQUEST_METHOD} ^TRACE RewriteRule .* - [F]