mejorando configuracion de seguridad
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -45,21 +45,91 @@ export default defineNuxtConfig({
|
|||||||
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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user