# Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting

Si tienes un WordPress corriendo sobre **Nginx + PHP-FPM**, estas dos configuraciones van a mejorar drásticamente el rendimiento y la seguridad de tu sitio: **FastCGI Cache** para servir páginas a velocidad de vértigo y **Rate Limiting** para proteger las rutas más atacadas.

Todo esto asume que ya tienes Nginx sirviendo WordPress a través de PHP 8.4 mediante un socket Unix (`php8.4-fpm.sock`).

## FastCGI Cache: sirve WordPress sin tocar PHP

![Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting](fig-01.webp)

La idea es simple: cuando un visitante anónimo solicita una página, Nginx la procesa una vez a través de PHP-FPM y guarda el resultado en disco. Las siguientes visitas a esa misma URL se sirven directamente desde la caché de Nginx, sin que PHP ni la base de datos se enteren.

### Definir la zona de caché

En el bloque `http {}` de tu `nginx.conf`:

```nginx
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2
                   keys_zone=WORDPRESS:100m
                   inactive=60m
                   max_size=512m
                   use_temp_path=off;

fastcgi_cache_key "$scheme$request_method$host$request_uri";

fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
```

 - **`keys_zone=WORDPRESS:100m`**: reserva 100 MB en memoria para las claves de caché. Suficiente para millones de entradas.
 - **`inactive=60m`**: si una entrada no se accede en 60 minutos, se elimina.
 - **`max_size=512m`**: tamaño máximo en disco. Ajústalo según tu espacio disponible.
 - **`use_temp_path=off`**: escribe directamente en el directorio de caché, evitando una copia intermedia.
 - **`fastcgi_ignore_headers`**: PHP y WordPress envían headers que invalidarían la caché innecesariamente. Los ignoramos.

### Decidir qué NO cachear

No todo debe cachearse. Los usuarios logueados, el panel de administración, las búsquedas y las páginas de WooCommerce (carrito, checkout, mi cuenta) deben llegar siempre a PHP:

```nginx
set $skip_cache 0;

# Peticiones POST
if ($request_method = POST) { set $skip_cache 1; }

# URLs con query strings
if ($query_string != "") { set $skip_cache 1; }

# Rutas de administración y dinámicas
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/wp-cron.php|/cart/|/checkout/|/my-account/") {
    set $skip_cache 1;
}

# Cookies de usuario logueado o carrito
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_cart_hash|woocommerce_items_in_cart") {
    set $skip_cache 1;
}
```

### Aplicar la caché en el bloque PHP

```nginx
location ~ \.php$ {
    try_files $uri =404;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Caché
    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 200 301 302 60m;
    fastcgi_cache_valid 404 1m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;

    # Resiliencia
    fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
    fastcgi_cache_lock on;
    fastcgi_cache_lock_timeout 5s;
    fastcgi_cache_background_update on;

    # Header de debug
    add_header X-FastCGI-Cache $upstream_cache_status;
}
```

Tres directivas clave aquí:

 - **`fastcgi_cache_use_stale ... updating`**: si PHP-FPM falla o la caché está refrescándose, Nginx sirve la versión anterior en lugar de devolver un error. Tu sitio se vuelve prácticamente indestructible ante picos de tráfico.
 - **`fastcgi_cache_lock on`**: ante un cache MISS con múltiples peticiones simultáneas a la misma URL, solo una pasa a PHP-FPM. El resto espera el resultado. Evita la «estampida» de requests idénticos.
 - **`fastcgi_cache_background_update on`**: cuando una entrada está a punto de expirar, Nginx la refresca en segundo plano mientras sigue sirviendo la versión actual.

### Archivos estáticos

Los archivos estáticos no deben pasar por PHP. Sírvelos directamente con cabeceras de caché agresivas:

```nginx
location ~* \.(js|css|png|jpg|jpeg|gif|webp|avif|ico|svg|woff2?|ttf|eot|otf)$ {
    expires 365d;
    access_log off;
    add_header Cache-Control "public, immutable";
    try_files $uri =404;
}
```

### Buffers y compresión

```nginx
# Buffers FastCGI
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 32k;

# Gzip
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
           text/xml application/xml application/xml+rss text/javascript
           image/svg+xml application/x-font-ttf font/opentype;
```

### Crear el directorio y verificar

```bash
mkdir -p /var/cache/nginx/fastcgi
chown www-data:www-data /var/cache/nginx/fastcgi
nginx -t && systemctl reload nginx
```

Para comprobar que funciona:

```bash
curl -I https://tudominio.com | grep X-FastCGI-Cache
```

La primera visita mostrará `MISS`, la segunda `HIT`. Si ves `BYPASS`, es que la petición cumple alguna de las reglas de exclusión (usuario logueado, POST, etc.).

## Rate Limiting: proteger las rutas sensibles

WordPress tiene varias rutas que son objetivo constante de ataques de fuerza bruta y abuso. Vamos a limitar cuántas peticiones por segundo puede hacer una misma IP a cada una.

### Definir las zonas

En el bloque `http {}`:

```nginx
limit_req_zone $binary_remote_addr zone=login:10m rate=3r/m;
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=1r/m;
limit_req_zone $binary_remote_addr zone=admin:10m rate=10r/s;

limit_req_log_level warn;
```

Cada zona usa `$binary_remote_addr` (la IP del cliente en formato binario, ocupa solo 16 bytes para IPv6) como clave y reserva 10 MB de memoria compartida.

### Aplicar los límites

```nginx
# wp-login.php — 3 peticiones por minuto
location = /wp-login.php {
    limit_req zone=login burst=5 nodelay;
    limit_req_status 429;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

# xmlrpc.php — 1 petición por minuto (o bloquearlo directamente)
location = /xmlrpc.php {
    # Opción A: rate limit
    limit_req zone=xmlrpc burst=2 nodelay;
    limit_req_status 429;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Opción B: bloquearlo del todo
    # deny all;
    # return 444;
}

# wp-admin — más permisivo para uso normal
location /wp-admin/ {
    limit_req zone=admin burst=20 nodelay;
    limit_req_status 429;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

# wp-cron.php
location = /wp-cron.php {
    limit_req zone=xmlrpc burst=2 nodelay;
    limit_req_status 429;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
```

### Tabla resumen de límites

| Ruta | Rate | Burst | Motivo |
|---|---|---|---|
| `wp-login.php` | 3/min | 5 | Protección contra fuerza bruta |
| `xmlrpc.php` | 1/min | 2 | Vector de ataque habitual |
| `wp-admin/` | 10/s | 20 | Permite uso normal, frena abuso |
| `wp-cron.php` | 1/min | 2 | Evitar abuso de cron externo |

El parámetro **`burst`** define cuántas peticiones extra se permiten por encima del rate antes de empezar a rechazar. Con **`nodelay`**, esas peticiones extra del burst se procesan inmediatamente en lugar de encolarse.

Cuando una IP supera el límite, recibe un **HTTP 429 Too Many Requests**.

### Monitorizar los bloqueos

```bash
tail -f /var/log/nginx/error.log | grep "limiting"
```

### Nota sobre xmlrpc.php

Si no usas aplicaciones externas que se conecten a tu WordPress mediante XML-RPC (la app móvil de WordPress, Jetpack, pingbacks…), bloquéalo directamente con `return 444`. Es el vector de ataque más explotado en WordPress, usado tanto para fuerza bruta como para ataques de amplificación DDoS.

## Resultado

![Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting](fig-02.webp)

Con estas dos configuraciones:

 - **Los visitantes anónimos** reciben las páginas directamente desde la caché de Nginx, sin que PHP ni MySQL intervengan. El tiempo de respuesta pasa de cientos de milisegundos a unos pocos.
 - **Los picos de tráfico** se absorben sin problemas gracias a `fastcgi_cache_lock` y `fastcgi_cache_use_stale`.
 - **Los ataques de fuerza bruta** contra wp-login.php y xmlrpc.php quedan limitados a unas pocas peticiones por minuto por IP.
 - **PHP-FPM** se dedica solo a lo que realmente necesita procesamiento dinámico: el panel de administración, usuarios logueados y formularios.
