Patrón Gatekeeper: Seguridad Centralizada en el Borde
Centraliza autenticación, limitación de tasa y sanitización de entrada en el borde del sistema. Patrón Gatekeeper con ejemplos en Python, Java y JavaScript.
Visión General
Cada petición que llega a tus servicios consume cómputo, y cada ruta /debug olvidada o parámetro de consulta sin validar es un incidente esperando a que un escáner lo encuentre. El Patrón Gatekeeper coloca una frontera de validación y seguridad dedicada en el borde del sistema: un único punto de control que inspecciona, sanitiza, autentica y autoriza el tráfico entrante antes de que nada de él toque los servicios internos.
En lugar de duplicar comprobaciones de seguridad en cada servicio, el gatekeeper centraliza el trabajo transversal — validación de tokens, rate limiting, sanitización de entrada, terminación TLS, filtrado DDoS — en un solo lugar. Todo lo que falla la inspección se rechaza con una razón registrada y nunca consume un ciclo de backend. El resultado es una superficie de ataque más pequeña por servicio y un único sitio donde la política de seguridad realmente vive.
En producción esta frontera suele ser un API gateway (Kong, AWS API Gateway), un proxy inverso con WAF (Nginx + ModSecurity, Cloudflare), un ingress de service mesh (Istio Gateway) o — a la menor escala — simple middleware de aplicación, que es lo que construyen los ejemplos de abajo.
Cuándo Usarlo
Usa el Patrón Gatekeeper cuando:
- Varios servicios backend comparten los mismos requisitos de autenticación, rate limiting y validación de entrada
- Los servicios internos no deberían ser alcanzables directamente desde internet
- El compliance o la auditoría exigen un punto único donde registrar todas las peticiones externas
- Necesitas aplicar la política de seguridad en un solo lugar en lugar de confiar en que cada equipo la recuerde por servicio
Si todavía no has mapeado qué puntos de entrada expone tu sistema, empieza por ahí: un análisis de amenazas te dirá qué tiene que vigilar el gatekeeper.
Cuándo No Usarlo
- Una aplicación de un solo servicio donde la validación en el borde añade un salto pero ningún beneficio de seguridad
- Rutas de ultra-baja latencia donde una capa extra de inspección de red es inaceptable
- Validación que depende del estado de negocio (¿este pedido pertenece a este cliente?) — eso pertenece al servicio, no al borde
- Cuando el gatekeeper no puede hacerse de alta disponibilidad; una única instancia es un punto único de fallo
Solución
Python (middleware de FastAPI)
import os
import re
import time
import jwt
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
# Falla rápido al arrancar si falta el secret — nunca uses un valor por defecto adivinable.
JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALGORITHM = "HS256"
class GatekeeperMiddleware(BaseHTTPMiddleware):
"""Valida, sanitiza y autentica peticiones en el borde."""
BLOCKED_PATHS = {"/admin", "/internal", "/debug"}
PUBLIC_PREFIXES = ("/api/public", "/health")
SQL_INJECTION_PATTERNS = [
r"(\b(union|select|insert|update|delete|drop)\b)",
r"(--|;|/\*|\*/)",
r"(\b(or|and)\b\s+\d+\s*=\s*\d+)",
]
RATE_LIMIT = 100 # peticiones por ventana
RATE_WINDOW = 60 # segundos
def __init__(self, app):
super().__init__(app)
self.request_counts: dict[str, list[float]] = {}
async def dispatch(self, request: Request, call_next):
client_ip = request.client.host if request.client else "unknown"
# 1. Validación de ruta
if self._is_blocked_path(request.url.path):
return JSONResponse(
status_code=403,
content={"error": "Acceso denegado", "code": "BLOCKED_PATH"},
)
# 2. Rate limiting
if self._is_rate_limited(client_ip):
return JSONResponse(
status_code=429,
content={"error": "Límite de tasa excedido", "code": "RATE_LIMITED"},
)
# 3. Sanitización de entrada
if self._contains_injection(request):
return JSONResponse(
status_code=400,
content={"error": "Petición malformada", "code": "INJECTION_DETECTED"},
)
# 4. Autenticación — solo en rutas protegidas; las públicas la omiten.
if not request.url.path.startswith(self.PUBLIC_PREFIXES):
auth_result = self._authenticate(request)
if not auth_result["valid"]:
return JSONResponse(
status_code=401,
content={"error": auth_result["error"], "code": "AUTH_FAILED"},
)
request.state.user = auth_result["user"]
request.state.request_id = f"req-{int(time.time() * 1000)}"
response = await call_next(request)
# 5. Cabeceras de seguridad en la salida
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-Request-ID"] = request.state.request_id
return response
def _is_blocked_path(self, path: str) -> bool:
return any(path.startswith(b) for b in self.BLOCKED_PATHS)
def _is_rate_limited(self, client_ip: str) -> bool:
now = time.time()
window_start = now - self.RATE_WINDOW
timestamps = [t for t in self.request_counts.get(client_ip, []) if t > window_start]
if len(timestamps) >= self.RATE_LIMIT:
self.request_counts[client_ip] = timestamps
return True
timestamps.append(now)
self.request_counts[client_ip] = timestamps
return False
def _contains_injection(self, request: Request) -> bool:
target = f"{request.url.path}?{request.url.query}"
return any(
re.search(p, target, re.IGNORECASE) for p in self.SQL_INJECTION_PATTERNS
)
def _authenticate(self, request: Request) -> dict:
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return {"valid": False, "error": "Cabecera de autorización ausente o inválida"}
try:
payload = jwt.decode(auth_header[7:], JWT_SECRET, algorithms=[JWT_ALGORITHM])
return {"valid": True, "user": payload}
except jwt.ExpiredSignatureError:
return {"valid": False, "error": "Token expirado"}
except jwt.InvalidTokenError:
return {"valid": False, "error": "Token inválido"}
app.add_middleware(GatekeeperMiddleware)
@app.get("/api/protected/users/me")
async def get_current_user(request: Request):
user = request.state.user
return {"user_id": user["sub"], "email": user["email"]}
@app.get("/api/public/products")
async def list_products():
return {"products": [{"id": 1, "name": "Widget"}]}
Java (filtro de Spring Cloud Gateway)
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.UUID;
@Component
class GatekeeperFilter extends AbstractGatewayFilterFactory<GatekeeperFilter.Config> {
private static final List<String> BLOCKED_PREFIXES = List.of("/admin", "/internal", "/debug");
private static final String PUBLIC_PREFIX = "/api/public";
private final JwtValidator jwtValidator; // el secret JWT viene de la config, no del código
private final RateLimiter rateLimiter;
public GatekeeperFilter(JwtValidator jwtValidator, RateLimiter rateLimiter) {
super(Config.class);
this.jwtValidator = jwtValidator;
this.rateLimiter = rateLimiter;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// 1. Bloquear rutas internas
if (isBlockedPath(path)) {
return reject(exchange, HttpStatus.FORBIDDEN);
}
// 2. Rate limiting — getRemoteAddress() puede ser null detrás de algunos proxies
InetSocketAddress remote = request.getRemoteAddress();
String clientIp = remote != null ? remote.getAddress().getHostAddress() : "unknown";
if (!rateLimiter.allowRequest(clientIp)) {
return reject(exchange, HttpStatus.TOO_MANY_REQUESTS);
}
// 3. Autenticación — solo en rutas protegidas
if (!path.startsWith(PUBLIC_PREFIX)) {
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return reject(exchange, HttpStatus.UNAUTHORIZED);
}
if (!jwtValidator.isValid(authHeader.substring(7))) {
return reject(exchange, HttpStatus.UNAUTHORIZED);
}
}
// 4. Etiquetar la petición y reenviar
ServerHttpRequest mutated = request.mutate()
.header("X-Request-ID", UUID.randomUUID().toString())
.header("X-Authenticated", "true")
.build();
return chain.filter(exchange.mutate().request(mutated).build());
};
}
private boolean isBlockedPath(String path) {
return BLOCKED_PREFIXES.stream().anyMatch(path::startsWith);
}
private Mono<Void> reject(ServerWebExchange exchange, HttpStatus status) {
exchange.getResponse().setStatusCode(status);
return exchange.getResponse().setComplete();
}
public static class Config {
// Propiedades de configuración
}
}
JavaScript (stack de middleware en Express)
const express = require('express');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const jwt = require('jsonwebtoken');
const app = express();
// Falla rápido al arrancar si falta el secret — nunca despliegues un secret por defecto.
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) throw new Error('JWT_SECRET environment variable is required');
// 1. Cabeceras de seguridad (Helmet)
app.use(helmet());
// 2. Rate limiting
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: { error: 'Límite de tasa excedido', code: 'RATE_LIMITED' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// 3. Bloqueo de rutas
const blockedPaths = ['/admin', '/internal', '/debug', '/.env', '/wp-admin'];
app.use((req, res, next) => {
if (blockedPaths.some((p) => req.path.startsWith(p))) {
return res.status(403).json({ error: 'Acceso denegado', code: 'BLOCKED_PATH' });
}
next();
});
// 4. Sanitización de entrada — revisa la ruta y la query por patrones de inyección
const sqlInjectionPattern = /(\b(union|select|insert|update|delete|drop)\b|--|;)/i;
app.use((req, res, next) => {
const target = `${req.path}?${new URLSearchParams(req.query).toString()}`;
if (sqlInjectionPattern.test(target)) {
return res.status(400).json({ error: 'Petición malformada', code: 'INJECTION_DETECTED' });
}
next();
});
// 5. Autenticación JWT — solo en rutas protegidas
app.use('/api/protected', (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Autenticación requerida', code: 'AUTH_FAILED' });
}
try {
req.user = jwt.verify(authHeader.slice(7), JWT_SECRET);
req.requestId = `req-${Date.now()}`;
next();
} catch {
return res.status(401).json({ error: 'Token inválido', code: 'AUTH_FAILED' });
}
});
// Rutas backend — protegidas y públicas ahora son explícitas
app.get('/api/protected/users/me', (req, res) => {
res.json({ userId: req.user.sub, requestId: req.requestId });
});
app.get('/api/public/products', (req, res) => {
res.json({ products: [{ id: 1, name: 'Widget' }] });
});
app.listen(3000, () => console.log('Gatekeeper escuchando en el puerto 3000'));
Fíjate en que los tres ejemplos trazan la misma línea: /api/public/* supera la inspección pero omite la autenticación, mientras todo lo demás necesita un JWT válido. Si trazas mal esa frontera el fallo es silencioso — un middleware que autentica todas las rutas, incluida la lista pública de productos, se despliega sin problema y luego alguien nota que el endpoint público devuelve 401.
Cómo Funciona
Cada capa del gatekeeper corresponde a un código de rechazo: las rutas bloqueadas reciben un 403, los clientes que exceden el límite un 429, la entrada malformada un 400 y los tokens ausentes o inválidos un 401. Solo una petición que supera todas las capas llega al backend, y el rechazo queda registrado para que los patrones de ataque aparezcan en el monitoreo antes de aparecer en un informe de incidente.
El borde valida formato, no semántica. El gatekeeper puede comprobar que un token está firmado y no expirado, que una ruta no está en la lista de bloqueo, que una query no parece inyección SQL. No puede comprobar si este usuario tiene permiso para ver ese pedido — eso necesita estado de negocio, y el estado de negocio vive en el servicio. Si te encuentras cargando entidades desde la base de datos en el gatekeeper, la frontera se ha filtrado.
Fallar cerrado o fallar auditado. Cuando falla la obtención de la clave de firma JWT o el almacén del rate limiter, el gatekeeper tiene dos opciones: rechazarlo todo (fail closed) o dejar pasar el tráfico sin inspeccionar (fail open). Fail open convierte un tropiezo de infraestructura en un incidente de seguridad. Si la disponibilidad hace inaceptable fallar cerrado, degrada a un conjunto mínimo de reglas — bloqueo de rutas más registro — y alerta con fuerza.
El punto de estrangulamiento corta en ambos sentidos. Un único punto de aplicación de políticas significa un solo lugar donde actualizar reglas — y un solo lugar que puede tumbar el sistema entero. Ejecuta al menos dos réplicas detrás de un balanceador, mantén el gatekeeper sin estado para que las réplicas sean intercambiables (el estado del rate limit va a Redis, no a la memoria del proceso), y haz pruebas de carga a la propia capa: añade latencia a cada petición, no solo a las malas.
Un gatekeeper no sustituye al zero trust. Los servicios internos deberían seguir verificando los tokens que el borde ya comprobó — una petición que evade el gateway (un ingress mal configurado, un puerto abierto, un llamante interno) no debería recibir un pase libre. El gatekeeper reduce la superficie de ataque; la autenticación por servicio la mantiene reducida.
Variantes
| Variante | Tecnología | Caso de uso |
|---|---|---|
| API Gateway | Kong, AWS API Gateway, Azure APIM | Stack de borde completo con políticas, gestión de claves y enrutado |
| Proxy inverso + WAF | Nginx + ModSecurity, Cloudflare | Conjuntos de reglas a nivel de red (OWASP CRS) sin tocar el código de aplicación |
| Ingress de service mesh | Istio Gateway, Linkerd | Borde nativo de Kubernetes con mTLS entre servicios |
| Cómputo en el borde CDN | Cloudflare Workers, Lambda@Edge | Validación ejecutada geográficamente cerca del cliente |
| Middleware de aplicación | Express, FastAPI, Spring | Control a nivel de código sin infraestructura extra — los ejemplos de arriba |
Para una guía paso a paso de construir un API gateway gestionado con enrutado, throttling y gestión de claves, consulta la receta de API gateway.
Gatekeeper vs. Patrones Relacionados
| Gatekeeper | Intercepting Filter | Front Controller | |
|---|---|---|---|
| Dónde vive | En el borde, delante del sistema | Dentro de la app, alrededor de los manejadores | Punto de entrada de la app |
| Trabajo principal | Rechazar tráfico malo | Pre/postprocesar peticiones | Enrutar peticiones a manejadores |
| Tecnología típica | Gateway, WAF, proxy | Middleware, filtros de servlet | Servlet dispatcher, router |
| ¿Descarta peticiones? | Sí — ese es el punto | Raramente — normalmente transforma | No — despacha |
Estos patrones se componen en lugar de competir: un gatekeeper se sitúa en el borde, un front controller enruta lo que queda, y los intercepting filters corren dentro de la app alrededor del manejador.
Qué Funciona
- Fallar cerrado. Una petición que el gatekeeper no puede validar es una petición que el backend nunca ve.
- Registrar cada rechazo con su razón. Los patrones de rechazo son tu señal más temprana de escaneos e intentos de ataque.
- Mantener el gatekeeper sin estado. Los contadores de rate limit y las cachés de tokens pertenecen a Redis o al propio almacén del gateway, así las réplicas siguen siendo intercambiables.
- Versionar el conjunto de reglas como código. Las reglas WAF y las listas de bloqueo cambian; revísalas en Git y despliégalas por CI/CD como cualquier otra cosa que pueda romper producción.
- Probar el bypass. Verifica regularmente que los servicios internos realmente son inalcanzables si se salta el gatekeeper; esa es la suposición sobre la que descansa todo lo demás.
Código companion: El repo companion de gatekeeper-pattern tiene versiones ejecutables de los tres stacks de middleware con tests que cubren cada ruta de rechazo.
Errores Comunes
- Confiar en el tráfico porque es “interno”. Una petición que se saltó el gatekeeper — un ingress mal configurado, un puerto expuesto — sigue obteniendo acceso completo al servicio salvo que el servicio revalide. Zero trust significa que el borde es una capa, no la única capa.
- Lógica de negocio en el borde. El gatekeeper no sabe si el pedido #512 pertenece a este cliente. Las comprobaciones del borde se quedan genéricas: firmas, formatos, tasas, rutas.
- Fail-open ante errores de dependencia. Si el almacén de rate limit o el endpoint JWKS están caídos, reenviar tráfico sin inspeccionar convierte una caída en una ventana de brecha.
- Secrets en el conjunto de reglas. Las claves JWT, API keys y certificados se inyectan desde un gestor de secrets o el entorno — los ejemplos de arriba leen
JWT_SECRETal arrancar precisamente porque una clave hardcodeada acaba en el repo. - Reglas WAF afinadas a ojo. Las regex agresivas bloquean usuarios legítimos (
selectaparece en montones de consultas inocuas). Despliega reglas nuevas primero en modo solo-registro, y aplica una vez conocidos los falsos positivos.
Ejemplos del Mundo Real
Cloudflare
Cloudflare termina TLS, filtra inundaciones DDoS, aplica reglas WAF y examina bots en su red de borde antes de que el tráfico llegue al origen — para millones de sitios, es el gatekeeper, y el servidor de origen detrás asume que el tráfico llega pre-inspeccionado.
AWS API Gateway
API Gateway limita por clave de API, valida JWTs contra Cognito o un authorizer Lambda, transforma peticiones y registra todo en CloudWatch antes de invocar el backend Lambda o EC2. El servicio detrás puede seguir siendo mínimo porque el borde ya hizo las comprobaciones.
Azure Front Door + WAF
Microsoft documenta este patrón bajo el nombre Gatekeeper en el Azure Architecture Center: Azure Front Door o Application Gateway con el Web Application Firewall se encargan de la terminación TLS, los conjuntos de reglas OWASP y el rate limiting delante de backends en AKS o App Service.
Lecturas Adicionales
- Patrón Gatekeeper — Azure Architecture Center — la entrada canónica del catálogo para este patrón.
- OWASP Input Validation Cheat Sheet — qué debería y qué no debería intentar la sanitización en el borde.
- ModSecurity (OWASP CRS) — el conjunto de reglas que ejecutan la mayoría de WAF de proxy inverso.
Preguntas frecuentes
¿Cuál es la diferencia entre Gatekeeper y API Gateway?
Un API gateway es un superconjunto: enruta peticiones, traduce protocolos, agrega respuestas y a menudo gestiona claves de API y portales de desarrolladores. Un gatekeeper es el corte de seguridad de ese trabajo — validar, sanitizar, autenticar, rechazar. Todo API gateway puede actuar como gatekeeper; un gatekeeper no necesita enrutar nada.
¿El gatekeeper debería autenticar o solo pasar los tokens?
Valida en el borde — firma, expiración, emisor — para que los tokens falsificados o expirados mueran barato. Mantén la autorización ("¿puede el usuario X tocar el recurso Y?") gruesa en el borde (scopes, roles) o fina en el servicio. La división que funciona: el gatekeeper demuestra quién llama; el servicio decide qué puede hacer.
¿Cómo se relaciona Gatekeeper con un service mesh?
Cubren direcciones distintas. El gatekeeper maneja el tráfico north-south: clientes externos que golpean el sistema. Un mesh como Istio maneja el tráfico east-west: llamadas entre servicios con mTLS y políticas por servicio. En Kubernetes, el gateway de ingress es el gatekeeper y el mesh continúa la misma aplicación de políticas hacia dentro.
¿Qué pasa cuando el gatekeeper se convierte en el cuello de botella?
Escálalo como cualquier capa sin estado: réplicas detrás de un balanceador, estado compartido en Redis, y trabajo pesado de CPU (puntuación de bots, inspección de payloads) descargado o muestreado. Si el gatekeeper sigue añadiendo latencia inaceptable, normalmente significa que está haciendo trabajo que pertenece a los servicios — la pregunta de auditoría fail-closed es qué comprobaciones pueden moverse aguas abajo sin abrir agujeros.
¿Debería terminarse TLS en el gatekeeper?
Normalmente sí. El borde es donde se gestionan los certificados y ocurre la inspección. Si el compliance exige cifrado extremo a extremo, re-cifra del gatekeeper a los servicios (que es lo que el mTLS del service mesh te da gratis). El error es terminar TLS y luego mandar texto plano dentro de una red que no es realmente de confianza.
Recursos Relacionados
Patron de Throttling
Limita la tasa a la que un sistema procesa solicitudes o consume recursos para prevenir sobrecarga, asegurar uso justo y mantener rendimiento predecible.
PatternIntercepting Filter: Filtros de Petición Reutilizables
Compón responsabilidades transversales en una cadena de filtros reutilizables que interceptan peticiones y respuestas, permitiendo lógica de preprocesamiento y postprocesamiento compartida.
PatternPatrón Front Controller
Enruta todas las peticiones entrantes a través de un único handler que despacha al comando de página apropiado, centralizando el procesamiento de requests y seguridad.
RecipeDiseñar un API Gateway Escalable para Microservicios
Construí un gateway de API que enrute requests, maneje autenticación, rate limiting, caching y traducción de protocolos entre clientes y microservicios backend.
GuideAPI Security
Asegurar tus APIs end-to-end. Cubre rate limiting, autenticacion, input validation, CORS, prevencion de SQL injection, API gateway patterns, limites de request size, pagination security, mass assignment, versioning, audit logging y API security testing con ejemplos de codigo.
GuideModelado de Amenazas
Guía paso a paso de modelado de amenazas: STRIDE, árboles de ataque, diagramas de flujo de datos e integración de revisión de diseño de seguridad en tu proceso de desarrollo.