Saltar al contenido

Redes Zero Trust para desarrolladores de aplicaciones

Redes zero trust desde la óptica del desarrollador: TLS mutuo, autenticación de service mesh y autorización por solicitud, sin importar la red.

3 min de lectura
Una topología de red en la que cada llamada de servicio a servicio pasa por comprobaciones de autenticación y autorización

El perímetro ha muerto

La seguridad de red tradicional traza una línea alrededor de tu infraestructura: todo lo que está dentro del perímetro es confiable, todo lo que está fuera no lo es. Este modelo falla cuando un solo servicio comprometido puede moverse lateralmente por toda tu red. Zero trust no asume ninguna confianza implícita: cada solicitud, sin importar su origen, debe demostrar su identidad y autorización.

TLS mutuo entre servicios

En el TLS estándar, solo el servidor presenta un certificado. El TLS mutuo (mTLS) exige que tanto el cliente como el servidor se autentiquen. Cada servicio tiene su propio certificado, y cada conexión verifica ambos extremos.

tstypescript
import { createServer, createSecureContext } from "node:tls";
import { readFileSync } from "node:fs";
 
// Server setup with mTLS
const server = createServer(
  {
    key: readFileSync("/certs/service-a.key"),
    cert: readFileSync("/certs/service-a.crt"),
    ca: readFileSync("/certs/ca.crt"),
    requestCert: true,          // Require client certificate
    rejectUnauthorized: true,   // Reject if client cert is invalid
  },
  (socket) => {
    const clientCert = socket.getPeerCertificate();
    console.log(`Authenticated client: ${clientCert.subject.CN}`);
  }
);
 
// HTTP client with mTLS
async function callService(url: string, body: unknown): Promise<Response> {
  const agent = new Agent({
    cert: readFileSync("/certs/service-b.crt"),
    key: readFileSync("/certs/service-b.key"),
    ca: readFileSync("/certs/ca.crt"),
  });
 
  return fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
    // @ts-expect-error -- Node fetch supports dispatcher
    dispatcher: agent,
  });
}

Identidad de servicio y autorización

mTLS demuestra la identidad. La autorización decide qué puede hacer esa identidad. Extrae la identidad del servicio a partir del certificado y compárala con una política antes de procesar la solicitud.

tstypescript
// ❌ Trusting any request from the internal network
app.post("/api/internal/process-payment", async (req, res) => {
  // No identity check — any service on the network can call this
  await processPayment(req.body);
  res.json({ success: true });
});
 
// ✅ Verify service identity and check authorization
interface ServicePolicy {
  allowedCallers: string[];
  requiredScopes?: string[];
}
 
const endpointPolicies: Record<string, ServicePolicy> = {
  "/api/internal/process-payment": {
    allowedCallers: ["checkout-service", "subscription-service"],
    requiredScopes: ["payments:write"],
  },
  "/api/internal/get-user": {
    allowedCallers: ["*"], // Any authenticated service
  },
};
 
function authorizeService(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const clientCert = (req.socket as TLSSocket).getPeerCertificate();
 
  if (!clientCert || !clientCert.subject) {
    res.status(401).json({ error: "No client certificate" });
    return;
  }
 
  const serviceId = clientCert.subject.CN;
  const policy = endpointPolicies[req.path];
 
  if (!policy) {
    res.status(403).json({ error: "No policy defined for endpoint" });
    return;
  }
 
  const isAllowed =
    policy.allowedCallers.includes("*") ||
    policy.allowedCallers.includes(serviceId);
 
  if (!isAllowed) {
    console.warn(
      `Service ${serviceId} denied access to ${req.path}`
    );
    res.status(403).json({ error: "Service not authorized" });
    return;
  }
 
  next();
}

Propagación del contexto a nivel de solicitud

Zero trust va más allá de la identidad del servicio. Cada solicitud transporta contexto (identidad del usuario, scopes, IDs de traza) que los servicios posteriores validan de forma independiente, en lugar de confiar ciegamente en las afirmaciones del servicio anterior.

tstypescript
interface RequestContext {
  userId: string;
  roles: string[];
  scopes: string[];
  traceId: string;
  sourceService: string;
  requestTimestamp: string;
}
 
// Middleware: extract and verify request context
function extractRequestContext(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const contextHeader = req.headers["x-request-context"];
 
  if (!contextHeader || typeof contextHeader !== "string") {
    res.status(401).json({ error: "Missing request context" });
    return;
  }
 
  try {
    // Context is signed by the API gateway
    const verified = verifySignedContext(contextHeader);
    req.context = verified;
    next();
  } catch (error) {
    res.status(401).json({ error: "Invalid request context signature" });
  }
}
 
// Every service independently verifies — never trust upstream
function verifySignedContext(token: string): RequestContext {
  const decoded = jwt.verify(token, process.env.CONTEXT_PUBLIC_KEY!, {
    algorithms: ["ES256"],
    maxAge: "5m", // Context expires quickly
  });
 
  return decoded as RequestContext;
}

Políticas de red en Kubernetes

El zero trust a nivel de aplicación funciona junto con los controles a nivel de red. Los NetworkPolicies de Kubernetes restringen qué pods pueden comunicarse entre sí, aportando una defensa en profundidad.

ymlyaml
# Only allow checkout-service to reach payment-service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payment-service-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: payment-service
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: checkout-service
        - podSelector:
            matchLabels:
              app: subscription-service
      ports:
        - protocol: TCP
          port: 8443
tstypescript
// Health check for zero-trust readiness
interface ZeroTrustCheck {
  name: string;
  check: () => Promise<boolean>;
}
 
const readinessChecks: ZeroTrustCheck[] = [
  {
    name: "mTLS certificates valid",
    check: async () => {
      const cert = readFileSync("/certs/service.crt", "utf8");
      const parsed = new crypto.X509Certificate(cert);
      return new Date(parsed.validTo) > new Date();
    },
  },
  {
    name: "Policy engine reachable",
    check: async () => {
      const response = await fetch("http://policy-engine:8181/health");
      return response.ok;
    },
  },
  {
    name: "Context signing key loaded",
    check: async () => {
      return !!process.env.CONTEXT_PUBLIC_KEY;
    },
  },
];
 
async function checkZeroTrustReadiness(): Promise<{
  ready: boolean;
  checks: Array<{ name: string; passed: boolean }>;
}> {
  const results = await Promise.all(
    readinessChecks.map(async (c) => ({
      name: c.name,
      passed: await c.check().catch(() => false),
    }))
  );
 
  return {
    ready: results.every((r) => r.passed),
    checks: results,
  };
}

Conclusiones clave

Zero trust significa que cada solicitud demuestra su identidad y autorización, sin excepciones para el tráfico "interno". Implementa mTLS entre servicios para que cada conexión quede mutuamente autenticada. Define políticas de autorización explícitas por endpoint, especificando qué servicios pueden llamar a qué endpoints.

Propaga el contexto de la solicitud con tokens firmados y de corta duración que cada servicio posterior verifica de forma independiente. Superpón políticas de red sobre los controles a nivel de aplicación para lograr una defensa en profundidad. La complejidad adicional es real, pero la alternativa (un solo servicio comprometido controlando toda tu red) es peor. Empieza con mTLS y autorización a nivel de endpoint, y luego añade propagación de contexto y motores de políticas a medida que crezca el número de servicios.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX