Sem Agente · Sem Instalação

Hardening de Apache: Guía Completa

Hardening de Apache: La Guía para Amplificar tu Seguridad (con todo lo que puede salir mal si no lo haces) Cada configuración predeterminada de Apache que ignoras es una puerta entreabierta. Esta guía te muestra exactamente qué significa cada una en la práctica — y cómo cerrarlas, una por una.

Hardening de Apache: La Guía para Amplificar tu Seguridad (con todo lo que puede salir mal si no lo haces)

Cada configuración predeterminada de Apache que ignoras es una puerta entreabierta. Esta guía te muestra exactamente qué significa cada una en la práctica — y cómo cerrarlas, una por una.

Introducción

Apache HTTP Server es uno de los servidores web más utilizados del planeta desde hace más de 25 años. Y precisamente por eso, también es uno de los objetivos más estudiados por los atacantes. Su configuración predeterminada está diseñada para funcionar en cualquier lugar, no para ser segura en cualquier lugar. Esta diferencia es lo que separa un servidor saludable de uno que se convierte en titular.

En esta guía, vamos a pasar por cada elemento de hardening explicando tres cosas:

  1. Lo que la configuración predeterminada hace (o no hace)
  2. Lo que un atacante puede hacer con eso — con escenarios reales
  3. Cómo corregirlo, con configuración lista para copiar

Al final, tendrás un Apache listo para producción y entenderás exactamente por qué cada línea está ahí.

1. ServerTokens y ServerSignature: estás entregando un mapa del tesoro

Lo que pasa por defecto

Cuando Apache responde a cualquier solicitud, incluye un header Server que por defecto contiene algo como esto:

Server: Apache/2.4.52 (Ubuntu)

Y en páginas de error (404, 500, 403), agrega un pie de página:

Apache/2.4.52 (Ubuntu) Server at example.com Port 443

Puede parecer inofensivo. No lo es.

Lo que causa en la práctica

Imagina que estás ejecutando Apache 2.4.52 en Ubuntu. Un atacante que escanea Internet con Shodan, Censys o un simple script Python descubre tu servidor. En segundos:

  1. Consulta la base de CVEs filtrando por "Apache 2.4.52" — y encuentra una lista de vulnerabilidades conocidas para esa versión exacta.
  2. Sabe que es Ubuntu, así que sabe qué paquetes están instalados, cuál es la ruta predeterminada de los archivos (/var/www/html), dónde están los logs, quién es el usuario que ejecuta el servicio (www-data).
  3. Filtra exploits listos en Exploit-DB o Metasploit que coinciden con tu versión. Si hay un exploit sin parchar, en pocos minutos ya está probando.

El costo de que un atacante descubra todo esto pasó de "horas de reconocimiento" a "una solicitud HTTP". Le ahorraste trabajo.

Lo peor: escáneres automatizados como Mirai y sus derivados escanean Internet 24/7 buscando exactamente versiones vulnerables específicas. No es personal — es industrial.

La corrección

En /etc/apache2/conf-enabled/security.conf (Debian/Ubuntu) o /etc/httpd/conf/httpd.conf (RHEL/CentOS/Rocky):

# Muestra solo "Apache" en el header Server, sin versión ni SO
ServerTokens Prod

# Quita el pie de página de las páginas de error
ServerSignature Off

Después de esto, el header Server se convierte simplemente en Apache. El atacante aún sabe que es Apache (no hay forma de ocultarlo al 100%), pero perdió la versión y el sistema operativo. Ya no puede filtrar exploits específicos sin trabajo extra.

Importante: esto es "seguridad por obscuridad" y no reemplaza mantener el servidor actualizado. Pero reduce drásticamente el número de ataques automatizados que pueden encontrarte como objetivo viable.

2. expose_php: PHP gritando su versión al mundo

Lo que pasa por defecto

Cada respuesta generada por una página PHP trae un header como:

X-Powered-By: PHP/8.1.2

Lo que causa

El mismo problema de ServerTokens, con un agravante: las vulnerabilidades de PHP suelen ser mucho más críticas que las vulnerabilidades de Apache, porque PHP ejecuta código. Algunas de las CVEs más conocidas de los últimos años (CVE-2019-11043 en PHP-FPM, por ejemplo) permitían ejecución remota de código con una única solicitud bien formada.

Si el atacante sabe que estás ejecutando PHP 8.1.2, consulta las CVEs de esa versión exacta y lo intenta. Si tu versión es vulnerable y aún no ha sido parcheada, es game over.

Hay otro efecto secundario menos obvio: las herramientas de bug bounty y escáneres agresivos priorizan objetivos con versiones antiguas. Exponer tu versión es como colgar un cartel que diga "dispara aquí primero".

La corrección

En php.ini (usa php --ini para descubrir la ruta exacta):

expose_php = Off

Reinicia Apache (o PHP-FPM, dependiendo de tu configuración). El header desaparece completamente.

Atención: si usas PHP-FPM, hay dos php.ini diferentes — uno para CLI y otro para FPM. Ajusta el del FPM (/etc/php/8.x/fpm/php.ini).

3. TraceEnable: el método HTTP que olvidamos matar

Lo que pasa por defecto

El método TRACE de HTTP fue creado en los años 90 para propósitos de debug — cuando envías un TRACE, el servidor te responde con la solicitud completa que recibió, intacta. Por defecto, Apache viene con TraceEnable On.

Lo que causa

Existe un ataque conocido llamado Cross-Site Tracing (XST). La idea: usando JavaScript en una página comprometida, un atacante fuerza el navegador de la víctima a hacer un TRACE en el servidor objetivo. La respuesta incluye todos los headers — incluyendo cookies marcadas como HttpOnly, que normalmente JavaScript no puede leer.

Resultado: el atacante captura cookies de sesión que deberían ser inaccesibles y secuestra la sesión de la víctima. Funciona hasta hoy en servidores mal configurados.

Además, los escáneres de vulnerabilidad automatizados marcan TRACE habilitado como vulnerabilidad de severidad media, lo que afecta los puntajes de cumplimiento (PCI-DSS, por ejemplo, requiere TRACE deshabilitado).

La corrección

TraceEnable Off

Listo. Sin efectos secundarios, sin impacto en aplicaciones reales. No conozco un único sitio legítimo que necesite TRACE habilitado en producción.

4. FileETag: filtrando información del sistema de archivos

Lo que pasa por defecto

El header ETag se usa para caché: el navegador guarda el ETag de un archivo y, en la siguiente visita, pregunta al servidor "¿este archivo sigue teniendo ese ETag?". Si es así, el servidor responde 304 Not Modified y el navegador usa la versión local. Eficiente.

El problema: por defecto, Apache genera el ETag a partir del inode, tamaño y fecha de modificación del archivo. El inode es un número interno del sistema de archivos.

Lo que causa

En sí, filtrar un inode parece tontería. Pero:

  1. Identificación de servidor en cluster: si tienes 5 servidores detrás de un balanceador de carga, cada archivo tiene un inode diferente en cada servidor. Un atacante puede mapear cuántos servidores tienes, identificar inconsistencias entre ellos, y atacar el más débil.
  1. Fuga de NFS: en algunas configuraciones antiguas con NFS, el inode podía revelar información de la configuración del almacenamiento.
  1. Cumplimiento: el elemento está listado en auditorías de PCI-DSS y CIS Benchmark como vulnerabilidad.

No es el fin del mundo, pero es trivial de corregir.

La corrección

FileETag None

Aún tendrás caché funcionando a través de headers Last-Modified y Cache-Control — que son suficientes para cualquier caso real.

5. Strict-Transport-Security (HSTS): impidiendo el degradación

Lo que pasa por defecto

Apache no envía HSTS por defecto. Esto significa que incluso si tu sitio se ejecuta en HTTPS, un navegador que accede por primera vez aún habla HTTP en la primera solicitud — y solo después es redirigido.

Lo que causa

Existe una clase de ataques llamada SSL Stripping, popularizada por la herramienta sslstrip. El escenario típico:

  1. Víctima se conecta al Wi-Fi de un café (o de un aeropuerto, o de la casa del vecino hackeado)
  2. Atacante en la misma red intercepta el tráfico (man-in-the-middle)
  3. Víctima escribe "mibanko.com" en el navegador
  4. El navegador hace primero una solicitud HTTP (no HTTPS)
  5. Atacante intercepta ese HTTP, se conecta al banco real vía HTTPS, y devuelve a la víctima la versión HTTP del sitio, reenviando todo
  6. Víctima nunca ve el candado, pero tampoco se da cuenta — e ingresa usuario y contraseña
  7. Atacante captura todo

Con HSTS activo, el navegador se niega a hablar HTTP con ese dominio. Incluso si el atacante intercepta, el navegador muestra un error insuperable y bloquea la conexión.

La corrección

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

Lo que significa cada parte:

  • max-age=63072000 — el navegador debe forzar HTTPS durante 2 años desde la última visita
  • includeSubDomains — aplica también a todos los subdominios (¡cuidado! ver advertencia abajo)
  • preload — solicita inclusión en la lista codificada en navegadores (Chrome, Firefox, Safari)

ADVERTENCIA CRÍTICA: una vez aplicado, el navegador "recuerda" el HSTS por el max-age definido. Si tu certificado expira, o si necesitas acceder al sitio vía HTTP por alguna razón, no hay vuelta atrás — el usuario queda bloqueado. Comienza con max-age=300 (5 minutos) durante las pruebas, valida todo, y solo entonces aumenta al valor de producción.

Sobre includeSubDomains: si tienes legado.ejemplo.com que aún solo habla HTTP, esta directiva va a romper el acceso a él. Verifica todos los subdominios antes de aplicar.

6. X-Frame-Options: el anti-clickjacking clásico

Lo que pasa por defecto

Sin este header, cualquier sitio en Internet puede cargar el tuyo dentro de un <iframe>. Sí, cualquiera.

Lo que causa

El ataque se llama clickjacking y funciona así:

  1. Atacante crea un sitio cualquiera ("sorteo del iPhone", "prueba tu cociente intelectual", etc.)
  2. Ese sitio carga tu sitio real (digamos, el panel admin de tu sistema) dentro de un iframe invisible, con opacity: 0
  3. Encima del iframe, el atacante pone botones falsos: "¡Haz clic aquí para ganar!"
  4. Víctima — que está logueada en tu sistema en otra pestaña — hace clic en el botón falso
  5. En realidad, el clic va al iframe invisible, y está haciendo clic en "Eliminar mi cuenta" o "Transferir saldo"

Casos reales ya han sucedido con Twitter, Facebook, banca por Internet. Es un vector especialmente peligroso para paneles administrativos.

La corrección

Header always set X-Frame-Options "SAMEORIGIN"

Esto permite que solo tu propio dominio coloque el sitio en iframe. Para bloquearlo completamente, usa DENY. Para permitir un dominio específico, usa la directiva más moderna Content-Security-Policy: frame-ancestors (que veremos en el elemento 9).

7. X-Content-Type-Options: el ataque del olfateo MIME

Lo que pasa por defecto

Cuando un navegador recibe un archivo y el Content-Type parece extraño o está ausente, intenta adivinar el tipo de archivo mirando el contenido. Este comportamiento se llama "olfateo MIME".

Lo que causa

Escenario clásico: tu sitio permite cargar avatares de imágenes. Validas que el archivo sea un PNG verificando la extensión. El atacante envía un archivo llamado foto.png cuyo contenido es, en realidad, JavaScript:

<script>robasCookies()</script>

Guardas el archivo, queda en /uploads/foto.png. Cuando otra víctima accede a esa URL, el servidor responde con Content-Type: image/png. Pero el navegador mira el contenido, ve que tiene <script>, y piensa: "¡Ah, eso parece HTML!" — y lo ejecuta como HTML. El JavaScript se ejecuta en el contexto de tu dominio. XSS almacenado en pocos pasos.

