La configuración por defecto de Apache y Nginx está pensada para funcionar a la primera, no para exponer lo mínimo posible. En este tutorial aplicarás en Ubuntu 24.04 un conjunto de ajustes concretos para cada servidor: ocultar la versión, quitar módulos y listados de directorios que no necesitas, limitar el tamaño y los métodos de las peticiones, bloquear archivos ocultos, dejar solo TLS 1.2 y 1.3 y añadir cabeceras de seguridad. Al final comprobarás cada cambio con curl.

Las secciones de Apache y Nginx son independientes: sigue solo la del servidor que uses.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Apache (apache2) o Nginx instalado desde los repositorios de Ubuntu y sirviendo al menos un sitio.
  • Para la parte de TLS, un dominio (your_domain) apuntando al servidor y un certificado válido, por ejemplo de Let's Encrypt con Certbot.
  • Acceso a la consola del VPS por si un error de configuración deja el servidor web sin arrancar.

Antes de empezar, haz una copia de la configuración actual para poder volver atrás:

sudo cp -a /etc/apache2 /etc/apache2.bak    # si usas Apache
sudo cp -a /etc/nginx /etc/nginx.bak        # si usas Nginx

Hardening de Apache

Paso 1: Ocultar la versión y el sistema operativo

Por defecto Apache devuelve una cabecera como Server: Apache/2.4.58 (Ubuntu) y firma las páginas de error con la versión. Esa información solo ayuda a quien busca vulnerabilidades de una versión concreta. En Ubuntu estos ajustes están en /etc/apache2/conf-available/security.conf, que ya viene activado:

sudo nano /etc/apache2/conf-available/security.conf

Localiza las directivas ServerTokens, ServerSignature y TraceEnable y déjalas así:

ServerTokens Prod
ServerSignature Off
TraceEnable Off

TraceEnable Off desactiva el método HTTP TRACE, que no tiene uso legítimo en producción.

Paso 2: Desactivar módulos y listados de directorios

Cada módulo cargado es código que puede tener fallos. Lista los módulos activos:

apache2ctl -M

En una instalación estándar de Ubuntu hay dos que casi nunca hacen falta en un servidor público: autoindex, que genera listados de directorios cuando no hay index.html, y status, que expone /server-status. Desactívalos:

sudo a2dismod -f autoindex status

El parámetro -f es necesario porque Debian marca autoindex como módulo esencial y pide confirmación. Si alguna aplicación usa /server-status para monitorización, deja status activo y restringe el acceso por IP.

Aunque autoindex esté desactivado, conviene quitar también la opción Indexes en el directorio web. Edita la configuración principal:

sudo nano /etc/apache2/apache2.conf

Busca el bloque de /var/www/ y déjalo así:

<Directory /var/www/>
        Options -Indexes +FollowSymLinks
        AllowOverride None
        Require all granted
</Directory>

Deja AllowOverride None salvo que tu aplicación (WordPress, Laravel...) necesite archivos .htaccess; en ese caso usa AllowOverride All solo en el <Directory> de ese sitio.

Paso 3: Limitar peticiones y bloquear archivos ocultos

Crea un archivo de configuración propio para agrupar el resto de ajustes y no mezclarlos con los del paquete:

sudo nano /etc/apache2/conf-available/hardening.conf
# Tamaño máximo del cuerpo de una petición (10 MB)
LimitRequestBody 10485760

# Tiempo máximo de espera de E/S (el valor por defecto es 300 segundos)
Timeout 60

# Bloquear archivos y directorios ocultos (.git, .env, .htpasswd)
# excepto /.well-known/, que usan Let's Encrypt y otros servicios
<LocationMatch "/\.(?!well-known/)">
    Require all denied
</LocationMatch>

Ajusta LimitRequestBody si tu aplicación acepta subidas de archivos más grandes. El módulo reqtimeout, activo por defecto en Ubuntu, ya protege contra clientes que envían las cabeceras muy despacio (ataques tipo Slowloris).

Activa el archivo:

sudo a2enconf hardening

Paso 4: Configurar TLS moderno

Si tus sitios usan HTTPS, limita los protocolos a TLS 1.2 y 1.3 y usa solo cifrados con secreto perfecto hacia adelante. Crea otro archivo de configuración:

sudo nano /etc/apache2/conf-available/tls-hardening.conf
<IfModule mod_ssl.c>
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    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
    SSLHonorCipherOrder off
    SSLSessionTickets off
</IfModule>

SSLCipherSuite solo afecta a TLS 1.2; los cifrados de TLS 1.3 ya son seguros por diseño. Actívalo:

sudo a2enconf tls-hardening

Paso 5: Añadir cabeceras de seguridad

Las cabeceras de seguridad indican al navegador cómo tratar tu contenido. Activa mod_headers:

sudo a2enmod headers

Añade las cabeceras al final de /etc/apache2/conf-available/hardening.conf:

sudo nano /etc/apache2/conf-available/hardening.conf
<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

La cabecera HSTS obliga al navegador a usar siempre HTTPS, por lo que debe ir solo en el virtual host HTTPS (el de :443, por ejemplo /etc/apache2/sites-available/your_domain-le-ssl.conf), dentro de <VirtualHost>:

Header always set Strict-Transport-Security "max-age=31536000"

Paso 6: Validar y aplicar los cambios

Comprueba la sintaxis antes de recargar:

sudo apache2ctl configtest
Syntax OK

Si ves un error, el mensaje indica el archivo y la línea. Cuando la sintaxis sea correcta, recarga Apache:

sudo systemctl reload apache2

Continúa en la sección "Verificar el hardening", al final de la guía.

Hardening de Nginx

Paso 1: Ocultar la versión

Nginx incluye su versión en la cabecera Server y en las páginas de error. En Ubuntu la directiva está comentada en el bloque http de /etc/nginx/nginx.conf:

sudo nano /etc/nginx/nginx.conf

Descomenta la línea para que quede así:

server_tokens off;

La cabecera seguirá diciendo Server: nginx, pero sin número de versión. Eliminarla del todo requiere módulos externos y no aporta seguridad real.

Paso 2: Ajustar límites globales

Crea un archivo en /etc/nginx/conf.d/, que Nginx incluye dentro del bloque http:

sudo nano /etc/nginx/conf.d/hardening.conf
# Tamaño máximo del cuerpo de una petición
client_max_body_size 10m;

# Tiempos de espera para clientes lentos
client_body_timeout 15s;
client_header_timeout 15s;
send_timeout 15s;
keepalive_timeout 30s;

# Sin listados de directorios (es el valor por defecto, lo dejamos explícito)
autoindex off;

# Cabeceras de seguridad
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Sube client_max_body_size en el server de las aplicaciones que acepten archivos grandes.

Paso 3: Rechazar peticiones a hosts desconocidos

Si alguien accede por IP o con un Host que no es tuyo, Nginx sirve el primer sitio que encuentra. Es mejor cerrar esas conexiones con un servidor por defecto que no responde nada. Primero desactiva el sitio de ejemplo de Ubuntu, que ya ocupa default_server:

sudo rm /etc/nginx/sites-enabled/default

Crea el servidor por defecto:

sudo nano /etc/nginx/sites-available/000-default-deny
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;

    ssl_reject_handshake on;
    return 444;
}

return 444 cierra la conexión sin respuesta y ssl_reject_handshake on rechaza el handshake TLS sin necesidad de un certificado. Si ninguno de tus sitios usa HTTPS, quita las dos líneas listen 443 y ssl_reject_handshake. Actívalo:

sudo ln -s /etc/nginx/sites-available/000-default-deny /etc/nginx/sites-enabled/

Paso 4: Proteger cada sitio

Dentro del bloque server de cada sitio (por ejemplo /etc/nginx/sites-available/your_domain), bloquea los archivos ocultos y los métodos HTTP que no uses:

