Skip to content

Zero Trust Security Model for Developers

What zero trust means beyond the buzzword: identity-based access, mutual TLS, network microsegmentation and applying it at the application layer.

5 min read
Network diagram showing zero trust architecture with identity verification at every service boundary

The traditional security model is a castle with a moat: a strong perimeter (firewall, VPN) protecting a trusted internal network. Once you are inside, you can access everything. This model fails catastrophically when an attacker breaches the perimeter — which they inevitably do via phishing, compromised credentials, or supply chain attacks.

Zero trust flips the model: never trust, always verify. Every request — whether it comes from inside or outside the network — must be authenticated, authorized, and encrypted. There is no trusted internal network.

The Core Principles

Zero trust is not a product you buy. It is a set of architectural principles that change how you think about access control, network communication, and identity verification.

tstypescript
// Traditional security: trust the network
interface CastleAndMoat {
  perimeter: 'firewall + VPN';
  internalTraffic: 'trusted by default';  // ← The vulnerability
  authCheck: 'at the perimeter only';
  // Once inside, an attacker has lateral movement to everything
}
 
// Zero trust: trust nothing, verify everything
interface ZeroTrustModel {
  perimeter: 'does not exist';
  allTraffic: 'encrypted and authenticated';
  authCheck: 'at every service, every request';
  principle: 'least privilege — minimum access for minimum time';
  assumption: 'the network is already compromised';
}
 
// Zero trust principles as code
const zeroTrustPrinciples = {
  verifyExplicitly: {
    rule: 'Authenticate and authorize every request',
    how: 'Use identity tokens (JWT, mTLS) — not network location',
  },
  leastPrivilege: {
    rule: 'Grant minimum required access for minimum time',
    how: 'Short-lived tokens, scoped permissions, JIT access',
  },
  assumeBreach: {
    rule: 'Design as if attackers are already inside',
    how: 'Encrypt internal traffic, segment networks, log everything',
  },
};

Identity-Based Access (Not Network-Based)

In a zero trust model, access decisions are based on identity — who you are, what device you are using, and what context you are in — not on which network you are connected to.

tstypescript
// ❌ Network-based access control
function handleRequest(req: Request): Response {
  // If the request comes from an internal IP, trust it
  const clientIp = req.headers.get('x-forwarded-for');
  if (isInternalIp(clientIp)) {
    // No authentication required — "it's internal"
    return processRequest(req);  // Attackers love this
  }
  return new Response('Forbidden', { status: 403 });
}
 
// ✅ Identity-based access control
async function handleRequest(req: Request): Promise<Response> {
  // Every request must present a valid identity token
  const token = req.headers.get('authorization')?.replace('Bearer ', '');
  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }
 
  // Verify the token regardless of network origin
  const identity = await verifyToken(token);
  if (!identity) {
    return new Response('Invalid token', { status: 401 });
  }
 
  // Check if this identity has access to this specific resource
  const hasAccess = await checkPermission(identity, req.url, req.method);
  if (!hasAccess) {
    return new Response('Forbidden', { status: 403 });
  }
 
  return processRequest(req, identity);
}

Mutual TLS Between Services

In traditional architectures, internal services communicate over plain HTTP. In a zero trust architecture, every service-to-service connection is authenticated using mutual TLS (mTLS) — both sides present certificates to prove their identity.

tstypescript
// Setting up mTLS in Node.js
import { createServer, request } from 'https';
import { readFileSync } from 'fs';
 
// Server: requires client certificates
const server = createServer({
  key: readFileSync('server-key.pem'),
  cert: readFileSync('server-cert.pem'),
  ca: readFileSync('ca-cert.pem'),    // Trust only certs signed by this CA
  requestCert: true,                   // Require client certificate
  rejectUnauthorized: true,            // Reject connections without valid certs
}, (req, res) => {
  // The client's identity is in the certificate
  const clientCert = (req as any).socket.getPeerCertificate();
  const clientService = clientCert.subject.CN;  // e.g., "billing-service"
 
  console.log(`Authenticated request from: ${clientService}`);
 
  // Authorize based on the service identity
  if (!isAllowedCaller(clientService, req.url)) {
    res.writeHead(403);
    res.end('Service not authorized for this endpoint');
    return;
  }
 
  handleRequest(req, res, clientService);
});
 
// Client: presents its own certificate
function callService(url: string): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const req = request(url, {
      key: readFileSync('client-key.pem'),
      cert: readFileSync('client-cert.pem'),
      ca: readFileSync('ca-cert.pem'),
    }, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    });
    req.on('error', reject);
    req.end();
  });
}

Service-Level Authorization

Authentication tells you who is making the request. Authorization tells you whether they are allowed to do what they are requesting. In zero trust, every service enforces its own authorization — no relying on upstream services to have checked permissions.

tstypescript
// Policy-based authorization at every service
interface AccessPolicy {
  service: string;          // Which service can access
  resource: string;         // Which endpoint/resource
  actions: string[];        // Which HTTP methods
  conditions?: Condition[]; // Additional constraints
}
 
const policies: AccessPolicy[] = [
  {
    service: 'api-gateway',
    resource: '/api/users/*',
    actions: ['GET'],
    conditions: [{ type: 'time', allowedHours: { start: 6, end: 22 } }],
  },
  {
    service: 'billing-service',
    resource: '/api/users/*/payment-methods',
    actions: ['GET'],
    // Billing can read payment methods but not user profiles
  },
  {
    service: 'admin-service',
    resource: '/api/users/*',
    actions: ['GET', 'PUT', 'DELETE'],
    conditions: [{ type: 'approval', requiredApprovers: 1 }],
  },
];
 
function isAllowedCaller(
  callerService: string,
  requestPath: string,
  method: string = 'GET'
): boolean {
  return policies.some(policy =>
    policy.service === callerService &&
    matchesPath(requestPath, policy.resource) &&
    policy.actions.includes(method) &&
    (policy.conditions?.every(c => evaluateCondition(c)) ?? true)
  );
}
tstypescript
// ❌ Trusting upstream services to enforce authorization
function userService(req: Request): Response {
  // "The API gateway already checked permissions"
  // But what if the request bypasses the gateway?
  // What if the gateway has a bug in its auth logic?
  return getUser(req.params.id);  // No authorization check here
}
 
// ✅ Every service enforces its own authorization
async function userService(req: AuthenticatedRequest): Promise<Response> {
  // Verify the caller's identity (from mTLS or token)
  const caller = req.identity;
 
  // Check if THIS specific service allows THIS action
  if (!isAllowedCaller(caller.service, req.url, req.method)) {
    auditLog.warn('Unauthorized access attempt', {
      caller: caller.service,
      resource: req.url,
      method: req.method,
    });
    return new Response('Forbidden', { status: 403 });
  }
 
  return getUser(req.params.id);
}

Short-Lived Credentials

Long-lived API keys and service account passwords are a major risk. If compromised, they provide indefinite access. Zero trust uses short-lived, automatically rotated credentials.

tstypescript
// Token lifecycle management
interface TokenPolicy {
  maxLifetime: number;      // 15 minutes for access tokens
  refreshWindow: number;    // Can refresh within 5 minutes of expiry
  rotationSchedule: string; // Service credentials rotate daily
}
 
// Short-lived service-to-service tokens
async function getServiceToken(
  targetService: string
): Promise<string> {
  const token = await tokenService.issue({
    issuer: 'user-service',
    audience: targetService,
    expiresIn: '15m',           // 15 minutes — not days or months
    scopes: ['read:users'],      // Minimum required scope
  });
 
  return token;
}
 
// The token service issues short-lived JWTs
interface ServiceToken {
  iss: string;    // Issuing service
  aud: string;    // Target service
  exp: number;    // Expires in 15 minutes
  iat: number;    // Issued at
  scopes: string[]; // Minimum required permissions
  jti: string;    // Unique ID for revocation tracking
}

Logging and Monitoring

Zero trust requires comprehensive logging because you can no longer assume that internal traffic is safe. Every authentication decision, authorization check, and access attempt must be logged for audit and anomaly detection.

tstypescript
// Audit logging for zero trust
interface AuditLogEntry {
  timestamp: string;
  requestId: string;
  caller: {
    service: string;
    identity: string;
    ipAddress: string;
  };
  target: {
    service: string;
    resource: string;
    method: string;
  };
  decision: 'allowed' | 'denied';
  reason: string;
  policyMatched?: string;
}
 
function logAccessDecision(entry: AuditLogEntry): void {
  // All access decisions are logged — both allowed and denied
  // Denied requests are especially important for detecting attacks
  console.log(JSON.stringify(entry));
  
  // Alert on anomalies
  if (entry.decision === 'denied') {
    anomalyDetector.track(entry);
  }
}

Key Takeaways

  1. Never trust, always verify — authenticate and authorize every request regardless of network origin
  2. Use identity, not IP addresses — access decisions should be based on who is making the request, verified by tokens or certificates
  3. Enforce authorization at every service — do not rely on upstream services to have checked permissions
  4. Use mutual TLS between services — both client and server authenticate each other with certificates
  5. Issue short-lived credentials — 15-minute access tokens limit the blast radius of a compromised credential
  6. Log every access decision — comprehensive audit trails enable anomaly detection and forensic investigation
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX