Saltar al contenido
Volver al inicio

00 / Arquitectura

Cómo está construida esta web

Trabajo en un SOC, así que esta web es también una declaración de intenciones: si no fuera capaz de asegurar mi propio sitio, poco tendría que aportar asegurando el de nadie. Abajo están las decisiones que hay detrás y el motivo de cada una, no una lista de tecnologías.

Resumen del stack

Frontend
Astro 7 (SSG) · Tailwind CSS · TypeScript
Backend
PHP 8 (PDO) · MySQL / MariaDB · sin framework
Infraestructura
CDMON · Apache · Cloudflare · TLS
Idiomas
Español · English · Català

01 / Superficie de ataque del frontend

El sitio se compila a HTML plano, asi que la mayor parte del riesgo clasico de servidor desaparece. Lo que queda se cierra por cabecera.

  • CSP con hashes recalculados en cada compilacion

    script-src lleva el SHA-256 de cada script del sitio y no incluye ‘unsafe-inline’. Un <script> inyectado no se ejecuta: no puede llevar un hash que la politica no conozca. Los hashes se generan solos al compilar, no se mantienen a mano.

    astro.config.mjs

  • HSTS a un ano, deliberadamente sin preload

    includeSubDomains activo. Se deja fuera de la lista de precarga a proposito: entrar es facil y salir tarda meses, y para un portfolio ese compromiso no compensa.

    server/.htaccess

  • Aislamiento del documento

    X-Frame-Options DENY contra clickjacking, Cross-Origin-Opener-Policy y Cross-Origin-Resource-Policy en same-origin, y Permissions-Policy que apaga geolocalizacion, microfono y camara aunque nada las pida.

    server/.htaccess

  • Nada de terceros

    Sin CDN, sin Google Fonts, sin etiquetas de analitica externa. Las tipografias van autoalojadas con subset latino. Cada dominio que se quita es un tercero menos que puede caerse, cambiar el archivo que sirve o registrar la IP del visitante.

02 / Panel de administracion

La parte con acceso a la base de datos y a las subidas. Aqui es donde de verdad importa el detalle.

  • Consultas preparadas reales, no emuladas

    PDO con ATTR_EMULATE_PREPARES a false. Con la emulacion activada el driver interpola los valores en el cliente antes de enviarlos, y esa interpolacion es justo donde han vivido historicamente los saltos de entrecomillado en ciertos juegos de caracteres.

    server/db.php

  • Login que no revela si el usuario existe

    Contra un usuario inexistente se verifica igualmente un hash ficticio. Sin ese paso, la respuesta vuelve antes cuando el usuario no existe y el tiempo permite enumerar cuentas validas. Las contrasenas van con bcrypt y se rehashean solas si sube el coste.

    server/admin/auth.php

  • CSRF en tiempo constante, con el caso vacio cubierto

    El token se compara con hash_equals. Ademas se exige que el de la sesion exista y no este vacio: sin esa condicion, una peticion sin cookie creaba una sesion nueva con el token a cadena vacia y la comparacion de dos cadenas vacias devolvia cierto, dejando pasar la peticion.

    server/admin/auth.php

  • Sesiones endurecidas y con caducidad doble

    Regeneracion del identificador tras el login (fijacion de sesion), cookies HttpOnly, Secure y SameSite=Lax, use_strict_mode para que nadie imponga un identificador propio, y cierre tanto por inactividad como por duracion maxima absoluta.

    server/admin/auth.php

  • Bloqueo de fuerza bruta por IP

    Los intentos fallidos se cuentan en una ventana temporal y bloquean la IP al superar el umbral. La IP se toma de REMOTE_ADDR salvo que se declare explicitamente un proxy de confianza: fiarse de X-Forwarded-For sin proxy delante permite falsear la IP y saltarse el limite entero.

    server/lib/http.php

03 / Contenido que no me pertenece

Imagenes subidas al panel y mensajes escritos por desconocidos: todo lo que entra desde fuera se trata como hostil hasta que se demuestre lo contrario.

  • Las imagenes se validan por contenido, no por extension

    Primero el tipo MIME real con finfo, y despues getimagesize(), que solo reconoce imagenes de verdad. Un polyglot con cabecera GIF falsificada pasa el primer filtro pero no el segundo. Hay ademas un tope de dimensiones para que nadie tumbe el servidor con una imagen de 30000 px de lado.

    server/lib/upload.php

  • La carpeta de subidas no ejecuta codigo

    Defensa en profundidad: aunque la validacion fallara y se colara un .php, el .htaccess de esa carpeta retira los handlers y apaga el motor, asi que se serviria como texto plano. Los archivos se guardan con nombre aleatorio, nunca con el que envia el cliente.

    server/uploads/.htaccess

  • El formulario no puede convertirse en un relay de spam

    Se eliminan saltos de linea y caracteres de control antes de que ningun valor toque una cabecera de correo. Sin ese saneado, un nombre con CRLF permite inyectar cabeceras (un Bcc:, por ejemplo) y usar el formulario para enviar correo a terceros.

    server/api/contact.php

  • Exportaciones CSV sin inyeccion de formulas

    Excel y LibreOffice ejecutan como formula cualquier celda que empiece por =, +, - o @. Como el buzon exporta texto escrito por desconocidos, esas celdas se prefijan para que se traten como texto y no como codigo al abrir el archivo.

    server/admin/partials/layout.php

