Saltar a contenido

Zero Trust Architecture

Zero Trust es el modelo de seguridad que parte de la premisa "nunca confíes, siempre verifica". A diferencia del modelo perimetral tradicional donde todo lo que está dentro de la red se considera confiable, Zero Trust exige verificación explícita de identidad, dispositivo y contexto en cada solicitud, independientemente del origen. En esta plataforma, Keycloak, Traefik y Spring WebFlux se combinan para implementar Zero Trust de forma coherente en cada capa del stack.

La adopción de Zero Trust es especialmente crítica en un entorno multi-tenant con usuarios internos (realm-internal) y socios externos (realm-partners), donde la superficie de ataque es más amplia y los requisitos de aislamiento son más estrictos.

Los 5 Principios de Zero Trust Aplicados

1. Verificación Explícita

Cada solicitud debe ser autenticada y autorizada usando todos los datos disponibles: identidad, ubicación, dispositivo, servicio solicitado y clasificación del dato. No existe la confianza implícita basada en red o IP.

Implementación en la plataforma:

  • Traefik valida que toda solicitud lleve un JWT antes de reenviarla al servicio.
  • Spring WebFlux verifica la firma del JWT en cada request usando las JWKs de Keycloak.
  • Los claims del token (realm_access.roles, azp, iss) se verifican por cada servicio individualmente.

2. Mínimo Privilegio (Least Privilege)

Los sujetos y sistemas reciben solo los permisos mínimos necesarios para su función, durante el tiempo mínimo necesario. El acceso se revoca tan pronto como deja de ser necesario.

Implementación en la plataforma:

  • Los tokens tienen TTL cortos: 5 minutos para access tokens en producción.
  • Los clientes OAuth2 declaran explícitamente sus scopes; Keycloak no emite claims no solicitados.
  • Los roles se asignan por realm: un usuario de realm-partners no tiene acceso a recursos de realm-internal.
  • Los clientes M2M (partner-service-m2m) usan client_credentials sin acceso a datos de usuario.

3. Asumir Brecha (Assume Breach)

El sistema se diseña asumiendo que el atacante ya tiene acceso a alguna parte de la infraestructura. Esto implica segmentación, cifrado en tránsito, logs detallados y detección de anomalías.

Implementación en la plataforma:

  • Toda comunicación entre contenedores usa TLS (Traefik gestiona certificados Let's Encrypt).
  • La red Docker segmenta Keycloak, PostgreSQL y los servicios en redes internas distintas.
  • Cada servicio Spring WebFlux registra todos los intentos de acceso denegado.
  • PostgreSQL solo acepta conexiones desde la red interna de Docker.

4. Microsegmentación

El acceso a recursos se controla a nivel de servicio, endpoint y operación, no solo a nivel de red. Cada microservicio es responsable de su propia autorización.

Implementación en la plataforma:

  • Traefik implementa rate limiting y circuit breaking por ruta.
  • Cada endpoint de Spring WebFlux declara sus requisitos de autorización explícitamente.
  • Los scopes OAuth2 mapean a operaciones específicas (read:users, write:partners).

5. Visibilidad y Analítica

Toda actividad se registra y está disponible para auditoría. Los eventos de seguridad se correlacionan para detectar patrones anómalos.

Implementación en la plataforma:

  • Keycloak genera eventos de login, logout y error configurables en la Admin Console.
  • Spring WebFlux propaga traceId y spanId (Micrometer Tracing) en todos los logs.
  • PostgreSQL almacena el audit trail inmutable generado por el Audit Context.

Flujo de Verificación Continua

sequenceDiagram
    autonumber
    actor U as Usuario / Sistema
    participant T as Traefik v3
    participant K as Keycloak 26
    participant S as Spring WebFlux Service
    participant DB as PostgreSQL

    U->>T: HTTPS request + JWT Bearer
    T->>T: TLS termination + rate limit check
    T->>K: GET /.well-known/openid-configuration (cached JWKs)
    T->>T: Verificar firma JWT con JWK público
    alt JWT inválido o expirado
        T-->>U: 401 Unauthorized
    else JWT válido
        T->>S: Reenvía request con headers X-User-Id, X-Realm
        S->>S: SecurityWebFilterChain verifica JWT localmente
        S->>S: Verificar claim iss == Keycloak esperado
        S->>S: Verificar claim azp == cliente autorizado
        S->>S: Verificar roles requeridos en realm_access.roles
        alt Permisos insuficientes
            S-->>T: 403 Forbidden
            T-->>U: 403 Forbidden
            S--)DB: INSERT audit_event (ACCESS_DENIED, userId, resource)
        else Autorizado
            S->>DB: Operación de negocio
            DB-->>S: Resultado
            S--)DB: INSERT audit_event (ACCESS_GRANTED, userId, resource)
            S-->>T: 200 OK
            T-->>U: 200 OK
        end
    end

Verificación local de JWT

Spring WebFlux no llama a Keycloak en cada request para validar el token. Descarga las JWKs al inicio y las cachea, rotándolas periódicamente. La verificación es local y de muy bajo latencia (menos de 1ms por request).

