Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ FROM nginx:1.31-alpine

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf
COPY security-headers.conf /etc/nginx/security-headers.conf

# Copy built assets from builder
COPY --from=builder /build/dist /usr/share/nginx/html
Expand Down
15 changes: 6 additions & 9 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,8 @@ server {
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;
# Security headers (shared include; see security-headers.conf)
include /etc/nginx/security-headers.conf;

root /usr/share/nginx/html;
index index.html;
Expand All @@ -110,24 +103,28 @@ server {
# these must be no-cache (the real filenames are sw.js/registerSW.js,
# not service-worker.js).
location = /sw.js {
include /etc/nginx/security-headers.conf;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}
location = /registerSW.js {
include /etc/nginx/security-headers.conf;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}

# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)\$ {
include /etc/nginx/security-headers.conf;
expires 1y;
add_header Cache-Control "public, immutable";
}

# Health check
location /health {
include /etc/nginx/security-headers.conf;
access_log off;
default_type text/plain;
return 200 "healthy\n";
Expand Down
11 changes: 6 additions & 5 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,7 @@ http {
index index.html;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;
include /etc/nginx/security-headers.conf;

# Service worker + its registration bootstrap. vite-plugin-pwa's
# generateSW mode emits sw.js and registerSW.js (not the
Expand All @@ -128,31 +124,36 @@ http {
# content-hashed, so a new build simply ships a differently-named
# file rather than requiring cache invalidation of the old one.
location = /sw.js {
include /etc/nginx/security-headers.conf;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}
location = /registerSW.js {
include /etc/nginx/security-headers.conf;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}

# Runtime env config — must never be cached (changes per deployment)
location = /env-config.js {
include /etc/nginx/security-headers.conf;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}

# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
include /etc/nginx/security-headers.conf;
expires 1y;
add_header Cache-Control "public, immutable";
}

# Health check endpoint
location /health {
include /etc/nginx/security-headers.conf;
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
Expand Down
16 changes: 16 additions & 0 deletions security-headers.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Security headers shared by every location that serves content.
#
# nginx's add_header is inherited from an outer block ONLY when the current
# level defines no add_header of its own -- and the `always` flag does not
# change that. Any location that sets its own header (e.g. Cache-Control)
# therefore silently drops the entire server-level set. Every such location
# includes this file so the headers survive.
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; worker-src 'self' blob:; manifest-src 'self'" always;
# Ignored by user agents when received over plain HTTP (per RFC 6797), so it is
# safe to emit unconditionally and keeps the TLS server block from needing its
# own add_header -- which would re-trigger the inheritance problem above.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Loading