Arquitectura

Seguridad en microservicios: JWT, mTLS y autorización centralizada mediante REST API en Kubernetes

Ruslan Ismailov Publicado 18 min de lectura
S

Introducción: por qué la seguridad en microservicios es más compleja que en un monolito

En 2026, la arquitectura de microservicios se ha convertido en el estándar para sistemas de alta carga, pero junto con la flexibilidad ha traído un modelo de amenazas fundamentalmente diferente. En un monolito, el perímetro de seguridad era claro: un proceso, una base de datos, un conjunto de permisos. En los microservicios, la superficie de ataque es distribuida: decenas de servicios intercambian datos por red, y cada uno de ellos es un potencial punto de entrada.

Las amenazas actuales para sistemas de microservicios en Kubernetes incluyen:

  • Lateral movement — la compromisión de un servicio abre el acceso a la red interna del clúster
  • Token leakage — interceptación de tokens JWT o de sesión a través de tráfico no cifrado entre pods
  • Privilege escalation — un servicio obtiene más permisos de los necesarios mediante una autorización configurada incorrectamente
  • Ataques SSRF — el atacante explota la confianza entre servicios para acceder a recursos internos
  • Ataques a la cadena de suministro — la compromisión de dependencias en un servicio afecta a todo el ecosistema

La respuesta a estas amenazas es una protección multicapa: autenticación de usuarios mediante JWT, autenticación mutua de servicios mediante mTLS y autorización centralizada a través de un Policy Decision Point. Analizaremos cada capa en detalle.

JWT en microservicios: autenticación stateless y validación en el gateway

Arquitectura de autenticación JWT

JSON Web Token permite implementar autenticación stateless: el servidor no almacena sesiones, toda la información está codificada en el propio token y se verifica criptográficamente. En el contexto de microservicios, esto significa que cada servicio puede verificar de forma independiente la autenticidad de una solicitud sin consultar un almacenamiento centralizado.

El esquema típico de interacción es el siguiente:

Cliente → [POST /auth/login] → Auth Service
              ↓
         Emite JWT (access + refresh)
              ↓
Cliente → [GET /api/orders] → API Gateway
              ↓
         Valida JWT (firma, exp, iss)
              ↓
         Proxy de solicitud → Order Service
              ↓
         Order Service confía en el gateway (mTLS)

Validación de JWT en el nivel del API Gateway

La validación de JWT debe realizarse en el nivel del API Gateway, no en cada microservicio. Esto elimina la duplicación de lógica y centraliza el control. El Gateway verifica:

  • La firma del token (algoritmo RS256 o ES256 — nunca none)
  • El tiempo de vida (claim exp)
  • El emisor (claim iss)
  • La audiencia (claim aud)
  • La presencia del token en la lista de revocación (Redis)

Ejemplo de middleware en Go para la validación de JWT usando la librería golang-jwt/jwt:

package middleware

import (
    "context"
    "fmt"
    "net/http"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/redis/go-redis/v9"
)

type Claims struct {
    UserID string   `json:"sub"`
    Roles  []string `json:"roles"`
    jwt.RegisteredClaims
}

type JWTMiddleware struct {
    publicKey  interface{}
    redisClient *redis.Client
}

func NewJWTMiddleware(publicKey interface{}, rc *redis.Client) *JWTMiddleware {
    return &JWTMiddleware{publicKey: publicKey, redisClient: rc}
}

func (m *JWTMiddleware) Validate(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if !strings.HasPrefix(authHeader, "Bearer ") {
            http.Error(w, "missing or invalid Authorization header", http.StatusUnauthorized)
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")

        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
            // Verificamos el algoritmo de forma explícita
            if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
            }
            return m.publicKey, nil
        })

        if err != nil || !token.Valid {
            http.Error(w, "invalid token", http.StatusUnauthorized)
            return
        }

        // Verificación de la lista de revocación en Redis
        ctx := context.Background()
        revoked, err := m.redisClient.Exists(ctx, "revoked:"+tokenString[:16]).Result()
        if err != nil || revoked > 0 {
            http.Error(w, "token revoked", http.StatusUnauthorized)
            return
        }

        // Pasamos los claims más adelante a través del contexto
        ctx = context.WithValue(r.Context(), "claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Rotación de claves

Para la rotación de claves de firma se utiliza el mecanismo JWKS (JSON Web Key Set). El Auth Service publica las claves públicas en el endpoint /.well-known/jwks.json, y el Gateway las almacena en caché con TTL y las actualiza periódicamente. Durante la rotación, la clave antigua permanece disponible hasta que expiren todos los tokens emitidos, lo que evita interrupciones del servicio durante los cambios de clave planificados.

mTLS entre servicios: cifrado y autenticación mutua sin service mesh

Por qué mTLS en microservicios

TLS en modo unidireccional solo autentica al servidor. Mutual TLS (mTLS) requiere que ambas partes presenten un certificado: el cliente demuestra su identidad al servidor y viceversa. En el contexto de Kubernetes, esto significa que incluso si un atacante obtiene acceso a la red del clúster, no podrá hacerse pasar por un servicio legítimo sin un certificado de cliente válido.

Sin service mesh (Istio, Linkerd), mTLS se implementa en el nivel de la aplicación. Cada servicio en Go se configura para usar un certificado de cliente en las solicitudes salientes y para exigir un certificado de cliente en las solicitudes entrantes.

Implementación del cliente mTLS en Go

package transport

import (
    "crypto/tls"
    "crypto/x509"
    "net/http"
    "os"
    "time"
)

// NewMTLSClient crea un cliente HTTP con autenticación TLS mutua
func NewMTLSClient(certFile, keyFile, caFile string) (*http.Client, error) {
    // Cargamos el certificado y la clave del cliente
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, fmt.Errorf("load client cert: %w", err)
    }

    // Cargamos la CA para verificar el servidor
    caCert, err := os.ReadFile(caFile)
    if err != nil {
        return nil, fmt.Errorf("read CA cert: %w", err)
    }

    caCertPool := x509.NewCertPool()
    if !caCertPool.AppendCertsFromPEM(caCert) {
        return nil, fmt.Errorf("failed to append CA cert")
    }

    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        RootCAs:      caCertPool,
        MinVersion:   tls.VersionTLS13, // Mínimo TLS 1.3
    }

    transport := &http.Transport{
        TLSClientConfig: tlsConfig,
        // Configuración del pool de conexiones para producción
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    }

    return &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }, nil
}

Configuración del servidor mTLS en Go

package server

import (
    "crypto/tls"
    "crypto/x509"
    "net/http"
    "os"
)

func NewMTLSServer(certFile, keyFile, caFile string, handler http.Handler) (*http.Server, error) {
    caCert, err := os.ReadFile(caFile)
    if err != nil {
        return nil, err
    }

    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
        ClientAuth: tls.RequireAndVerifyClientCert, // Verificación obligatoria del cliente
        ClientCAs:  caCertPool,
        MinVersion: tls.VersionTLS13,
    }

    return &http.Server{
        Addr:      ":8443",
        Handler:   handler,
        TLSConfig: tlsConfig,
    }, nil
}

Esquema de interacción con mTLS:

Order Service (cliente)                   Inventory Service (servidor)
      │                                           │
      │── ClientHello (TLS 1.3) ──────────────→  │
      │← ServerHello + ServerCert ───────────── │
      │── ClientCert ────────────────────────→  │
      │   (Verificación por CA) ←──────────────  │
      │← Verified (200 OK) ────────────────────  │
      │                                           │
  Cada servicio sabe con quién está hablando

Servicio de autorización centralizado: Policy Decision Point y Open Policy Agent

Patrón Policy Decision Point

En un sistema distribuido, la lógica de autorización no debe estar dispersa entre los microservicios. El patrón Policy Decision Point (PDP) externaliza las decisiones de autorización a un componente separado. Los servicios envían la pregunta: «¿puede el usuario X realizar la acción Y sobre el recurso Z?» y reciben como respuesta allow o deny.

Open Policy Agent (OPA) es el estándar de facto para implementar PDP en el ecosistema Kubernetes. Las políticas se describen en el lenguaje Rego, lo que las hace versionables, testeables e independientes del código de la aplicación.

Ejemplo de política Rego

package authz.orders

import future.keywords.if
import future.keywords.in

# Permitimos la lectura de pedidos al propietario o al gestor
default allow := false

