Tus microservicios comparten una VPC, así que se confían entre sí. Esa confianza es autoridad ambiental: el permiso para invocar un servicio se otorga no por prueba criptográfica, sino por topología de red. Un atacante que viola el perímetro hereda toda ella.

Los servicios pueden autenticarse perfectamente sin autoridad ambiental. La pregunta más difícil es por qué tu plataforma hace que se sienta como una traición.

Cómo se ve la autoridad ambiental en producción

La mayor parte de la “autenticación” entre servicios en realidad es solo segmentación de red. Los servicios se encuentran en una subred privada, detrás de un API gateway, o dentro de un cluster de Kubernetes con un service mesh. El supuesto es simple: si la petición viene de una IP interna, es legítima.

Este modelo se colapsa cuando el perímetro lo hace. Un pod comprometido, movimiento lateral después de una filtración de credenciales, o una inyección en la cadena de suministro pone al atacante dentro de la zona de confianza. En ese punto, todos los servicios son accesibles y la mayoría no pedirá prueba de identidad. El atacante no necesita robar más credenciales. Simplemente hereda la autoridad ambiental de la red.

La autenticación real entre servicios significa que cada llamador demuestra quién es, y quien recibe la llamada verifica esa demostración. La red es solo un transporte. No debería ser una credencial.

Cómo funciona la autenticación de servicios basada en capabilities

Hay dos enfoques prácticos: identidad criptográfica (cada servicio tiene un nombre demostrable) y capability tokens (cada petición lleva una autorización con scope definido e infalsificable).

Identidad criptográfica con mTLS

En mutual TLS, tanto el cliente como el servidor presentan certificados X.509. El servidor verifica el certificado del cliente antes de manejar la petición. El cliente verifica el certificado del servidor antes de enviar datos. Ninguno de los dos lados confía en el otro por la dirección IP.

El desafío de infraestructura es la distribución de certificados. SPIFFE y SPIRE resuelven esto dándole a cada workload una identidad criptográfica derivada de sus propiedades, no de su ubicación. Un pod ejecutando payments-api obtiene un SPIFFE ID como spiffe://prod.example.com/payments-api. El certificado es de corta duración y se rota automáticamente.

Así es como se ve esto en Go:

package main

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

// loadClientTLS returns a config that verifies the server AND presents a client cert.
func loadClientTLS(caCert, clientCert, clientKey []byte) *tls.Config {
    pool := x509.NewCertPool()
    pool.AppendCertsFromPEM(caCert)

    cert, err := tls.X509KeyPair(clientCert, clientKey)
    if err != nil {
        log.Fatalf("failed to load client cert: %v", err)
    }

    return &tls.Config{
        Certificates: []tls.Certificate{cert},
        RootCAs:      pool,
        // Do not skip verification based on IP or DNS name alone.
        InsecureSkipVerify: false,
    }
}

func main() {
    caCert, _ := os.ReadFile("ca.crt")
    clientCert, _ := os.ReadFile("service.crt")
    clientKey, _ := os.ReadFile("service.key")

    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: loadClientTLS(caCert, clientCert, clientKey),
        },
    }

    resp, err := client.Get("https://orders-api.internal:8443/orders")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.Status)
}

La línea crítica es InsecureSkipVerify: false. Sin ella, vuelves a la confianza ambiental. Con ella, el cliente se niega a conectarse a menos que el servidor presente un certificado válido firmado por la CA. El servidor hace lo mismo para el cliente.

Capability tokens para autorización con scope

Los certificados prueban identidad. Las capabilities prueban autorización. En muchos sistemas, quieres ambos: demuestra quién eres, luego demuestra que tienes permitido hacer esta cosa específica.

Un capability token vincula una identidad, una action y un recurso en un package infalsificable. El servicio lo acuña. El servicio lo verifica. No hay un session store central para consultar.

import hmac
import hashlib
import secrets
import time

SECRET = secrets.token_bytes(32)

def mint_service_capability(
    caller_id: str,
    service: str,
    action: str,
    resource: str,
    ttl_seconds: int = 300
) -> str:
    """Mint a time-bound capability for a specific service action."""
    expires = int(time.time()) + ttl_seconds
    payload = f"{caller_id}:{service}:{action}:{resource}:{expires}"
    sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:24]
    return f"{payload}:{sig}"

def verify_service_capability(
    token: str,
    expected_service: str,
    expected_action: str,
    expected_resource: str
) -> bool:
    """Verify a capability token is unexpired and correctly scoped."""
    try:
        caller_id, svc, action, resource, expires, sig = token.rsplit(":", 5)
    except ValueError:
        return False

    if int(expires) < time.time():
        return False
    if svc != expected_service or action != expected_action or resource != expected_resource:
        return False

    payload = f"{caller_id}:{svc}:{action}:{resource}:{expires}"
    expected_sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:24]
    return hmac.compare_digest(sig, expected_sig)

# Service A requests a capability to call Service B.
token = mint_service_capability("payments-api", "orders-api", "read", "order-123")

# Service B verifies the capability before handling the request.
if not verify_service_capability(token, "orders-api", "read", "order-123"):
    raise PermissionError("Invalid or expired capability")

El token es auto-contenido y de corta duración. El Servicio B no necesita llamar a un authorization server. Solo verifica el HMAC y la expiración. El scope es explícito: este token solo es válido para leer order-123 de orders-api.

Por qué esto es más difícil de lo que debería

La tecnología existe. La dificultad es operativa.

El bootstrapping de confianza es el primer problema. Si cada servicio necesita un certificado, algo necesita emitir esos certificados. Ese algo, la certificate authority o el SPIRE server, se convierte en un nuevo objetivo. No has eliminado la confianza. La has concentrado.

También está el problema de revocación. Si un servicio es comprometido, ¿cómo invalidas su identidad? Los certificados mTLS pueden ser de corta duración, lo cual ayuda, pero revocar un certificado específico aún requiere distribuir una certificate revocation list o usar OCSP. Los capability tokens expiran rápido, lo cual limita el daño, pero una signing key robada permite a un atacante acuñar tokens ilimitados.

El rendimiento es otra preocupación. Los TLS handshakes agregan latencia. En un service mesh con sidecars, el overhead usualmente es aceptable (unos pocos milisegundos), pero en rutas de alto throughput, esos milisegundos se acumulan. Los capability tokens evitan un round trip a un auth server, pero la verificación HMAC no es gratis a escala.

Dónde la autoridad ambiental aún tiene sentido

No toda llamada interna necesita prueba criptográfica. Un cron job que corre dentro del mismo container que el database client no necesita mTLS para hablar con localhost. Los health checks entre un load balancer y un node no necesitan capability tokens.

La meta no es autoridad ambiental cero. La meta es saber dónde vive tu autoridad ambiental y si el blast radius es aceptable. Una VPC compartida con 200 servicios y sin autenticación interna es un blast radius grande e invisible. Un socket Unix local con permisos de archivo 0600 es uno pequeño y explícito.

Cómo empezar a eliminar la autoridad ambiental de las llamadas entre servicios

No necesitas un service mesh desde el día uno. Empieza con los servicios que tocan datos sensibles o están en los trust boundaries.

Primero, identifica tus delegados. ¿Qué servicios tienen privilegios que otros no tienen? ¿Qué APIs internas serían peligrosas si las llama el servicio equivocado? Esos son tus candidatos.

Segundo, agrega mTLS a la API layer. Si estás usando un reverse proxy como Envoy o NGINX, puedes terminar mTLS ahí sin cambiar el application code. El proxy pasa un header con la identidad del cliente verificada al upstream. El upstream verifica ese header.

Tercero, delimita el scope de tus tokens. Si ya usas JWTs para autenticación de servicios, deja de poner role: service en el payload. Pon allowed_services: ["orders-api"] y allowed_resources: ["order:*"]. Haz que la autorización sea tan específica como la operación.

Preguntas frecuentes sobre autenticación de servicios sin autoridad ambiental

¿Es suficiente mTLS, o también necesito capabilities?

mTLS prueba identidad. Las capabilities prueban autorización. Resuelven problemas diferentes. A un servicio con un certificado válido aún no se le debería permitir borrar datos arbitrarios. Usa mTLS para transport security e identidad, capabilities para access control.

¿Qué pasa con service meshes como Istio o Linkerd?

Los service meshes automatizan mTLS e identidad entre pods. Son una forma práctica de eliminar autoridad ambiental sin reescribir cada servicio. El mesh maneja certificate rotation, identity attestation y encriptación de tráfico. La compensación es complejidad y un nuevo control plane que asegurar.

¿Esto funciona para serverless functions?

Sí, pero es más difícil. Las serverless functions son efímeras, así que certificate rotation e identity attestation necesitan suceder en tiempo de invocación. Los proveedores de nube ofrecen workload identity para esto (AWS IAM Roles for Service Accounts, GCP Workload Identity). Estos son como capabilities: la función recibe un token scoped a lo que le está permitido hacer, no una credencial de cobertura general.

¿Cuál es la diferencia entre un service account y una capability?

Un service account es una identidad que porta autoridad ambiental por defecto. Si el Servicio A corre como payments-sa, usualmente puede hacer cualquier cosa que payments-sa tenga permitido hacer. Una capability está scoped a una action específica sobre un recurso específico. Incluso si la capability es robada, no puede ser reproducida contra un objetivo diferente.

Empieza con el perímetro que ya tienes

Tu VPC nunca fue un security boundary. Era un network boundary que pretendíamos que era un security boundary. Eliminar la autoridad ambiental significa tratar cada llamada de servicio como si cruzara ese boundary. La identidad criptográfica y las capabilities scoped son las herramientas. La parte difícil es admitir que el modelo antiguo era conveniencia disfrazada de seguridad.

Elige una API interna. Agrega client certificate verification. Observa qué se rompe. La mayoría de las veces, lo que se rompe es un supuesto que no sabías que tu código estaba haciendo.