From d966fab4caf07e8a29098f6adadcfc2321bc444d Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 11 Oct 2025 18:06:11 -0600 Subject: [PATCH] mejorando configuracion de seguridad --- docker-compose.yml | 42 +++++++++++++++-- nuxt.config.ts | 74 +++++++++++++++++++++++++++++- server/middleware/proxy-headers.ts | 72 +++++++++++++++++++++++------ 3 files changed, 167 insertions(+), 21 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1e9f6f3..be62842 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,26 +16,60 @@ services: # Habilitar Traefik - "traefik.enable=true" - # Router + # ========================================== + # Router público para recursos PWA (sin autenticación) + # ========================================== + - "traefik.http.routers.musica-nucleoriofrio-public.rule=Host(`musica.nucleoriofrio.com`) && (PathPrefix(`/_nuxt`) || PathPrefix(`/assets`) || Path(`/sw.js`) || PathPrefix(`/workbox-`) || Path(`/manifest.webmanifest`) || Path(`/manifest.json`) || Path(`/favicon.ico`) || Path(`/logo.png`) || Path(`/logo-192.png`) || Path(`/logo-512.png`) || Path(`/logo-maskable-512.png`) || Path(`/icon.svg`))" + - "traefik.http.routers.musica-nucleoriofrio-public.entrypoints=websecure" + - "traefik.http.routers.musica-nucleoriofrio-public.tls.certresolver=letsencrypt" + - "traefik.http.routers.musica-nucleoriofrio-public.priority=100" + # Solo headers de seguridad y cache para assets PWA + - "traefik.http.routers.musica-nucleoriofrio-public.middlewares=musica-pwa-headers" + - "traefik.http.routers.musica-nucleoriofrio-public.service=musica-nucleoriofrio-service" + + # ========================================== + # Router protegido para el resto de la app + # ========================================== - "traefik.http.routers.musica-nucleoriofrio.rule=Host(`musica.nucleoriofrio.com`)" - "traefik.http.routers.musica-nucleoriofrio.entrypoints=websecure" - "traefik.http.routers.musica-nucleoriofrio.tls.certresolver=letsencrypt" - + - "traefik.http.routers.musica-nucleoriofrio.priority=50" # Middlewares (orden: auth -> headers -> body-size) - "traefik.http.routers.musica-nucleoriofrio.middlewares=authentik-forward-auth@file,musica-headers,musica-body-size" + - "traefik.http.routers.musica-nucleoriofrio.service=musica-nucleoriofrio-service" - # Middleware: Headers personalizados + # ========================================== + # Middleware: Headers para assets PWA (sin cache agresivo) + # ========================================== + - "traefik.http.middlewares.musica-pwa-headers.headers.customrequestheaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.musica-pwa-headers.headers.customrequestheaders.X-Forwarded-Scheme=https" + - "traefik.http.middlewares.musica-pwa-headers.headers.customresponseheaders.X-Content-Type-Options=nosniff" + - "traefik.http.middlewares.musica-pwa-headers.headers.customresponseheaders.X-XSS-Protection=1; mode=block" + # Cache controlado por el Service Worker, no por Traefik + - "traefik.http.middlewares.musica-pwa-headers.headers.customresponseheaders.Cache-Control=public, max-age=0, must-revalidate" + # Permitir CORS para PWA + - "traefik.http.middlewares.musica-pwa-headers.headers.accesscontrolallowmethods=GET,OPTIONS" + - "traefik.http.middlewares.musica-pwa-headers.headers.accesscontrolalloworiginlist=https://musica.nucleoriofrio.com" + - "traefik.http.middlewares.musica-pwa-headers.headers.accesscontrolmaxage=100" + + # ========================================== + # Middleware: Headers personalizados para app protegida + # ========================================== - "traefik.http.middlewares.musica-headers.headers.customrequestheaders.X-Forwarded-Proto=https" - "traefik.http.middlewares.musica-headers.headers.customrequestheaders.X-Forwarded-Scheme=https" - "traefik.http.middlewares.musica-headers.headers.customresponseheaders.X-Frame-Options=SAMEORIGIN" - "traefik.http.middlewares.musica-headers.headers.customresponseheaders.X-Content-Type-Options=nosniff" - "traefik.http.middlewares.musica-headers.headers.customresponseheaders.X-XSS-Protection=1; mode=block" - - "traefik.http.middlewares.musica-headers.headers.customresponseheaders.Cache-Control=public, max-age=3600" + # Removed global cache header - let app control caching + # ========================================== # Middleware: Tamaño máximo de body (100MB para subir archivos) + # ========================================== - "traefik.http.middlewares.musica-body-size.buffering.maxrequestbodybytes=104857600" + # ========================================== # Service + # ========================================== - "traefik.http.services.musica-nucleoriofrio-service.loadbalancer.server.port=3000" - "traefik.http.services.musica-nucleoriofrio-service.loadbalancer.passhostheader=true" diff --git a/nuxt.config.ts b/nuxt.config.ts index 36a473f..7a5fe4f 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -39,27 +39,97 @@ export default defineNuxtConfig({ modules: [ '@vueuse/nuxt', - '@pinia/nuxt', + '@pinia/nuxt', ['@vite-pwa/nuxt', { registerType: 'autoUpdate', includeAssets: ['favicon.ico', 'logo.png', 'logo-192.png', 'logo-512.png', 'logo-maskable-512.png', 'icon.svg'], workbox: process.env.NODE_ENV === 'production' ? { navigateFallback: '/', + navigateFallbackDenylist: [ + // Never cache authentication redirects + /^\/outpost\.goauthentik\.io/, + /^\/akprox/, + ], cleanupOutdatedCaches: true, globPatterns: [ '**/*.{js,css,html,ico,png,svg}', '_nuxt/**/*.{js,css}', 'assets/**/*.{png,jpg,jpeg,svg,gif,webp}' ], + // Exclude authentication and sensitive paths from precaching + globIgnores: [ + '**/_payload.json', + '_nuxt/builds/**/*.json', + ], runtimeCaching: [ + // Static images: Cache-First (offline-ready) { - urlPattern: /\.(png|jpg|jpeg|svg|gif|webp)$/, + urlPattern: /\.(png|jpg|jpeg|svg|gif|webp|ico)$/, handler: 'CacheFirst', options: { cacheName: 'images-cache', expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 // 7 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + // API Music list: Network-First with offline fallback + { + urlPattern: /\/api\/music$/, + handler: 'NetworkFirst', + options: { + cacheName: 'api-music-list', + networkTimeoutSeconds: 10, + expiration: { + maxEntries: 5, + maxAgeSeconds: 60 * 5 // 5 minutes + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + // API Music files: Cache-First for downloaded tracks + { + urlPattern: /\/api\/music\/.+/, + handler: 'CacheFirst', + options: { + cacheName: 'music-files-cache', + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days + }, + cacheableResponse: { + statuses: [0, 200, 206] // Include partial content + }, + plugins: [ + { + // Handle authentication errors gracefully + handlerDidError: async ({ request }) => { + console.warn('[SW] Failed to fetch:', request.url) + // Return null to let the app handle the error + return null + } + } + ] + } + }, + // Nuxt build assets: Cache-First (immutable) + { + urlPattern: /\/_nuxt\/.+\.(js|css)$/, + handler: 'CacheFirst', + options: { + cacheName: 'nuxt-assets', + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year for hashed assets + }, + cacheableResponse: { + statuses: [0, 200] } } } diff --git a/server/middleware/proxy-headers.ts b/server/middleware/proxy-headers.ts index a432d18..7ade533 100644 --- a/server/middleware/proxy-headers.ts +++ b/server/middleware/proxy-headers.ts @@ -1,10 +1,30 @@ export default defineEventHandler(async (event) => { const url = getRequestURL(event) - - // Fix MIME type issues for Nuxt assets + + // ========================================== + // PWA Critical Resources: Service Worker & Manifest + // ========================================== + if (url.pathname === '/sw.js' || url.pathname.startsWith('/workbox-')) { + // Service Worker must NEVER be cached by browser - always check for updates + setHeader(event, 'Content-Type', 'application/javascript; charset=utf-8') + setHeader(event, 'Cache-Control', 'no-cache, no-store, must-revalidate') + setHeader(event, 'Service-Worker-Allowed', '/') + return + } + + if (url.pathname === '/manifest.webmanifest' || url.pathname === '/manifest.json') { + setHeader(event, 'Content-Type', 'application/manifest+json; charset=utf-8') + setHeader(event, 'Cache-Control', 'public, max-age=0, must-revalidate') + return + } + + // ========================================== + // Nuxt Build Assets: Let Service Worker control cache + // ========================================== if (url.pathname.startsWith('/_nuxt/')) { const ext = url.pathname.split('.').pop()?.toLowerCase() - + + // Set proper MIME types switch (ext) { case 'js': setHeader(event, 'Content-Type', 'application/javascript; charset=utf-8') @@ -22,36 +42,58 @@ export default defineEventHandler(async (event) => { setHeader(event, 'Content-Type', 'image/svg+xml; charset=utf-8') break } + + // Immutable assets (with hash in filename) can be cached aggressively + if (url.pathname.match(/\.[a-f0-9]{8,}\.(js|css|json|svg|png|jpg|webp|woff2?)$/)) { + setHeader(event, 'Cache-Control', 'public, max-age=31536000, immutable') + } else { + // Non-hashed assets: let Service Worker decide + setHeader(event, 'Cache-Control', 'public, max-age=0, must-revalidate') + } } - - // Handle proxy headers for API requests + + // ========================================== + // Static Assets (logos, icons, images) + // ========================================== + if (url.pathname.match(/\.(png|jpg|jpeg|svg|gif|webp|ico)$/)) { + // Let Service Worker control static image caching + setHeader(event, 'Cache-Control', 'public, max-age=0, must-revalidate') + } + + // ========================================== + // API Endpoints + // ========================================== if (url.pathname.startsWith('/api/')) { // Trust proxy headers const realIP = getHeader(event, 'x-real-ip') || getHeader(event, 'x-forwarded-for') const proto = getHeader(event, 'x-forwarded-proto') || 'http' const host = getHeader(event, 'host') - - // Set CORS headers for cross-origin requests through proxy + + // Set CORS headers for PWA offline support setHeader(event, 'Access-Control-Allow-Origin', '*') setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS') setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Range') - - // Handle music file requests specially + + // Handle music file requests if (url.pathname.startsWith('/api/music/')) { - // Ensure proper caching headers for audio files - setHeader(event, 'Cache-Control', 'public, max-age=3600') // 1 hour cache + // Music files: cache by Service Worker, not browser + setHeader(event, 'Cache-Control', 'public, max-age=0, must-revalidate') setHeader(event, 'Accept-Ranges', 'bytes') - - // Add security headers (but allow DevTools) + + // Security headers setHeader(event, 'X-Content-Type-Options', 'nosniff') - // Don't set X-Frame-Options DENY for development if (process.env.NODE_ENV === 'production') { setHeader(event, 'X-Frame-Options', 'DENY') } + } else { + // Other API endpoints: no cache + setHeader(event, 'Cache-Control', 'no-store, must-revalidate') } } - + + // ========================================== // Handle OPTIONS preflight requests + // ========================================== if (event.node.req.method === 'OPTIONS') { setHeader(event, 'Access-Control-Allow-Origin', '*') setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS')