# Blueprint de Seguridad — onapb.com
**Generado por:** The Architect  
**Fecha:** 2026-04-01  
**Para ejecutar con:** Claude Code en el repositorio del sistema

---

## Contexto

Este documento es una instrucción completa y autónoma para que Claude Code implemente todas las mejoras de seguridad detectadas en el sitio **onapb.com**.

- **Stack:** Laravel (PHP 8.4.11) + LiteSpeed + Hostinger
- **Framework detectado:** Laravel (confirmado por cookies `laravel-session` y `XSRF-TOKEN`)
- **Dev responsable:** único desarrollador con acceso completo al repositorio

Ejecutá cada paso en orden. No saltes pasos. Al final, hacé deploy a producción.

---

## CLAUDE.md para este proyecto

```
Sos el implementador de seguridad del sitio onapb.com.
Tu tarea es implementar los cambios de seguridad descritos en este archivo en orden.
El stack es Laravel. No rompas funcionalidad existente.
Al terminar cada paso, confirmá que los tests pasen antes de avanzar.
```

---

## Orden de implementación

### PASO 1 — Middleware de Security Headers

**Problema:** Faltan 5 headers de seguridad críticos:
- `Strict-Transport-Security` (HSTS)
- `X-Frame-Options`
- `X-Content-Type-Options`
- `X-XSS-Protection`
- `Content-Security-Policy` (actualmente solo tiene `upgrade-insecure-requests`)

**Qué hacer:**

1. Crear el archivo `app/Http/Middleware/SecurityHeaders.php`:

```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set(
            'Permissions-Policy',
            'camera=(), microphone=(), geolocation=()'
        );
        $response->headers->set(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains; preload'
        );
        $response->headers->set(
            'Content-Security-Policy',
            "default-src 'self'; " .
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " .
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " .
            "font-src 'self' data: https://fonts.gstatic.com; " .
            "img-src 'self' data: https:; " .
            "connect-src 'self'; " .
            "frame-ancestors 'self';"
        );

        return $response;
    }
}
```

2. Registrar el middleware en `app/Http/Kernel.php`, dentro de `$middlewareGroups['web']`, al final del array:

```php
\App\Http\Middleware\SecurityHeaders::class,
```

3. Verificar: hacer `php artisan route:list` y luego una request curl local para confirmar que los headers aparecen.

---

### PASO 2 — Ocultar versión de PHP y stack

**Problema:** El header `X-Powered-By: PHP/8.4.11` y `Server: LiteSpeed` revelan el stack exacto.

**Qué hacer:**

1. En el archivo `public/.htaccess`, agregar al inicio (después de `<IfModule mod_rewrite.c>` o antes):

```apache
# Ocultar headers de servidor
<IfModule mod_headers.c>
    Header unset X-Powered-By
    Header always unset X-Powered-By
    Header unset Server
</IfModule>
```

2. Si tenés acceso a `php.ini` (en Hostinger hPanel → PHP → php.ini personalizado):

```ini
expose_php = Off
```

3. Si usás un archivo `php.ini` en la raíz del proyecto, agregar esa línea ahí también.

---

### PASO 3 — Forzar HTTPS y HSTS en .htaccess

**Problema:** El sitio no sirve headers HSTS. El HSTS del Paso 1 solo actúa cuando ya estás en HTTPS, pero si alguien entra por HTTP no hay redirect forzado a nivel servidor.

**Qué hacer:**

En `public/.htaccess`, asegurar que exista este bloque (Laravel suele tenerlo pero verificar):

```apache
<IfModule mod_rewrite.c>
    RewriteEngine On

    # Forzar HTTPS
    RewriteCond %{HTTPS} off
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

    # Resto de reglas de Laravel...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>
```

---

### PASO 4 — Proteger archivo .env y rutas sensibles

**Problema:** Laravel ya protege `.env` por defecto, pero verificar que las rutas de storage no sean accesibles.

**Qué hacer:**

En `public/.htaccess`, agregar:

```apache
# Bloquear acceso a archivos sensibles
<FilesMatch "\.(env|log|git|gitignore|htpasswd|DS_Store)$">
    Order allow,deny
    Deny from all
</FilesMatch>

# Bloquear acceso directo a /storage si está symlinkado
<IfModule mod_rewrite.c>
    RewriteRule ^storage/app/(.*)$ - [F,L]
</IfModule>
```

---

### PASO 5 — Mejorar robots.txt

**Problema:** El `robots.txt` actual tiene `Disallow: ""` (vacío), lo que permite que todos los bots indexen todo, incluidas rutas como `/recuperar` y rutas de admin.

**Qué hacer:**

Reemplazar el contenido de `public/robots.txt` (o crearlo si no existe):

```
User-agent: *
Disallow: /admin
Disallow: /recuperar
Disallow: /api/
Disallow: /storage/
Disallow: /vendor/
Allow: /

Sitemap: https://onapb.com/sitemap.xml
```

---

### PASO 6 — Agregar security.txt

**Problema:** No existe `/well-known/security.txt`. Es una buena práctica de divulgación responsable.