allow if {
    input.action == "read"
    input.resource.owner_id == input.user.id
}

allow if {
    input.action == "read"
    "manager" in input.user.roles
}

# Solo admin puede eliminar pedidos
allow if {
    input.action == "delete"
    "admin" in input.user.roles
}

Integración con el servicio Go mediante REST API

package authz

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
)

type OPAClient struct {
    baseURL    string
    httpClient *http.Client
}

type AuthzRequest struct {
    Input AuthzInput `json:"input"`
}

type AuthzInput struct {
    User     UserContext     `json:"user"`
    Action   string          `json:"action"`
    Resource ResourceContext `json:"resource"`
}

type AuthzResponse struct {
    Result bool `json:"result"`
}

func (c *OPAClient) IsAllowed(ctx context.Context, input AuthzInput) (bool, error) {
    payload, err := json.Marshal(AuthzRequest{Input: input})
    if err != nil {
        return false, err
    }

    req, err := http.NewRequestWithContext(
        ctx,
        http.MethodPost,
        c.baseURL+"/v1/data/authz/orders/allow",
        bytes.NewReader(payload),
    )
    if err != nil {
        return false, err
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return false, fmt.Errorf("OPA request failed: %w", err)
    }
    defer resp.Body.Close()

    var result AuthzResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return false, err
    }

    return result.Result, nil
}

Implementación práctica: servicio Go y Laravel como consumidor

Servicio Go con JWT middleware y mTLS

La cadena completa de procesamiento de solicitudes en un servicio Go es la siguiente:

package main

import (
    "log"
    "net/http"

    "github.com/yourorg/service/internal/authz"
    "github.com/yourorg/service/internal/middleware"
    "github.com/yourorg/service/internal/server"
    "github.com/yourorg/service/internal/transport"
)

func main() {
    // Inicialización del cliente HTTP mTLS para solicitudes salientes
    mtlsClient, err := transport.NewMTLSClient(
        "/etc/certs/client.crt",
        "/etc/certs/client.key",
        "/etc/certs/ca.crt",
    )
    if err != nil {
        log.Fatalf("failed to create mTLS client: %v", err)
    }

    // El cliente OPA usa mTLS para comunicarse con el servicio AuthZ
    opaClient := authz.NewOPAClient("https://opa.internal:8443", mtlsClient)

    // Enrutamiento con JWT middleware
    mux := http.NewServeMux()
    jwtMW := middleware.NewJWTMiddleware(loadPublicKey(), initRedis())

    mux.Handle("/api/orders", jwtMW.Validate(
        authzMiddleware(opaClient, "read",
            http.HandlerFunc(handleGetOrders),
        ),
    ))

    // Arranque del servidor mTLS
    srv, err := server.NewMTLSServer(
        "/etc/certs/server.crt",
        "/etc/certs/server.key",
        "/etc/certs/ca.crt",
        mux,
    )
    if err != nil {
        log.Fatalf("failed to create server: %v", err)
    }

    log.Fatal(srv.ListenAndServeTLS("", ""))
}

Servicio Laravel como consumidor

El servicio Laravel interactúa con los servicios Go mediante REST API, usando certificados de cliente para mTLS. Configuración del cliente HTTP en Laravel con Guzzle:

// config/services.php
'order_service' => [
    'base_url' => env('ORDER_SERVICE_URL', 'https://order-service.internal:8443'),
    'cert'     => env('MTLS_CERT_PATH', '/etc/certs/client.crt'),
    'key'      => env('MTLS_KEY_PATH', '/etc/certs/client.key'),
    'ca'       => env('MTLS_CA_PATH', '/etc/certs/ca.crt'),
],

// app/Services/OrderServiceClient.php
class OrderServiceClient
{
    private Client $client;

    public function __construct()
    {
        $config = config('services.order_service');
        $this->client = new Client([
            'base_uri' => $config['base_url'],
            'cert'     => [$config['cert'], ''],   // ruta al certificado
            'ssl_key'  => $config['key'],
            'verify'   => $config['ca'],           // CA para verificar el servidor
            'timeout'  => 5.0,
        ]);
    }

