Proteger una API casera de un flood sin gastar un euro

Proteger una API casera de un flood sin gastar un euro
ContenidoContents

Tengo unas cuantas herramientas en la web que, además de funcionar en el navegador, exponen una API pública y gratuita: generar datos falsos españoles, validar un NIF, calcular un IBAN, generar UUID… Todo eso lo sirve un Mac mini en mi casa, detrás de un túnel de Cloudflare. Funciona genial, pero hay una pregunta que da un poco de vértigo: ¿y si alguien se pone a machacar una de esas APIs?

Esta semana añadí la API de UUID y me hice esa pregunta en serio. La respuesta tiene una parte interesante: el peligro no estaba donde yo pensaba.

El miedo equivocado

Mi primer reflejo fue mirar el coste de generar los datos. Pero generar un UUID, o un NIF, o un IBAN, es baratísimo: unos bytes aleatorios y cuatro cuentas. Aunque me llegaran miles de peticiones por segundo, la CPU ni se entera. Por ahí no era.

El problema real estaba en algo mucho más tonto: la telemetría.

El cuello de botella de verdad

Cada llamada a una de mis APIs —y cada vez que una herramienta del navegador registra que se ha usado— escribe dos ficheros: un contador de usos y un log fino para el panel de analítica. Y esa escritura usa un lock exclusivo y bloqueante (flock con LOCK_EX) sobre un fichero compartido por todas las herramientas.

Junta eso con cómo está montado Apache: prefork, con un máximo de 250 procesos atendiendo peticiones. El razonamiento del desastre es así:

  1. Llega un flood a la API.
  2. Todas las peticiones intentan escribir el mismo fichero y se ponen en cola esperando el lock.
  3. Cada petición en cola retiene un proceso de Apache.
  4. Si se llenan los 250… Apache deja de atender todo: no solo la API, sino la web, el blog, el aula de Moodle. Todo lo que sirve esa máquina.

Es decir: un flood a /api/uuid/ podía tumbarme el servidor entero, y no por generar UUIDs, sino por esperar para apuntar que los había generado. Absurdo, pero real.

Capa 1: que el origen nunca se bloquee

La primera defensa es de sentido común: la telemetría no merece bloquear a nadie. Cambié el lock a no bloqueante (LOCK_EX | LOCK_NB):

1if ($fh && flock($fh, LOCK_EX | LOCK_NB)) {
2    // ... actualizar el contador ...
3    flock($fh, LOCK_UN); fclose($fh);
4} elseif ($fh) {
5    fclose($fh); // ocupado: nos saltamos el registro y seguimos
6}

La idea: si el fichero está ocupado, no espero, me lo salto. Bajo un pico extremo perderé algún conteo en la analítica, pero ningún proceso de Apache se queda colgado en el lock. Mejor perder un número que tumbar la máquina.

Esto cierra el peor caso en el origen. Pero lo ideal es que el flood ni siquiera llegue al Mac.

Capa 2: cortar el flood en el edge (gratis)

Aquí entra Cloudflare. Y aquí está la parte que cuesta encontrar bien explicada: el rate-limiting gratis existe, pero no donde parece.

  • Si vas a Seguridad → WAF a nivel de cuenta, te pide comprar un complemento. Eso es Enterprise, de pago. No es ahí.
  • El rate-limiting gratis está dentro de cada dominio: entras en el dominio → Seguridad → Reglas de seguridad → Reglas de limitación de tasa. El plan Free te da una regla.

Esa regla la configuré para que cubra todas mis APIs y el endpoint de telemetría:

starts_with(http.request.uri.path, "/api/") or http.request.uri.path eq "/track.php"

Con estos límites (en Free el período y la duración solo permiten 10 segundos):

  • Contar por: IP
  • Umbral: 15 peticiones cada 10 segundos
  • Acción: bloquear durante 10 segundos

Para un humano es inalcanzable —mis propias APIs ya limitan a 300 peticiones/hora por IP—, pero para un script que dispara cientos por segundo es un muro inmediato. Y lo mejor: el bloqueo ocurre en los servidores de Cloudflare, así que esas peticiones jamás tocan mi Mac.

Probarlo

La teoría está muy bien, pero esto hay que verlo con un 429 en la cara. Una ráfaga de 25 peticiones seguidas:

1for i in $(seq 1 25); do
2  curl -s -o /dev/null -w "%{http_code} " "https://sergiocomeron.com/api/uuid/?v=4"
3done

Resultado:

200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 429 429 429 429 429 ...

Las 15 primeras pasan; de la 16 en adelante, Cloudflare las corta con un 429 Too Many Requests. Pasados los 10 segundos, el uso normal vuelve a responder 200. Justo lo que quería.

El resultado

Dos capas, cero euros:

  • Edge (Cloudflare): frena el flood antes de llegar a casa.
  • Origen (Apache/PHP): aunque algo se colara, no puede bloquear los procesos.

Y el uso normal ni se entera. Me quedo tranquilo sabiendo que una de mis herramientas, por mucho que la quieran reventar, no se va a llevar por delante el resto del servidor. Que, conviviendo todos los servicios en una sola máquina, es justo lo que necesitaba.

CompartirShare