StackPractices
intermediate Por Mathias Paulenko

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

flowchart diagram: Cliente

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

VarianteTecnologíaCaso de uso
API GatewayKong, AWS API Gateway, Azure APIMStack de borde completo con políticas, gestión de claves y enrutado
Proxy inverso + WAFNginx + ModSecurity, CloudflareConjuntos de reglas a nivel de red (OWASP CRS) sin tocar el código de aplicación
Ingress de service meshIstio Gateway, LinkerdBorde nativo de Kubernetes con mTLS entre servicios
Cómputo en el borde CDNCloudflare Workers, Lambda@EdgeValidación ejecutada geográficamente cerca del cliente
Middleware de aplicaciónExpress, FastAPI, SpringControl 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

GatekeeperIntercepting FilterFront Controller
Dónde viveEn el borde, delante del sistemaDentro de la app, alrededor de los manejadoresPunto de entrada de la app
Trabajo principalRechazar tráfico maloPre/postprocesar peticionesEnrutar peticiones a manejadores
Tecnología típicaGateway, WAF, proxyMiddleware, filtros de servletServlet dispatcher, router
¿Descarta peticiones?Sí — ese es el puntoRaramente — normalmente transformaNo — 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_SECRET al arrancar precisamente porque una clave hardcodeada acaba en el repo.
  • Reglas WAF afinadas a ojo. Las regex agresivas bloquean usuarios legítimos (select aparece 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

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.