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 4 · 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, así que la mayor parte del riesgo clásico de servidor desaparece. Lo que queda se cierra por cabecera.

  • CSP con hashes recalculados en cada compilación

    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 política no conozca. Los hashes se generan solos al compilar, no se mantienen a mano.

    astro.config.mjs

  • HSTS a un año, deliberadamente sin preload

    includeSubDomains activo. Se deja fuera de la lista de precarga a proposito: entrar es fácil 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 geolocalización, micrófono y cámara aunque nada las pida.

    server/.htaccess

  • Nada de terceros

    Sin CDN, sin Google Fonts, sin etiquetas de analítica externa. Las tipografías 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.

  • Filtrado de proyectos interactivo en cliente sin estado

    El filtrado de proyectos por etiquetas se ejecuta íntegramente en el navegador mediante manipulación de clases CSS. Evita guardar estados en sesión o realizar consultas HTTP adicionales para re-renderizar, minimizando la transferencia de datos y la carga del servidor.

    src/components/Projects.astro

02 / Panel de administración

La parte con acceso a la base de datos y a las subidas. Aquí 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 interpolación es justo donde han vivido históricamente 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 contraseñas van con bcrypt y se rehashean solas si sube el coste.

    server/admin/auth.php

  • CSRF en tiempo constante, con el caso vacío cubierto

    El token se compara con hash_equals. Además se exige que el de la sesión exista y no esté vacío: sin esa condición, una petición sin cookie creaba una sesión nueva con el token a cadena vacía y la comparación de dos cadenas vacías devolvía cierto, dejando pasar la petición.

    server/admin/auth.php

  • Sesiones endurecidas y con caducidad doble

    Regeneración del identificador tras el login (fijación de sesión), cookies HttpOnly, Secure y SameSite=Lax, use_strict_mode para que nadie imponga un identificador propio, y cierre tanto por inactividad como por duración máxima 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 límite entero.

    server/lib/http.php

  • Markdown seguro en el banner superior

    El texto del banner superior se edita en Markdown y se parsea en el navegador. Antes de la conversión a HTML (enlaces, negritas, código), la cadena se escapa completamente contra XSS, impidiendo que una potencial inyección en base de datos ejecute código script.

    src/components/Announcement.astro

  • Controlador preventivo de migraciones de base de datos

    El script de inicialización verifica las tablas y columnas necesarias al cargar el panel de administración. Si falta alguna columna (por ejemplo, tras desplegar cambios del esquema), muestra una pantalla de mantenimiento con instrucciones de migración seguras en vez de un fallo fatal de SQL.

    server/lib/bootstrap.php

03 / Contenido que no me pertenece

Imágenes 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 imágenes se validan por contenido, no por extension

    Primero el tipo MIME real con finfo, y después getimagesize(), que solo reconoce imágenes de verdad. Un polyglot con cabecera GIF falsificada pasa el primer filtro pero no el segundo. Hay además 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 código

    Defensa en profundidad: aunque la validación fallara y se colara un .php, el .htaccess de esa carpeta retira los handlers y apaga el motor, así que se serviría 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 línea y caracteres de control antes de que ningún 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 inyección de fórmulas

    Excel y LibreOffice ejecutan como fórmula cualquier celda que empiece por =, +, - o @. Como el buzón exporta texto escrito por desconocidos, esas celdas se prefijan para que se traten como texto y no como código al abrir el archivo.

    server/admin/partials/layout.php

04 / Analítica propia y privacidad

Saber cuanta gente entra no exige montar un perfil de nadie. La analítica es propia y guarda lo mínimo.

  • La dirección IP no se almacena

    Se guarda un SHA-256 de la IP con una sal del servidor. Sirve para contar visitantes únicos aproximados y para frenar floods, pero no permite reconstruir la dirección 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 señal Do Not Track, la visita ni siquiera se registra.

  • La sesión vive y muere con la pestaña

    Para saber si dos páginas vistas son la misma visita, el navegador guarda un identificador aleatorio en sessionStorage (no localStorage, no una cookie). Técnicamente sigue siendo "almacenamiento en el equipo" bajo la ePrivacy, pero al morir al cerrar la pestaña y no combinarse nunca con otra visita, encaja en la excepción de medición de audiencia que reconocen la mayoría de autoridades de protección de datos de la UE: por eso no cambia la respuesta de arriba.

    src/scripts/analytics.ts

  • Del referrer se descarta la query

    Solo se conserva esquema, host y ruta. Los parámetros de una URL ajena suelen llevar identificadores de sesión o de campaña, y no hay ninguna razón para que acaben en mi base de datos.

    server/api/visit.php

  • Retención acotada y purga automática

    Las visitas se borran a los 400 días, los intentos de login a los 90 y el registro de auditoría al año. La purga va incorporada en el propio flujo, sin depender de un cron que nadie recuerda revisar.

    server/api/visit.php

05 / SEO técnico

Un portfolio que no aparece no sirve de nada. El posicionamiento aquí es una cuestión de estructura, no de repetir palabras clave.

  • Un único 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 artículo del blog anade además 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 página podría volver, así que la reintenta durante semanas; un 410 le dice que la retirada és deliberada y definitiva, y la saca del índice bastante antes.

    server/.htaccess

  • hreflang reciproco, y solo donde existe traducción

    Cada página 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 señales coherentes

    Badalona y Barcelona aparecen en el titulo, en la descripción, en las etiquetas geo y dentro del bloque Person con address, homeLocation y areaServed. La señal 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 más 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 compilación

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

    src/pages/llms.txt.ts

  • Política 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, están bloqueados. Es una decisión tomada, no el resultado de no haber tocado el archivo.

    public/robots.txt

  • Divulgación 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 razón habitual de que un aviso acabe en un formulario genérico o en ninguna parte.

    public/.well-known/security.txt

  • Análisis estático en cada push

    CodeQL cubre el frontend en TypeScript y JavaScript. Como CodeQL no analiza PHP, el backend lo revisa Semgrep con reglas de inyección, XSS y secretos, y ambos resultados llegan en formato SARIF a la misma pestaña. 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)

66
Rendimiento
96
Accesibilidad
92
Buenas prácticas
100
SEO

* Vista Móvil, medida el 23/08/2026 con Lighthouse contra la producción (CDMON, HTTPS y TLS activos). Es una foto de un momento concreto, no una garantía permanente: compruébalo tú mismo si te interesa.