**Qué hacer:**

1. Crear el directorio `public/.well-known/`
2. Crear el archivo `public/.well-known/security.txt`:

```
Contact: mailto:admin@onapb.com
Expires: 2027-04-01T00:00:00Z
Preferred-Languages: es, en
Canonical: https://onapb.com/.well-known/security.txt
```

(Reemplazar `admin@onapb.com` con el email real del responsable de seguridad.)

---

### PASO 7 — Rate limiting en rutas críticas

**Problema:** No se detectó rate limiting explícito. Rutas como `/recuperar` (recuperación de contraseña) son vectores de ataque por fuerza bruta y enumeración de usuarios.

**Qué hacer:**

En `routes/web.php` o en el middleware de rutas, asegurar rate limiting en rutas sensibles:

```php
Route::middleware(['throttle:5,1'])->group(function () {
    Route::post('/recuperar', [PasswordController::class, 'sendResetLink']);
    Route::post('/login', [AuthController::class, 'login']);
});
```

El `5,1` significa 5 intentos por minuto. Ajustar según la lógica del negocio.

Si el proyecto usa Laravel Breeze o Fortify, ya tiene esto incorporado — verificar que esté habilitado en `config/fortify.php`:

```php
'limiters' => [
    'login' => 'login',
],
```

---

### PASO 8 — Configurar Content Security Policy estricta (opcional pero recomendado)

**Problema:** La CSP del Paso 1 usa `'unsafe-inline'` y `'unsafe-eval'` como fallback seguro. Si el proyecto no necesita eval, endurecerla.

**Qué hacer:**

Una vez que el sitio esté corriendo con el middleware del Paso 1, revisar la consola del navegador por errores de CSP. Si no hay errores de scripts inline, cambiar en `SecurityHeaders.php`:

```php
"script-src 'self';" // quitar unsafe-inline y unsafe-eval
```

Esto requiere que todos los scripts estén en archivos externos, no inline.

---

## Pasos que requieren acción MANUAL (fuera del repositorio)

Estos cambios no se hacen en el código — son configuraciones del servidor/DNS. Documentarlos para hacerlos en paralelo:

### A — Cerrar puerto 3306 (CRÍTICO — hacer YA)
- En Hostinger hPanel → Seguridad → Firewall
- Bloquear acceso externo al puerto 3306 (MySQL)
- Solo debe aceptar conexiones desde `127.0.0.1` (localhost)
- Verificar en `.env`: `DB_HOST=127.0.0.1`

### B — Deshabilitar FTP (puerto 21)
- En Hostinger hPanel → Archivos → Administrador FTP
- Deshabilitar FTP o cambiar a SFTP (puerto 22)

### C — Migrar DNS a Cloudflare (recomendado para WAF)
- Crear cuenta en cloudflare.com
- Agregar el dominio `onapb.com`
- Cambiar los nameservers en Registrar.eu (registrar actual) a los de Cloudflare
- Habilitar WAF en plan gratuito
- Habilitar DNSSEC en Cloudflare (un click)
- Habilitar proxy (nube naranja) para ocultar IP real

### D — Configurar DMARC + endurecer SPF
En el panel DNS (Hostinger o Cloudflare):

**Cambiar SPF existente** (`~all` → `-all`):
```
v=spf1 include:_spf.mail.hostinger.com -all
```

**Agregar registro DMARC** (nuevo TXT en `_dmarc.onapb.com`):
```
v=DMARC1; p=quarantine; rua=mailto:admin@onapb.com; fo=1
```

### E — Verificar DKIM
En Hostinger hPanel → Email → Configuración de dominio → habilitar DKIM si no está activo.

---

## Orden de deploy

Una vez implementados los pasos de código (1 al 8):

1. Correr tests: `php artisan test`
2. Verificar que la app levanta: `php artisan serve` + revisar consola del browser
3. Hacer commit con mensaje: `security: add security headers middleware and hardening`
4. Push a rama de staging si existe, sino directo a `main`
5. Deploy a producción (según el proceso actual del proyecto)
6. Verificar headers en producción: usar [securityheaders.com](https://securityheaders.com) con `onapb.com`
7. Verificar que el sitio funciona 100% antes de cerrar puertos (paso manual A)

---

## Verificación post-deploy

Correr estos checks para confirmar que todo quedó bien:

| Check | Herramienta | URL |
|-------|-------------|-----|
| Headers HTTP | Security Headers | securityheaders.com |
| SSL/TLS | SSL Labs | ssllabs.com/ssltest |
| HSTS | hstspreload.org | hstspreload.org |
| Reputación IP | VirusTotal | virustotal.com |
| Puertos | Shodan | shodan.io |

---

## Resultado esperado

Al completar todos los pasos, el sitio debe:
- Obtener calificación **A** en securityheaders.com (actualmente F)
- No exponer versión de PHP ni stack en headers
- Tener HSTS activo con preload
- No tener puertos de base de datos expuestos a internet
- Tener protección WAF básica via Cloudflare
- Tener autenticación de email (SPF + DKIM + DMARC)
