Qué resuelve
Happy Hacking hace una cosa: te pone un reto corto, respondes, y el servidor te dice si acertaste, por qué, y cuánto XP te llevas. Los retos son de análisis defensivo y están en español: leer una cabecera HTTP, interpretar una línea de log simulado, razonar un permiso de Linux.
El límite está escrito antes que el catálogo: nada de técnicas ofensivas. Dentro del producto no caben la intrusión paso a paso, los payloads reutilizables, el robo de credenciales, la evasión ni la persistencia. Las IPs de los ejercicios salen de los rangos reservados para documentación (RFC 5737) y del direccionamiento privado (RFC 1918); los dominios son example.com, reservado por la RFC 2606.
Las rutas cubren seguridad web, Linux defensivo, Bash, Python, y logs y respuesta a incidentes. Sobre tres de esos temas hay artículos en este sitio: ejecutar scripts en Linux, el shebang en Bash y Python y automatizar tareas con Python.
Cómo está construido, y por qué así
El repositorio es privado, así que lo que sigue describe decisiones; no enlaza al código.
Un repositorio, dos destinos. El frontend es Astro estático y lo publica Vercel al mergear a main. El backend es FastAPI en un contenedor Docker sobre un droplet, detrás de Caddy, que gestiona el TLS. El puerto de la aplicación no se publica al exterior: Caddy es lo único que lo alcanza. El navegador llama a api.happyhacking.app porque ese origen se inyecta en el build desde vercel.json. El despliegue del backend es un job encadenado detrás de las compuertas y condicionado a que exista su configuración: mientras falte, se ve como «skipped». Sin esa condición fallaba en su primer paso y dejaba main en rojo en cada merge, que es la forma más rápida de entrenar a alguien para ignorar el CI.
Por qué un archivo y un droplet, y no una base gestionada. La decisión está fechada el 24 de agosto de 2026 en la documentación del repositorio: con los créditos del plan de pago ya consumidos, la base gestionada costaba bastante más que el droplet. Lo que había que guardar entonces era el correo de quien se apuntaba a la lista de espera, y eso cabe en un archivo JSONL de solo anexar sobre un volumen de Docker con nombre físico fijo. Se documentó con su caducidad puesta: deja de valer en cuanto haya varios workers, réplicas o carga sostenida, porque cada alta recorre el archivo entero y cada registro confirmado paga flush() más fsync(). Por eso el compose de producción arranca sin --workers.
El catálogo se exporta en el build, no se pide por fetch. Antes el contenido se pedía desde el navegador y el HTML servido no traía ni un título: para un buscador el sitio estaba vacío. Ahora un script lo exporta desde Python a un JSON commiteado y Astro lo renderiza durante el build. Una prueba obliga a regenerarlo cuando cambia el contenido, y aborta si detecta claves de solución dentro.
La solución no viaja con la pregunta. El payload público de un reto se construye campo a campo: fuera answer, accepted_answers, los dos feedbacks, la explicación y las pistas. Las pistas se piden de una en una por su propio endpoint; el reto solo dice cuántas hay. La explicación vuelve al acertar o al gastar el último intento, no antes. El repositorio anota también el límite: es una barrera estructural, no una garantía semántica. La auditoría del 24 de agosto de 2026 lo demostró: encontró retos aprobables sin resolverlos, unos con una variante aceptada dentro de su propio identificador —que viaja al navegador— y otros filtrándola en el feedback de error. Se cerró, y hoy una compuerta lo comprueba. Lo más interesante es que la primera versión de esa compuerta prometía dos cosas que no cumplía: comparaba con el normalizador en vez de preguntarle al calificador, y miraba cada reto aislado cuando la respuesta también se puede copiar de la lección que lo rodea. Una refutación cruzada lo midió y se reescribió.
Un módulo de contenido roto no tumba la API. El agregador importa cada ruta por separado: si una falla, /health responde content_status: degraded nombrando el módulo y las demás se siguen sirviendo. Un catálogo degradado que parece sano es peor que uno caído.
El calificador normaliza en vez de enumerar. La comparación era exacta, así que cada reto tenía que listar a mano todas las formas en que un humano escribe lo mismo. Con el tope de intentos, eso bloqueaba a gente que sí sabía la respuesta: «el comando pwd», «pwd.» y «$ pwd» quemaban los intentos, y el siguiente —«pwd» exacto— se topaba con el límite. Hoy baja a minúsculas y quita acentos, prompt de shell, puntuación de los bordes y una lista declarada de muletillas. Es conservador a propósito: no recorta por dentro del token, para que 203.0.113.10 y x-frame-options no colapsen en otra respuesta.
Las cabeceras que el producto enseña, servidas. Un middleware pone CSP, nosniff, Referrer-Policy, X-Frame-Options, CORP y HSTS en cada respuesta de la API, y el frontend las declara en vercel.json. El origen está anotado en el código: una auditoría encontró que uno de los retos enseñaba X-Content-Type-Options y ni el sitio ni la API la enviaban. Con el mismo criterio, un 422 dice qué campo falló, nunca con qué valor: la respuesta por defecto devolvía íntegro un campo de cien mil caracteres, medido el 27 de agosto de 2026.
Las compuertas de CI son las que ya se pusieron rojas. Dos miran el bundle ya construido: no puede contener localhost, y tiene que contener el origen que declara vercel.json, leído de ese archivo en vez de repetido a mano. La primera nació de un defecto real: frontend/.env no está versionado, un build sin esa variable caía a http://localhost:8000, y eso publica un sitio donde el navegador de cada visitante manda su correo a su propia máquina, en silencio y en el cliente.
Qué hace y qué no hace
Hace: cargar rutas, mostrar lecciones y retos, servir pistas de una en una, calificar en el servidor con explicación y XP, limitar los intentos por reto y por sesión, y abrir el resto del temario con el acceso anual.
No hace, y no es un descuido: no hay cuentas ni perfiles, y tu avance solo se recupera en el navegador donde lo hiciste. Lo que se guarda en el servidor va en archivos, no en una base de datos. X-Session-Id es antifricción para contar por persona, no una frontera de seguridad: sale de una cabecera que el cliente puede cambiar y, si falta, de una IP que puede rotar.
Dónde está
En línea, en happyhacking.app.