Implementación Conjunta: Keycloak + Traefik + Spring WebFlux

Capa 1: Traefik — Control de Acceso en el Borde

# traefik/dynamic/middlewares.yml
http:
  middlewares:
    jwt-auth:
      forwardAuth:
        address: "http://keycloak:8080/realms/realm-internal/protocol/openid-connect/userinfo"
        authResponseHeaders:
          - "X-User-Id"
          - "X-User-Email"
          - "X-Realm"
        trustForwardHeader: false

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

    security-headers:
      headers:
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        contentTypeNosniff: true
        frameDeny: true
        customResponseHeaders:
          X-Powered-By: ""

Capa 2: Keycloak — Emisión y Gestión de Tokens

Keycloak implementa Zero Trust en la emisión de tokens:

  • Token TTL mínimo: access tokens con 5 minutos de vida en producción.
  • Refresh token rotation: cada uso del refresh token emite uno nuevo e invalida el anterior.
  • Brute force protection: bloqueo tras 5 intentos fallidos, desbloqueo automático en 10 minutos.
  • Detección de sesiones simultáneas: configurable por realm.
# Configurar brute force protection via Admin CLI
/opt/keycloak/bin/kcadm.sh update realms/realm-internal \
  -s bruteForceProtected=true \
  -s failureFactor=5 \
  -s waitIncrementSeconds=60 \
  -s maxFailureWaitSeconds=900 \
  --server http://localhost:8080 \
  --realm master \
  --user admin

Capa 3: Spring WebFlux — Autorización a Nivel de Servicio

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class ZeroTrustSecurityConfig {

    @Bean
    public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http,
                                                       ReactiveJwtDecoder jwtDecoder) {
        return http
            .csrf(csrf -> csrf.disable())
            .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
            .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
            .authorizeExchange(auth -> auth
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .anyExchange().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtDecoder(jwtDecoder)
                    .jwtAuthenticationConverter(realmRolesConverter())
                )
            )
            .build();
    }

    @Bean
    public ReactiveJwtDecoder jwtDecoder(
            @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuerUri) {
        return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
    }

    @Bean
    public Converter<Jwt, Mono<AbstractAuthenticationToken>> realmRolesConverter() {
        return jwt -> {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            List<String> roles = realmAccess != null
                    ? (List<String>) realmAccess.get("roles")
                    : List.of();
            List<GrantedAuthority> authorities = roles.stream()
                    .map(r -> new SimpleGrantedAuthority("ROLE_" + r.toUpperCase()))
                    .collect(Collectors.toList());
            return Mono.just(new JwtAuthenticationToken(jwt, authorities));
        };
    }
}
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping
    @PreAuthorize("hasRole('ADMIN') or hasRole('USER_READER')")
    public Flux<UserResponse> listUsers(@AuthenticationPrincipal Jwt jwt) {
        validateIssuer(jwt.getIssuer().toString());
        return userService.findAll();
    }

    @DeleteMapping("/{userId}")
    @PreAuthorize("hasRole('ADMIN')")
    public Mono<Void> deleteUser(@PathVariable String userId,
                                  @AuthenticationPrincipal Jwt jwt) {
        auditService.log(AuditEvent.userDeletion(userId, jwt.getSubject()));
        return userService.delete(userId);
    }
}

Controles Implementados

Control Componente Configuración
Verificación de firma JWT Spring Security 7 ReactiveJwtDecoder con JWKS automático
Validación de emisor (iss) Spring Security 7 issuer-uri en application.yml
Validación de audiencia (aud) Spring Security 7 audiences en resource server config
Expiración de tokens Keycloak accessTokenLifespan: 300 segundos
Rotación de refresh tokens Keycloak refreshTokenMaxReuse: 0
Rate limiting por IP Traefik middleware rateLimit
TLS obligatorio + HSTS Traefik Let's Encrypt + stsSeconds: 31536000
Segmentación de red Docker Compose Redes iam-internal y iam-edge separadas
Audit log inmutable PostgreSQL Tabla append-only, sin permisos UPDATE/DELETE
Brute force protection Keycloak bruteForceProtected: true por realm
Sin confianza implícita entre servicios Spring WebFlux Cada servicio valida su propio JWT

Error común: confiar en la red interna de Docker

Un servicio Spring WebFlux que acepta llamadas sin JWT desde la red interna de Docker viola Zero Trust. Aunque Traefik proteja el borde, un contenedor comprometido puede llamar directamente a otros servicios. Cada servicio DEBE validar el JWT de forma independiente.

Cache de JWKs y disponibilidad

Configura el jwk-set-uri con timeout de conexión corto (2s) y activa la caché. Si Keycloak está temporalmente indisponible, los servicios deben seguir validando tokens con las JWKs cacheadas, no rechazarlos todos. Spring Security cachea las JWKs por defecto; no desactives esta funcionalidad.

Monitoreo de eventos de seguridad

Activa el Event Listener de Keycloak para exportar eventos a un sistema SIEM o al menos a los logs del contenedor. Los eventos LOGIN_ERROR, CLIENT_LOGIN_ERROR y LOGOUT son críticos para detectar ataques de fuerza bruta y sesiones anómalas.