    public function getOrders(string $jwtToken): array
    {
        $response = $this->client->get('/api/orders', [
            'headers' => [
                'Authorization' => 'Bearer ' . $jwtToken,
                'X-Request-ID'  => (string) Str::uuid(),
            ],
        ]);

        return json_decode($response->getBody()->getContents(), true);
    }
}

Laravel transmite el JWT del usuario al llamar a los servicios internos. Esto permite que los servicios downstream conozcan el contexto del usuario sin necesidad de una nueva autenticación.

Gestión de certificados en Kubernetes: cert-manager y rotación automática

Arquitectura PKI en el clúster

Para la gestión de certificados TLS en Kubernetes se utiliza cert-manager. Emite y renueva certificados automáticamente, los almacena como Kubernetes Secrets y los monta en los pods.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: order-service-cert
  namespace: production
spec:
  secretName: order-service-tls
  duration: 24h          # TTL corto para certificados mTLS
  renewBefore: 8h        # Renovación 8 horas antes de la expiración
  subject:
    organizations:
      - your-org
  commonName: order-service.production.svc.cluster.local
  dnsNames:
    - order-service
    - order-service.production
    - order-service.production.svc
    - order-service.production.svc.cluster.local
  issuerRef:
    name: internal-ca-issuer
    kind: ClusterIssuer
  usages:
    - digital signature
    - key encipherment
    - client auth    # Para mTLS: el certificado se usa como cliente
    - server auth

Montaje de certificados en el Pod

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
        - name: order-service
          image: your-registry/order-service:latest
          volumeMounts:
            - name: tls-certs
              mountPath: /etc/certs
              readOnly: true
          env:
            - name: TLS_CERT_PATH
              value: /etc/certs/tls.crt
            - name: TLS_KEY_PATH
              value: /etc/certs/tls.key
            - name: TLS_CA_PATH
              value: /etc/certs/ca.crt
      volumes:
        - name: tls-certs
          secret:
            secretName: order-service-tls

Durante la rotación del certificado, cert-manager actualiza el Secret y Kubernetes remontan los archivos en el contenedor en ejecución. El servicio Go debe soportar la recarga en caliente de certificados mediante tls.Config.GetCertificate en lugar de la carga estática al inicio.

Almacenamiento y transmisión de tokens: Redis como lista de revocación

El problema de la revocación de JWT

JWT es por naturaleza stateless: no es posible «revocar» un token válido sin un mecanismo adicional. Si un usuario cierra sesión, cambia su contraseña o su cuenta es comprometida, se necesita una forma de invalidar inmediatamente los tokens emitidos antes de que expiren.

Lista de revocación en Redis

La solución es almacenar los identificadores de los tokens revocados en Redis con un TTL igual al tiempo de vida restante del token. El Gateway verifica cada token entrante contra esta lista.

package token

import (
    "context"
    "time"

    "github.com/redis/go-redis/v9"
)

type RevocationStore struct {
    client *redis.Client
}

const revokedKeyPrefix = "token:revoked:"

// RevokeToken añade el JTI del token a la lista de revocación
func (s *RevocationStore) RevokeToken(ctx context.Context, jti string, expiresAt time.Time) error {
    ttl := time.Until(expiresAt)
    if ttl <= 0 {
        // El token ya ha expirado — no hay nada que revocar
        return nil
    }

    return s.client.Set(ctx, revokedKeyPrefix+jti, "1", ttl).Err()
}

// IsRevoked verifica si el token ha sido revocado
func (s *RevocationStore) IsRevoked(ctx context.Context, jti string) (bool, error) {
    result, err := s.client.Exists(ctx, revokedKeyPrefix+jti).Result()
    if err != nil {
        return false, err
    }
    return result > 0, nil
}

Para la escalabilidad, Redis Cluster con replicación garantiza la disponibilidad de la lista de revocación incluso ante fallos de nodos. Utilice el comando WAIT para confirmar la replicación al revocar tokens de alta criticidad.

Registro y auditoría de eventos de seguridad

Qué es necesario registrar

La auditoría de seguridad en microservicios debe abarcar los siguientes eventos:

  • Cada intento de autenticación (exitoso y fallido) con User-Agent, IP y timestamp
  • Denegaciones de autorización de OPA con contexto: quién, qué y sobre qué intentó actuar
  • Errores de handshake mTLS: certificado inválido, certificado expirado, CA desconocida
  • Revocación de tokens: quién la inició, qué JTI, motivo
  • Cambios de claves de firma JWT
  • Cambios en las políticas de OPA (a través de CI/CD)