04 / Analitica propia y privacidad

Saber cuanta gente entra no exige montar un perfil de nadie. La analitica es propia y guarda lo minimo.

  • La direccion IP no se almacena

    Se guarda un SHA-256 de la IP con una sal del servidor. Sirve para contar visitantes unicos aproximados y para frenar floods, pero no permite reconstruir la direccion ni cruzarla con otra base de datos.

    server/api/visit.php

  • Sin cookies de seguimiento y con Do Not Track respetado

    No hay banner de consentimiento porque no hay nada que consentir. Si el navegador envia la senal Do Not Track, la visita ni siquiera se registra.

  • Del referrer se descarta la query

    Solo se conserva esquema, host y ruta. Los parametros de una URL ajena suelen llevar identificadores de sesion o de campana, y no hay ninguna razon para que acaben en mi base de datos.

    server/api/visit.php

  • Retencion acotada y purga automatica

    Las visitas se borran a los 400 dias, los intentos de login a los 90 y el registro de auditoria al ano. La purga va incorporada en el propio flujo, sin depender de un cron que nadie recuerda revisar.

    server/api/visit.php

05 / SEO tecnico

Un portfolio que no aparece no sirve de nada. El posicionamiento aqui es una cuestion de estructura, no de repetir palabras clave.

  • Un unico grafo de datos estructurados

    Person, WebSite, ProfilePage, BreadcrumbList y FAQPage viajan en un solo bloque JSON-LD enlazado por @id, en vez de en bloques sueltos que pueden contradecirse entre si. Cada articulo del blog anade ademas su propio BlogPosting.

    src/components/Seo.astro

  • 410 Gone en lugar de 404 para lo retirado

    Dos proyectos se retiraron del sitio con sus URLs ya indexadas. Un 404 le dice al buscador que la pagina podria volver, asi que la reintenta durante semanas; un 410 le dice que la retirada es deliberada y definitiva, y la saca del indice bastante antes.

    server/.htaccess

  • hreflang reciproco, y solo donde existe traduccion

    Cada pagina traducida declara a todas sus hermanas y un x-default. Las que existen en un solo idioma no declaran ninguna: anunciar un alterno que no existe rompe el grupo entero, y Google entonces lo descarta completo.

    src/pages/sitemap.xml.ts

  • SEO local con senales coherentes

    Badalona y Barcelona aparecen en el titulo, en la descripcion, en las etiquetas geo y dentro del bloque Person con address, homeLocation y areaServed. La senal importa cuando es la misma en todos los sitios, no cuando se repite en uno.

    src/config.ts

06 / Legible para modelos de lenguaje

Cada vez se pregunta mas a un asistente y menos a un buscador. Si la respuesta sobre mi la va a dar una IA, prefiero que lea datos ordenados y no que improvise.

  • llms.txt generado en cada compilacion

    Un resumen en texto plano siguiendo la convencion de llmstxt.org, sin HTML ni ruido. Sale de las mismas fuentes que alimentan la web, asi que no puede quedarse desfasado respecto a lo que se ve en pantalla.

    src/pages/llms.txt.ts

  • Politica explicita para rastreadores de IA

    GPTBot, ClaudeBot, PerplexityBot, Google-Extended y Applebot tienen permiso escrito. Los rastreadores de SEO comercial, que solo consumen ancho de banda del hosting compartido, estan bloqueados. Es una decision tomada, no el resultado de no haber tocado el archivo.

    public/robots.txt

  • Divulgacion de vulnerabilidades por RFC 9116

    Un /.well-known/security.txt con contacto, idiomas y fecha de caducidad. Es lo primero que busca quien encuentra un fallo y quiere reportarlo bien; no tenerlo es la razon habitual de que un aviso acabe en un formulario generico o en ninguna parte.

    public/.well-known/security.txt

  • Analisis estatico en cada push

    CodeQL cubre el frontend en TypeScript y JavaScript. Como CodeQL no analiza PHP, el backend lo revisa Semgrep con reglas de inyeccion, XSS y secretos, y ambos resultados llegan en formato SARIF a la misma pestana. Dependabot vigila las dependencias cada semana.

    .github/workflows/codeql.yml

Cada punto de esta página corresponde a código que puedes abrir en el repositorio. Si encuentras algo que no cuadra, el canal para avisarme está en /.well-known/security.txt

Métricas de rendimiento (PageSpeed Insights)

100
Rendimiento
100
Accesibilidad
100
Buenas prácticas
100
SEO

* Medición sobre el servidor de producción de CDMON con HTTPS y TLS activos. Es una foto de un momento concreto, no una garantía permanente: compruébalo tú mismo si te interesa.