Este era un ataque devastador en Internet Explorer de los años 2000, pero aún funciona en navegadores modernos en ciertas condiciones.

La corrección

Header always set X-Content-Type-Options "nosniff"

Este header fuerza a los navegadores a respetar el Content-Type enviado, sin intentar adivinar. Es una línea. Actívalo siempre.

8. Referrer-Policy: controlando qué se filtra hacia afuera

Lo que pasa por defecto

Cuando un usuario hace clic en un enlace en tu sitio hacia un sitio externo, el navegador envía un header Referer (sí, con error de ortografía — es histórico) informando al destino exactamente de cuál URL vino. Por defecto, esto incluye la URL completa.

Lo que causa

Imagina que tu sistema tiene URLs como:

https://misistema.com/admin/usuarios?token=eyJhbG...

O:

https://misistema.com/resetear-contraseña?key=abc123def456

O incluso:

https://mihospital.com/paciente/juan-silva-dni-12345/examenes

Si el usuario hace clic en un enlace externo en esa página (o si la página carga una imagen de otro dominio), toda esa URL se envía al dominio externo, que la verá en sus logs. Tokens, IDs, datos personales — todo filtrándose.

Esto ya ha causado filtraciones famosas — el caso más conocido es el de sistemas de salud estadounidenses que filtraron identificadores de pacientes a Facebook por culpa de píxeles de seguimiento.

La corrección

Header always set Referrer-Policy "strict-origin-when-cross-origin"

Lo que hace esta política:

  • Para enlaces dentro del mismo sitio: envía URL completa (útil para analytics interno)
  • Para enlaces externos vía HTTPS: envía solo el dominio (https://misistema.com), sin ruta ni query string
  • Para enlaces saliendo de HTTPS a HTTP: no envía nada

Es un muy buen equilibrio entre privacidad y funcionalidad.

9. Content-Security-Policy (CSP): la navaja suiza de defensa contra XSS

Lo que pasa por defecto

Sin CSP, el navegador ejecuta cualquier JavaScript que aparezca en la página, de cualquier origen. Carga imágenes de cualquier lugar. Acepta estilos de cualquier fuente. Se conecta a cualquier servidor vía fetch().

Lo que causa

CSP es la defensa moderna más poderosa contra Cross-Site Scripting (XSS) — y XSS es, según OWASP, una de las vulnerabilidades web más comunes desde hace más de 20 años.

Escenario sin CSP:

  1. Atacante encuentra una vulnerabilidad de XSS en tu sitio (tal vez un campo de comentario que no filtra <script>)
  2. Inyecta: <script src="https://atacante.com/malware.js"></script>
  3. El script malicioso se ejecuta en el contexto de tu dominio, con acceso a cookies, localStorage, y todo más
  4. Puede robar sesiones, hacer solicitudes en nombre de la víctima, deformar la página, redirigir a phishing

Con CSP bien configurada, el navegador simplemente se niega a cargar el script de atacante.com, incluso si está inyectado en el HTML. La vulnerabilidad XSS sigue existiendo en el código, pero el impacto es cero.

La corrección (versión restrictiva inicial)

Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"

Descifrando:

  • default-src 'self' — por defecto, solo carga cosas del propio dominio
  • script-src 'self' — JavaScript solo del propio dominio (sin inline, sin CDN externo)
  • style-src 'self' 'unsafe-inline' — CSS del propio dominio + permite estilos inline (necesario para muchos frameworks)
  • img-src 'self' data: https: — imágenes del propio dominio, data URIs y cualquier HTTPS
  • connect-src 'self' — fetch/XHR solo al propio dominio
  • frame-ancestors 'self' — reemplaza el X-Frame-Options moderno
  • base-uri 'self' — impide que <base> sea inyectado para cambiar la ruta base
  • form-action 'self' — formularios solo pueden enviar al propio dominio

CSP es el header más difícil de configurar. La política anterior va a romper sitios que usan Google Analytics, Google Fonts, jQuery vía CDN, mapas incrustados, etc. Siempre prueba primero con Content-Security-Policy-Report-Only, que solo reporta violaciones sin bloquear. Úsalo durante algunos días, monitorea los logs del navegador, ajusta, y solo entonces cambia a Content-Security-Policy real.

10. Permissions-Policy: bloqueando APIs sensibles del navegador

Lo que pasa por defecto

Sin este header, cualquier página de tu sitio puede solicitar acceso a cámara, micrófono, geolocalización, USB, sensores de movimiento, etc. Incluso si tu página nunca usa esto, si hay un XSS, el atacante puede inyectar código que solicite estos accesos.

Lo que causa

Imagina un XSS en tu sitio corporativo. Un atacante inyecta:

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => /* envía stream al servidor del atacante */)

La víctima ve el popup de "¿permitir cámara?" — y como está en un sitio que confía, hace clic en permitir. Listo: webcam y micrófono del ejecutivo de la empresa, transmitiendo en vivo al atacante.

La corrección

Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"

Esto bloquea completamente estas APIs. Si tu aplicación realmente necesita una de ellas (una página específica que usa cámara), la habilitas solo para ese origen usando camera=(self).

11. Encabezados Cross-Origin (COOP, CORP, COEP): el aislamiento moderno

Lo que pasa por defecto

Sin estos headers, tu ventana comparte un proceso con otras ventanas que pueden haber sido abiertas desde tu sitio, y tus recursos pueden ser cargados por cualquier origen.

Lo que causa

Después de los ataques Spectre y Meltdown (2018), se descubrió que era posible, vía JavaScript, leer memoria de otros procesos del navegador a través de canales laterales de la CPU. La defensa: aislar procesos por origen.

  • COOP (Cross-Origin-Opener-Policy) — cuando se establece como same-origin, el navegador garantiza que tu ventana está en un proceso aislado de las ventanas de otros orígenes
  • CORP (Cross-Origin-Resource-Policy) — controla qué sitios pueden cargar tus recursos (imágenes, scripts, etc.)
  • COEP (Cross-Origin-Embedder-Policy) — requiere que los recursos incrustados declaren explícitamente que pueden ser incrustados

La corrección

Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"
# COEP solo si no incrustas recursos de terceros
Header always set Cross-Origin-Embedder-Policy "require-corp"

Advertencia sobre COEP: el require-corp rompe cualquier recurso de terceros que no envíe el header Cross-Origin-Resource-Policy. Si tu sitio incrustra YouTube, mapas Google, fuentes de CDN, etc., quita COEP o cambia a unsafe-none.

12. Métodos HTTP peligrosos (PUT, DELETE, CONNECT)

Lo que pasa por defecto

Apache acepta prácticamente todos los métodos HTTP por defecto, incluyendo PUT, DELETE, OPTIONS, CONNECT, PATCH. Si hay algún módulo mal configurado (o un WebDAV antiguo olvidado), métodos como PUT pueden permitir carga arbitraria de archivos.

Lo que causa

Hay casos documentados de servidores Apache con WebDAV habilitado por error donde los atacantes hacían PUT /shell.php e instalaban una webshell en segundos. Game over total — ejecución remota de código con una única solicitud.

Incluso sin WebDAV, los métodos peligrosos habilitados aparecen en escáneres de vulnerabilidad e impactan el cumplimiento.

La corrección

Restringir los métodos en el <Directory> raíz:

<Directory /var/www/html>
    <LimitExcept GET POST HEAD>
        Require all denied
    </LimitExcept>
</Directory>

Si tu aplicación es una API REST que necesita PUT, DELETE y PATCH, ajusta:

<LimitExcept GET POST HEAD PUT DELETE PATCH OPTIONS>
    Require all denied
</LimitExcept>

13. Listado de directorios (Options Indexes)

Lo que pasa por defecto

En muchas configuraciones, si accedes a una URL de carpeta (/uploads/) y no hay un index.html adentro, Apache lista todos los archivos de la carpeta. Bonito, organizado, con fecha y tamaño.

Lo que causa

Escenarios reales:

  • /backups/ — listando todos los dumps de bases de datos
  • /uploads/ — todos los documentos enviados por usuarios, incluyendo PDFs internos
  • /.git/ — todo el histórico del código fuente
  • /old/ — versiones antiguas del sitio con vulnerabilidades ya corregidas en la versión nueva

Ya he visto esto filtrar contratos confidenciales, dumps de base de datos con hashes de contraseñas, código fuente completo de aplicaciones comerciales, y (en un caso particularmente desagradable) fotos personales de empleados que habían subido el contenido de la carpeta de fotos equivocada.

La corrección

En el <Directory> o en un .htaccess:

Options -Indexes

Y para garantizar defensa en profundidad, bloquea el acceso directo a archivos sensibles:

<FilesMatch "(^\.|\.(bak|backup|swp|old|sql|sql\.gz|tar|tar\.gz|zip|log|env|ini|conf|config|yml|yaml|json|lock)$|composer\.(json|lock)|package(-lock)?\.json|\.git|\.svn|\.htaccess|\.htpasswd|wp-config\.php)">
    Require all denied
</FilesMatch>

E impide el acceso a cualquier archivo/carpeta que comience con .:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule "(^|/)\." - [F]
</IfModule>

Esto protege .git, .env, .svn, .htaccess, .DS_Store, etc.

14. Ejecución de PHP en directorios de carga

Lo que pasa por defecto

Por defecto, Apache ejecuta PHP en cualquier carpeta dentro del DocumentRoot. Incluyendo la carpeta de cargas.

Lo que causa

Combina esto con cualquier fallo de carga — filtrado débil de extensión, validación solo por MIME type, cualquier cosa — y tienes carga de webshell. El atacante envía shell.php, accede a /uploads/shell.php, y tiene acceso de ejecución de comandos en el servidor con el usuario de Apache.

Este es, desde hace más de 15 años, uno de los patrones de compromiso más comunes en sitios WordPress, Joomla, y cualquier aplicación PHP con gestión de cargas.

La corrección

<Directory /var/www/html/uploads>
    php_flag engine off
    AddType text/plain .php .phtml .php3 .php4 .php5 .pht .phar
    
    <FilesMatch "\.(php|phtml|php3|php4|php5|pht|phar)$">
        Require all denied
    </FilesMatch>
</Directory>

Tres capas de protección: deshabilita el motor PHP, fuerza las extensiones PHP a servirse como texto, y bloquea el acceso directo a archivos con esas extensiones. Defensa en profundidad.

15. SSL/TLS: ¿aún hablas TLS 1.0?

Lo que pasa por defecto

En muchas distribuciones, Apache viene con soporte para SSLv3, TLS 1.0 y TLS 1.1 habilitados por defecto. Estos protocolos tienen vulnerabilidades conocidas (POODLE, BEAST, FREAK) y fueron oficialmente descontinuados por el IETF en 2021.

Lo que causa

  • POODLE (SSLv3) — permite descifrar tráfico vía padding oracle attack
  • BEAST (TLS 1.0) — ataque contra cifras CBC
  • FREAK / Logjam — fuerza degradación a cifras export-grade débiles

Incluso si tu navegador moderno negocia TLS 1.3, el hecho de que el servidor acepte TLS 1.0 significa que clientes maliciosos pueden forzar degradación. Además, cualquier certificación seria (PCI-DSS, ISO 27001, LGPD en algunas interpretaciones) requiere TLS 1.2 como mínimo.

La corrección

SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder     off
SSLSessionTickets       off

# OCSP Stapling — mejora rendimiento y privacidad de la validación del certificado
SSLUseStapling          on
SSLStaplingCache        "shmcb:logs/ssl_stapling(32768)"

Para generar una configuración personalizada (incluyendo perfiles "modern", "intermediate" y "old" para diferentes niveles de compatibilidad), usa el Mozilla SSL Configuration Generator.

16. LimitRequestBody: protegiéndote contra DoS aficionado

Lo que pasa por defecto

Apache acepta solicitudes de tamaño arbitrario. Sin límite.

Lo que causa

Un atacante puede enviar un POST de 4GB a cualquier endpoint de tu sitio y agotar la memoria del servidor. Repitiéndolo desde varios orígenes, derriba el servidor sin necesidad de botnet. Es un DoS aficionado, pero funciona.

La corrección

# Limita cargas a 10MB por solicitud (ajusta según tu necesidad)
LimitRequestBody 10485760

Para endpoints específicos que necesitan aceptar cargas más grandes (por ejemplo, carga de vídeo), define el límite localmente.

Configuración final: todo junto

Aquí está el archivo completo listo para copiar. Guarda en /etc/apache2/conf-available/security-hardening.conf y activa con:

sudo a2enconf security-hardening
sudo apachectl configtest
sudo systemctl restart apache2
# ============================================================
#  SECURITY HARDENING - Apache HTTP Server
#  Coloca en /etc/apache2/conf-available/security-hardening.conf
#  Activa con: sudo a2enconf security-hardening
# ============================================================

# --- Ocultar versiones e información del servidor ---
ServerTokens Prod
ServerSignature Off
TraceEnable Off
FileETag None

# --- Headers de seguridad HTTP ---
<IfModule mod_headers.c>
    # Fuerza HTTPS por 2 años (CUIDADO: ¡comienza con max-age=300 para probar!)
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    
    # Anti-clickjacking
    Header always set X-Frame-Options "SAMEORIGIN"
    
    # Anti olfateo MIME
    Header always set X-Content-Type-Options "nosniff"
    
    # Controla filtración de URL vía Referer
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    
    # Bloquea APIs sensibles del navegador
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
    
    # CSP base (¡PRUEBA primero con Content-Security-Policy-Report-Only!)
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
    
    # Aislamiento de origen cruzado
    Header always set Cross-Origin-Opener-Policy "same-origin"
    Header always set Cross-Origin-Resource-Policy "same-origin"
    # COEP solo si NO incrustas recursos de terceros:
    # Header always set Cross-Origin-Embedder-Policy "require-corp"
    
    # Quita headers que filtran información
    Header always unset X-Powered-By
    Header always unset Server
    Header unset X-Powered-By
    Header unset Server
</IfModule>

# --- Bloqueo de métodos HTTP peligrosos ---
<Directory /var/www/html>
    Options -Indexes -Includes -ExecCGI
    AllowOverride None
    Require all granted
    
    <LimitExcept GET POST HEAD>
        Require all denied
    </LimitExcept>
</Directory>

# --- Bloqueo de archivos sensibles ---
<FilesMatch "(^\.|\.(bak|backup|swp|old|sql|sql\.gz|tar|tar\.gz|zip|log|env|ini|conf|config|yml|yaml|json|lock)$|composer\.(json|lock)|package(-lock)?\.json|\.git|\.svn|\.htaccess|\.htpasswd|wp-config\.php)">
    Require all denied
</FilesMatch>

# --- Bloqueo de carpetas/archivos que comienzan con . ---
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule "(^|/)\." - [F]
</IfModule>

# --- Carpeta de cargas sin ejecución de PHP ---
<Directory /var/www/html/uploads>
    php_flag engine off
    AddType text/plain .php .phtml .php3 .php4 .php5 .pht .phar
    
    <FilesMatch "\.(php|phtml|php3|php4|php5|pht|phar)$">
        Require all denied
    </FilesMatch>
</Directory>

# --- Límite de tamaño de solicitud ---
LimitRequestBody 10485760

# --- SSL/TLS fuerte ---
<IfModule mod_ssl.c>
    SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder     off
    SSLSessionTickets       off
    
    SSLUseStapling          on
    SSLStaplingCache        "shmcb:logs/ssl_stapling(32768)"
</IfModule>

Y el .htaccess complementario (para alojamiento compartido o entornos donde no tienes acceso al conf principal):

# .htaccess - colocar en la raíz del sitio

<IfModule mod_rewrite.c>
    RewriteEngine On
    
    # Fuerza HTTPS
    RewriteCond %{HTTPS} !=on
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
    
    # Bloquea acceso a archivos/carpetas que comienzan con punto
    RewriteRule "(^|/)\." - [F]
    
    # Bloquea user agents de escáneres maliciosos comunes
    RewriteCond %{HTTP_USER_AGENT} (nikto|sqlmap|fimap|nessus|whatweb|jbrofuzz|libwhisker|webshag|grabber|dirbuster) [NC]
    RewriteRule .* - [F,L]
</IfModule>

# Bloquea listado de directorios
Options -Indexes

# Bloquea archivos sensibles
<FilesMatch "(^\.|\.(bak|backup|swp|old|sql|sql\.gz|tar|tar\.gz|zip|log|env|ini|conf|config|yml|yaml|json|lock)$|composer\.(json|lock)|package(-lock)?\.json|\.git|\.svn|\.htaccess|\.htpasswd|wp-config\.php)">
    Require all denied
</FilesMatch>

# Límite de carga (10MB)
LimitRequestBody 10485760

Validando el resultado

Después de aplicar todo:

# Valida sintaxis de Apache
sudo apachectl configtest

# Reinicia
sudo systemctl restart apache2

# Prueba headers
curl -I https://tudominio.com

Usa también herramientas externas:

  • securityheaders.com — analiza todos los headers HTTP y da una calificación de A+ a F
  • ssllabs.com/ssltest — análisis completo de TLS
  • Mozilla Observatory — resumen general de seguridad
  • SentinelHub — hace todo eso e incluso monitorea 24/7, alerta cuando algo cambia, identifica vulnerabilidades en tecnologías detectadas y genera reportes en español

Checklist final

  • [ ] ServerTokens Prod y ServerSignature Off
  • [ ] TraceEnable Off
  • [ ] FileETag None
  • [ ] expose_php = Off en php.ini
  • [ ] Módulos headers, rewrite, ssl habilitados
  • [ ] HSTS configurado (¡probado con max-age corto antes!)
  • [ ] X-Frame-Options aplicado
  • [ ] X-Content-Type-Options aplicado
  • [ ] Referrer-Policy aplicada
  • [ ] Permissions-Policy aplicada
  • [ ] CSP definida (¡probada en report-only primero!)
  • [ ] COOP/CORP aplicados
  • [ ] Métodos HTTP peligrosos bloqueados
  • [ ] Options -Indexes aplicado
  • [ ] Archivos sensibles bloqueados vía FilesMatch
  • [ ] PHP deshabilitado en carpetas de carga
  • [ ] TLS 1.0 y 1.1 deshabilitados
  • [ ] Cifras modernas configuradas
  • [ ] LimitRequestBody definido
  • [ ] Sitio probado en securityheaders.com (objetivo: A o A+)
  • [ ] Sitio probado en ssllabs.com (objetivo: A o A+)
  • [ ] Sitio monitoreado continuamente (¡porque mañana lo olvidas!)

Consideraciones finales

Hardening no es un evento, es un proceso continuo. ¿Aplicaste todo esto hoy? Excelente. Pero mañana alguien va a instalar un plugin nuevo en WordPress, alguien va a actualizar PHP y resetear configuraciones, alguien va a poner un archivo de backup en una carpeta pública "solo por un instante", alguien va a habilitar TRACE para debug y olvidarse.

La única forma realista de mantener un servidor seguro a largo plazo es monitorear continuamente — alguna herramienta tiene que estar mirando, todos los días, si algo cambió a peor.

Es exactamente para eso que existe SentinelHub: escáner continuo de seguridad web, con alertas en tiempo real cuando algo cambia, descripciones en español, reportes listos para mostrar al jefe, y detección de todo lo que esta guía cubrió (y mucho más).

Próximo post de la serie: Hardening de Nginx — mismos principios, configuración diferente, algunos trucos exclusivos. No te lo pierdas.

¿Te gustó? Comparte con el sysadmin de tu empresa. ¿Crees que debería haber leído esto ayer? Probablemente sí.