Estructura de un evento de seguridad

{
  "timestamp": "2026-03-15T14:32:01.123Z",
  "level": "WARN",
  "event_type": "authz.denied",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "service": "order-service",
  "user_id": "usr_01HX4M",
  "action": "delete",
  "resource_type": "order",
  "resource_id": "ord_09KL2P",
  "policy": "authz.orders",
  "decision": "deny",
  "client_cert_cn": "inventory-service",
  "source_ip": "10.0.1.45"
}

Los logs estructurados en formato JSON se envían a un sistema centralizado (ELK, Loki, CloudWatch). Configure alertas para anomalías: un aumento brusco de authz.denied, errores mTLS repetidos desde la misma IP, intentos de uso de tokens revocados.

Vulnerabilidades comunes y cómo evitarlas

Token leakage

El JWT se transmite en el encabezado Authorization y puede ser comprometido cuando:

  • Se registran los encabezados HTTP completos — nunca registre el header Authorization completo
  • Se transmite como parámetro en la URL — el token queda expuesto en los logs de acceso y en el historial del navegador
  • El tráfico entre servicios no está cifrado — precisamente para esto existe mTLS

Solución: registre solo los primeros 8 caracteres del token (para correlación), use exclusivamente HTTPS/mTLS y establezca un TTL corto (15 minutos para el access token).

SSRF a través de la confianza entre servicios

Si el servicio A confía en todas las solicitudes del servicio B basándose en mTLS, un atacante que comprometa B puede explotar esa confianza para lanzar ataques SSRF contra recursos internos. Contramedidas:

  • mTLS confirma la identidad del servicio, pero no la intención de la solicitud — la autorización mediante OPA es obligatoria para cada acción
  • Network policy en Kubernetes: los servicios solo pueden comunicarse con direcciones explícitamente permitidas
  • Valide todos los URLs entrantes en los parámetros de solicitud contra una whitelist

Escalada de privilegios mediante claims

Nunca confíe en los claims del JWT sin verificarlos en el contexto de la operación actual. Ejemplo de error: el servicio acepta el rol admin del token sin verificarlo mediante OPA, lo que permite falsificar el payload con un algoritmo de firma débil. Soluciones:

  • Use solo algoritmos asimétricos (RS256, ES256) — nunca HS256 en entornos multiservicio donde cada servicio debe conocer el secreto
  • Valide el algoritmo explícitamente en el código, no confíe en el encabezado alg del token
  • OPA recibe los claims del token, pero la decisión final se toma en base a datos actualizados de la base de datos

Conclusión: checklist de seguridad para microservicios

La protección integral de una arquitectura de microservicios no es un conjunto de herramientas independientes, sino un sistema interconectado. JWT garantiza la identidad del usuario, mTLS asegura la identidad de los servicios, OPA centraliza la lógica de autorización, cert-manager automatiza la gestión de certificados y Redis proporciona el mecanismo de revocación de tokens.

Checklist para un sistema listo para producción:

  1. JWT firmado con algoritmo RS256 o ES256, el algoritmo se valida explícitamente en el código
  2. El access token tiene un TTL no mayor de 15 minutos, el refresh token no mayor de 7 días
  3. Las claves públicas se publican mediante JWKS y se almacenan en caché con actualización automática
  4. Todo el tráfico entre servicios está protegido con mTLS con versión mínima TLS 1.3
  5. Los certificados se gestionan mediante cert-manager con rotación automática
  6. La autorización está centralizada mediante OPA, las políticas están versionadas en Git
  7. La lista de revocación se almacena en Redis con TTL = tiempo de vida restante del token
  8. Todos los eventos de seguridad se registran en formato JSON estructurado
  9. Las Network policy en Kubernetes restringen la comunicación entre servicios mediante whitelist
  10. Los encabezados Authorization están excluidos de los logs de la aplicación

La implementación de cada una de estas capas requiere esfuerzo de ingeniería, pero es precisamente su conjunto lo que garantiza la defensa en profundidad: una situación en la que la compromisión de un componente no implica la compromisión de todo el sistema.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →