La web quedó estática, pero los bots siguieron llegando
La web quedó estática, pero los bots siguieron llegando
Seguridad, errores 404 y rutas antiguas después de abandonar WordPress
En el artículo anterior conté por qué decidí reemplazar mi página personal en WordPress por una estructura mucho más sencilla.
HTML.
CSS.
Un poco de JavaScript.
Y Blogger para administrar los artículos.
La idea era reducir el mantenimiento, liberar recursos del servidor y dejar de sostener una plataforma completa para mostrar una página que prácticamente no cambia.
El resultado fue el esperado: una web más rápida, más fácil de mover y mucho más simple de entender.
Pero apareció un detalle importante.
Eliminar WordPress no elimina automáticamente todo lo que internet recuerda sobre el sitio.
Los buscadores siguen conociendo direcciones antiguas.
Los bots continúan intentando acceder a rutas que alguna vez existieron.
Los escáneres automatizados siguen buscando wp-login.php, xmlrpc.php, archivos de configuración y respaldos olvidados.
Y algunos rastreadores directamente inventan direcciones para comprobar qué responde el servidor.
La página se había vuelto estática.
El tráfico automatizado, no.
1. Una web estática tiene menos superficie de ataque, pero no es invisible
Convertir el sitio en archivos HTML redujo bastante el número de componentes que debía proteger.
Ya no había:
- Base de datos.
- Panel de administración.
- Plugins.
- Tema dinámico.
- PHP procesando cada visita.
- Inicio de sesión.
- Formularios.
- Tareas programadas de WordPress.
Eso elimina muchos problemas posibles.
Pero el servidor sigue recibiendo solicitudes.
Cada vez que alguien visita una URL, exista o no, el servidor debe analizar la petición y decidir qué respuesta entregar.
Si una persona entra en:
https://ckdyd.net/
el servidor entrega index.html.
Si intenta acceder a:
https://ckdyd.net/pagina-que-no-existe
el servidor debe detectar que el recurso no está disponible y responder con un error.
Una petición aislada no representa ningún problema.
El inconveniente aparece cuando cientos o miles de solicitudes comienzan a repetirse sobre rutas inexistentes.
La web puede ser pequeña.
La cantidad de ruido que recibe no necesariamente lo es.
2. El error 404 no es solamente una página bonita
Yo ya tenía un archivo 404.html.
Visualmente cumplía su función: explicaba que la página no existía y permitía volver al inicio.
Pero tener el archivo no era suficiente.
El servidor también debía saber cuándo mostrarlo.
Para eso agregué una instrucción en .htaccess:
ErrorDocument 404 /404.html
A partir de ese momento, cualquier dirección inexistente comenzó a mostrar la página personalizada.
Lo importante no es solo lo que ve la persona.
También importa el código HTTP que recibe el navegador o el bot.
Una página inexistente debe responder:
404 Not Found
No debería devolver 200 OK.
Si devuelve 200, los buscadores pueden interpretar que la dirección existe, aunque visualmente muestre un mensaje de error.
Eso genera lo que suele llamarse un soft 404: una página que parece un error para el usuario, pero técnicamente se presenta como contenido válido.
La diferencia parece pequeña.
Para los buscadores, no lo es.
La página de error debe parecer un error y también comportarse como uno.
3. No todas las rutas inexistentes significan lo mismo
Mientras revisaba los registros de acceso encontré muchas solicitudes repetidas sobre estructuras como:
/products/detail/123
/prizes/456
/tag/alguna-palabra
Esas rutas no pertenecían al nuevo sitio.
En algunos casos podían provenir de estructuras antiguas.
En otros, simplemente eran direcciones generadas o descubiertas por rastreadores.
Podía dejar que todas devolvieran 404, pero decidí diferenciar aquellas rutas que sabía que no volverían a existir.
Para ellas configuré una respuesta:
410 Gone
La diferencia conceptual es sencilla:
- 404: el recurso no fue encontrado.
- 410: el recurso fue retirado y no volverá.
En .htaccess, una regla de este tipo puede expresarse así:
RewriteEngine On
RewriteRule ^products/detail(?:/.*)?$ - [G,L,NC]
RewriteRule ^prizes(?:/.*)?$ - [G,L,NC]
RewriteRule ^tag(?:/.*)?$ - [G,L,NC]
El objetivo no era redirigir esas direcciones hacia la página principal.
Eso habría sido un error.
Enviar todas las rutas antiguas al inicio puede confundir tanto al visitante como al buscador. Una dirección sobre un producto inexistente no debería terminar silenciosamente en la portada como si nada hubiera ocurrido.
Cuando un contenido ya no existe, la respuesta correcta puede ser más útil que una redirección forzada.
El 410 comunica que esa estructura fue retirada de forma definitiva.
No obliga a los bots a desaparecer inmediatamente, pero les ofrece una señal más precisa.
4. WordPress se fue, pero sus atacantes no recibieron el aviso
Aunque el sitio ya no utilizaba WordPress, continuaron apareciendo solicitudes a rutas como:
/wp-login.php
/xmlrpc.php
/wp-admin/
/wp-json/
/wp-content/
Esto no significa necesariamente que alguien estuviera atacando mi sitio de forma personal.
Gran parte de internet es recorrida constantemente por herramientas automáticas.
Esas herramientas no saben qué tecnología utilizo antes de visitar el dominio.
Prueban.
Buscan rutas conocidas.
Analizan respuestas.
Y continúan con el siguiente sitio.
Como esas direcciones ya no tenían ninguna función, configuré respuestas 410 para indicar que habían desaparecido.
RewriteRule ^wp-admin(?:/.*)?$ - [G,L,NC]
RewriteRule ^wp-content(?:/.*)?$ - [G,L,NC]
RewriteRule ^wp-includes(?:/.*)?$ - [G,L,NC]
RewriteRule ^wp-json(?:/.*)?$ - [G,L,NC]
RewriteRule ^(?:wp-login\.php|xmlrpc\.php|wp-cron\.php)$ - [G,L,NC]
La intención no era “proteger WordPress”.
WordPress ya no estaba.
La intención era evitar que esas solicitudes recorrieran más pasos de los necesarios y dejar una respuesta clara.
Un sitio estático no necesita fingir que tal vez existe un panel de administración.
Puede responder directamente:
Eso ya no está aquí.
5. Proteger los archivos que nunca deberían ser públicos
Otro punto importante fue pensar no solo en las páginas visibles, sino también en los archivos que podrían subirse por error.
Un sitio estático normalmente necesita publicar:
- HTML.
- CSS.
- JavaScript.
- Imágenes.
- Fuentes.
- Iconos.
No necesita exponer:
- Bases de datos.
- Archivos
.env.
- Registros.
- Copias de seguridad.
- Exportaciones SQL.
- Archivos ZIP.
- Configuraciones internas.
- Repositorios Git.
- Scripts ejecutables.
El problema de estos archivos no es únicamente que el sitio los utilice.
El riesgo real aparece cuando alguien los sube accidentalmente dentro de public_html.
Un respaldo llamado:
sitio-anterior.zip
puede contener código, configuraciones, correos, bases de datos o credenciales.
Aunque nadie coloque un enlace visible hacia él, sigue siendo descargable si alguien descubre o adivina la ruta.
Por eso agregué reglas para impedir el acceso a extensiones sensibles:
<FilesMatch "(?i)\.(sql|sqlite|log|env|bak|backup|old|zip|rar|7z|tar|gz)$">
Require all denied
</FilesMatch>
También bloqueé archivos ocultos como:
.git
.env
.svn
.htaccess
Manteniendo la excepción de .well-known, que puede ser necesaria para certificados y validaciones.
La regla más importante sigue siendo otra:
Lo que no debe ser público no debería estar dentro de la carpeta pública.
El .htaccess funciona como una protección adicional.
No reemplaza una estructura de archivos ordenada.
6. Un sitio HTML no necesita ejecutar scripts del servidor
Como la nueva página está construida únicamente con recursos estáticos, no necesita ejecutar PHP, Python, Perl ni scripts de shell.
Eso permitió aplicar una regla más estricta:
<FilesMatch "(?i)\.(php[0-9]?|phtml|phar|cgi|pl|py|sh)$">
Require all denied
</FilesMatch>
La lógica es simple.
Si el sitio no necesita esos archivos, no existe una razón válida para servirlos.
Esta decisión reduce el riesgo de que un script subido accidentalmente —o introducido mediante una cuenta comprometida— pueda utilizarse desde el navegador.
No aplicaría esta medida en cualquier proyecto.
Una aplicación dinámica puede necesitar PHP o algún otro lenguaje del lado del servidor.
Pero en un sitio completamente estático, permitir su acceso sería conservar una capacidad que ya no se utiliza.
Cada función innecesaria es también una posibilidad innecesaria.
7. Permisos simples y predecibles
El archivo .htaccess quedó con permisos:
644
Eso significa:
- El propietario puede leerlo y modificarlo.
- El grupo puede leerlo.
- El resto puede leerlo.
- Nadie fuera del propietario puede escribirlo.
Las carpetas, en general, pueden utilizar:
755
No había ninguna necesidad de usar permisos 777.
Dar permisos totales suele presentarse como una solución rápida cuando algo no funciona, pero muchas veces solamente oculta el verdadero problema y amplía el riesgo.
En una web estática, la estructura debería ser sencilla:
- Archivos públicos con permisos de lectura.
- Carpetas accesibles para recorrer su contenido cuando corresponda.
- Escritura limitada al propietario.
- Nada ejecutable salvo que exista una razón concreta.
No se trata de aplicar números de memoria.
Se trata de preguntarse:
¿Quién necesita leer, escribir o ejecutar este archivo?
Si la respuesta es “nadie”, ese permiso no debería estar habilitado.
8. Encabezados para limitar comportamientos innecesarios
También incorporé algunos encabezados HTTP básicos:
<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"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
Estos encabezados no convierten el sitio en una fortaleza.
Tampoco reemplazan una configuración correcta.
Pero ayudan a definir comportamientos que la página no necesita permitir.
Por ejemplo:
- Evitar que el navegador intente interpretar un archivo como otro tipo de contenido.
- Limitar la posibilidad de incrustar la página dentro de marcos externos.
- Reducir la información enviada mediante el encabezado de referencia.
- Desactivar permisos como cámara, micrófono o ubicación.
Mi sitio personal no necesita acceder a un micrófono.
No necesita solicitar la ubicación del visitante.
No necesita utilizar la cámara.
Entonces puede declararlo explícitamente.
La seguridad también consiste en desactivar capacidades que no forman parte del proyecto.
9. El problema del CPU no estaba en el peso del HTML
Después de reemplazar WordPress por archivos estáticos, esperaba una reducción inmediata del uso de recursos.
Por eso resultó extraño encontrar momentos de consumo elevado de CPU.
Al revisar los registros aparecían rastreadores como:
- Google.
- Bing.
- Meta.
- PetalBot.
- Otros bots automatizados.
Muchos solicitaban rutas inexistentes de forma repetitiva.
Una página 404.html puede pesar muy poco.
Pero si recibe miles de solicitudes, el servidor debe procesar miles de respuestas.
Además, en un alojamiento compartido pueden intervenir otras capas:
- LiteSpeed o Apache.
- Reglas de reescritura.
- ModSecurity.
- Registros de acceso.
- Sistemas antimalware.
- Controles del proveedor.
El problema no era que el archivo HTML fuera pesado.
Era el volumen.
Esto también permitió entender un límite importante del .htaccess.
Bloquear un bot por su User-Agent puede evitar que siga recorriendo el sitio, pero la solicitud ya llegó al servidor.
El servidor todavía debe leerla, analizar la regla y devolver un 403.
Por eso el bloqueo local ayuda, pero no siempre resuelve una avalancha por sí solo.
Cuando el volumen es realmente alto, la solución más efectiva suele encontrarse antes del servidor de origen:
- Firewall.
- CDN.
- Rate limiting.
- Reglas de Cloudflare.
- Bloqueos por reputación o comportamiento.
El .htaccess organiza las respuestas del sitio.
No puede impedir que internet toque la puerta.
10. Bloquear todos los bots tampoco era la respuesta
La reacción inmediata frente a un consumo elevado podría ser bloquear cualquier rastreador.
Pero no todos cumplen la misma función.
Google y Bing pueden ser importantes para la indexación.
Los bots de Meta pueden generar vistas previas cuando alguien comparte un enlace.
Otros rastreadores pueden no aportar nada relevante para mi sitio.
La decisión no debía basarse solamente en el nombre del bot.
Había que observar:
- Cuántas solicitudes realizaba.
- Qué rutas visitaba.
- Con qué frecuencia regresaba.
- Si respetaba los códigos de respuesta.
- Si aportaba algún beneficio.
- Si su comportamiento afectaba los recursos.
Bloquear por User-Agent también tiene una limitación evidente:
El nombre declarado por el bot puede falsificarse.
Una herramienta maliciosa puede presentarse como Googlebot.
Por eso este tipo de regla sirve para controlar rastreadores identificables y relativamente honestos, pero no reemplaza una validación más profunda cuando existe tráfico abusivo.
El objetivo no era construir una lista infinita de nombres.
Era reducir el ruido sin perjudicar la visibilidad legítima del sitio.
11. La página 404 también debía ser simple
Otro detalle fue revisar la propia página de error.
Un 404.html no debería transformarse en otra aplicación.
Debe cargar rápido y utilizar pocos recursos.
También conviene que sus rutas sean absolutas desde la raíz:
<link rel="stylesheet" href="/assets/css/styles.css">
En lugar de:
<link rel="stylesheet" href="assets/css/styles.css">
La diferencia importa cuando el error ocurre dentro de una ruta profunda.
Si alguien visita:
/products/detail/123
una referencia relativa podría intentar cargar:
/products/detail/assets/css/styles.css
Ese archivo tampoco existe.
Entonces se genera otra solicitud 404.
Un error provoca otro error.
Y la página creada para manejar direcciones inexistentes comienza a producir nuevas direcciones inexistentes.
Por eso mantuve el 404.html liviano, sin procesos dinámicos y con recursos correctamente referenciados.
La página de error no necesita impresionar.
Necesita responder bien.
12. El resultado final
La estructura terminó siendo bastante sencilla:
public_html/
├── .htaccess
├── index.html
├── 404.html
├── privacy.html
└── assets/
El .htaccess cumple funciones concretas:
- Define la página 404.
- Desactiva el listado de carpetas.
- Responde
410en rutas retiradas.
- Rechaza rastreadores seleccionados.
- Bloquea archivos sensibles.
- Impide el acceso a scripts innecesarios.
- Agrega encabezados básicos de seguridad.
No intenta convertir una web estática en un sistema de seguridad complejo.
Solamente establece límites coherentes con el tipo de sitio.
Y esa es probablemente la idea más importante de todo el proceso:
La seguridad debe acompañar la arquitectura real, no una arquitectura imaginaria.
Si el sitio no utiliza PHP, lo bloqueo.
Si no recibe formularios, no necesita aceptar escrituras.
Si una ruta no volverá, responde 410.
Si un archivo no debe ser público, no lo subo o impido su acceso.
Si un bot consume recursos sin aportar valor, lo limito antes de que llegue al servidor cuando sea posible.
Simplificar también significa definir límites
Reemplazar WordPress por HTML no consistió únicamente en reducir espacio.
También implicó redefinir qué podía hacer el sitio.
Y, sobre todo, qué ya no necesitaba hacer.
Una web estática ofrece menos funciones.
Pero esa limitación puede convertirse en una ventaja cuando está alineada con el objetivo real del proyecto.
El sitio no necesita administrar usuarios.
No necesita ejecutar código en cada visita.
No necesita recibir archivos.
No necesita conectarse a una base de datos.
Necesita mostrar información, cargar rápido y permanecer disponible.
Todo lo demás puede bloquearse, retirarse o delegarse.
La simplicidad no elimina la necesidad de seguridad.
La vuelve más clara.
Porque cuando un sistema hace pocas cosas, también resulta más fácil identificar qué comportamientos no deberían estar permitidos.
La página ahora tiene menos componentes.
Menos puntos de fallo.
Menos archivos sensibles.
Menos mantenimiento.
Los bots siguen llegando.
Los escáneres siguen buscando WordPress.
Las rutas falsas continúan apareciendo en los registros.
Pero el servidor ya no tiene que preguntarse qué hacer con cada una.
Ahora puede responder con claridad:
Esto existe.
Esto no existe.
Esto fue retirado.
Y a esto no podés acceder.
Comentarios
Publicar un comentario