mejorando configuracion de seguridad
All checks were successful
build-and-deploy / build (push) Successful in 26s
build-and-deploy / deploy (push) Successful in 3s

This commit is contained in:
2025-10-11 18:06:11 -06:00
parent 100ba45f57
commit d966fab4ca
3 changed files with 167 additions and 21 deletions

View File

@@ -16,26 +16,60 @@ services:
# Habilitar Traefik # Habilitar Traefik
- "traefik.enable=true" - "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.rule=Host(`musica.nucleoriofrio.com`)"
- "traefik.http.routers.musica-nucleoriofrio.entrypoints=websecure" - "traefik.http.routers.musica-nucleoriofrio.entrypoints=websecure"
- "traefik.http.routers.musica-nucleoriofrio.tls.certresolver=letsencrypt" - "traefik.http.routers.musica-nucleoriofrio.tls.certresolver=letsencrypt"
- "traefik.http.routers.musica-nucleoriofrio.priority=50"
# Middlewares (orden: auth -> headers -> body-size) # 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.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-Proto=https"
- "traefik.http.middlewares.musica-headers.headers.customrequestheaders.X-Forwarded-Scheme=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-Frame-Options=SAMEORIGIN"
- "traefik.http.middlewares.musica-headers.headers.customresponseheaders.X-Content-Type-Options=nosniff" - "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.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) # Middleware: Tamaño máximo de body (100MB para subir archivos)
# ==========================================
- "traefik.http.middlewares.musica-body-size.buffering.maxrequestbodybytes=104857600" - "traefik.http.middlewares.musica-body-size.buffering.maxrequestbodybytes=104857600"
# ==========================================
# Service # Service
# ==========================================
- "traefik.http.services.musica-nucleoriofrio-service.loadbalancer.server.port=3000" - "traefik.http.services.musica-nucleoriofrio-service.loadbalancer.server.port=3000"
- "traefik.http.services.musica-nucleoriofrio-service.loadbalancer.passhostheader=true" - "traefik.http.services.musica-nucleoriofrio-service.loadbalancer.passhostheader=true"

View File

@@ -39,27 +39,97 @@ export default defineNuxtConfig({
modules: [ modules: [
'@vueuse/nuxt', '@vueuse/nuxt',
'@pinia/nuxt', '@pinia/nuxt',
['@vite-pwa/nuxt', { ['@vite-pwa/nuxt', {
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'logo.png', 'logo-192.png', 'logo-512.png', 'logo-maskable-512.png', 'icon.svg'], includeAssets: ['favicon.ico', 'logo.png', 'logo-192.png', 'logo-512.png', 'logo-maskable-512.png', 'icon.svg'],
workbox: process.env.NODE_ENV === 'production' ? { workbox: process.env.NODE_ENV === 'production' ? {
navigateFallback: '/', navigateFallback: '/',
navigateFallbackDenylist: [
// Never cache authentication redirects
/^\/outpost\.goauthentik\.io/,
/^\/akprox/,
],
cleanupOutdatedCaches: true, cleanupOutdatedCaches: true,
globPatterns: [ globPatterns: [
'**/*.{js,css,html,ico,png,svg}', '**/*.{js,css,html,ico,png,svg}',
'_nuxt/**/*.{js,css}', '_nuxt/**/*.{js,css}',
'assets/**/*.{png,jpg,jpeg,svg,gif,webp}' 'assets/**/*.{png,jpg,jpeg,svg,gif,webp}'
], ],
// Exclude authentication and sensitive paths from precaching
globIgnores: [
'**/_payload.json',
'_nuxt/builds/**/*.json',
],
runtimeCaching: [ runtimeCaching: [
// Static images: Cache-First (offline-ready)
{ {
urlPattern: /\.(png|jpg|jpeg|svg|gif|webp)$/, urlPattern: /\.(png|jpg|jpeg|svg|gif|webp|ico)$/,
handler: 'CacheFirst', handler: 'CacheFirst',
options: { options: {
cacheName: 'images-cache', cacheName: 'images-cache',
expiration: { expiration: {
maxEntries: 50, maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 * 7 // 7 days 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]
} }
} }
} }

View File

@@ -1,10 +1,30 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const url = getRequestURL(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/')) { if (url.pathname.startsWith('/_nuxt/')) {
const ext = url.pathname.split('.').pop()?.toLowerCase() const ext = url.pathname.split('.').pop()?.toLowerCase()
// Set proper MIME types
switch (ext) { switch (ext) {
case 'js': case 'js':
setHeader(event, 'Content-Type', 'application/javascript; charset=utf-8') 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') setHeader(event, 'Content-Type', 'image/svg+xml; charset=utf-8')
break 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/')) { if (url.pathname.startsWith('/api/')) {
// Trust proxy headers // Trust proxy headers
const realIP = getHeader(event, 'x-real-ip') || getHeader(event, 'x-forwarded-for') const realIP = getHeader(event, 'x-real-ip') || getHeader(event, 'x-forwarded-for')
const proto = getHeader(event, 'x-forwarded-proto') || 'http' const proto = getHeader(event, 'x-forwarded-proto') || 'http'
const host = getHeader(event, 'host') 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-Origin', '*')
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS') setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Range') 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/')) { if (url.pathname.startsWith('/api/music/')) {
// Ensure proper caching headers for audio files // Music files: cache by Service Worker, not browser
setHeader(event, 'Cache-Control', 'public, max-age=3600') // 1 hour cache setHeader(event, 'Cache-Control', 'public, max-age=0, must-revalidate')
setHeader(event, 'Accept-Ranges', 'bytes') setHeader(event, 'Accept-Ranges', 'bytes')
// Add security headers (but allow DevTools) // Security headers
setHeader(event, 'X-Content-Type-Options', 'nosniff') setHeader(event, 'X-Content-Type-Options', 'nosniff')
// Don't set X-Frame-Options DENY for development
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
setHeader(event, 'X-Frame-Options', 'DENY') setHeader(event, 'X-Frame-Options', 'DENY')
} }
} else {
// Other API endpoints: no cache
setHeader(event, 'Cache-Control', 'no-store, must-revalidate')
} }
} }
// ==========================================
// Handle OPTIONS preflight requests // Handle OPTIONS preflight requests
// ==========================================
if (event.node.req.method === 'OPTIONS') { if (event.node.req.method === 'OPTIONS') {
setHeader(event, 'Access-Control-Allow-Origin', '*') setHeader(event, 'Access-Control-Allow-Origin', '*')
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS') setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS')