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: trueen todos los contenedores -
cap_drop: ALLen 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)
-
.envcon 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.