sudo nano /etc/nginx/sites-available/your_domain
    # Solo los métodos que usa el sitio
    if ($request_method !~ ^(GET|HEAD|POST)$) {
        return 405;
    }

    # Bloquear .git, .env, .htpasswd, etc., salvo /.well-known/
    location ~ /\.(?!well-known/) {
        deny all;
    }

Si sirves una API REST que usa PUT, PATCH o DELETE, añádelos a la expresión regular.

Paso 5: Configurar TLS moderno

Certbot incluye /etc/letsencrypt/options-ssl-nginx.conf en cada sitio HTTPS, y ese archivo ya limita los protocolos a TLS 1.2 y 1.3. Si gestionas los certificados a mano, crea un fragmento reutilizable:

sudo nano /etc/nginx/snippets/tls-hardening.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 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;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

E inclúyelo en el server HTTPS, junto a tus directivas ssl_certificate:

    include snippets/tls-hardening.conf;

No incluyas este fragmento en un sitio que ya incluye options-ssl-nginx.conf: definir dos veces la misma directiva en un bloque da error. Añade HSTS solo en el server que escucha en el puerto 443 (recuerda repetir aquí las cabeceras del paso 2 si este bloque define su propio add_header):

    add_header Strict-Transport-Security "max-age=31536000" always;

Paso 6: Validar y aplicar los cambios

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Recarga Nginx:

sudo systemctl reload nginx

Verificar el hardening

Comprueba las cabeceras de tu sitio desde el propio servidor o desde tu equipo:

curl -sI https://your_domain
HTTP/2 200
server: nginx
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin

Con Apache verás server: Apache, sin versión ni sistema operativo. Comprueba que los archivos ocultos están bloqueados (debe devolver 403):

curl -s -o /dev/null -w "%{http_code}\n" https://your_domain/.git/config
403

Comprueba que el servidor negocia TLS 1.3:

openssl s_client -connect your_domain:443 -servername your_domain </dev/null 2>/dev/null | grep "Protocol"
    Protocol  : TLSv1.3

Si usas Nginx, verifica también que las peticiones por IP se cierran sin respuesta:

curl -I http://your_server_ip
curl: (52) Empty reply from server

Para confirmar que TLS 1.0 y 1.1 están desactivados y obtener un análisis externo completo, pasa tu dominio por SSL Labs y por securityheaders.com.

Solución de problemas

El servicio no recarga tras un cambio. Ejecuta sudo apache2ctl configtest o sudo nginx -t y revisa el archivo y la línea que indican. Para ver el error completo del servicio usa sudo journalctl -u apache2 -n 30 o sudo journalctl -u nginx -n 30. Si no encuentras el fallo, restaura la copia del principio.

Error 403 en páginas que antes funcionaban. Con Options -Indexes, un directorio sin index.html o index.php devuelve 403 en lugar de un listado. Añade el archivo de índice o revisa la directiva DirectoryIndex (Apache) o index (Nginx).

Error 413 Request Entity Too Large al subir archivos. El límite de LimitRequestBody o client_max_body_size es menor que el archivo. Súbelo solo en el sitio o location que recibe las subidas.

Una integración deja de funcionar con 405. La aplicación usa un método que has bloqueado en el paso 4 de Nginx. Añádelo a la lista de métodos permitidos.

Conclusión

Tu servidor web ya no anuncia su versión, no lista directorios, rechaza archivos ocultos, peticiones sobredimensionadas y protocolos TLS antiguos, y envía cabeceras de seguridad básicas. Estos ajustes reducen la superficie de ataque sin cambiar el comportamiento de las aplicaciones legítimas.

Como siguientes pasos puedes:

  • Limitar la tasa de peticiones por IP con los módulos limit_req de Nginx para frenar ataques de fuerza bruta.
  • Instalar Fail2ban para bloquear en el firewall las IP que generan errores repetidos en los logs.
  • Definir una Content-Security-Policy adaptada a tu aplicación, empezando en modo Content-Security-Policy-Report-Only.