Saltar a contenido

Hardening de la plataforma

El hardening reduce la superficie de ataque de cada componente. Se aplica en capas: VPS, Docker, Traefik, Keycloak y las aplicaciones Spring WebFlux. Ninguna capa asume que las capas anteriores son perfectas.

VPS — Ubuntu

# Actualizar el sistema
apt-get update && apt-get upgrade -y && apt-get autoremove -y

# Configurar SSH: solo autenticación por clave, sin root
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2

systemctl restart sshd

# UFW: solo puertos necesarios
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp    # SSH
ufw allow 80/tcp    # HTTP → redirige a HTTPS en Traefik
ufw allow 443/tcp   # HTTPS
ufw enable

# Fail2ban para SSH
apt-get install -y fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 5
bantime = 3600
findtime = 600

Docker

# compose.yml — hardening de contenedores

services:
  keycloak:
    image: quay.io/keycloak/keycloak:26
    user: "1000:1000"          # No correr como root
    read_only: true             # Filesystem de solo lectura
    tmpfs:                      # Solo /tmp es escribible
      - /tmp
    security_opt:
      - no-new-privileges:true  # Sin escalada de privilegios
    cap_drop:
      - ALL                     # Sin capabilities de Linux
    cap_add:
      - NET_BIND_SERVICE        # Solo si necesita puertos < 1024
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "3"

  postgres:
    image: postgres:16-alpine
    user: "999:999"
    read_only: true
    tmpfs:
      - /tmp
      - /var/run/postgresql
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL

Traefik — Security headers

# traefik/dynamic/middlewares.yml
http:
  middlewares:

    security-headers:
      headers:
        # HTTPS forzado durante 1 año
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        stsPreload: true
        # Anti-clickjacking
        frameDeny: true
        # Anti-sniffing
        contentTypeNosniff: true
        # XSS (legacy)
        browserXssFilter: true
        # Ocultar servidor
        customResponseHeaders:
          X-Powered-By: ""
          Server: ""
        # Content Security Policy
        contentSecurityPolicy: >
          default-src 'self';
          script-src 'self' 'unsafe-inline';
          style-src 'self' 'unsafe-inline';
          img-src 'self' data:;
          font-src 'self';
          connect-src 'self' https://auth.example.com

    rate-limit-api:
      rateLimit:
        average: 100
        burst: 50
        period: 1s

    rate-limit-auth:
      rateLimit:
        average: 20
        burst: 10
        period: 1s

    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

Keycloak

Configurar en Realm Settings

Realm Settings → General:
  User-Managed Access: ❌ (deshabilitar si no se usa)
  Organizations: ❌ (deshabilitar si no se usa)

Realm Settings → Login:
  User Registration: ❌ (registro solo por admin en producción)
  Forgot Password: ✅ (con email verificado)
  Verify Email: ✅
  Login with Email: ✅

Realm Settings → Security Defenses:
  Brute Force Detection: ✅
  Max Login Failures: 5
  Wait Increment: 60s
  Max Wait: 900s
  Failure Reset Time: 12h

  X-Frame-Options: SAMEORIGIN
  Content-Security-Policy: frame-src 'self'; ...
  X-Content-Type-Options: nosniff
  X-XSS-Protection: 1; mode=block
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Robots-Tag: none

Realm Settings → Sessions:
  SSO Session Idle: 30 minutos
  SSO Session Max: 10 horas
  Access Token Lifespan: 5 minutos
  Refresh Token Max Reuse: 0

Deshabilitar flujos no usados

# Via Admin CLI: deshabilitar flujos obsoletos
kcadm.sh update clients/<client-id> \
  -r realm-internal \
  -s implicitFlowEnabled=false \
  -s directAccessGrantsEnabled=false

Admin Console — solo red privada

# Traefik: bloquear /admin desde internet
- "traefik.http.routers.kc-admin.rule=Host(`auth.example.com`) && PathPrefix(`/admin`)"
- "traefik.http.routers.kc-admin.middlewares=admin-ip-whitelist@file"

Spring WebFlux

@Bean
public SecurityWebFilterChain chain(ServerHttpSecurity http) {
    return http
        .headers(h -> h
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'"))
            .frameOptions(ServerHttpSecurity.HeaderSpec.FrameOptionsSpec::deny)
            .contentTypeOptions(Customizer.withDefaults())
            .hsts(hsts -> hsts
                .includeSubdomains(true)
                .maxAge(Duration.ofDays(365)))
        )
        // ... resto de la configuración
        .build();
}

Checklist de hardening

  • SSH: solo autenticación por clave, sin acceso root
  • UFW: solo puertos 22, 80, 443 abiertos
  • Fail2ban activo para SSH
  • Contenedores ejecutan sin root (user: "1000:1000")
  • no-new-privileges: true en todos los contenedores
  • cap_drop: ALL en todos los contenedores
  • Consola admin Keycloak bloqueada desde internet
  • Brute force protection habilitada en todos los realms
  • Flujo Implicit deshabilitado en todos los clientes
  • Direct Access Grants deshabilitado en producción
  • Security headers configurados en Traefik
  • HSTS con preload habilitado
  • Rate limiting en endpoints de autenticación
  • PostgreSQL sin puertos expuestos (solo red interna Docker)
  • .env con secretos nunca versionado en git
  • Backups encriptados con GPG

El .env nunca debe entrar al repositorio git

Añade .env al .gitignore del proyecto. Usa .env.example con valores ficticios como plantilla. Revisa el historial de git periódicamente con git log --all -- .env para asegurarte de que nunca fue versionado.