Skip to content

Zero-Trust Networking for Application Developers

Zero-trust networking from an application developer's view: mutual TLS, service mesh authentication and request-level authorization for every call.

3 min read
A network topology where every service-to-service call passes through authentication and authorization checks

The Perimeter Is Dead

Traditional network security draws a line around your infrastructure: everything inside the perimeter is trusted, everything outside is not. This model fails when a single compromised service can move laterally through your entire network. Zero trust assumes no implicit trust—every request, from every source, must prove its identity and authorization.

Mutual TLS Between Services

In standard TLS, only the server presents a certificate. Mutual TLS (mTLS) requires both client and server to authenticate. Every service has its own certificate, and every connection verifies both ends.

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,
  });
}

Service Identity and Authorization

mTLS proves identity. Authorization decides what that identity can do. Extract the service identity from the certificate and check it against a policy before processing the request.

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();
}

Request-Level Context Propagation

Zero trust extends beyond service identity. Each request carries context—user identity, scopes, trace IDs—that downstream services validate independently rather than trusting upstream claims blindly.

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;
}

Network Policies in Kubernetes

Application-level zero trust works alongside network-level controls. Kubernetes NetworkPolicies restrict which pods can talk to each other, providing defense in depth.

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,
  };
}

Key Takeaways

Zero trust means every request proves its identity and authorization—no exceptions for "internal" traffic. Implement mTLS between services so every connection is mutually authenticated. Define explicit authorization policies per endpoint, naming which services can call which endpoints.

Propagate request context with signed, short-lived tokens that every downstream service verifies independently. Layer network policies on top of application-level controls for defense in depth. The additional complexity is real, but the alternative—a single compromised service owning your entire network—is worse. Start with mTLS and endpoint-level authorization, then add context propagation and policy engines as your service count grows.

Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX