
Todos los posts de este blog viven en una tabla de SQLite. Un campo content con HTML, un title, un slug, unas categorías. Nada más. Si alguien consigue acceso a esa base de datos, ya sea por un backup que se filtra, una credencial comprometida o una vulnerabilidad en el servidor, puede abrir el fichero, cambiar el contenido de cualquier artículo y nadie se enteraría.
No hablo de desfigurar la portada con un mensaje en rojo. Hablo de algo más sutil, como cambiar una recomendación técnica, alterar una cifra en un artículo sobre inversión o insertar un enlace malicioso en un tutorial. El tipo de modificación que pasa desapercibida porque el post sigue pareciendo legítimo.
Eso nos llevó a construir un sistema de verificación de integridad que funciona desde fuera del blog. Un segundo servidor que descarga periódicamente el contenido de cada post publicado, calcula un hash criptográfico y lo compara con el que tenía la última vez. Si algo cambió sin que nosotros lo editáramos, salta una alerta. Desde la perspectiva de seguridad, en seguridad, SEO y rendimiento del blog profundizamos en este aspecto.
La primera idea que viene a la cabeza es sencilla. Cada vez que publicas un post, calculas un hash SHA-256 del contenido y lo guardas en una columna extra de la base de datos. Antes de servir el post, verificas que el hash coincide. Problema resuelto.
Excepto que no.
Si el atacante tiene acceso a la base de datos, puede modificar tanto el contenido como el hash. Actualiza el HTML, recalcula el SHA-256, guarda ambos. Tu verificación interna dirá que todo está bien porque el hash del contenido nuevo coincide con el hash nuevo. Es como poner una cerradura en una puerta y dejar la llave pegada con cinta adhesiva en el marco.
La integridad solo funciona si la fuente de verdad está fuera del sistema que intentas proteger. Por eso el hash tiene que vivir en otro sitio. En nuestro caso, en un segundo VPS que no comparte infraestructura con el blog.
El blog expone dos endpoints protegidos con una API key de solo lectura (INTEGRITY_API_KEY), separada de la clave de administración.
El primero, GET /api/integrity/manifest, devuelve la lista de posts publicados con sus slug y un timestamp llamado contentUpdatedAt. El segundo, GET /api/integrity/post, devuelve sin parámetros los datos crudos de todos los posts publicados en una sola respuesta, con título, contenido HTML, extracto, imagen de portada, categorías y tags.
El verificador hace exactamente dos peticiones HTTP por ejecución, independientemente de cuántos posts haya. Primero el manifiesto para saber qué hay publicado, luego el bulk para obtener los datos con los que calcular los hashes. Sin paginación, sin rate limiting que se interponga, sin complejidad.
Para que dos sistemas independientes calculen el mismo hash a partir de los mismos datos, necesitas una representación canónica. No puedes hashear el JSON tal cual llega de la API porque el orden de las claves, los espacios en blanco o un campo null frente a una cadena vacía producirían hashes distintos para el mismo contenido.
Nuestra función de canonicalización toma siete campos de cada post y los serializa siempre en el mismo orden, con las mismas reglas de normalización.
function canonicalize(post: PostData): string {
return JSON.stringify({
title: (post.title ?? "").trim(),
slug: post.slug,
content: post.content ?? "",
excerpt: (post.excerpt ?? "").trim(),
coverImage: post.coverImage ?? "",
categories: [...post.categories].sort(),
tags: [...post.tags].sort(),
});
}Los null se convierten en cadena vacía. Los títulos y extractos se recortan. Las categorías y tags se ordenan alfabéticamente por su slug para que el orden de inserción en la base de datos no afecte al resultado. La cadena es determinista y siempre produce el mismo SHA-256 para el mismo contenido visible.
No incluimos campos como readingTime, publishedAt o seriesId porque son metadatos operativos. Un cambio en el tiempo de lectura calculado o en el orden dentro de una serie no constituye una alteración del contenido que un lector ve. Incluirlos generaría falsos positivos cada vez que reorganizamos una serie o corregimos un cálculo interno.
Cuando editas un post desde el panel de administración, el blog actualiza un campo llamado contentUpdatedAt. Este timestamp solo cambia cuando se modifican campos visibles como el título, el contenido, el extracto o la imagen. Si alguien modifica la base de datos directamente con un UPDATE de SQL, ese campo no se actualiza porque la modificación no pasa por la lógica de la aplicación.
El verificador usa esta señal para clasificar cada cambio. Si el hash difiere y contentUpdatedAt también cambió, se trata de una edición legítima y el verificador actualiza su baseline sin generar alarma. Pero si el hash difiere y contentUpdatedAt sigue igual, alguien modificó el contenido sin pasar por el flujo normal. Eso dispara una alerta crítica.
Un atacante sofisticado podría actualizar también contentUpdatedAt si sabe que existe y entiende cómo funciona. Por eso el verificador implementa una auditoría aleatoria que en cada ejecución verifica un 20% de los posts al azar, aunque su timestamp no haya cambiado. En cinco ciclos, cada post se audita al menos una vez con alta probabilidad.
El verificador almacena sus baselines en una base de datos SQLite local en el segundo servidor. Cada entrada contiene el slug del post, el hash SHA-256 esperado, el timestamp de la última verificación y los datos canonicalizados para poder hacer diff si algo cambia.
Pero esa base de datos local también es un vector de ataque. Si alguien compromete los dos servidores, tanto el blog como el verificador, podría modificar el contenido del post y al mismo tiempo el hash almacenado en el baseline. La verificación seguiría pasando.
Para dificultar este escenario, cada baseline se firma con una clave privada Ed25519 que se genera automáticamente en la primera ejecución del verificador. Antes de confiar en un baseline almacenado, el verificador verifica la firma. Si alguien manipuló la base de datos del verificador sin tener la clave privada, la firma no valida y salta una alerta.
// Al guardar un baseline
const sig = signHash(contentHash);
// firma Ed25519 del hash, almacenada junto al baseline
// Antes de comparar
if (!verifySignature(baseline.contentHash, baseline.signature)) {
// ALERTA, baselines.db manipulado
}La clave privada se guarda con permisos 400, solo lectura para el owner del proceso. No se comparte, no se sube a ningún repositorio, no sale del servidor verificador.
El cron cada ocho horas está bien para detección rutinaria, pero a veces quieres verificar ahora. En nuestro caso, tenemos un agente con capacidades autónomas llamado Claudio corriendo en el mismo VPS que el verificador. Es un bot conectado a Telegram con acceso a herramientas del sistema, capaz de leer ficheros, ejecutar comandos y revisar logs.
Le dimos un fichero de contexto (AGENT.md) que describe qué es el verificador, dónde está instalado y qué comandos puede ejecutar. Cuando le pides una verificación no necesita instrucciones detalladas, ya sabe que tiene que ir a /opt/blog-verifier y lanzar node dist/index.js --force.
Lo que de verdad cambia las cosas es que le hablas en lenguaje natural. No hace falta que recuerdes rutas, flags ni la sintaxis exacta de cada comando. Le dices "ejecuta una verificación forzada" y lo hace. Le preguntas "cuándo fue la última verificación" y busca en el log. Si ves una línea en la salida que no entiendes, se la pegas y te la explica. Es como tener a alguien de guardia que entiende el sistema y te responde en el acto.


Y todo esto desde el Telegram del móvil, en cualquier sitio. En el sofá, en el tren, tomando un café. No necesitas abrir un portátil, conectarte por SSH ni recordar en qué servidor vive cada cosa. Le mandas un mensaje, te contesta con el estado y sigues con tu vida.
Eso convierte una herramienta de seguridad pasiva en algo que consultas activamente cada vez que despliegas un cambio, restauras un backup o simplemente quieres dormir tranquilo.
Y no estás limitado al móvil. Telegram funciona igual en el ordenador de sobremesa, en el portátil o en la versión web del navegador. Da igual dónde estés o qué dispositivo tengas a mano, la conversación con el agente es la misma y el contexto se mantiene. Le preguntas algo desde el móvil mientras vas en el metro, llegas a casa y sigues desde el portátil. La verificación de integridad deja de ser algo que ejecutas cuando te acuerdas para convertirse en algo que consultas cuando te apetece.


Así funciona una ejecución típica del verificador.
Descarga el manifiesto del blog en una petición HTTP.
Compara los timestamps contentUpdatedAt con los baselines almacenados para decidir qué posts verificar.
Descarga todos los posts en bulk con una segunda petición.
Para cada post que necesita verificación, canonicaliza los datos y calcula el SHA-256.
Verifica la firma Ed25519 del baseline almacenado.
Compara el hash actual con el baseline. Si coincide, todo bien. Si difiere con cambio en contentUpdatedAt, es una edición legítima y actualiza el baseline. Si difiere sin cambio en el timestamp, alerta crítica.
Detecta posts que desaparecieron del manifiesto, ya sea porque se eliminaron o se despublicaron.
Registra todo en un log de verificación y, si hay alertas críticas, notifica por Telegram.
El resultado es un sistema que protege la integridad del contenido con tres capas independientes. Hashes SHA-256 calculados fuera del blog, la señal contentUpdatedAt para separar ediciones legítimas de manipulación directa, y firmas Ed25519 que protegen la propia base de datos del verificador. Sobre esta técnica, en firma digital y esteganografía para trazabilidad entramos en profundidad.
La seguridad no es un producto que instalas, es un hábito que construyes. Verificar periódicamente que tu contenido no ha sido alterado es tan básico como hacer backups, y casi nadie lo hace.

Defensa contra inyección de prompts, prevención de alucinaciones del modelo, rate limiting en capas y el resto de cambios que endurecieron ScamDetector para producción real.

ScamDetector combina inteligencia artificial, búsqueda de reputación de teléfonos y escaneo de URLs para ayudarte a identificar estafas digitales. Sin registro, sin datos almacenados.

Repaso completo de las medidas de seguridad que puedes aplicar a un VPS Linux: desde CrowdSec y el firewall hasta el hardening del kernel, pasando por SSH, Docker y las actualizaciones automáticas.