ansango / wiki
 ·  7 min de lectura

Automatización de HTTP Headers: CSP y Cache Control

Diseño e implementación de un sistema automatizado de generación de headers HTTP

Descripción General

Esta guía documenta el diseño e implementación de un sistema automatizado de generación de headers HTTP, enfocado en Content Security Policy (CSP) y políticas de cache control.


Problema

Gestión Manual de Headers

La configuración tradicional de headers HTTP presenta varios problemas:

Solución Propuesta

Sistema automatizado que:


Arquitectura del Sistema

Componentes Principales

1. Manifiesto de Configuración

Archivo JavaScript que actúa como única fuente de verdad para todas las políticas de headers.

2. Script Generador

Script Node.js que procesa la configuración y genera el archivo final de headers.

3. Archivo de Headers Generado

Artefacto de salida (ej: _headers o .htaccess) que no debe editarse manualmente.


Content Security Policy (CSP)

Conceptos Fundamentales

CSP implementa el principio de mínimo privilegio: solo se otorgan los permisos estrictamente necesarios para el funcionamiento del sitio.

Directivas Principales

default-src

Define la política por defecto para todos los tipos de recursos.

'default-src': ["'self'"]

script-src

Controla qué scripts pueden ejecutarse. Acepta:

'script-src': [
  "'self'",
  "'sha256-abc123...'",
  'https://cdn.ejemplo.com'
]

style-src

Controla hojas de estilo CSS.

'style-src': ["'self'", "'unsafe-inline'"]

img-src

Controla fuentes de imágenes.

'img-src': ["'self'", 'data:', 'https:', 'blob:']

connect-src

Controla conexiones AJAX, WebSockets y EventSource.

'connect-src': ["'self'", 'https://api.ejemplo.com']

Gestión de Scripts Inline

Tres enfoques posibles:

MétodoDescripciónUso Recomendado
'unsafe-inline'Permite todos los scripts inlineEvitar en producción
NoncesTokens únicos por requestAplicaciones dinámicas
Hashes SHA-256Firma criptográfica del contenidoSitios estáticos (SSG)

Recomendación: Para sitios generados estáticamente, usar hashes SHA-256 por ser reproducibles y no requerir servidor en runtime.


Cache Control

Estrategias de Caché

Assets Inmutables

Archivos con hash en el nombre pueden cachearse indefinidamente.

{
  maxAge: 31536000, // 1 año
  directive: 'public, max-age=31536000, immutable',
  patterns: ['/*.js', '/*.css', '/assets/*']
}

HTML

Debe revalidarse siempre para asegurar contenido actualizado.

{
  maxAge: 0,
  directive: 'public, max-age=0, must-revalidate'
}

Stale-While-Revalidate

Sirve contenido en caché mientras busca actualizaciones en segundo plano.

{
  maxAge: 604800, // 1 semana
  staleWhileRevalidate: 86400, // 1 día
  directive: 'public, max-age=604800, stale-while-revalidate=86400'
}

Tabla de Tiempos de Caché

DuraciónSegundosUso Recomendado
1 hora3600Feeds RSS, APIs con cambios frecuentes
1 día86400Manifiestos, favicons, robots.txt
1 semana604800Imágenes de contenido
1 mes2592000Assets con versionado manual
1 año31536000Assets con hash, fuentes, inmutables

Implementación

Archivo de Configuración

// config/headers.config.js
export default {
  csp: {
    inlineScripts: [
      {
        file: 'src/components/theme.script.astro',
        description: 'Script de cambio de tema',
      },
    ],

    directives: {
      'default-src': ["'self'"],
      'script-src': [
        "'self'",
        'https://analytics.example.com',
      ],
      'style-src': ["'self'", "'unsafe-inline'"],
      'img-src': ["'self'", 'data:', 'https:', 'blob:'],
      'connect-src': ["'self'", 'https://api.example.com'],
      'frame-src': ['https://widgets.example.com'],
      'object-src': ["'none'"],
      'base-uri': ["'self'"],
      'form-action': ["'self'"],
      'frame-ancestors': ["'none'"],
      'upgrade-insecure-requests': [],
    },

    reportUri: '/api/csp-report',
  },

  security: {
    'X-Frame-Options': 'DENY',
    'X-Content-Type-Options': 'nosniff',
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
  },

  cache: {
    html: {
      maxAge: 0,
      directive: 'public, max-age=0, must-revalidate',
    },

    immutable: {
      maxAge: 31536000,
      directive: 'public, max-age=31536000, immutable',
      patterns: ['/*.js', '/*.css', '/assets/*'],
    },

    fonts: {
      maxAge: 31536000,
      directive: 'public, max-age=31536000, immutable',
      additionalHeaders: {
        'Access-Control-Allow-Origin': '*',
      },
      patterns: ['/fonts/*'],
    },

    images: {
      maxAge: 604800,
      staleWhileRevalidate: 86400,
      directive: 'public, max-age=604800, stale-while-revalidate=86400',
      patterns: ['/images/*'],
    },

    static: {
      maxAge: 86400,
      directive: 'public, max-age=86400',
      patterns: ['/*.png', '/*.ico', '/*.svg', '/*.webmanifest'],
    },

    rss: {
      maxAge: 3600,
      directive: 'public, max-age=3600',
      contentType: 'application/xml; charset=utf-8',
      patterns: ['/rss.xml'],
    },

    api: {
      directive: 'no-store',
      patterns: ['/api/*'],
    },
  },

  generator: {
    output: 'public/_headers',
    backup: true,
    verbose: true,
  },
};

Script Generador

#!/usr/bin/env node
import fs from 'node:fs';
import crypto from 'node:crypto';

/**
 * Extrae el contenido de scripts de un archivo
 */
function extractScriptContent(filePath) {
  const content = fs.readFileSync(filePath, 'utf8');
  const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/g;
  const matches = [];
  let match;
  
  while ((match = scriptRegex.exec(content)) !== null) {
    matches.push(match[1]);
  }
  
  return matches.join('\n');
}

/**
 * Genera hash SHA-256
 */
function generateHash(content) {
  return crypto
    .createHash('sha256')
    .update(content, 'utf8')
    .digest('base64');
}

/**
 * Procesa scripts inline y genera hashes
 */
function processInlineScripts(inlineScripts) {
  const hashes = [];
  
  for (const script of inlineScripts) {
    const content = extractScriptContent(script.file);
    const hash = generateHash(content);
    hashes.push(`'sha256-${hash}'`);
  }
  
  return hashes;
}

/**
 * Construye la directiva CSP completa
 */
function buildCSPDirective(directives, hashes) {
  const parts = [];
  
  for (const [directive, sources] of Object.entries(directives)) {
    if (directive === 'script-src') {
      const allSources = ["'self'", ...hashes, ...sources.filter(s => s !== "'self'")];
      parts.push(`${directive} ${allSources.join(' ')}`);
    } else if (sources.length > 0) {
      parts.push(`${directive} ${sources.join(' ')}`);
    } else {
      parts.push(directive);
    }
  }
  
  return parts.join('; ');
}

/**
 * Genera el contenido completo del archivo _headers
 */
function generateHeadersContent(config, hashes) {
  const lines = [];
  
  lines.push('# Headers HTTP - Generado Automáticamente');
  lines.push('# ⚠️ NO EDITAR MANUALMENTE');
  lines.push('');
  
  lines.push('/*');
  
  for (const [header, value] of Object.entries(config.security)) {
    lines.push(`  ${header}: ${value}`);
  }
  
  const csp = buildCSPDirective(config.csp.directives, hashes);
  const cspWithReport = config.csp.reportUri 
    ? `${csp}; report-uri ${config.csp.reportUri}`
    : csp;
  lines.push(`  Content-Security-Policy: ${cspWithReport}`);
  lines.push('');
  
  // Generar reglas de cache...
  
  return lines.join('\n');
}

async function main() {
  const config = await import('./config/headers.config.js');
  const hashes = processInlineScripts(config.default.csp.inlineScripts);
  const content = generateHeadersContent(config.default, hashes);
  
  fs.writeFileSync(config.default.generator.output, content, 'utf8');
  console.log('✨ Headers generados exitosamente');
}

main();

Integración en Build

{
  "scripts": {
    "generate:headers": "node scripts/generate-headers.js",
    "build": "npm run generate:headers && [comando-build]"
  }
}

Endpoint de Reportes CSP

Implementación

// api/csp-report.js
export async function POST({ request }) {
  const report = await request.json();
  const violation = report['csp-report'] || report;
  
  console.error('CSP Violation:', {
    blockedUri: violation['blocked-uri'],
    violatedDirective: violation['violated-directive'],
    documentUri: violation['document-uri'],
  });
  
  // En producción: enviar a sistema de logging
  // await sendToSentry(violation);
  
  return new Response(null, { status: 204 });
}

Utilidad

Los reportes CSP proporcionan información sobre:


Mejores Prácticas

Recomendaciones

Evitar


Ciclo de Desarrollo

Desarrollo Local

# Modificar configuración
vim config/headers.config.js

# Regenerar headers (opcional)
npm run generate:headers

# Iniciar servidor de desarrollo
npm run dev

Build y Deploy

# Build (regenera headers automáticamente)
npm run build

# Preview
npm run preview

# Deploy
npm run deploy

Verificación en Producción

# Verificar headers aplicados
curl -I https://tu-sitio.com | grep "Content-Security-Policy"

# Revisar logs de reportes CSP
tail -f logs/csp-violations.log

Casos de Uso

Añadir Script Inline

  1. Agregar a configuración:
inlineScripts: [
  {
    file: 'src/components/nuevo-feature.script.js',
    description: 'Nueva funcionalidad',
  },
]
  1. Ejecutar build:
npm run build

Integrar Servicio Externo

  1. Agregar dominios necesarios:
directives: {
  'script-src': [
    "'self'",
    'https://www.googletagmanager.com',
  ],
  'connect-src': [
    "'self'",
    'https://www.google-analytics.com',
  ],
}
  1. Regenerar y desplegar.

Optimizar Cache de Imágenes

  1. Ajustar política:
images: {
  maxAge: 604800, // 1 semana
  staleWhileRevalidate: 86400, // revalidar en 1 día
}

Troubleshooting

CSP Bloqueó un Script

Síntomas: Error en consola del navegador

Soluciones:

npm run generate:headers
npm run build

Imágenes No Cargan

Síntomas: Imágenes rotas, errores CSP

Solución: Verificar img-src:

'img-src': [
  "'self'",
  'https:', // permite cualquier imagen HTTPS
  'data:',
  'blob:',
]

Headers No se Aplican

Verificar:


Referencia de Directivas CSP

DirectivaControlaEjemplo
default-srcFallback para todos los recursos'self'
script-srcScripts JavaScript'self' 'sha256-…' https://cdn.com
style-srcHojas de estilo CSS'self' 'unsafe-inline'
img-srcImágenes'self' data: https:
font-srcFuentes web'self' data:
connect-srcFetch, XHR, WebSockets'self' https://api.com
frame-srciFrameshttps://widgets.com
media-srcAudio, video'self' https://cdn.com
object-src<object>, <embed>'none'
worker-srcWeb Workers'self'
manifest-srcWeb app manifests'self'
base-uriTag <base>'self'
form-actionEnvío de formularios'self'
frame-ancestorsEmbedding del sitio'none'

Valores Especiales CSP

ValorSignificado
'none'Bloquea todo
'self'Mismo origen (protocolo + dominio + puerto)
'unsafe-inline'Permite scripts/styles inline
'unsafe-eval'Permite eval()
'sha256-…'Hash SHA-256 del contenido
'nonce-…'Token aleatorio único
https:Cualquier URL HTTPS
data:Data URIs
blob:Blob URLs

Referencias


Versión: 1.0
Última actualización: 3 de noviembre, 2025