Motivación

Quería un sitio personal que reflejara mi filosofía de trabajo: rápido, seguro, reproducible y automatizado. Nada de WordPress, nada de CMS pesados. Un sitio estático generado con Hugo, servido detrás de un reverse proxy hardened, desplegado con Docker Compose y listo para escalar.

Arquitectura

┌──────────────────────────────────────────────────┐
│                   Internet                        │
└──────────────────────┬───────────────────────────┘
                       │
              ┌────────▼────────┐
              │  Cloudflare DNS │
              └────────┬────────┘
                       │
              ┌────────▼────────┐
              │   Traefik v3    │
              │  (Reverse Proxy)│
              │  :80 → :443    │
              └────────┬────────┘
                       │
              ┌────────▼────────┐
              │  Hugo Server    │
              │  (PaperMod)     │
              │    :1313        │
              └─────────────────┘

Componentes

Componente Tecnología Función
Generador estático Hugo + PaperMod Genera el sitio HTML/CSS/JS
Reverse proxy Traefik v3 TLS termination, security headers, rate limiting
Certificados Let’s Encrypt + DNS Challenge TLS automático via Cloudflare
Contenedores Docker Compose Orquestación de servicios
DNS Cloudflare DNS management y caching

Hugo + PaperMod

Elegí Hugo por su velocidad extrema y PaperMod por su diseño limpio y minimalista.

Características del sitio

  • Bilingüe (ES/EN): Contenido completo en español e inglés con selector de idioma
  • Tema oscuro/claro automático: Respeta la preferencia del sistema con toggle manual
  • Búsqueda integrada: Índice JSON + Fuse.js para búsqueda client-side sin servidor
  • SEO optimizado: OpenGraph, meta tags, sitemap.xml, robots.txt
  • Sin JavaScript externo: Cero trackers, cero analytics invasivos

Estructura del contenido

content/
├── about.md / about.en.md          # Sobre mí
├── search.md / search.en.md        # Búsqueda
├── experience/
│   ├── index.md / index.en.md      # Experiencia profesional
├── projects/
│   ├── index.md / index.en.md      # Proyectos
└── posts/
    ├── _index.md / _index.en.md    # Índice del blog
    ├── homelab-hashicorp-stack.md   # Posts...
    └── ...

Configuración multilingüe

Hugo permite definir idiomas de forma declarativa en hugo.yaml:

defaultContentLanguage: es

languages:
  es:
    languageName: "🇪🇸 Español"
    weight: 1
  en:
    languageName: "🇬🇧 English"
    weight: 2

Cada fichero de contenido tiene su variante .en.md para la versión en inglés. Hugo se encarga del routing automático (/en/about/, /en/posts/, etc.).

Seguridad con Traefik

La seguridad no es opcional. Configuré Traefik con un middleware chain que aplica:

Security Headers (OWASP)

secure-headers:
  headers:
    stsSeconds: 31536000          # HSTS 1 año
    stsIncludeSubdomains: true
    stsPreload: true
    frameDeny: true               # Anti-clickjacking
    contentTypeNosniff: true      # Anti MIME-sniffing
    browserXssFilter: true        # XSS protection
    referrerPolicy: "strict-origin-when-cross-origin"
    contentSecurityPolicy: "default-src 'self'; ..."

Rate Limiting

rate-limit:
  rateLimit:
    average: 100
    burst: 200
    period: 1s

TLS Hardened

tls:
  options:
    default:
      minVersion: "VersionTLS12"
      cipherSuites:
        - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
        - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
      sniStrict: true

Cadena de Middlewares

Todos los middlewares se encadenan en un único security-chain:

security-chain:
  chain:
    middlewares:
      - secure-headers
      - rate-limit
      - compress-response
      - www-redirect

Despliegue con Docker Compose

El despliegue es un simple docker compose up -d:

services:
  traefik:
    image: "traefik:v3.7.7"
    security_opt:
      - no-new-privileges:true
    # ... configuración de Traefik

  web:
    image: hugomods/hugo:exts-non-root-0.154.5
    command:
      - server
      - "--bind=0.0.0.0"
      - "--appendPort=false"
      - "--baseURL=https://www.gbt55.es"
      - "--disableLiveReload"
    labels:
      - "traefik.http.routers.web.rule=Host(`www.gbt55.es`) || Host(`gbt55.es`)"
      - "traefik.http.routers.web.middlewares=security-chain@file"

Decisiones de diseño

  1. Hugo en modo server en lugar de build estático: permite hot-reload del contenido sin rebuild del contenedor
  2. no-new-privileges: previene escalación de privilegios dentro del contenedor
  3. Imagen non-root: el proceso Hugo corre como usuario no-root
  4. Red dedicada proxy: aislamiento de red entre servicios
  5. Let’s Encrypt con DNS Challenge: no necesita exponer puerto 80 para validación

Certificados TLS Automáticos

Los certificados se gestionan automáticamente con Let’s Encrypt usando el DNS challenge de Cloudflare:

- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"

Esto permite renovación automática sin downtime y sin necesidad de abrir puertos adicionales.

Hardening del VPS

El VPS que aloja el portfolio incluye un script de hardening (vps-harden.sh) que configura:

  • Firewall (UFW) con reglas restrictivas
  • SSH hardening (no root login, key-only auth)
  • Fail2ban para protección contra brute force
  • Automatic security updates

Resultado

El resultado es un sitio:

  • ⚡ Ultra-rápido: Hugo genera páginas en <1ms, servidas con compresión gzip
  • 🔒 Seguro: Headers OWASP, TLS 1.2+, rate limiting, CSP restrictivo
  • 🐳 Reproducible: Un solo docker compose up -d despliega todo
  • 🌍 Bilingüe: Contenido completo en ES/EN
  • 📱 Responsive: PaperMod es totalmente responsive out-of-the-box
  • 🎨 Limpio: Sin ads, sin trackers, sin cookies

Stack completo

Hugo (SSG) + PaperMod (tema)
     │
Docker Compose (orquestación)
     │
Traefik v3 (reverse proxy + TLS + security)
     │
Let's Encrypt (certificados automáticos)
     │
Cloudflare (DNS + edge caching)
     │
VPS hardened (Linux)

El código fuente de este portfolio está disponible en mi GitHub.